Adds onHover and onLongPress to IconButton widget (#160032)

Adds `onHover` and `onLongPress` to `IconButton` widget

fix #159972 

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.
This commit is contained in:
Mohammed CHAHBOUN 2025-01-08 20:57:44 +01:00 committed by GitHub
parent 603484f1bf
commit 8cc303ef60
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 377 additions and 1 deletions

View File

@ -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<bool>? 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<VoidCallback>('onPressed', onPressed, ifNull: 'disabled'));
properties.add(ObjectFlagProperty<ValueChanged<bool>>('onHover', onHover, ifNull: 'disabled'));
properties.add(
ObjectFlagProperty<VoidCallback>('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<bool>? 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;

View File

@ -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: <Widget>[
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}) {