Skip to content

Commit afdfc56

Browse files
authored
Fix tooltips don't dismiss when using TooltipTriggerMode.tap (flutter#103960)
1 parent 0428f42 commit afdfc56

File tree

2 files changed

+94
-9
lines changed

2 files changed

+94
-9
lines changed

packages/flutter/lib/src/material/tooltip.dart

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -216,11 +216,13 @@ class Tooltip extends StatefulWidget {
216216
/// Defaults to 0 milliseconds (tooltips are shown immediately upon hover).
217217
final Duration? waitDuration;
218218

219-
/// The length of time that the tooltip will be shown after a long press
220-
/// is released or mouse pointer exits the widget.
219+
/// The length of time that the tooltip will be shown after a long press is
220+
/// released (if triggerMode is [TooltipTriggerMode.longPress]) or a tap is
221+
/// released (if triggerMode is [TooltipTriggerMode.tap]) or mouse pointer
222+
/// exits the widget.
221223
///
222-
/// Defaults to 1.5 seconds for long press released or 0.1 seconds for mouse
223-
/// pointer exits the widget.
224+
/// Defaults to 1.5 seconds for long press and tap released or 0.1 seconds
225+
/// for mouse pointer exits the widget.
224226
final Duration? showDuration;
225227

226228
/// The [TooltipTriggerMode] that will show the tooltip.
@@ -495,7 +497,7 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
495497
_dismissTimer = null;
496498
_showTimer?.cancel();
497499
_showTimer = null;
498-
if (_entry!= null) {
500+
if (_entry != null) {
499501
_entry!.remove();
500502
}
501503
_controller.reverse();
@@ -674,6 +676,18 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
674676
widget.onTriggered?.call();
675677
}
676678

679+
void _handleTap() {
680+
_handlePress();
681+
// When triggerMode is not [TooltipTriggerMode.tap] the tooltip is dismissed
682+
// by _handlePointerEvent, which listens to the global pointer events.
683+
// When triggerMode is [TooltipTriggerMode.tap] and the Tooltip GestureDetector
684+
// competes with other GestureDetectors, the disambiguation process will complete
685+
// after the global pointer event is received. As we can't rely on the global
686+
// pointer events to dismiss the Tooltip, we have to call _handleMouseExit
687+
// to dismiss the tooltip after _showDuration expired.
688+
_handleMouseExit();
689+
}
690+
677691
@override
678692
Widget build(BuildContext context) {
679693
// If message is empty then no need to create a tooltip overlay to show
@@ -733,9 +747,8 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
733747
if (_visible) {
734748
result = GestureDetector(
735749
behavior: HitTestBehavior.opaque,
736-
onLongPress: (_triggerMode == TooltipTriggerMode.longPress) ?
737-
_handlePress : null,
738-
onTap: (_triggerMode == TooltipTriggerMode.tap) ? _handlePress : null,
750+
onLongPress: (_triggerMode == TooltipTriggerMode.longPress) ? _handlePress : null,
751+
onTap: (_triggerMode == TooltipTriggerMode.tap) ? _handleTap : null,
739752
excludeFromSemantics: true,
740753
child: result,
741754
);

packages/flutter/test/material/tooltip_test.dart

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -930,6 +930,72 @@ void main() {
930930
await gesture.up();
931931
});
932932

933+
testWidgets('Tooltip is dismissed after a long press and showDuration expired', (WidgetTester tester) async {
934+
const Duration showDuration = Duration(seconds: 3);
935+
await setWidgetForTooltipMode(tester, TooltipTriggerMode.longPress, showDuration: showDuration);
936+
937+
final Finder tooltip = find.byType(Tooltip);
938+
final TestGesture gesture = await tester.startGesture(tester.getCenter(tooltip));
939+
940+
// Long press reveals tooltip
941+
await tester.pump(kLongPressTimeout);
942+
await tester.pump(const Duration(milliseconds: 10));
943+
expect(find.text(tooltipText), findsOneWidget);
944+
await gesture.up();
945+
946+
// Tooltip is dismissed after showDuration expired
947+
await tester.pump(showDuration);
948+
await tester.pump(const Duration(milliseconds: 10));
949+
expect(find.text(tooltipText), findsNothing);
950+
});
951+
952+
testWidgets('Tooltip is dismissed after a tap and showDuration expired', (WidgetTester tester) async {
953+
const Duration showDuration = Duration(seconds: 3);
954+
await setWidgetForTooltipMode(tester, TooltipTriggerMode.tap, showDuration: showDuration);
955+
956+
final Finder tooltip = find.byType(Tooltip);
957+
expect(find.text(tooltipText), findsNothing);
958+
959+
await testGestureTap(tester, tooltip);
960+
expect(find.text(tooltipText), findsOneWidget);
961+
962+
// Tooltip is dismissed after showDuration expired
963+
await tester.pump(showDuration);
964+
await tester.pump(const Duration(milliseconds: 10));
965+
expect(find.text(tooltipText), findsNothing);
966+
});
967+
968+
testWidgets('Tooltip is dismissed after a tap and showDuration expired when competing with a GestureDetector', (WidgetTester tester) async {
969+
// Regression test for https://github.com/flutter/flutter/issues/98854
970+
const Duration showDuration = Duration(seconds: 3);
971+
await tester.pumpWidget(
972+
MaterialApp(
973+
home: GestureDetector(
974+
onVerticalDragStart: (_) { /* Do nothing */ },
975+
child: const Tooltip(
976+
message: tooltipText,
977+
triggerMode: TooltipTriggerMode.tap,
978+
showDuration: showDuration,
979+
child: SizedBox(width: 100.0, height: 100.0),
980+
),
981+
),
982+
),
983+
);
984+
final Finder tooltip = find.byType(Tooltip);
985+
expect(find.text(tooltipText), findsNothing);
986+
987+
await tester.tap(tooltip);
988+
// Wait for GestureArena disambiguation, delay is kPressTimeout to disambiguate
989+
// between onTap and onVerticalDragStart
990+
await tester.pump(kPressTimeout);
991+
expect(find.text(tooltipText), findsOneWidget);
992+
993+
// Tooltip is dismissed after showDuration expired
994+
await tester.pump(showDuration);
995+
await tester.pump(const Duration(milliseconds: 10));
996+
expect(find.text(tooltipText), findsNothing);
997+
});
998+
933999
testWidgets('Dispatch the mouse events before tip overlay detached', (WidgetTester tester) async {
9341000
// Regression test for https://github.com/flutter/flutter/issues/96890
9351001
const Duration waitDuration = Duration.zero;
@@ -1840,13 +1906,19 @@ void main() {
18401906
});
18411907
}
18421908

1843-
Future<void> setWidgetForTooltipMode(WidgetTester tester, TooltipTriggerMode triggerMode, {TooltipTriggeredCallback? onTriggered}) async {
1909+
Future<void> setWidgetForTooltipMode(
1910+
WidgetTester tester,
1911+
TooltipTriggerMode triggerMode, {
1912+
Duration? showDuration,
1913+
TooltipTriggeredCallback? onTriggered,
1914+
}) async {
18441915
await tester.pumpWidget(
18451916
MaterialApp(
18461917
home: Tooltip(
18471918
message: tooltipText,
18481919
triggerMode: triggerMode,
18491920
onTriggered: onTriggered,
1921+
showDuration: showDuration,
18501922
child: const SizedBox(width: 100.0, height: 100.0),
18511923
),
18521924
),

0 commit comments

Comments
 (0)