Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add onLongPress to Buttons #40641

Merged
merged 29 commits into from Oct 30, 2019
Merged

Add onLongPress to Buttons #40641

merged 29 commits into from Oct 30, 2019

Conversation

bernaferrari
Copy link
Contributor

Description

It is too hard to add onLongPress to Buttons. You need to add an InkWell that goes on top of another InkWell and this causes additional issues. You also need to deal with clipBehavior: Clip.antiAlias and onPressed from Button is required but needs to be empty. Two possible solutions to overcome the problem:

  1. allow passing InkWell as a parameter (which would allow more advanced usages, like onDoubleTap or onHover).
  2. just add a onLongPress parameter to buttons.

While I think there is room for both options, it was suggested I could pursue the second, which I really like and is good enough for me right now.

Related Issues

#40558

Tests

Should I add tests? If so, how? All of onPressed tests are focused on how it changes the color of Button, not on if it works or not.

Checklist

Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes ([x]). This will ensure a smooth and quick review process.

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I signed the [CLA].
  • I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
  • I updated/added relevant documentation (doc comments with ///).
  • All existing and new tests are passing.
  • The analyzer (flutter analyze --flutter-repo) does report problems everywhere. I don't know how to use it, how do you use it? It looks like there are issues with const/icons/a lot of things. Not on this PR, but everywhere.
  • I am willing to follow-up on review comments in a timely manner.

Breaking Change

Does your PR require Flutter developers to manually update their apps to accommodate your change?

  • Yes, this is a breaking change.
  • No, this is not a breaking change.

@fluttergithubbot fluttergithubbot added f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels. labels Sep 17, 2019
@fluttergithubbot
Copy link
Contributor

It looks like this pull request may not have tests. Please make sure to add tests before merging. While there are exceptions to this rule, if this patch modifies code it is probably not an exception.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

@HansMuller
Copy link
Contributor

Material buttons, like most material components, handle long-press by showing a tooltip. It's not clear that it would be useful to override this behavior. What's the use case?

@bernaferrari
Copy link
Contributor Author

There is no tooltip attribute for buttons. It only has a null onLongPress. I was making long press to show a Bottom Sheet with selectable options/settings.

@HansMuller
Copy link
Contributor

You are right, buttons currently don't show a tooltip.

Should a button with only a longPress callback specified be disabled? I don't think so, but supporting that will complicate this PR somewhat.

Tests are needed which verify that longPress input gestures do what's expected. If the existing onPressed tests aren't verifying that their callback is actually running, then we might as well do that here as well.

@bernaferrari
Copy link
Contributor Author

Done! @HansMuller I am having a hard time figuring out how to test my code (and the tests). I did two tests, but I'm not sure how to test them (weren't included on the previous commit). Could you please shine some light (or update the contributing wiki documentation)? flutter test only works for samples. I want to test the material code I just modified.

@HansMuller
Copy link
Contributor

HansMuller commented Sep 27, 2019

You'll want to extend the tests in packages/flutter/test/material/buttons_test.dart that verify that longPress is null by default. Similarly, add a test to the end of the file, like the one called 'MaterialButton disabled default is correct', which verifies that just setting longPress enables a button. This test should also check the enabled property.

/// default will resemble a flat button in the [disabledColor]. If you are
/// trying to change the button's [color] and it is not having any effect, check
/// that you are passing a non-null [onPressed] handler.
/// that you are passing a non-null [onPressed] or [onLongPress] handler.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize that this doc used to say "handler". Since we seem to be consistently referring to onPressed etc as "callbacks", please use callback here.

final VoidCallback onPressed;

/// The callback that is called when the button is long-pressed.
///
/// If this and [onPressed] are set to null, the button will be disabled.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this callback and [onPressed] are null, then the button will be disabled.

@@ -88,9 +89,14 @@ class MaterialButton extends StatelessWidget {

/// The callback that is called when the button is tapped or otherwise activated.
///
/// If this is set to null, the button will be disabled.
/// If this and [onLongPress] are set to null, the button will be disabled.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this callback and [onLongPress] are null, then the button will be disabled.

@@ -21,8 +21,8 @@ import 'theme_data.dart';
///
/// The button's size will expand to fit the child widget, if necessary.
///
/// MaterialButtons whose [onPressed] handler is null will be disabled. To have
/// an enabled button, make sure to pass a non-null value for onPressed.
/// MaterialButtons whose [onPressed] and [onLongPress] handlers are null will be disabled. To have
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handlers => callbacks

@@ -29,7 +29,7 @@ import 'theme_data.dart';
/// interactive, with ink splashes, without also committing to these stylistic
/// choices, consider using [InkWell] instead.
///
/// If the [onPressed] callback is null, then the button will be disabled,
/// If [onPressed] or [onLongPress] callbacks are null, then the button will be disabled,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the [onPressed] and [onLongPress] callbacks are null, then this button will be disabled.

@@ -75,9 +76,14 @@ class RawMaterialButton extends StatefulWidget {

/// Called when the button is tapped or otherwise activated.
///
/// If this is set to null, the button will be disabled, see [enabled].
/// If this and [onLongPress] are set to null, the button will be disabled, see [enabled].
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this callback and [onLongPress] are null, then this button will be disabled.

Here and elsewhere: to add a link a related method, include a "See also" section:

///
/// See also:
///
///  * [enabled], which is true if the button is enabled.

@bernaferrari
Copy link
Contributor Author

Tests are there. Please take a look.

I had a hard time testing the tests, because I don't know how to test them. I know there is no syntax issue in them, but I'm not sure if flutter tests is checking them or the multiple error messages (for unknown reasons) makes it exit before running the tests.

image

@shihaohong
Copy link
Contributor

shihaohong commented Oct 4, 2019

Based on the compiler errors, it might be from recent changes to flutter/dev/packages/flutter_test. Check to see if the Flutter repo you're working on is in sync with the current master branch

@bernaferrari
Copy link
Contributor Author

Thanks, @shihaohong! All tests are now passing. :)

@shihaohong
Copy link
Contributor

There seems to still be analyzer test failures. You might want to fix those before requesting another review :)

@bernaferrari
Copy link
Contributor Author

Analyzer is fine. The issue is with: Whitespace detected at the end of source code lines. Please remove:

What is going on? Shouldn't there be a newline at the end of each file?

@shihaohong
Copy link
Contributor

shihaohong commented Oct 4, 2019

That refers to whitespace after a line, not a newline. For example

var someDartVar = "Hello"; [extra space here]

@shihaohong
Copy link
Contributor

shihaohong commented Oct 4, 2019

It seems to be these lines:

packages/flutter/lib/src/material/button.dart:80:  /// 
packages/flutter/lib/src/material/button.dart:87:  /// 
packages/flutter/lib/src/material/button.dart:89:  /// 
packages/flutter/lib/src/material/material_button.dart:93:  /// 
packages/flutter/lib/src/material/material_button.dart:102:  /// 

@bernaferrari
Copy link
Contributor Author

Oh, should work fine now, thanks! All of these are new to me.

@shihaohong
Copy link
Contributor

No worries! I submitted an issue to improve the error message as well, since it was pretty confusing for me when I first bumped into this as well.

@shihaohong
Copy link
Contributor

shihaohong commented Oct 15, 2019

It seems the failing tests are caused by changes to debugFillProperties. Also, you still have to remove the ObjectFlagProperty for onPressed and onLongPress

@bernaferrari
Copy link
Contributor Author

lol, thanks! @shihaohong I am having a hard time figuring out stage vs stash vs commit on vscode and I guess that slipped. Should be fixed now.

Copy link
Contributor

@shihaohong shihaohong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is getting very close! The only thing from my end would be adding tests to ensure that onLongPress's callbacks are properly invoked for buttons other than MaterialButton when a long press is detected.

expect(button.enabled, true);
});

testWidgets('FlatButton should be enabled when onLongPress is not null.', (WidgetTester tester) async {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized that buttons_test.dart is an odd file and contains an amalgamation of tests for the different types of buttons, even though flat_button_test.dart, raised_button_test.dart, and material_button_test.dart exist.

It could be nice to have each button widget's tests in their own files, but maybe that's out of the scope of this PR since there are preexisting tests in this file that test for the different buttons as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, you are right. For some reason I thought everything was on the same (because, well, there are everything on the same). I'm going to split those on the correct files.

Just the same question as I commented below. How many tests. 2 (onPressed true + onLongPress false / onPressed false + onLongPress true) or 4 (onPressed false + onLongPress false.. and so on)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the following combinations will be important:

  1. just onPressed to make sure that functionality works
  2. just onLongPress to make sure that functionality works
  3. both onPressed and onLongPress activated, then testing between different the two gestures to ensure that both are properly and distinctly recognized
  4. both deactivated to ensure that neither callbacks are being invoked when they shouldn't be Edit: I just realized that this doesn't make sense, since you cannot invoke a non-existent callback

Definitely check the other test files to see if some of this is already implemented -- I'd be surprised if the onPressed ones weren't already tested, but you never know :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So.. outlineButton contained onTap test. I modified it to include onLongPress and replicated into the others. RawMaterialButton already contained one test, I kept it, but added the new one, more complex, in the end of the file. The others had no onTap/onPressed test.

TODO for you. Wish I could receive a notification when analyzer/CI/github/cirrus/whoever is testing finished the tests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, running flutter analyze --flutter-repo will run the analyzer locally so you do not have to wait for the CI to finish running its tests

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the issue where the repo version I was using was newer than flutter I had, therefore that wouldn't work. I needed to start using ../../bin/flutter instead. Wasnt trivial. And that doesn't check the whitespace thing.

expect(button.enabled, true);
});

testWidgets('MaterialButton onPressed callback', (WidgetTester tester) async {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If onPress callbacks of the other buttons haven't been tested, we should test them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, so do I add another 4 tests? Or even more? (onPressed true/onLongPress false, onPressed false/OnLongPress false, and so on)? I agree with you that file is a mess.

expect(didPressButton, isTrue);
});

testWidgets('MaterialButton onLongPress callback', (WidgetTester tester) async {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should definitely add tests to ensure that onLongPress's callback works.

@@ -333,6 +333,79 @@ void main() {
paintsExactlyCountTimes(#clipPath, 0),
);
});

testWidgets('FlatButton responds to tap and onLongPress when enabled', (WidgetTester tester) async {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit for consistency

Suggested change
testWidgets('FlatButton responds to tap and onLongPress when enabled', (WidgetTester tester) async {
testWidgets('FlatButton onPressed and onLongPress callbacks are correctly called when non-null', (WidgetTester tester) async {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Yeah. I had that question. It's the tap method that works with on Pressed, lol.

I'm traveling, I'll be back in about 8 days. See you soon, then. Have a nice week! (and please maybe review my other PR related to sliders)

expect(pressedCount, 1);

// onPressed null, onLongPress not null.
pressedCount = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it feels odd that pressedCount is reset after every test. The variable name implies that the number of taps/presses matters. However, in this case, it's only used to determine if the callback was invoked when the button was pressed.

A bool wasPressed would achieve the same effect, but be clearer to future readers of the code.

expect(pressedCount, 0);
});

testWidgets('FlatButton onPressed and onLongPress callbacks are distincly recognized', (WidgetTester tester) async {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
testWidgets('FlatButton onPressed and onLongPress callbacks are distincly recognized', (WidgetTester tester) async {
testWidgets('FlatButton onPressed and onLongPress callbacks are distinctly recognized', (WidgetTester tester) async {

await tester.pumpWidget(
buildFrame(onPressed: null, onLongPress: () { pressedCount += 1; }),
);
expect(tester.widget<FlatButton>(find.byType(FlatButton)).enabled, true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Splitting the call into multiple lines makes it easier to understand what is tested. That, or you could make the tester/find call its own separate line of code, just like you did for your other test. The same applies elsewhere

Suggested change
expect(tester.widget<FlatButton>(find.byType(FlatButton)).enabled, true);
expect(tester.widget<FlatButton>(
find.byType(FlatButton)
).enabled, isTrue);

),
);

final Finder flatButton = find.byType(FlatButton);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is much easier to read 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree.. But there is a problem. I'm redefining the button 3 times in the other tests. So I would need to either use flatbutton1, flatbutton2, flatbutton.. or a var, which also doesn't sound good.

Ideas?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could remove the final declaration and re-use the same variable, which is fine


testWidgets('MaterialButton responds to tap and onLongPress when enabled', (WidgetTester tester) async {

int pressedCount = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above

);
expect(tester.widget<OutlineButton>(find.byType(OutlineButton)).enabled, true);
await tester.tap(find.byType(OutlineButton));
await tester.pumpAndSettle();
expect(pressedCount, 1);

// onPressed null, onLongPress not null.
pressedCount = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above


testWidgets('RaisedButton responds to tap and onLongPress when enabled', (WidgetTester tester) async {

int pressedCount = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above

buildFrame(onPressed: null, onLongPress: null),
);
expect(tester.widget<RaisedButton>(find.byType(RaisedButton)).enabled, false);
await tester.tap(find.byType(RaisedButton));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above


testWidgets('RawMaterialButton responds to tap and onLongPress when enabled', (WidgetTester tester) async {

int pressedCount = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above

await tester.longPress(find.byType(FlatButton));
flatButton = find.byType(FlatButton);
expect(tester.widget<FlatButton>(flatButton).enabled, false);
await tester.tap(flatButton);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we discussed removing the tap and longPress checks here, since the callbacks are null anyway and don't really result in testing anything meaningful

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oooops! Fixing it now, thanks for all the patience over the past weeks! I am learning a lot over how internal Flutter works.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries :) Thanks for addressing all the feedback comments!

await tester.longPress(find.byType(MaterialButton));
materialButton = find.byType(MaterialButton);
expect(tester.widget<MaterialButton>(materialButton).enabled, false);
await tester.tap(materialButton);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above

await tester.longPress(find.byType(OutlineButton));
outlineButton = find.byType(OutlineButton);
expect(tester.widget<OutlineButton>(outlineButton).enabled, false);
await tester.tap(outlineButton);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above

await tester.longPress(find.byType(RaisedButton));
raisedButton = find.byType(RaisedButton);
expect(tester.widget<RaisedButton>(raisedButton).enabled, false);
await tester.tap(raisedButton);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above

await tester.longPress(find.byType(RawMaterialButton));
rawMaterialButton = find.byType(RawMaterialButton);
expect(tester.widget<RawMaterialButton>(rawMaterialButton).enabled, false);
await tester.tap(rawMaterialButton);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above

Copy link
Contributor

@shihaohong shihaohong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Contributor

@HansMuller HansMuller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM too

@shihaohong
Copy link
Contributor

I resolved a merge conflict caused by a separate PR that refactored some MaterialButton tests from buttons_test.dart into the material_button_test.dart. It should be good to go once the tree is green.

@shihaohong shihaohong merged commit 28a214b into flutter:master Oct 30, 2019
Inconnu08 pushed a commit to Inconnu08/flutter that referenced this pull request Nov 26, 2019
* Add onLongPress to Buttons.

* Button enabled status will now respond to onLongPress
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 3, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants