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
Add onLongPress to Buttons #40641
Conversation
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. |
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? |
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. |
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. |
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)? |
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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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]. |
There was a problem hiding this comment.
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.
Based on the compiler errors, it might be from recent changes to |
Thanks, @shihaohong! All tests are now passing. :) |
There seems to still be analyzer test failures. You might want to fix those before requesting another review :) |
Analyzer is fine. The issue is with: What is going on? Shouldn't there be a newline at the end of each file? |
That refers to whitespace after a line, not a newline. For example
|
It seems to be these lines:
|
Oh, should work fine now, thanks! All of these are new to me. |
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. |
It seems the failing tests are caused by changes to |
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. |
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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:
- just onPressed to make sure that functionality works
- just onLongPress to make sure that functionality works
- both onPressed and onLongPress activated, then testing between different the two gestures to ensure that both are properly and distinctly recognized
both deactivated to ensure that neither callbacks are being invoked when they shouldn't beEdit: 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 :)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
… replicated into others. Possibly WIP.
@@ -333,6 +333,79 @@ void main() { | |||
paintsExactlyCountTimes(#clipPath, 0), | |||
); | |||
}); | |||
|
|||
testWidgets('FlatButton responds to tap and onLongPress when enabled', (WidgetTester tester) async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit for consistency
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 { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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); |
There was a problem hiding this comment.
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
expect(tester.widget<FlatButton>(find.byType(FlatButton)).enabled, true); | |
expect(tester.widget<FlatButton>( | |
find.byType(FlatButton) | |
).enabled, isTrue); |
), | ||
); | ||
|
||
final Finder flatButton = find.byType(FlatButton); |
There was a problem hiding this comment.
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 👍
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM too
I resolved a merge conflict caused by a separate PR that refactored some MaterialButton tests from |
* Add onLongPress to Buttons. * Button enabled status will now respond to onLongPress
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
andonPressed
from Button is required but needs to be empty. Two possible solutions to overcome the problem:onDoubleTap
oronHover
).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.///
).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.Breaking Change
Does your PR require Flutter developers to manually update their apps to accommodate your change?