As #142465 states, tooltips often interrupt widget interactivity by not allowing events to pass through to the Tooltip child, which is especially poor UX when hovering interact-able widgets on web when the mouse happens to land on the tooltip. I've gone with defaulting ignorePointer to true when a simple message is supplied, since there won't ever be anything interact-able on the Tooltip, and defaulting to false when richMessage is supplied, so it doesn't break anyone's code that has interact-able widgets in the Tooltip. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [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]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
parent
ccae8cc794
commit
aa27e785e0
@ -186,6 +186,7 @@ class Tooltip extends StatefulWidget {
|
||||
this.enableFeedback,
|
||||
this.onTriggered,
|
||||
this.mouseCursor,
|
||||
this.ignorePointer,
|
||||
this.child,
|
||||
}) : assert(
|
||||
(message == null) != (richMessage == null),
|
||||
@ -363,6 +364,17 @@ class Tooltip extends StatefulWidget {
|
||||
/// If this property is null, [MouseCursor.defer] will be used.
|
||||
final MouseCursor? mouseCursor;
|
||||
|
||||
/// Whether this tooltip should be invisible to hit testing.
|
||||
///
|
||||
/// If no value is passed, pointer events are ignored unless the tooltip has a
|
||||
/// [richMessage] instead of a [message].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [IgnorePointer], for more information about how pointer events are
|
||||
/// handled or ignored.
|
||||
final bool? ignorePointer;
|
||||
|
||||
static final List<TooltipState> _openedTooltips = <TooltipState>[];
|
||||
|
||||
/// Dismiss all of the tooltips that are currently shown on the screen,
|
||||
@ -846,6 +858,7 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
||||
verticalOffset:
|
||||
widget.verticalOffset ?? tooltipTheme.verticalOffset ?? _defaultVerticalOffset,
|
||||
preferBelow: widget.preferBelow ?? tooltipTheme.preferBelow ?? _defaultPreferBelow,
|
||||
ignorePointer: widget.ignorePointer ?? widget.message != null,
|
||||
);
|
||||
|
||||
return SelectionContainer.maybeOf(context) == null
|
||||
@ -971,6 +984,7 @@ class _TooltipOverlay extends StatelessWidget {
|
||||
required this.target,
|
||||
required this.verticalOffset,
|
||||
required this.preferBelow,
|
||||
required this.ignorePointer,
|
||||
this.onEnter,
|
||||
this.onExit,
|
||||
});
|
||||
@ -988,6 +1002,7 @@ class _TooltipOverlay extends StatelessWidget {
|
||||
final bool preferBelow;
|
||||
final PointerEnterEventListener? onEnter;
|
||||
final PointerExitEventListener? onExit;
|
||||
final bool ignorePointer;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -1024,7 +1039,7 @@ class _TooltipOverlay extends StatelessWidget {
|
||||
verticalOffset: verticalOffset,
|
||||
preferBelow: preferBelow,
|
||||
),
|
||||
child: result,
|
||||
child: IgnorePointer(ignoring: ignorePointer, child: result),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1403,7 +1403,8 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('Tooltip is dismissed after tap to dismiss immediately', (WidgetTester tester) async {
|
||||
await setWidgetForTooltipMode(tester, TooltipTriggerMode.tap);
|
||||
// This test relies on not ignoring pointer events.
|
||||
await setWidgetForTooltipMode(tester, TooltipTriggerMode.tap, ignorePointer: false);
|
||||
|
||||
final Finder tooltip = find.byType(Tooltip);
|
||||
expect(find.text(tooltipText), findsNothing);
|
||||
@ -1421,7 +1422,13 @@ void main() {
|
||||
testWidgets('Tooltip is not dismissed after tap if enableTapToDismiss is false', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
await setWidgetForTooltipMode(tester, TooltipTriggerMode.tap, enableTapToDismiss: false);
|
||||
// This test relies on not ignoring pointer events.
|
||||
await setWidgetForTooltipMode(
|
||||
tester,
|
||||
TooltipTriggerMode.tap,
|
||||
enableTapToDismiss: false,
|
||||
ignorePointer: false,
|
||||
);
|
||||
|
||||
final Finder tooltip = find.byType(Tooltip);
|
||||
expect(find.text(tooltipText), findsNothing);
|
||||
@ -1727,6 +1734,8 @@ void main() {
|
||||
const MaterialApp(
|
||||
home: Center(
|
||||
child: Tooltip(
|
||||
// This test relies on not ignoring pointer events.
|
||||
ignorePointer: false,
|
||||
message: tooltipText,
|
||||
waitDuration: waitDuration,
|
||||
child: Text('I am tool tip'),
|
||||
@ -3220,6 +3229,167 @@ void main() {
|
||||
await tester.pump();
|
||||
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), customCursor);
|
||||
});
|
||||
|
||||
testWidgets('Tooltip overlay ignores pointer by default when passing simple message', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
const String tooltipMessage = 'Tooltip message';
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: Tooltip(
|
||||
message: tooltipMessage,
|
||||
child: ElevatedButton(onPressed: () {}, child: const Text('Hover me')),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Finder buttonFinder = find.text('Hover me');
|
||||
expect(buttonFinder, findsOneWidget);
|
||||
|
||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
await gesture.addPointer();
|
||||
await gesture.moveTo(tester.getCenter(buttonFinder));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final Finder tooltipFinder = find.text(tooltipMessage);
|
||||
expect(tooltipFinder, findsOneWidget);
|
||||
|
||||
final Finder ignorePointerFinder = find.byType(IgnorePointer);
|
||||
|
||||
final IgnorePointer ignorePointer = tester.widget<IgnorePointer>(ignorePointerFinder.last);
|
||||
expect(ignorePointer.ignoring, isTrue);
|
||||
|
||||
await gesture.removePointer();
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
"Tooltip overlay with simple message doesn't ignore pointer when passing ignorePointer: false",
|
||||
(WidgetTester tester) async {
|
||||
const String tooltipMessage = 'Tooltip message';
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: Tooltip(
|
||||
ignorePointer: false,
|
||||
message: tooltipMessage,
|
||||
child: ElevatedButton(onPressed: () {}, child: const Text('Hover me')),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Finder buttonFinder = find.text('Hover me');
|
||||
expect(buttonFinder, findsOneWidget);
|
||||
|
||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
await gesture.addPointer();
|
||||
await gesture.moveTo(tester.getCenter(buttonFinder));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final Finder tooltipFinder = find.text(tooltipMessage);
|
||||
expect(tooltipFinder, findsOneWidget);
|
||||
|
||||
final Finder ignorePointerFinder = find.byType(IgnorePointer);
|
||||
|
||||
final IgnorePointer ignorePointer = tester.widget<IgnorePointer>(ignorePointerFinder.last);
|
||||
expect(ignorePointer.ignoring, isFalse);
|
||||
|
||||
await gesture.removePointer();
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets("Tooltip overlay doesn't ignore pointer by default when passing rich message", (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
const InlineSpan richMessage = TextSpan(
|
||||
children: <InlineSpan>[
|
||||
TextSpan(text: 'Rich ', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
TextSpan(text: 'Tooltip'),
|
||||
],
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: Tooltip(
|
||||
richMessage: richMessage,
|
||||
child: ElevatedButton(onPressed: () {}, child: const Text('Hover me')),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Finder buttonFinder = find.text('Hover me');
|
||||
expect(buttonFinder, findsOneWidget);
|
||||
|
||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
await gesture.addPointer();
|
||||
await gesture.moveTo(tester.getCenter(buttonFinder));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final Finder tooltipFinder = find.textContaining('Rich Tooltip');
|
||||
expect(tooltipFinder, findsOneWidget);
|
||||
|
||||
final Finder ignorePointerFinder = find.byType(IgnorePointer);
|
||||
|
||||
final IgnorePointer ignorePointer = tester.widget<IgnorePointer>(ignorePointerFinder.last);
|
||||
expect(ignorePointer.ignoring, isFalse);
|
||||
|
||||
await gesture.removePointer();
|
||||
});
|
||||
|
||||
testWidgets('Tooltip overlay with richMessage ignores pointer when passing ignorePointer: true', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
const InlineSpan richMessage = TextSpan(
|
||||
children: <InlineSpan>[
|
||||
TextSpan(text: 'Rich ', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
TextSpan(text: 'Tooltip'),
|
||||
],
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: Tooltip(
|
||||
ignorePointer: true,
|
||||
richMessage: richMessage,
|
||||
child: ElevatedButton(onPressed: () {}, child: const Text('Hover me')),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Finder buttonFinder = find.text('Hover me');
|
||||
expect(buttonFinder, findsOneWidget);
|
||||
|
||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
await gesture.addPointer();
|
||||
await gesture.moveTo(tester.getCenter(buttonFinder));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final Finder tooltipFinder = find.textContaining('Rich Tooltip');
|
||||
expect(tooltipFinder, findsOneWidget);
|
||||
|
||||
final Finder ignorePointerFinder = find.byType(IgnorePointer);
|
||||
|
||||
final IgnorePointer ignorePointer = tester.widget<IgnorePointer>(ignorePointerFinder.last);
|
||||
expect(ignorePointer.ignoring, isTrue);
|
||||
|
||||
await gesture.removePointer();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> setWidgetForTooltipMode(
|
||||
@ -3228,6 +3398,7 @@ Future<void> setWidgetForTooltipMode(
|
||||
Duration? showDuration,
|
||||
bool? enableTapToDismiss,
|
||||
TooltipTriggeredCallback? onTriggered,
|
||||
bool? ignorePointer,
|
||||
}) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
@ -3237,6 +3408,7 @@ Future<void> setWidgetForTooltipMode(
|
||||
onTriggered: onTriggered,
|
||||
showDuration: showDuration,
|
||||
enableTapToDismiss: enableTapToDismiss ?? true,
|
||||
ignorePointer: ignorePointer,
|
||||
child: const SizedBox(width: 100.0, height: 100.0),
|
||||
),
|
||||
),
|
||||
|
Loading…
x
Reference in New Issue
Block a user