Unverified Commit afdfc56b authored by Bruno Leroux's avatar Bruno Leroux Committed by GitHub

Fix tooltips don't dismiss when using TooltipTriggerMode.tap (#103960)

parent 0428f421
...@@ -216,11 +216,13 @@ class Tooltip extends StatefulWidget { ...@@ -216,11 +216,13 @@ class Tooltip extends StatefulWidget {
/// Defaults to 0 milliseconds (tooltips are shown immediately upon hover). /// Defaults to 0 milliseconds (tooltips are shown immediately upon hover).
final Duration? waitDuration; final Duration? waitDuration;
/// The length of time that the tooltip will be shown after a long press /// The length of time that the tooltip will be shown after a long press is
/// is released or mouse pointer exits the widget. /// released (if triggerMode is [TooltipTriggerMode.longPress]) or a tap is
/// released (if triggerMode is [TooltipTriggerMode.tap]) or mouse pointer
/// exits the widget.
/// ///
/// Defaults to 1.5 seconds for long press released or 0.1 seconds for mouse /// Defaults to 1.5 seconds for long press and tap released or 0.1 seconds
/// pointer exits the widget. /// for mouse pointer exits the widget.
final Duration? showDuration; final Duration? showDuration;
/// The [TooltipTriggerMode] that will show the tooltip. /// The [TooltipTriggerMode] that will show the tooltip.
...@@ -495,7 +497,7 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -495,7 +497,7 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
_dismissTimer = null; _dismissTimer = null;
_showTimer?.cancel(); _showTimer?.cancel();
_showTimer = null; _showTimer = null;
if (_entry!= null) { if (_entry != null) {
_entry!.remove(); _entry!.remove();
} }
_controller.reverse(); _controller.reverse();
...@@ -674,6 +676,18 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -674,6 +676,18 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
widget.onTriggered?.call(); widget.onTriggered?.call();
} }
void _handleTap() {
_handlePress();
// When triggerMode is not [TooltipTriggerMode.tap] the tooltip is dismissed
// by _handlePointerEvent, which listens to the global pointer events.
// When triggerMode is [TooltipTriggerMode.tap] and the Tooltip GestureDetector
// competes with other GestureDetectors, the disambiguation process will complete
// after the global pointer event is received. As we can't rely on the global
// pointer events to dismiss the Tooltip, we have to call _handleMouseExit
// to dismiss the tooltip after _showDuration expired.
_handleMouseExit();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// If message is empty then no need to create a tooltip overlay to show // 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 { ...@@ -733,9 +747,8 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
if (_visible) { if (_visible) {
result = GestureDetector( result = GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onLongPress: (_triggerMode == TooltipTriggerMode.longPress) ? onLongPress: (_triggerMode == TooltipTriggerMode.longPress) ? _handlePress : null,
_handlePress : null, onTap: (_triggerMode == TooltipTriggerMode.tap) ? _handleTap : null,
onTap: (_triggerMode == TooltipTriggerMode.tap) ? _handlePress : null,
excludeFromSemantics: true, excludeFromSemantics: true,
child: result, child: result,
); );
......
...@@ -930,6 +930,72 @@ void main() { ...@@ -930,6 +930,72 @@ void main() {
await gesture.up(); await gesture.up();
}); });
testWidgets('Tooltip is dismissed after a long press and showDuration expired', (WidgetTester tester) async {
const Duration showDuration = Duration(seconds: 3);
await setWidgetForTooltipMode(tester, TooltipTriggerMode.longPress, showDuration: showDuration);
final Finder tooltip = find.byType(Tooltip);
final TestGesture gesture = await tester.startGesture(tester.getCenter(tooltip));
// Long press reveals tooltip
await tester.pump(kLongPressTimeout);
await tester.pump(const Duration(milliseconds: 10));
expect(find.text(tooltipText), findsOneWidget);
await gesture.up();
// Tooltip is dismissed after showDuration expired
await tester.pump(showDuration);
await tester.pump(const Duration(milliseconds: 10));
expect(find.text(tooltipText), findsNothing);
});
testWidgets('Tooltip is dismissed after a tap and showDuration expired', (WidgetTester tester) async {
const Duration showDuration = Duration(seconds: 3);
await setWidgetForTooltipMode(tester, TooltipTriggerMode.tap, showDuration: showDuration);
final Finder tooltip = find.byType(Tooltip);
expect(find.text(tooltipText), findsNothing);
await testGestureTap(tester, tooltip);
expect(find.text(tooltipText), findsOneWidget);
// Tooltip is dismissed after showDuration expired
await tester.pump(showDuration);
await tester.pump(const Duration(milliseconds: 10));
expect(find.text(tooltipText), findsNothing);
});
testWidgets('Tooltip is dismissed after a tap and showDuration expired when competing with a GestureDetector', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/98854
const Duration showDuration = Duration(seconds: 3);
await tester.pumpWidget(
MaterialApp(
home: GestureDetector(
onVerticalDragStart: (_) { /* Do nothing */ },
child: const Tooltip(
message: tooltipText,
triggerMode: TooltipTriggerMode.tap,
showDuration: showDuration,
child: SizedBox(width: 100.0, height: 100.0),
),
),
),
);
final Finder tooltip = find.byType(Tooltip);
expect(find.text(tooltipText), findsNothing);
await tester.tap(tooltip);
// Wait for GestureArena disambiguation, delay is kPressTimeout to disambiguate
// between onTap and onVerticalDragStart
await tester.pump(kPressTimeout);
expect(find.text(tooltipText), findsOneWidget);
// Tooltip is dismissed after showDuration expired
await tester.pump(showDuration);
await tester.pump(const Duration(milliseconds: 10));
expect(find.text(tooltipText), findsNothing);
});
testWidgets('Dispatch the mouse events before tip overlay detached', (WidgetTester tester) async { testWidgets('Dispatch the mouse events before tip overlay detached', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/96890 // Regression test for https://github.com/flutter/flutter/issues/96890
const Duration waitDuration = Duration.zero; const Duration waitDuration = Duration.zero;
...@@ -1840,13 +1906,19 @@ void main() { ...@@ -1840,13 +1906,19 @@ void main() {
}); });
} }
Future<void> setWidgetForTooltipMode(WidgetTester tester, TooltipTriggerMode triggerMode, {TooltipTriggeredCallback? onTriggered}) async { Future<void> setWidgetForTooltipMode(
WidgetTester tester,
TooltipTriggerMode triggerMode, {
Duration? showDuration,
TooltipTriggeredCallback? onTriggered,
}) async {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: Tooltip( home: Tooltip(
message: tooltipText, message: tooltipText,
triggerMode: triggerMode, triggerMode: triggerMode,
onTriggered: onTriggered, onTriggered: onTriggered,
showDuration: showDuration,
child: const SizedBox(width: 100.0, height: 100.0), child: const SizedBox(width: 100.0, height: 100.0),
), ),
), ),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment