Unverified Commit d2c8b623 authored by yk3372's avatar yk3372 Committed by GitHub

make Elevated&Outlined&TextButton support onHover&onFocus callback (#90688)

parent a16b8263
......@@ -85,3 +85,4 @@ Callum Moffat <callum@moffatman.com>
Koutaro Mori <koutaro.mo@gmail.com>
Sergei Smitskoi <sergflutterdev@gmail.com>
Pradumna Saraf <pradumnasaraf@gmail.com>
Kai Yu <yk3372@gmail.com>
......@@ -33,6 +33,8 @@ abstract class ButtonStyleButton extends StatefulWidget {
Key? key,
required this.onPressed,
required this.onLongPress,
required this.onHover,
required this.onFocusChange,
required this.style,
required this.focusNode,
required this.autofocus,
......@@ -60,6 +62,19 @@ abstract class ButtonStyleButton extends StatefulWidget {
/// * [enabled], which is true if the button is enabled.
final VoidCallback? onLongPress;
/// Called when a pointer enters or exits the button response area.
///
/// The value passed to the callback is true if a pointer has entered this
/// part of the material and false if a pointer has exited this part of the
/// material.
final ValueChanged<bool>? onHover;
/// Handler called when the focus changes.
///
/// Called with true if this widget's node gains focus, and false if it loses
/// focus.
final ValueChanged<bool>? onFocusChange;
/// Customizes this button's appearance.
///
/// Non-null properties of this style override the corresponding
......@@ -335,12 +350,18 @@ class _ButtonStyleState extends State<ButtonStyleButton> with MaterialStateMixin
onTap: widget.onPressed,
onLongPress: widget.onLongPress,
onHighlightChanged: updateMaterialState(MaterialState.pressed),
onHover: updateMaterialState(MaterialState.hovered),
onHover: updateMaterialState(
MaterialState.hovered,
onChanged: widget.onHover,
),
mouseCursor: resolvedMouseCursor,
enableFeedback: resolvedEnableFeedback,
focusNode: widget.focusNode,
canRequestFocus: widget.enabled,
onFocusChange: updateMaterialState(MaterialState.focused),
onFocusChange: updateMaterialState(
MaterialState.focused,
onChanged: widget.onFocusChange,
),
autofocus: widget.autofocus,
splashFactory: resolvedSplashFactory,
overlayColor: overlayColor,
......
......@@ -65,6 +65,8 @@ class ElevatedButton extends ButtonStyleButton {
Key? key,
required VoidCallback? onPressed,
VoidCallback? onLongPress,
ValueChanged<bool>? onHover,
ValueChanged<bool>? onFocusChange,
ButtonStyle? style,
FocusNode? focusNode,
bool autofocus = false,
......@@ -74,6 +76,8 @@ class ElevatedButton extends ButtonStyleButton {
key: key,
onPressed: onPressed,
onLongPress: onLongPress,
onHover: onHover,
onFocusChange: onFocusChange,
style: style,
focusNode: focusNode,
autofocus: autofocus,
......@@ -92,6 +96,8 @@ class ElevatedButton extends ButtonStyleButton {
Key? key,
required VoidCallback? onPressed,
VoidCallback? onLongPress,
ValueChanged<bool>? onHover,
ValueChanged<bool>? onFocusChange,
ButtonStyle? style,
FocusNode? focusNode,
bool? autofocus,
......@@ -399,6 +405,8 @@ class _ElevatedButtonWithIcon extends ElevatedButton {
Key? key,
required VoidCallback? onPressed,
VoidCallback? onLongPress,
ValueChanged<bool>? onHover,
ValueChanged<bool>? onFocusChange,
ButtonStyle? style,
FocusNode? focusNode,
bool? autofocus,
......@@ -411,6 +419,8 @@ class _ElevatedButtonWithIcon extends ElevatedButton {
key: key,
onPressed: onPressed,
onLongPress: onLongPress,
onHover: onHover,
onFocusChange: onFocusChange,
style: style,
focusNode: focusNode,
autofocus: autofocus ?? false,
......
......@@ -70,6 +70,8 @@ class OutlinedButton extends ButtonStyleButton {
Key? key,
required VoidCallback? onPressed,
VoidCallback? onLongPress,
ValueChanged<bool>? onHover,
ValueChanged<bool>? onFocusChange,
ButtonStyle? style,
FocusNode? focusNode,
bool autofocus = false,
......@@ -79,6 +81,8 @@ class OutlinedButton extends ButtonStyleButton {
key: key,
onPressed: onPressed,
onLongPress: onLongPress,
onHover: onHover,
onFocusChange: onFocusChange,
style: style,
focusNode: focusNode,
autofocus: autofocus,
......
......@@ -70,6 +70,8 @@ class TextButton extends ButtonStyleButton {
Key? key,
required VoidCallback? onPressed,
VoidCallback? onLongPress,
ValueChanged<bool>? onHover,
ValueChanged<bool>? onFocusChange,
ButtonStyle? style,
FocusNode? focusNode,
bool autofocus = false,
......@@ -79,6 +81,8 @@ class TextButton extends ButtonStyleButton {
key: key,
onPressed: onPressed,
onLongPress: onLongPress,
onHover: onHover,
onFocusChange: onFocusChange,
style: style,
focusNode: focusNode,
autofocus: autofocus,
......@@ -97,6 +101,8 @@ class TextButton extends ButtonStyleButton {
Key? key,
required VoidCallback? onPressed,
VoidCallback? onLongPress,
ValueChanged<bool>? onHover,
ValueChanged<bool>? onFocusChange,
ButtonStyle? style,
FocusNode? focusNode,
bool? autofocus,
......@@ -362,6 +368,8 @@ class _TextButtonWithIcon extends TextButton {
Key? key,
required VoidCallback? onPressed,
VoidCallback? onLongPress,
ValueChanged<bool>? onHover,
ValueChanged<bool>? onFocusChange,
ButtonStyle? style,
FocusNode? focusNode,
bool? autofocus,
......@@ -374,6 +382,8 @@ class _TextButtonWithIcon extends TextButton {
key: key,
onPressed: onPressed,
onLongPress: onLongPress,
onHover: onHover,
onFocusChange: onFocusChange,
style: style,
focusNode: focusNode,
autofocus: autofocus ?? false,
......
......@@ -414,6 +414,182 @@ void main() {
expect(didLongPressButton, isTrue);
});
testWidgets("ElevatedButton response doesn't hover when disabled", (WidgetTester tester) async {
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch;
final FocusNode focusNode = FocusNode(debugLabel: 'ElevatedButton Focus');
final GlobalKey childKey = GlobalKey();
bool hovering = false;
await tester.pumpWidget(
Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: SizedBox(
width: 100,
height: 100,
child: ElevatedButton(
autofocus: true,
onPressed: () {},
onLongPress: () {},
onHover: (bool value) { hovering = value; },
focusNode: focusNode,
child: SizedBox(key: childKey),
),
),
),
),
);
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(tester.getCenter(find.byKey(childKey)));
await tester.pumpAndSettle();
expect(hovering, isTrue);
await tester.pumpWidget(
Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: SizedBox(
width: 100,
height: 100,
child: ElevatedButton(
focusNode: focusNode,
onHover: (bool value) { hovering = value; },
onPressed: null,
child: SizedBox(key: childKey),
),
),
),
),
);
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isFalse);
});
testWidgets('disabled and hovered ElevatedButton responds to mouse-exit', (WidgetTester tester) async {
int onHoverCount = 0;
late bool hover;
Widget buildFrame({ required bool enabled }) {
return Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
width: 100,
height: 100,
child: ElevatedButton(
onPressed: enabled ? () { } : null,
onHover: (bool value) {
onHoverCount += 1;
hover = value;
},
child: const Text('ElevatedButton'),
),
),
),
),
);
}
await tester.pumpWidget(buildFrame(enabled: true));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(tester.getCenter(find.byType(ElevatedButton)));
await tester.pumpAndSettle();
expect(onHoverCount, 1);
expect(hover, true);
await tester.pumpWidget(buildFrame(enabled: false));
await tester.pumpAndSettle();
await gesture.moveTo(Offset.zero);
// Even though the ElevatedButton has been disabled, the mouse-exit still
// causes onHover(false) to be called.
expect(onHoverCount, 2);
expect(hover, false);
await gesture.moveTo(tester.getCenter(find.byType(ElevatedButton)));
await tester.pumpAndSettle();
// We no longer see hover events because the ElevatedButton is disabled
// and it's no longer in the "hovering" state.
expect(onHoverCount, 2);
expect(hover, false);
await tester.pumpWidget(buildFrame(enabled: true));
await tester.pumpAndSettle();
// The ElevatedButton was enabled while it contained the mouse, however
// we do not call onHover() because it may call setState().
expect(onHoverCount, 2);
expect(hover, false);
await gesture.moveTo(tester.getCenter(find.byType(ElevatedButton)) - const Offset(1, 1));
await tester.pumpAndSettle();
// Moving the mouse a little within the ElevatedButton doesn't change anything.
expect(onHoverCount, 2);
expect(hover, false);
});
testWidgets('Can set ElevatedButton focus and Can set unFocus.', (WidgetTester tester) async {
final FocusNode node = FocusNode(debugLabel: 'ElevatedButton Focus');
bool gotFocus = false;
await tester.pumpWidget(
Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: ElevatedButton(
focusNode: node,
onFocusChange: (bool focused) => gotFocus = focused,
onPressed: () { },
child: const SizedBox(),
),
),
),
);
node.requestFocus();
await tester.pump();
expect(gotFocus, isTrue);
expect(node.hasFocus, isTrue);
node.unfocus();
await tester.pump();
expect(gotFocus, isFalse);
expect(node.hasFocus, isFalse);
});
testWidgets('When ElevatedButton disable, Can not set ElevatedButton focus.', (WidgetTester tester) async {
final FocusNode node = FocusNode(debugLabel: 'ElevatedButton Focus');
bool gotFocus = false;
await tester.pumpWidget(
Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: ElevatedButton(
focusNode: node,
onFocusChange: (bool focused) => gotFocus = focused,
onPressed: null,
child: const SizedBox(),
),
),
),
);
node.requestFocus();
await tester.pump();
expect(gotFocus, isFalse);
expect(node.hasFocus, isFalse);
});
testWidgets('Does ElevatedButton work with hover', (WidgetTester tester) async {
const Color hoverColor = Color(0xff001122);
......
......@@ -613,6 +613,182 @@ void main() {
expect(tester.widget<OutlinedButton>(outlinedButton).enabled, false);
});
testWidgets("OutlinedButton response doesn't hover when disabled", (WidgetTester tester) async {
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch;
final FocusNode focusNode = FocusNode(debugLabel: 'OutlinedButton Focus');
final GlobalKey childKey = GlobalKey();
bool hovering = false;
await tester.pumpWidget(
Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: SizedBox(
width: 100,
height: 100,
child: OutlinedButton(
autofocus: true,
onPressed: () {},
onLongPress: () {},
onHover: (bool value) { hovering = value; },
focusNode: focusNode,
child: SizedBox(key: childKey),
),
),
),
),
);
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(tester.getCenter(find.byKey(childKey)));
await tester.pumpAndSettle();
expect(hovering, isTrue);
await tester.pumpWidget(
Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: SizedBox(
width: 100,
height: 100,
child: OutlinedButton(
focusNode: focusNode,
onHover: (bool value) { hovering = value; },
onPressed: null,
child: SizedBox(key: childKey),
),
),
),
),
);
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isFalse);
});
testWidgets('disabled and hovered OutlinedButton responds to mouse-exit', (WidgetTester tester) async {
int onHoverCount = 0;
late bool hover;
Widget buildFrame({ required bool enabled }) {
return Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
width: 100,
height: 100,
child: OutlinedButton(
onPressed: enabled ? () { } : null,
onHover: (bool value) {
onHoverCount += 1;
hover = value;
},
child: const Text('OutlinedButton'),
),
),
),
),
);
}
await tester.pumpWidget(buildFrame(enabled: true));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(tester.getCenter(find.byType(OutlinedButton)));
await tester.pumpAndSettle();
expect(onHoverCount, 1);
expect(hover, true);
await tester.pumpWidget(buildFrame(enabled: false));
await tester.pumpAndSettle();
await gesture.moveTo(Offset.zero);
// Even though the OutlinedButton has been disabled, the mouse-exit still
// causes onHover(false) to be called.
expect(onHoverCount, 2);
expect(hover, false);
await gesture.moveTo(tester.getCenter(find.byType(OutlinedButton)));
await tester.pumpAndSettle();
// We no longer see hover events because the OutlinedButton is disabled
// and it's no longer in the "hovering" state.
expect(onHoverCount, 2);
expect(hover, false);
await tester.pumpWidget(buildFrame(enabled: true));
await tester.pumpAndSettle();
// The OutlinedButton was enabled while it contained the mouse, however
// we do not call onHover() because it may call setState().
expect(onHoverCount, 2);
expect(hover, false);
await gesture.moveTo(tester.getCenter(find.byType(OutlinedButton)) - const Offset(1, 1));
await tester.pumpAndSettle();
// Moving the mouse a little within the OutlinedButton doesn't change anything.
expect(onHoverCount, 2);
expect(hover, false);
});
testWidgets('Can set OutlinedButton focus and Can set unFocus.', (WidgetTester tester) async {
final FocusNode node = FocusNode(debugLabel: 'OutlinedButton Focus');
bool gotFocus = false;
await tester.pumpWidget(
Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: OutlinedButton(
focusNode: node,
onFocusChange: (bool focused) => gotFocus = focused,
onPressed: () { },
child: const SizedBox(),
),
),
),
);
node.requestFocus();
await tester.pump();
expect(gotFocus, isTrue);
expect(node.hasFocus, isTrue);
node.unfocus();
await tester.pump();
expect(gotFocus, isFalse);
expect(node.hasFocus, isFalse);
});
testWidgets('When OutlinedButton disable, Can not set OutlinedButton focus.', (WidgetTester tester) async {
final FocusNode node = FocusNode(debugLabel: 'OutlinedButton Focus');
bool gotFocus = false;
await tester.pumpWidget(
Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: OutlinedButton(
focusNode: node,
onFocusChange: (bool focused) => gotFocus = focused,
onPressed: null,
child: const SizedBox(),
),
),
),
);
node.requestFocus();
await tester.pump();
expect(gotFocus, isFalse);
expect(node.hasFocus, isFalse);
});
testWidgets("Outline button doesn't crash if disabled during a gesture", (WidgetTester tester) async {
Widget buildFrame(VoidCallback? onPressed) {
return Directionality(
......
......@@ -697,6 +697,182 @@ void main() {
expect(didLongPressButton, isTrue);
});
testWidgets("TextButton response doesn't hover when disabled", (WidgetTester tester) async {
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch;
final FocusNode focusNode = FocusNode(debugLabel: 'TextButton Focus');
final GlobalKey childKey = GlobalKey();
bool hovering = false;
await tester.pumpWidget(
Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: SizedBox(
width: 100,
height: 100,
child: TextButton(
autofocus: true,
onPressed: () {},
onLongPress: () {},
onHover: (bool value) { hovering = value; },
focusNode: focusNode,
child: SizedBox(key: childKey),
),
),
),
),
);
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(tester.getCenter(find.byKey(childKey)));
await tester.pumpAndSettle();
expect(hovering, isTrue);
await tester.pumpWidget(
Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: SizedBox(
width: 100,
height: 100,
child: TextButton(
focusNode: focusNode,
onHover: (bool value) { hovering = value; },
onPressed: null,
child: SizedBox(key: childKey),
),
),
),
),
);
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isFalse);
});
testWidgets('disabled and hovered TextButton responds to mouse-exit', (WidgetTester tester) async {
int onHoverCount = 0;
late bool hover;
Widget buildFrame({ required bool enabled }) {
return Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
width: 100,
height: 100,
child: TextButton(
onPressed: enabled ? () { } : null,
onHover: (bool value) {
onHoverCount += 1;
hover = value;
},
child: const Text('TextButton'),
),
),
),
),
);
}
await tester.pumpWidget(buildFrame(enabled: true));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(tester.getCenter(find.byType(TextButton)));
await tester.pumpAndSettle();
expect(onHoverCount, 1);
expect(hover, true);
await tester.pumpWidget(buildFrame(enabled: false));
await tester.pumpAndSettle();
await gesture.moveTo(Offset.zero);
// Even though the TextButton has been disabled, the mouse-exit still
// causes onHover(false) to be called.
expect(onHoverCount, 2);
expect(hover, false);
await gesture.moveTo(tester.getCenter(find.byType(TextButton)));
await tester.pumpAndSettle();
// We no longer see hover events because the TextButton is disabled
// and it's no longer in the "hovering" state.
expect(onHoverCount, 2);
expect(hover, false);
await tester.pumpWidget(buildFrame(enabled: true));
await tester.pumpAndSettle();
// The TextButton was enabled while it contained the mouse, however
// we do not call onHover() because it may call setState().
expect(onHoverCount, 2);
expect(hover, false);
await gesture.moveTo(tester.getCenter(find.byType(TextButton)) - const Offset(1, 1));
await tester.pumpAndSettle();
// Moving the mouse a little within the TextButton doesn't change anything.
expect(onHoverCount, 2);
expect(hover, false);
});
testWidgets('Can set TextButton focus and Can set unFocus.', (WidgetTester tester) async {
final FocusNode node = FocusNode(debugLabel: 'TextButton Focus');
bool gotFocus = false;
await tester.pumpWidget(
Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: TextButton(
focusNode: node,
onFocusChange: (bool focused) => gotFocus = focused,
onPressed: () { },
child: const SizedBox(),
),
),
),
);
node.requestFocus();
await tester.pump();
expect(gotFocus, isTrue);
expect(node.hasFocus, isTrue);
node.unfocus();
await tester.pump();
expect(gotFocus, isFalse);
expect(node.hasFocus, isFalse);
});
testWidgets('When TextButton disable, Can not set TextButton focus.', (WidgetTester tester) async {
final FocusNode node = FocusNode(debugLabel: 'TextButton Focus');
bool gotFocus = false;
await tester.pumpWidget(
Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: TextButton(
focusNode: node,
onFocusChange: (bool focused) => gotFocus = focused,
onPressed: null,
child: const SizedBox(),
),
),
),
);
node.requestFocus();
await tester.pump();
expect(gotFocus, isFalse);
expect(node.hasFocus, isFalse);
});
testWidgets('TextButton responds to density changes.', (WidgetTester tester) async {
const Key key = Key('test');
const Key childKey = Key('test child');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment