Remove a bad assert from tooltip implementation (#127629)
Fixes https://github.com/flutter/flutter/issues/127575 The `_handleMouseEnter` and `_handleMouseExit` calls are balanced, but the tooltip could be dismissed by an external `PointerDown` event interacting with a different UI component so the ` assert(_activeHoveringPointerDevices.isNotEmpty);` does not really make sense.
This commit is contained in:
parent
43de3365f5
commit
56808b486d
@ -425,6 +425,10 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
|||||||
TapGestureRecognizer? _tapRecognizer;
|
TapGestureRecognizer? _tapRecognizer;
|
||||||
|
|
||||||
// The ids of mouse devices that are keeping the tooltip from being dismissed.
|
// The ids of mouse devices that are keeping the tooltip from being dismissed.
|
||||||
|
//
|
||||||
|
// Device ids are added to this set in _handleMouseEnter, and removed in
|
||||||
|
// _handleMouseExit. The set is cleared in _handleTapToDismiss, typically when
|
||||||
|
// a PointerDown event interacts with some other UI component.
|
||||||
final Set<int> _activeHoveringPointerDevices = <int>{};
|
final Set<int> _activeHoveringPointerDevices = <int>{};
|
||||||
AnimationStatus _animationStatus = AnimationStatus.dismissed;
|
AnimationStatus _animationStatus = AnimationStatus.dismissed;
|
||||||
void _handleStatusChanged(AnimationStatus status) {
|
void _handleStatusChanged(AnimationStatus status) {
|
||||||
@ -469,18 +473,14 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
|||||||
'timer must not be active when the tooltip is fading out',
|
'timer must not be active when the tooltip is fading out',
|
||||||
);
|
);
|
||||||
switch (_controller.status) {
|
switch (_controller.status) {
|
||||||
case AnimationStatus.dismissed:
|
case AnimationStatus.dismissed when withDelay.inMicroseconds > 0:
|
||||||
if (withDelay.inMicroseconds > 0) {
|
_timer ??= Timer(withDelay, show);
|
||||||
_timer ??= Timer(withDelay, show);
|
|
||||||
} else {
|
|
||||||
show();
|
|
||||||
}
|
|
||||||
// If the tooltip is already fading in or fully visible, skip the
|
// If the tooltip is already fading in or fully visible, skip the
|
||||||
// animation and show the tooltip immediately.
|
// animation and show the tooltip immediately.
|
||||||
|
case AnimationStatus.dismissed:
|
||||||
case AnimationStatus.forward:
|
case AnimationStatus.forward:
|
||||||
case AnimationStatus.reverse:
|
case AnimationStatus.reverse:
|
||||||
case AnimationStatus.completed:
|
case AnimationStatus.completed:
|
||||||
// Fade in if needed and schedule to hide.
|
|
||||||
show();
|
show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -646,10 +646,9 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleMouseExit(PointerExitEvent event) {
|
void _handleMouseExit(PointerExitEvent event) {
|
||||||
if (!mounted) {
|
if (!mounted || _activeHoveringPointerDevices.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
assert(_activeHoveringPointerDevices.isNotEmpty);
|
|
||||||
_activeHoveringPointerDevices.remove(event.device);
|
_activeHoveringPointerDevices.remove(event.device);
|
||||||
if (_activeHoveringPointerDevices.isEmpty) {
|
if (_activeHoveringPointerDevices.isEmpty) {
|
||||||
_scheduleDismissTooltip(withDelay: _hoverShowDuration);
|
_scheduleDismissTooltip(withDelay: _hoverShowDuration);
|
||||||
@ -699,45 +698,36 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
|||||||
|
|
||||||
// https://material.io/components/tooltips#specs
|
// https://material.io/components/tooltips#specs
|
||||||
double _getDefaultTooltipHeight() {
|
double _getDefaultTooltipHeight() {
|
||||||
final ThemeData theme = Theme.of(context);
|
return switch (Theme.of(context).platform) {
|
||||||
switch (theme.platform) {
|
TargetPlatform.macOS ||
|
||||||
case TargetPlatform.macOS:
|
TargetPlatform.linux ||
|
||||||
case TargetPlatform.linux:
|
TargetPlatform.windows => 24.0,
|
||||||
case TargetPlatform.windows:
|
TargetPlatform.android ||
|
||||||
return 24.0;
|
TargetPlatform.fuchsia ||
|
||||||
case TargetPlatform.android:
|
TargetPlatform.iOS => 32.0,
|
||||||
case TargetPlatform.fuchsia:
|
};
|
||||||
case TargetPlatform.iOS:
|
|
||||||
return 32.0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
EdgeInsets _getDefaultPadding() {
|
EdgeInsets _getDefaultPadding() {
|
||||||
final ThemeData theme = Theme.of(context);
|
return switch (Theme.of(context).platform) {
|
||||||
switch (theme.platform) {
|
TargetPlatform.macOS ||
|
||||||
case TargetPlatform.macOS:
|
TargetPlatform.linux ||
|
||||||
case TargetPlatform.linux:
|
TargetPlatform.windows => const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||||
case TargetPlatform.windows:
|
TargetPlatform.android ||
|
||||||
return const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0);
|
TargetPlatform.fuchsia ||
|
||||||
case TargetPlatform.android:
|
TargetPlatform.iOS => const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0),
|
||||||
case TargetPlatform.fuchsia:
|
};
|
||||||
case TargetPlatform.iOS:
|
|
||||||
return const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double _getDefaultFontSize() {
|
double _getDefaultFontSize() {
|
||||||
final ThemeData theme = Theme.of(context);
|
return switch (Theme.of(context).platform) {
|
||||||
switch (theme.platform) {
|
TargetPlatform.macOS ||
|
||||||
case TargetPlatform.macOS:
|
TargetPlatform.linux ||
|
||||||
case TargetPlatform.linux:
|
TargetPlatform.windows => 12.0,
|
||||||
case TargetPlatform.windows:
|
TargetPlatform.android ||
|
||||||
return 12.0;
|
TargetPlatform.fuchsia ||
|
||||||
case TargetPlatform.android:
|
TargetPlatform.iOS => 14.0,
|
||||||
case TargetPlatform.fuchsia:
|
};
|
||||||
case TargetPlatform.iOS:
|
|
||||||
return 14.0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _createNewEntry() {
|
void _createNewEntry() {
|
||||||
|
@ -1993,7 +1993,7 @@ void main() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Tooltip trigger mode ignores mouse events', (WidgetTester tester) async {
|
testWidgetsWithLeakTracking('Tooltip trigger mode ignores mouse events', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
const MaterialApp(
|
const MaterialApp(
|
||||||
home: Tooltip(
|
home: Tooltip(
|
||||||
@ -2021,7 +2021,7 @@ void main() {
|
|||||||
expect(find.text(tooltipText), findsOneWidget);
|
expect(find.text(tooltipText), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Tooltip does not block other mouse regions', (WidgetTester tester) async {
|
testWidgetsWithLeakTracking('Tooltip does not block other mouse regions', (WidgetTester tester) async {
|
||||||
bool entered = false;
|
bool entered = false;
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
@ -2046,7 +2046,7 @@ void main() {
|
|||||||
expect(entered, isTrue);
|
expect(entered, isTrue);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Does not rebuild on mouse connect/disconnect', (WidgetTester tester) async {
|
testWidgetsWithLeakTracking('Does not rebuild on mouse connect/disconnect', (WidgetTester tester) async {
|
||||||
// Regression test for https://github.com/flutter/flutter/issues/117627
|
// Regression test for https://github.com/flutter/flutter/issues/117627
|
||||||
int buildCount = 0;
|
int buildCount = 0;
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
@ -2100,6 +2100,165 @@ void main() {
|
|||||||
await _testGestureTap(tester, textSpan);
|
await _testGestureTap(tester, textSpan);
|
||||||
expect(isTapped, isTrue);
|
expect(isTapped, isTrue);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgetsWithLeakTracking('Hold mouse button down and hover over the Tooltip widget', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const MaterialApp(
|
||||||
|
home: Center(
|
||||||
|
child: SizedBox.square(
|
||||||
|
dimension: 10.0,
|
||||||
|
child: Tooltip(
|
||||||
|
message: tooltipText,
|
||||||
|
waitDuration: Duration(seconds: 1),
|
||||||
|
triggerMode: TooltipTriggerMode.longPress,
|
||||||
|
child: SizedBox.expand(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final TestGesture mouseGesture = await tester.startGesture(Offset.zero, kind: PointerDeviceKind.mouse);
|
||||||
|
addTearDown(mouseGesture.removePointer);
|
||||||
|
await mouseGesture.moveTo(tester.getCenter(find.byTooltip(tooltipText)));
|
||||||
|
await tester.pump(const Duration(seconds: 1));
|
||||||
|
expect(
|
||||||
|
find.text(tooltipText), findsOneWidget, reason: 'Tooltip should be visible when hovered.');
|
||||||
|
|
||||||
|
await mouseGesture.up();
|
||||||
|
await tester.pump(const Duration(days: 10));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(
|
||||||
|
find.text(tooltipText),
|
||||||
|
findsOneWidget,
|
||||||
|
reason: 'Tooltip should be visible even when there is a PointerUp when hovered.',
|
||||||
|
);
|
||||||
|
|
||||||
|
await mouseGesture.moveTo(Offset.zero);
|
||||||
|
await tester.pump(const Duration(seconds: 1));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(
|
||||||
|
find.text(tooltipText),
|
||||||
|
findsNothing,
|
||||||
|
reason: 'Tooltip should be dismissed with no hovering mouse cursor.' ,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgetsWithLeakTracking('Hovered text should dismiss when clicked outside', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const MaterialApp(
|
||||||
|
home: Center(
|
||||||
|
child: SizedBox.square(
|
||||||
|
dimension: 10.0,
|
||||||
|
child: Tooltip(
|
||||||
|
message: tooltipText,
|
||||||
|
waitDuration: Duration(seconds: 1),
|
||||||
|
triggerMode: TooltipTriggerMode.longPress,
|
||||||
|
child: SizedBox.expand(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Avoid using startGesture here to avoid the PointDown event from also being
|
||||||
|
// interpreted as a PointHover event by the Tooltip.
|
||||||
|
final TestGesture mouseGesture1 = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||||
|
addTearDown(mouseGesture1.removePointer);
|
||||||
|
await mouseGesture1.moveTo(tester.getCenter(find.byTooltip(tooltipText)));
|
||||||
|
await tester.pump(const Duration(seconds: 1));
|
||||||
|
expect(find.text(tooltipText), findsOneWidget, reason: 'Tooltip should be visible when hovered.');
|
||||||
|
|
||||||
|
// Tapping on the Tooltip widget should dismiss the tooltip, since the
|
||||||
|
// trigger mode is longPress.
|
||||||
|
await tester.tap(find.byTooltip(tooltipText));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text(tooltipText), findsNothing);
|
||||||
|
await mouseGesture1.removePointer();
|
||||||
|
|
||||||
|
final TestGesture mouseGesture2 = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||||
|
addTearDown(mouseGesture2.removePointer);
|
||||||
|
await mouseGesture2.moveTo(tester.getCenter(find.byTooltip(tooltipText)));
|
||||||
|
await tester.pump(const Duration(seconds: 1));
|
||||||
|
expect(find.text(tooltipText), findsOneWidget, reason: 'Tooltip should be visible when hovered.');
|
||||||
|
|
||||||
|
await tester.tapAt(Offset.zero);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text(tooltipText), findsNothing, reason: 'Tapping outside of the Tooltip widget should dismiss the tooltip.');
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgetsWithLeakTracking('Mouse tap and hover over the Tooltip widget', (WidgetTester tester) async {
|
||||||
|
// Regression test for https://github.com/flutter/flutter/issues/127575 .
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const MaterialApp(
|
||||||
|
home: Center(
|
||||||
|
child: SizedBox.square(
|
||||||
|
dimension: 10.0,
|
||||||
|
child: Tooltip(
|
||||||
|
message: tooltipText,
|
||||||
|
waitDuration: Duration(seconds: 1),
|
||||||
|
triggerMode: TooltipTriggerMode.longPress,
|
||||||
|
child: SizedBox.expand(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// The PointDown event is also interpreted as a PointHover event by the
|
||||||
|
// Tooltip. This should be pretty rare but since it's more of a tap event
|
||||||
|
// than a hover event, the tooltip shouldn't show unless the triggerMode
|
||||||
|
// is set to tap.
|
||||||
|
final TestGesture mouseGesture1 = await tester.startGesture(
|
||||||
|
tester.getCenter(find.byTooltip(tooltipText)),
|
||||||
|
kind: PointerDeviceKind.mouse,
|
||||||
|
);
|
||||||
|
addTearDown(mouseGesture1.removePointer);
|
||||||
|
await tester.pump(const Duration(seconds: 1));
|
||||||
|
expect(
|
||||||
|
find.text(tooltipText),
|
||||||
|
findsNothing,
|
||||||
|
reason: 'Tooltip should NOT be visible when hovered and tapped, when trigger mode is not tap',
|
||||||
|
);
|
||||||
|
await mouseGesture1.up();
|
||||||
|
await mouseGesture1.removePointer();
|
||||||
|
await tester.pump(const Duration(days: 10));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const MaterialApp(
|
||||||
|
home: Center(
|
||||||
|
child: SizedBox.square(
|
||||||
|
dimension: 10.0,
|
||||||
|
child: Tooltip(
|
||||||
|
message: tooltipText,
|
||||||
|
waitDuration: Duration(seconds: 1),
|
||||||
|
triggerMode: TooltipTriggerMode.tap,
|
||||||
|
child: SizedBox.expand(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final TestGesture mouseGesture2 = await tester.startGesture(
|
||||||
|
tester.getCenter(find.byTooltip(tooltipText)),
|
||||||
|
kind: PointerDeviceKind.mouse,
|
||||||
|
);
|
||||||
|
addTearDown(mouseGesture2.removePointer);
|
||||||
|
// The tap should be ignored, since Tooltip does not track "trigger gestures"
|
||||||
|
// for mouse devices.
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
await mouseGesture2.up();
|
||||||
|
await tester.pump(const Duration(seconds: 1));
|
||||||
|
expect(
|
||||||
|
find.text(tooltipText),
|
||||||
|
findsNothing,
|
||||||
|
reason: 'Tooltip should NOT be visible when hovered and tapped, when trigger mode is tap',
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setWidgetForTooltipMode(
|
Future<void> setWidgetForTooltipMode(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user