Skip to content

Commit d2c8b62

Browse files
authored
make Elevated&Outlined&TextButton support onHover&onFocus callback (flutter#90688)
1 parent a16b826 commit d2c8b62

File tree

8 files changed

+576
-2
lines changed

8 files changed

+576
-2
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,4 @@ Callum Moffat <[email protected]>
8585
Koutaro Mori <[email protected]>
8686
Sergei Smitskoi <[email protected]>
8787
Pradumna Saraf <[email protected]>
88+

packages/flutter/lib/src/material/button_style_button.dart

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ abstract class ButtonStyleButton extends StatefulWidget {
3333
Key? key,
3434
required this.onPressed,
3535
required this.onLongPress,
36+
required this.onHover,
37+
required this.onFocusChange,
3638
required this.style,
3739
required this.focusNode,
3840
required this.autofocus,
@@ -60,6 +62,19 @@ abstract class ButtonStyleButton extends StatefulWidget {
6062
/// * [enabled], which is true if the button is enabled.
6163
final VoidCallback? onLongPress;
6264

65+
/// Called when a pointer enters or exits the button response area.
66+
///
67+
/// The value passed to the callback is true if a pointer has entered this
68+
/// part of the material and false if a pointer has exited this part of the
69+
/// material.
70+
final ValueChanged<bool>? onHover;
71+
72+
/// Handler called when the focus changes.
73+
///
74+
/// Called with true if this widget's node gains focus, and false if it loses
75+
/// focus.
76+
final ValueChanged<bool>? onFocusChange;
77+
6378
/// Customizes this button's appearance.
6479
///
6580
/// Non-null properties of this style override the corresponding
@@ -335,12 +350,18 @@ class _ButtonStyleState extends State<ButtonStyleButton> with MaterialStateMixin
335350
onTap: widget.onPressed,
336351
onLongPress: widget.onLongPress,
337352
onHighlightChanged: updateMaterialState(MaterialState.pressed),
338-
onHover: updateMaterialState(MaterialState.hovered),
353+
onHover: updateMaterialState(
354+
MaterialState.hovered,
355+
onChanged: widget.onHover,
356+
),
339357
mouseCursor: resolvedMouseCursor,
340358
enableFeedback: resolvedEnableFeedback,
341359
focusNode: widget.focusNode,
342360
canRequestFocus: widget.enabled,
343-
onFocusChange: updateMaterialState(MaterialState.focused),
361+
onFocusChange: updateMaterialState(
362+
MaterialState.focused,
363+
onChanged: widget.onFocusChange,
364+
),
344365
autofocus: widget.autofocus,
345366
splashFactory: resolvedSplashFactory,
346367
overlayColor: overlayColor,

packages/flutter/lib/src/material/elevated_button.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ class ElevatedButton extends ButtonStyleButton {
6565
Key? key,
6666
required VoidCallback? onPressed,
6767
VoidCallback? onLongPress,
68+
ValueChanged<bool>? onHover,
69+
ValueChanged<bool>? onFocusChange,
6870
ButtonStyle? style,
6971
FocusNode? focusNode,
7072
bool autofocus = false,
@@ -74,6 +76,8 @@ class ElevatedButton extends ButtonStyleButton {
7476
key: key,
7577
onPressed: onPressed,
7678
onLongPress: onLongPress,
79+
onHover: onHover,
80+
onFocusChange: onFocusChange,
7781
style: style,
7882
focusNode: focusNode,
7983
autofocus: autofocus,
@@ -92,6 +96,8 @@ class ElevatedButton extends ButtonStyleButton {
9296
Key? key,
9397
required VoidCallback? onPressed,
9498
VoidCallback? onLongPress,
99+
ValueChanged<bool>? onHover,
100+
ValueChanged<bool>? onFocusChange,
95101
ButtonStyle? style,
96102
FocusNode? focusNode,
97103
bool? autofocus,
@@ -399,6 +405,8 @@ class _ElevatedButtonWithIcon extends ElevatedButton {
399405
Key? key,
400406
required VoidCallback? onPressed,
401407
VoidCallback? onLongPress,
408+
ValueChanged<bool>? onHover,
409+
ValueChanged<bool>? onFocusChange,
402410
ButtonStyle? style,
403411
FocusNode? focusNode,
404412
bool? autofocus,
@@ -411,6 +419,8 @@ class _ElevatedButtonWithIcon extends ElevatedButton {
411419
key: key,
412420
onPressed: onPressed,
413421
onLongPress: onLongPress,
422+
onHover: onHover,
423+
onFocusChange: onFocusChange,
414424
style: style,
415425
focusNode: focusNode,
416426
autofocus: autofocus ?? false,

packages/flutter/lib/src/material/outlined_button.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ class OutlinedButton extends ButtonStyleButton {
7070
Key? key,
7171
required VoidCallback? onPressed,
7272
VoidCallback? onLongPress,
73+
ValueChanged<bool>? onHover,
74+
ValueChanged<bool>? onFocusChange,
7375
ButtonStyle? style,
7476
FocusNode? focusNode,
7577
bool autofocus = false,
@@ -79,6 +81,8 @@ class OutlinedButton extends ButtonStyleButton {
7981
key: key,
8082
onPressed: onPressed,
8183
onLongPress: onLongPress,
84+
onHover: onHover,
85+
onFocusChange: onFocusChange,
8286
style: style,
8387
focusNode: focusNode,
8488
autofocus: autofocus,

packages/flutter/lib/src/material/text_button.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ class TextButton extends ButtonStyleButton {
7070
Key? key,
7171
required VoidCallback? onPressed,
7272
VoidCallback? onLongPress,
73+
ValueChanged<bool>? onHover,
74+
ValueChanged<bool>? onFocusChange,
7375
ButtonStyle? style,
7476
FocusNode? focusNode,
7577
bool autofocus = false,
@@ -79,6 +81,8 @@ class TextButton extends ButtonStyleButton {
7981
key: key,
8082
onPressed: onPressed,
8183
onLongPress: onLongPress,
84+
onHover: onHover,
85+
onFocusChange: onFocusChange,
8286
style: style,
8387
focusNode: focusNode,
8488
autofocus: autofocus,
@@ -97,6 +101,8 @@ class TextButton extends ButtonStyleButton {
97101
Key? key,
98102
required VoidCallback? onPressed,
99103
VoidCallback? onLongPress,
104+
ValueChanged<bool>? onHover,
105+
ValueChanged<bool>? onFocusChange,
100106
ButtonStyle? style,
101107
FocusNode? focusNode,
102108
bool? autofocus,
@@ -362,6 +368,8 @@ class _TextButtonWithIcon extends TextButton {
362368
Key? key,
363369
required VoidCallback? onPressed,
364370
VoidCallback? onLongPress,
371+
ValueChanged<bool>? onHover,
372+
ValueChanged<bool>? onFocusChange,
365373
ButtonStyle? style,
366374
FocusNode? focusNode,
367375
bool? autofocus,
@@ -374,6 +382,8 @@ class _TextButtonWithIcon extends TextButton {
374382
key: key,
375383
onPressed: onPressed,
376384
onLongPress: onLongPress,
385+
onHover: onHover,
386+
onFocusChange: onFocusChange,
377387
style: style,
378388
focusNode: focusNode,
379389
autofocus: autofocus ?? false,

packages/flutter/test/material/elevated_button_test.dart

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,182 @@ void main() {
414414
expect(didLongPressButton, isTrue);
415415
});
416416

417+
testWidgets("ElevatedButton response doesn't hover when disabled", (WidgetTester tester) async {
418+
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch;
419+
final FocusNode focusNode = FocusNode(debugLabel: 'ElevatedButton Focus');
420+
final GlobalKey childKey = GlobalKey();
421+
bool hovering = false;
422+
await tester.pumpWidget(
423+
Material(
424+
child: Directionality(
425+
textDirection: TextDirection.ltr,
426+
child: SizedBox(
427+
width: 100,
428+
height: 100,
429+
child: ElevatedButton(
430+
autofocus: true,
431+
onPressed: () {},
432+
onLongPress: () {},
433+
onHover: (bool value) { hovering = value; },
434+
focusNode: focusNode,
435+
child: SizedBox(key: childKey),
436+
),
437+
),
438+
),
439+
),
440+
);
441+
await tester.pumpAndSettle();
442+
expect(focusNode.hasPrimaryFocus, isTrue);
443+
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
444+
await gesture.addPointer();
445+
addTearDown(gesture.removePointer);
446+
await gesture.moveTo(tester.getCenter(find.byKey(childKey)));
447+
await tester.pumpAndSettle();
448+
expect(hovering, isTrue);
449+
450+
await tester.pumpWidget(
451+
Material(
452+
child: Directionality(
453+
textDirection: TextDirection.ltr,
454+
child: SizedBox(
455+
width: 100,
456+
height: 100,
457+
child: ElevatedButton(
458+
focusNode: focusNode,
459+
onHover: (bool value) { hovering = value; },
460+
onPressed: null,
461+
child: SizedBox(key: childKey),
462+
),
463+
),
464+
),
465+
),
466+
);
467+
468+
await tester.pumpAndSettle();
469+
expect(focusNode.hasPrimaryFocus, isFalse);
470+
});
471+
472+
testWidgets('disabled and hovered ElevatedButton responds to mouse-exit', (WidgetTester tester) async {
473+
int onHoverCount = 0;
474+
late bool hover;
475+
476+
Widget buildFrame({ required bool enabled }) {
477+
return Material(
478+
child: Directionality(
479+
textDirection: TextDirection.ltr,
480+
child: Center(
481+
child: SizedBox(
482+
width: 100,
483+
height: 100,
484+
child: ElevatedButton(
485+
onPressed: enabled ? () { } : null,
486+
onHover: (bool value) {
487+
onHoverCount += 1;
488+
hover = value;
489+
},
490+
child: const Text('ElevatedButton'),
491+
),
492+
),
493+
),
494+
),
495+
);
496+
}
497+
498+
await tester.pumpWidget(buildFrame(enabled: true));
499+
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
500+
await gesture.addPointer();
501+
addTearDown(gesture.removePointer);
502+
503+
await gesture.moveTo(tester.getCenter(find.byType(ElevatedButton)));
504+
await tester.pumpAndSettle();
505+
expect(onHoverCount, 1);
506+
expect(hover, true);
507+
508+
await tester.pumpWidget(buildFrame(enabled: false));
509+
await tester.pumpAndSettle();
510+
await gesture.moveTo(Offset.zero);
511+
// Even though the ElevatedButton has been disabled, the mouse-exit still
512+
// causes onHover(false) to be called.
513+
expect(onHoverCount, 2);
514+
expect(hover, false);
515+
516+
await gesture.moveTo(tester.getCenter(find.byType(ElevatedButton)));
517+
await tester.pumpAndSettle();
518+
// We no longer see hover events because the ElevatedButton is disabled
519+
// and it's no longer in the "hovering" state.
520+
expect(onHoverCount, 2);
521+
expect(hover, false);
522+
523+
await tester.pumpWidget(buildFrame(enabled: true));
524+
await tester.pumpAndSettle();
525+
// The ElevatedButton was enabled while it contained the mouse, however
526+
// we do not call onHover() because it may call setState().
527+
expect(onHoverCount, 2);
528+
expect(hover, false);
529+
530+
await gesture.moveTo(tester.getCenter(find.byType(ElevatedButton)) - const Offset(1, 1));
531+
await tester.pumpAndSettle();
532+
// Moving the mouse a little within the ElevatedButton doesn't change anything.
533+
expect(onHoverCount, 2);
534+
expect(hover, false);
535+
});
536+
537+
testWidgets('Can set ElevatedButton focus and Can set unFocus.', (WidgetTester tester) async {
538+
final FocusNode node = FocusNode(debugLabel: 'ElevatedButton Focus');
539+
bool gotFocus = false;
540+
await tester.pumpWidget(
541+
Material(
542+
child: Directionality(
543+
textDirection: TextDirection.ltr,
544+
child: ElevatedButton(
545+
focusNode: node,
546+
onFocusChange: (bool focused) => gotFocus = focused,
547+
onPressed: () { },
548+
child: const SizedBox(),
549+
),
550+
),
551+
),
552+
);
553+
554+
node.requestFocus();
555+
556+
await tester.pump();
557+
558+
expect(gotFocus, isTrue);
559+
expect(node.hasFocus, isTrue);
560+
561+
node.unfocus();
562+
await tester.pump();
563+
564+
expect(gotFocus, isFalse);
565+
expect(node.hasFocus, isFalse);
566+
});
567+
568+
testWidgets('When ElevatedButton disable, Can not set ElevatedButton focus.', (WidgetTester tester) async {
569+
final FocusNode node = FocusNode(debugLabel: 'ElevatedButton Focus');
570+
bool gotFocus = false;
571+
await tester.pumpWidget(
572+
Material(
573+
child: Directionality(
574+
textDirection: TextDirection.ltr,
575+
child: ElevatedButton(
576+
focusNode: node,
577+
onFocusChange: (bool focused) => gotFocus = focused,
578+
onPressed: null,
579+
child: const SizedBox(),
580+
),
581+
),
582+
),
583+
);
584+
585+
node.requestFocus();
586+
587+
await tester.pump();
588+
589+
expect(gotFocus, isFalse);
590+
expect(node.hasFocus, isFalse);
591+
});
592+
417593
testWidgets('Does ElevatedButton work with hover', (WidgetTester tester) async {
418594
const Color hoverColor = Color(0xff001122);
419595

0 commit comments

Comments
 (0)