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 {
/// Defaults to 0 milliseconds (tooltips are shown immediately upon hover).
final Duration? waitDuration;
/// The length of time that the tooltip will be shown after a long press
/// is released or mouse pointer exits the widget.
/// The length of time that the tooltip will be shown after a long press is
/// 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
/// pointer exits the widget.
/// Defaults to 1.5 seconds for long press and tap released or 0.1 seconds
/// for mouse pointer exits the widget.
final Duration? showDuration;
/// The [TooltipTriggerMode] that will show the tooltip.
......@@ -495,7 +497,7 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
_dismissTimer = null;
_showTimer?.cancel();
_showTimer = null;
if (_entry!= null) {
if (_entry != null) {
_entry!.remove();
}
_controller.reverse();
......@@ -674,6 +676,18 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
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
Widget build(BuildContext context) {
// 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 {
if (_visible) {
result = GestureDetector(
behavior: HitTestBehavior.opaque,
onLongPress: (_triggerMode == TooltipTriggerMode.longPress) ?
_handlePress : null,
onTap: (_triggerMode == TooltipTriggerMode.tap) ? _handlePress : null,
onLongPress: (_triggerMode == TooltipTriggerMode.longPress) ? _handlePress : null,
onTap: (_triggerMode == TooltipTriggerMode.tap) ? _handleTap : null,
excludeFromSemantics: true,
child: result,
);
......
......@@ -930,6 +930,72 @@ void main() {
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 {
// Regression test for https://github.com/flutter/flutter/issues/96890
const Duration waitDuration = Duration.zero;
......@@ -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(
MaterialApp(
home: Tooltip(
message: tooltipText,
triggerMode: triggerMode,
onTriggered: onTriggered,
showDuration: showDuration,
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