diff --git a/packages/flutter/lib/src/material/tooltip.dart b/packages/flutter/lib/src/material/tooltip.dart index c83266ac60..5b60bd66a0 100644 --- a/packages/flutter/lib/src/material/tooltip.dart +++ b/packages/flutter/lib/src/material/tooltip.dart @@ -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 _openedTooltips = []; /// Dismiss all of the tooltips that are currently shown on the screen, @@ -846,6 +858,7 @@ class TooltipState extends State 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), ), ); } diff --git a/packages/flutter/test/material/tooltip_test.dart b/packages/flutter/test/material/tooltip_test.dart index 889ebfd71c..83a33b1fe5 100644 --- a/packages/flutter/test/material/tooltip_test.dart +++ b/packages/flutter/test/material/tooltip_test.dart @@ -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(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(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: [ + 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(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: [ + 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(ignorePointerFinder.last); + expect(ignorePointer.ignoring, isTrue); + + await gesture.removePointer(); + }); } Future setWidgetForTooltipMode( @@ -3228,6 +3398,7 @@ Future setWidgetForTooltipMode( Duration? showDuration, bool? enableTapToDismiss, TooltipTriggeredCallback? onTriggered, + bool? ignorePointer, }) async { await tester.pumpWidget( MaterialApp( @@ -3237,6 +3408,7 @@ Future setWidgetForTooltipMode( onTriggered: onTriggered, showDuration: showDuration, enableTapToDismiss: enableTapToDismiss ?? true, + ignorePointer: ignorePointer, child: const SizedBox(width: 100.0, height: 100.0), ), ),