diff --git a/packages/flutter/lib/src/material/tooltip.dart b/packages/flutter/lib/src/material/tooltip.dart index 6fb42527111c..c0ea84ce6663 100644 --- a/packages/flutter/lib/src/material/tooltip.dart +++ b/packages/flutter/lib/src/material/tooltip.dart @@ -665,19 +665,18 @@ class TooltipState extends State with SingleTickerProviderStateMixin { // tooltip is the first to be hit in the widget tree's hit testing order. // See also _ExclusiveMouseRegion for the exact behavior. _activeHoveringPointerDevices.add(event.device); - final List openedTooltips = Tooltip._openedTooltips.toList(); - bool otherTooltipsDismissed = false; - for (final TooltipState tooltip in openedTooltips) { + // Dismiss other open tooltips unless they're kept visible by other mice. + // The mouse tracker implementation always dispatches all `onExit` events + // before dispatching any `onEnter` events, so `event.device` must have + // already been removed from _activeHoveringPointerDevices of the tooltips + // that are no longer being hovered over. + final List tooltipsToDismiss = Tooltip._openedTooltips + .where((TooltipState tooltip) => tooltip._activeHoveringPointerDevices.isEmpty).toList(); + for (final TooltipState tooltip in tooltipsToDismiss) { assert(tooltip.mounted); - final Set hoveringDevices = tooltip._activeHoveringPointerDevices; - final bool shouldDismiss = tooltip != this - && (hoveringDevices.length == 1 && hoveringDevices.single == event.device); - if (shouldDismiss) { - otherTooltipsDismissed = true; - tooltip._scheduleDismissTooltip(withDelay: Duration.zero); - } + tooltip._scheduleDismissTooltip(withDelay: Duration.zero); } - _scheduleShowTooltip(withDelay: otherTooltipsDismissed ? Duration.zero : _waitDuration); + _scheduleShowTooltip(withDelay: tooltipsToDismiss.isNotEmpty ? Duration.zero : _waitDuration); } void _handleMouseExit(PointerExitEvent event) { diff --git a/packages/flutter/test/material/tooltip_test.dart b/packages/flutter/test/material/tooltip_test.dart index c9849c070ab6..76e5fc7524a3 100644 --- a/packages/flutter/test/material/tooltip_test.dart +++ b/packages/flutter/test/material/tooltip_test.dart @@ -1508,6 +1508,60 @@ void main() { expect(find.text(tooltipText), findsNothing); }); + // Regression test for https://github.com/flutter/flutter/issues/141644. + // This allows the user to quickly explore the UI via tooltips. + testWidgets('Tooltip shows without delay when the mouse moves from another tooltip', (WidgetTester tester) async { + const Duration waitDuration = Durations.extralong1; + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + addTearDown(() async { + return gesture.removePointer(); + }); + await gesture.addPointer(); + await gesture.moveTo(const Offset(1.0, 1.0)); + await tester.pump(); + await gesture.moveTo(Offset.zero); + + await tester.pumpWidget( + const MaterialApp( + home: Column( + children: [ + Tooltip( + message: 'first tooltip', + waitDuration: waitDuration, + child: SizedBox( + width: 100.0, + height: 100.0, + ), + ), + Tooltip( + message: 'last tooltip', + waitDuration: waitDuration, + child: SizedBox( + width: 100.0, + height: 100.0, + ), + ), + ], + ), + ), + ); + + await gesture.moveTo(Offset.zero); + await tester.pump(); + await gesture.moveTo(tester.getCenter(find.byType(Tooltip).first)); + await tester.pump(); + // Wait for the first tooltip to appear. + await tester.pump(waitDuration); + expect(find.text('first tooltip'), findsOneWidget); + expect(find.text('last tooltip'), findsNothing); + + // Move to the second tooltip and expect it to show up immediately. + await gesture.moveTo(tester.getCenter(find.byType(Tooltip).last)); + await tester.pump(); + expect(find.text('first tooltip'), findsNothing); + expect(find.text('last tooltip'), findsOneWidget); + }); + testWidgets('Tooltip text is also hoverable', (WidgetTester tester) async { const Duration waitDuration = Duration.zero; final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);