diff --git a/packages/flutter/lib/src/material/icon_button.dart b/packages/flutter/lib/src/material/icon_button.dart index 5c16825665..6454952bc6 100644 --- a/packages/flutter/lib/src/material/icon_button.dart +++ b/packages/flutter/lib/src/material/icon_button.dart @@ -197,6 +197,8 @@ class IconButton extends StatelessWidget { this.splashColor, this.disabledColor, required this.onPressed, + this.onHover, + this.onLongPress, this.mouseCursor, this.focusNode, this.autofocus = false, @@ -228,6 +230,8 @@ class IconButton extends StatelessWidget { this.splashColor, this.disabledColor, required this.onPressed, + this.onHover, + this.onLongPress, this.mouseCursor, this.focusNode, this.autofocus = false, @@ -261,6 +265,8 @@ class IconButton extends StatelessWidget { this.splashColor, this.disabledColor, required this.onPressed, + this.onHover, + this.onLongPress, this.mouseCursor, this.focusNode, this.autofocus = false, @@ -293,6 +299,8 @@ class IconButton extends StatelessWidget { this.splashColor, this.disabledColor, required this.onPressed, + this.onHover, + this.onLongPress, this.mouseCursor, this.focusNode, this.autofocus = false, @@ -478,6 +486,14 @@ class IconButton extends StatelessWidget { /// If this is set to null, the button will be disabled. final VoidCallback? onPressed; + /// The callback that is called when the button is hovered. + final ValueChanged? onHover; + + /// The callback that is called when the button is long-pressed. + /// + /// If onPressed is set to null, the onLongPress callback is not called. + final VoidCallback? onLongPress; + /// {@macro flutter.material.RawMaterialButton.mouseCursor} /// /// If set to null, will default to @@ -721,6 +737,8 @@ class IconButton extends StatelessWidget { return _SelectableIconButton( style: adjustedStyle, onPressed: onPressed, + onHover: onHover, + onLongPress: onPressed != null ? onLongPress : null, autofocus: autofocus, focusNode: focusNode, isSelected: isSelected, @@ -774,6 +792,8 @@ class IconButton extends StatelessWidget { autofocus: autofocus, canRequestFocus: onPressed != null, onTap: onPressed, + onHover: onHover, + onLongPress: onPressed != null ? onLongPress : null, mouseCursor: mouseCursor ?? (onPressed == null ? SystemMouseCursors.basic : SystemMouseCursors.click), enableFeedback: effectiveEnableFeedback, @@ -804,6 +824,10 @@ class IconButton extends StatelessWidget { super.debugFillProperties(properties); properties.add(StringProperty('tooltip', tooltip, defaultValue: null, quoted: false)); properties.add(ObjectFlagProperty('onPressed', onPressed, ifNull: 'disabled')); + properties.add(ObjectFlagProperty>('onHover', onHover, ifNull: 'disabled')); + properties.add( + ObjectFlagProperty('onLongPress', onLongPress, ifNull: 'disabled'), + ); properties.add(ColorProperty('color', color, defaultValue: null)); properties.add(ColorProperty('disabledColor', disabledColor, defaultValue: null)); properties.add(ColorProperty('focusColor', focusColor, defaultValue: null)); @@ -820,6 +844,8 @@ class _SelectableIconButton extends StatefulWidget { this.isSelected, this.style, this.focusNode, + this.onLongPress, + this.onHover, required this.variant, required this.autofocus, required this.onPressed, @@ -835,6 +861,8 @@ class _SelectableIconButton extends StatefulWidget { final VoidCallback? onPressed; final String? tooltip; final Widget child; + final VoidCallback? onLongPress; + final ValueChanged? onHover; @override State<_SelectableIconButton> createState() => _SelectableIconButtonState(); @@ -879,6 +907,8 @@ class _SelectableIconButtonState extends State<_SelectableIconButton> { autofocus: widget.autofocus, focusNode: widget.focusNode, onPressed: widget.onPressed, + onHover: widget.onHover, + onLongPress: widget.onPressed != null ? widget.onLongPress : null, variant: widget.variant, toggleable: toggleable, tooltip: widget.tooltip, @@ -898,13 +928,15 @@ class _IconButtonM3 extends ButtonStyleButton { required super.onPressed, super.style, super.focusNode, + super.onHover, + super.onLongPress, super.autofocus = false, super.statesController, required this.variant, required this.toggleable, super.tooltip, required Widget super.child, - }) : super(onLongPress: null, onHover: null, onFocusChange: null, clipBehavior: Clip.none); + }) : super(onFocusChange: null, clipBehavior: Clip.none); final _IconButtonVariant variant; final bool toggleable; diff --git a/packages/flutter/test/material/icon_button_test.dart b/packages/flutter/test/material/icon_button_test.dart index 904c5ad9f4..92cd2fb5ca 100644 --- a/packages/flutter/test/material/icon_button_test.dart +++ b/packages/flutter/test/material/icon_button_test.dart @@ -3017,6 +3017,350 @@ void main() { ..rect(color: const Color(0xFF00FF00)), // IconButton overlay. ); }); + + testWidgets('Material3 - IconButton variants hovered & onLongPressed', ( + WidgetTester tester, + ) async { + late bool onHovered; + bool onLongPressed = false; + + void onLongPress() { + onLongPressed = true; + } + + void onHover(bool hover) { + onHovered = hover; + } + + // IconButton + await tester.pumpWidget(buildAllVariants(onLongPress: onLongPress, onHover: onHover)); + + final Finder iconButton = find.widgetWithIcon(IconButton, Icons.favorite); + final Offset iconButtonOffset = tester.getCenter(iconButton); + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + addTearDown(gesture.removePointer); + + await gesture.moveTo(iconButtonOffset); + await tester.pump(); + expect(onHovered, true); + + await tester.longPressAt(iconButtonOffset); + await tester.pump(); + expect(onLongPressed, true); + + onHovered = false; + onLongPressed = false; + + await tester.pumpWidget( + buildAllVariants(enabled: false, onLongPress: onLongPress, onHover: onHover), + ); + await gesture.moveTo(iconButtonOffset); + await tester.pump(); + expect(onHovered, false); + + await tester.longPressAt(iconButtonOffset); + await tester.pump(); + expect(onLongPressed, false); + + await gesture.removePointer(); + + // IconButton.filled + await tester.pumpWidget(buildAllVariants(onLongPress: onLongPress, onHover: onHover)); + + final Finder iconButtonFilled = find.widgetWithIcon(IconButton, Icons.add); + final Offset iconButtonFilledOffset = tester.getCenter(iconButtonFilled); + + await gesture.moveTo(iconButtonFilledOffset); + await tester.pump(); + expect(onHovered, true); + + await tester.longPressAt(iconButtonFilledOffset); + await tester.pump(); + expect(onLongPressed, true); + + onHovered = false; + onLongPressed = false; + + await tester.pumpWidget( + buildAllVariants(enabled: false, onLongPress: onLongPress, onHover: onHover), + ); + await gesture.moveTo(iconButtonFilledOffset); + await tester.pump(); + expect(onHovered, false); + + await tester.longPressAt(iconButtonFilledOffset); + await tester.pump(); + expect(onLongPressed, false); + + await gesture.removePointer(); + + // IconButton.filledTonal + await tester.pumpWidget(buildAllVariants(onLongPress: onLongPress, onHover: onHover)); + + final Finder iconButtonFilledTonal = find.widgetWithIcon(IconButton, Icons.add); + final Offset iconButtonFilledTonalOffset = tester.getCenter(iconButtonFilledTonal); + + await gesture.moveTo(iconButtonFilledTonalOffset); + await tester.pump(); + expect(onHovered, true); + + await tester.longPressAt(iconButtonFilledTonalOffset); + await tester.pump(); + expect(onLongPressed, true); + + onHovered = false; + onLongPressed = false; + + await tester.pumpWidget( + buildAllVariants(enabled: false, onLongPress: onLongPress, onHover: onHover), + ); + await gesture.moveTo(iconButtonFilledTonalOffset); + await tester.pump(); + expect(onHovered, false); + + await tester.longPressAt(iconButtonFilledTonalOffset); + await tester.pump(); + expect(onLongPressed, false); + + await gesture.removePointer(); + + // IconButton.outlined + await tester.pumpWidget(buildAllVariants(onLongPress: onLongPress, onHover: onHover)); + + final Finder iconButtonOutlined = find.widgetWithIcon(IconButton, Icons.add); + final Offset iconButtonOutlinedOffset = tester.getCenter(iconButtonOutlined); + + await gesture.moveTo(iconButtonOutlinedOffset); + await tester.pump(); + expect(onHovered, true); + + await tester.longPressAt(iconButtonOutlinedOffset); + await tester.pump(); + expect(onLongPressed, true); + + onHovered = false; + onLongPressed = false; + + await tester.pumpWidget( + buildAllVariants(enabled: false, onLongPress: onLongPress, onHover: onHover), + ); + await gesture.moveTo(iconButtonOutlinedOffset); + await tester.pump(); + expect(onHovered, false); + + await tester.longPressAt(iconButtonOutlinedOffset); + await tester.pump(); + expect(onLongPressed, false); + }); + + testWidgets('Material2 - IconButton variants hovered & onLongPressed', ( + WidgetTester tester, + ) async { + late bool onHovered; + bool onLongPressed = false; + + void onLongPress() { + onLongPressed = true; + } + + void onHover(bool hover) { + onHovered = hover; + } + + // IconButton + await tester.pumpWidget( + buildAllVariants(onLongPress: onLongPress, onHover: onHover, useMaterial3: false), + ); + + final Finder iconButton = find.widgetWithIcon(IconButton, Icons.favorite); + final Offset iconButtonOffset = tester.getCenter(iconButton); + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + addTearDown(gesture.removePointer); + + await gesture.moveTo(iconButtonOffset); + await tester.pump(); + expect(onHovered, true); + + await tester.longPressAt(iconButtonOffset); + await tester.pump(); + expect(onLongPressed, true); + + onHovered = false; + onLongPressed = false; + + await tester.pumpWidget( + buildAllVariants( + enabled: false, + onLongPress: onLongPress, + onHover: onHover, + useMaterial3: false, + ), + ); + await gesture.moveTo(iconButtonOffset); + await tester.pump(); + expect(onHovered, false); + + await tester.longPressAt(iconButtonOffset); + await tester.pump(); + expect(onLongPressed, false); + + await gesture.removePointer(); + + // IconButton.filled + await tester.pumpWidget( + buildAllVariants(onLongPress: onLongPress, onHover: onHover, useMaterial3: false), + ); + + final Finder iconButtonFilled = find.widgetWithIcon(IconButton, Icons.add); + final Offset iconButtonFilledOffset = tester.getCenter(iconButtonFilled); + + await gesture.moveTo(iconButtonFilledOffset); + await tester.pump(); + expect(onHovered, true); + + await tester.longPressAt(iconButtonFilledOffset); + await tester.pump(); + expect(onLongPressed, true); + + onHovered = false; + onLongPressed = false; + + await tester.pumpWidget( + buildAllVariants( + enabled: false, + onLongPress: onLongPress, + onHover: onHover, + useMaterial3: false, + ), + ); + await gesture.moveTo(iconButtonFilledOffset); + await tester.pump(); + expect(onHovered, false); + + await tester.longPressAt(iconButtonFilledOffset); + await tester.pump(); + expect(onLongPressed, false); + + await gesture.removePointer(); + + // IconButton.filledTonal + await tester.pumpWidget( + buildAllVariants(onLongPress: onLongPress, onHover: onHover, useMaterial3: false), + ); + + final Finder iconButtonFilledTonal = find.widgetWithIcon(IconButton, Icons.add); + final Offset iconButtonFilledTonalOffset = tester.getCenter(iconButtonFilledTonal); + + await gesture.moveTo(iconButtonFilledTonalOffset); + await tester.pump(); + expect(onHovered, true); + + await tester.longPressAt(iconButtonFilledTonalOffset); + await tester.pump(); + expect(onLongPressed, true); + + onHovered = false; + onLongPressed = false; + + await tester.pumpWidget( + buildAllVariants( + enabled: false, + onLongPress: onLongPress, + onHover: onHover, + useMaterial3: false, + ), + ); + await gesture.moveTo(iconButtonFilledTonalOffset); + await tester.pump(); + expect(onHovered, false); + + await tester.longPressAt(iconButtonFilledTonalOffset); + await tester.pump(); + expect(onLongPressed, false); + + await gesture.removePointer(); + + // IconButton.outlined + await tester.pumpWidget( + buildAllVariants(onLongPress: onLongPress, onHover: onHover, useMaterial3: false), + ); + + final Finder iconButtonOutlined = find.widgetWithIcon(IconButton, Icons.add); + final Offset iconButtonOutlinedOffset = tester.getCenter(iconButtonOutlined); + + await gesture.moveTo(iconButtonOutlinedOffset); + await tester.pump(); + expect(onHovered, true); + + await tester.longPressAt(iconButtonOutlinedOffset); + await tester.pump(); + expect(onLongPressed, true); + + onHovered = false; + onLongPressed = false; + + await tester.pumpWidget( + buildAllVariants( + enabled: false, + onLongPress: onLongPress, + onHover: onHover, + useMaterial3: false, + ), + ); + await gesture.moveTo(iconButtonOutlinedOffset); + await tester.pump(); + expect(onHovered, false); + + await tester.longPressAt(iconButtonOutlinedOffset); + await tester.pump(); + expect(onLongPressed, false); + }); +} + +Widget buildAllVariants({ + bool enabled = true, + bool useMaterial3 = true, + void Function(bool)? onHover, + VoidCallback? onLongPress, +}) { + return MaterialApp( + theme: ThemeData(useMaterial3: useMaterial3), + home: Material( + child: Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + IconButton( + icon: const Icon(Icons.favorite), + onPressed: enabled ? () {} : null, + onHover: onHover, + onLongPress: onLongPress, + ), + IconButton.filled( + icon: const Icon(Icons.add), + onPressed: enabled ? () {} : null, + onHover: onHover, + onLongPress: onLongPress, + ), + IconButton.filledTonal( + icon: const Icon(Icons.settings), + onPressed: enabled ? () {} : null, + onHover: onHover, + onLongPress: onLongPress, + ), + IconButton.outlined( + icon: const Icon(Icons.home), + onPressed: enabled ? () {} : null, + onHover: onHover, + onLongPress: onLongPress, + ), + ], + ), + ), + ), + ); } Widget wrap({required Widget child, required bool useMaterial3}) {