diff --git a/packages/flutter/lib/src/material/outlined_button.dart b/packages/flutter/lib/src/material/outlined_button.dart index 35be334606..aa12d57ff0 100644 --- a/packages/flutter/lib/src/material/outlined_button.dart +++ b/packages/flutter/lib/src/material/outlined_button.dart @@ -95,6 +95,8 @@ class OutlinedButton extends ButtonStyleButton { Key? key, required VoidCallback? onPressed, VoidCallback? onLongPress, + ValueChanged? onHover, + ValueChanged? onFocusChange, ButtonStyle? style, FocusNode? focusNode, bool? autofocus, @@ -109,6 +111,8 @@ class OutlinedButton extends ButtonStyleButton { key: key, onPressed: onPressed, onLongPress: onLongPress, + onHover: onHover, + onFocusChange: onFocusChange, style: style, focusNode: focusNode, autofocus: autofocus ?? false, @@ -121,6 +125,8 @@ class OutlinedButton extends ButtonStyleButton { key: key, onPressed: onPressed, onLongPress: onLongPress, + onHover: onHover, + onFocusChange: onFocusChange, style: style, focusNode: focusNode, autofocus: autofocus ?? false, @@ -454,6 +460,8 @@ class _OutlinedButtonWithIcon extends OutlinedButton { super.key, required super.onPressed, super.onLongPress, + super.onHover, + super.onFocusChange, super.style, super.focusNode, bool? autofocus, diff --git a/packages/flutter/test/material/outlined_button_test.dart b/packages/flutter/test/material/outlined_button_test.dart index b4b0ea3788..6b9241b7bc 100644 --- a/packages/flutter/test/material/outlined_button_test.dart +++ b/packages/flutter/test/material/outlined_button_test.dart @@ -2467,6 +2467,173 @@ void main() { // The icon is aligned to the left of the button. expect(buttonTopLeft.dx, iconTopLeft.dx - 24.0); // 24.0 - padding between icon and button edge. }); + + testWidgets("OutlinedButton.icon response doesn't hover when disabled", (WidgetTester tester) async { + FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch; + final FocusNode focusNode = FocusNode(debugLabel: 'OutlinedButton.icon Focus'); + final GlobalKey childKey = GlobalKey(); + bool hovering = false; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: SizedBox( + width: 100, + height: 100, + child: OutlinedButton.icon( + autofocus: true, + onPressed: () {}, + onLongPress: () {}, + onHover: (bool value) { hovering = value; }, + focusNode: focusNode, + label: SizedBox(key: childKey), + icon: const Icon(Icons.add), + ), + ), + ), + ); + await tester.pumpAndSettle(); + expect(focusNode.hasPrimaryFocus, isTrue); + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + await gesture.moveTo(tester.getCenter(find.byKey(childKey))); + await tester.pumpAndSettle(); + expect(hovering, isTrue); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: SizedBox( + width: 100, + height: 100, + child: OutlinedButton.icon( + focusNode: focusNode, + onHover: (bool value) { hovering = value; }, + onPressed: null, + label: SizedBox(key: childKey), + icon: const Icon(Icons.add), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + expect(focusNode.hasPrimaryFocus, isFalse); + focusNode.dispose(); + }); + + testWidgets('disabled and hovered OutlinedButton.icon responds to mouse-exit', (WidgetTester tester) async { + int onHoverCount = 0; + late bool hover; + const Key key = Key('OutlinedButton.icon'); + Widget buildFrame({ required bool enabled }) { + return Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: SizedBox( + width: 100, + height: 100, + child: OutlinedButton.icon( + key: key, + onPressed: enabled ? () { } : null, + onHover: (bool value) { + onHoverCount += 1; + hover = value; + }, + label: const Text('OutlinedButton'), + icon: const Icon(Icons.add), + ), + ), + ), + ); + } + + await tester.pumpWidget(buildFrame(enabled: true)); + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + + await gesture.moveTo(tester.getCenter(find.byKey(key))); + 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.byKey(key))); + 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.byKey(key)) - 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.icon focus and Can set unFocus.', (WidgetTester tester) async { + final FocusNode node = FocusNode(debugLabel: 'OutlinedButton.icon Focus'); + bool gotFocus = false; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: OutlinedButton.icon( + focusNode: node, + onFocusChange: (bool focused) => gotFocus = focused, + onPressed: () { }, + label: const SizedBox(), + icon: const Icon(Icons.add), + ), + ), + ); + + node.requestFocus(); + await tester.pump(); + expect(gotFocus, isTrue); + expect(node.hasFocus, isTrue); + node.unfocus(); + await tester.pump(); + expect(gotFocus, isFalse); + expect(node.hasFocus, isFalse); + node.dispose(); + }); + + testWidgets('When OutlinedButton.icon disable, Can not set OutlinedButton.icon focus.', (WidgetTester tester) async { + final FocusNode node = FocusNode(debugLabel: 'OutlinedButton.icon Focus'); + bool gotFocus = false; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: OutlinedButton.icon( + focusNode: node, + onFocusChange: (bool focused) => gotFocus = focused, + onPressed: null, + label: const SizedBox(), + icon: const Icon(Icons.add), + ), + ), + ); + + node.requestFocus(); + await tester.pump(); + expect(gotFocus, isFalse); + expect(node.hasFocus, isFalse); + node.dispose(); + }); } TextStyle _iconStyle(WidgetTester tester, IconData icon) {