Unverified Commit 93de096e authored by Matan Shukry's avatar Matan Shukry Committed by GitHub

Add TooltipTriggerMode and provideTriggerFeedback to allow users to c… (#84434)

parent cc03623c
......@@ -112,6 +112,8 @@ class Tooltip extends StatefulWidget {
this.waitDuration,
this.showDuration,
this.child,
this.triggerMode,
this.enableFeedback,
}) : assert(message != null),
super(key: key);
......@@ -201,6 +203,25 @@ class Tooltip extends StatefulWidget {
/// pointer exits the widget.
final Duration? showDuration;
/// The [TooltipTriggerMode] that will show the tooltip.
///
/// If this property is null, then [TooltipThemeData.triggerMode] is used.
/// If [TooltipThemeData.triggerMode] is also null, the default mode is
/// [TooltipTriggerMode.longPress].
final TooltipTriggerMode? triggerMode;
/// Whether the tooltip should provide acoustic and/or haptic feedback.
///
/// For example, on Android a tap will produce a clicking sound and a
/// long-press will produce a short vibration, when feedback is enabled.
///
/// When null, the default value is true.
///
/// See also:
///
/// * [Feedback], for providing platform-specific feedback to certain actions.
final bool? enableFeedback;
static final Set<_TooltipState> _openedToolTips = <_TooltipState>{};
/// Dismiss all of the tooltips that are currently shown on the screen.
......@@ -234,6 +255,8 @@ class Tooltip extends StatefulWidget {
properties.add(FlagProperty('semantics', value: excludeFromSemantics, ifTrue: 'excluded', showName: true, defaultValue: null));
properties.add(DiagnosticsProperty<Duration>('wait duration', waitDuration, defaultValue: null));
properties.add(DiagnosticsProperty<Duration>('show duration', showDuration, defaultValue: null));
properties.add(DiagnosticsProperty<TooltipTriggerMode>('triggerMode', triggerMode, defaultValue: null));
properties.add(FlagProperty('enableFeedback', value: enableFeedback, ifTrue: 'true', showName: true, defaultValue: null));
}
}
......@@ -247,6 +270,8 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
static const Duration _defaultHoverShowDuration = Duration(milliseconds: 100);
static const Duration _defaultWaitDuration = Duration.zero;
static const bool _defaultExcludeFromSemantics = false;
static const TooltipTriggerMode _defaultTriggerMode = TooltipTriggerMode.longPress;
static const bool _defaultEnableFeedback = true;
late double height;
late EdgeInsetsGeometry padding;
......@@ -264,7 +289,9 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
late Duration hoverShowDuration;
late Duration waitDuration;
late bool _mouseIsConnected;
bool _longPressActivated = false;
bool _pressActivated = false;
late TooltipTriggerMode triggerMode;
late bool enableFeedback;
@override
void initState() {
......@@ -346,12 +373,12 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
_removeEntry();
return;
}
if (_longPressActivated) {
if (_pressActivated) {
_hideTimer ??= Timer(showDuration, _controller.reverse);
} else {
_hideTimer ??= Timer(hoverShowDuration, _controller.reverse);
}
_longPressActivated = false;
_pressActivated = false;
}
void _showTooltip({ bool immediately = false }) {
......@@ -463,11 +490,15 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
super.dispose();
}
void _handleLongPress() {
_longPressActivated = true;
void _handlePress() {
_pressActivated = true;
final bool tooltipCreated = ensureTooltipVisible();
if (tooltipCreated)
Feedback.forLongPress(context);
if (tooltipCreated && enableFeedback) {
if (triggerMode == TooltipTriggerMode.longPress)
Feedback.forLongPress(context);
else
Feedback.forTap(context);
}
}
@override
......@@ -508,10 +539,14 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
waitDuration = widget.waitDuration ?? tooltipTheme.waitDuration ?? _defaultWaitDuration;
showDuration = widget.showDuration ?? tooltipTheme.showDuration ?? _defaultShowDuration;
hoverShowDuration = widget.showDuration ?? tooltipTheme.showDuration ?? _defaultHoverShowDuration;
triggerMode = widget.triggerMode ?? tooltipTheme.triggerMode ?? _defaultTriggerMode;
enableFeedback = widget.enableFeedback ?? tooltipTheme.enableFeedback ?? _defaultEnableFeedback;
Widget result = GestureDetector(
behavior: HitTestBehavior.opaque,
onLongPress: _handleLongPress,
onLongPress: (triggerMode == TooltipTriggerMode.longPress) ?
_handlePress : null,
onTap: (triggerMode == TooltipTriggerMode.tap) ? _handlePress : null,
excludeFromSemantics: true,
child: Semantics(
label: excludeFromSemantics ? null : widget.message,
......
......@@ -37,6 +37,8 @@ class TooltipThemeData with Diagnosticable {
this.textStyle,
this.waitDuration,
this.showDuration,
this.triggerMode,
this.enableFeedback,
});
/// The height of [Tooltip.child].
......@@ -84,6 +86,22 @@ class TooltipThemeData with Diagnosticable {
/// The length of time that the tooltip will be shown once it has appeared.
final Duration? showDuration;
/// The [TooltipTriggerMode] that will show the tooltip.
final TooltipTriggerMode? triggerMode;
/// Whether the tooltip should provide acoustic and/or haptic feedback.
///
/// For example, on Android a tap will produce a clicking sound and a
/// long-press will produce a short vibration, when feedback is enabled.
///
/// This value is used if [Tooltip.enableFeedback] is null.
/// If this value is null, the default is true.
///
/// See also:
///
/// * [Feedback], for providing platform-specific feedback to certain actions.
final bool? enableFeedback;
/// Creates a copy of this object but with the given fields replaced with the
/// new values.
TooltipThemeData copyWith({
......@@ -97,6 +115,8 @@ class TooltipThemeData with Diagnosticable {
TextStyle? textStyle,
Duration? waitDuration,
Duration? showDuration,
TooltipTriggerMode? triggerMode,
bool? enableFeedback,
}) {
return TooltipThemeData(
height: height ?? this.height,
......@@ -109,6 +129,8 @@ class TooltipThemeData with Diagnosticable {
textStyle: textStyle ?? this.textStyle,
waitDuration: waitDuration ?? this.waitDuration,
showDuration: showDuration ?? this.showDuration,
triggerMode: triggerMode ?? this.triggerMode,
enableFeedback: enableFeedback ?? this.enableFeedback,
);
}
......@@ -146,6 +168,8 @@ class TooltipThemeData with Diagnosticable {
textStyle,
waitDuration,
showDuration,
triggerMode,
enableFeedback
);
}
......@@ -165,7 +189,9 @@ class TooltipThemeData with Diagnosticable {
&& other.decoration == decoration
&& other.textStyle == textStyle
&& other.waitDuration == waitDuration
&& other.showDuration == showDuration;
&& other.showDuration == showDuration
&& other.triggerMode == triggerMode
&& other.enableFeedback == enableFeedback;
}
@override
......@@ -181,6 +207,8 @@ class TooltipThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<TextStyle>('textStyle', textStyle, defaultValue: null));
properties.add(DiagnosticsProperty<Duration>('wait duration', waitDuration, defaultValue: null));
properties.add(DiagnosticsProperty<Duration>('show duration', showDuration, defaultValue: null));
properties.add(DiagnosticsProperty<TooltipTriggerMode>('triggerMode', triggerMode, defaultValue: null));
properties.add(FlagProperty('enableFeedback', value: enableFeedback, ifTrue: 'true', showName: true, defaultValue: null));
}
}
......@@ -250,3 +278,26 @@ class TooltipTheme extends InheritedTheme {
@override
bool updateShouldNotify(TooltipTheme oldWidget) => data != oldWidget.data;
}
/// The method of interaction that will trigger a tooltip.
/// Used in [Tooltip.triggerMode] and [TooltipThemeData.triggerMode].
enum TooltipTriggerMode {
/// Tooltip will only be shown by calling `ensureTooltipVisible`.
manual,
/// Tooltip will be shown after a long press.
///
/// See also:
///
/// * [GestureDetector.onLongPress], the event that is used for trigger.
/// * [Feedback.forLongPress], the feedback method called when feedback is enabled.
longPress,
/// Tooltip will be shown after a single tap.
///
/// See also:
///
/// * [GestureDetector.onTap], the event that is used for trigger.
/// * [Feedback.forTap], the feedback method called when feedback is enabled.
tap,
}
......@@ -1429,6 +1429,8 @@ void main() {
excludeFromSemantics: true,
preferBelow: false,
verticalOffset: 50.0,
triggerMode: TooltipTriggerMode.manual,
enableFeedback: true,
).debugFillProperties(builder);
final List<String> description = builder.properties
......@@ -1445,8 +1447,81 @@ void main() {
'semantics: excluded',
'wait duration: 0:00:01.000000',
'show duration: 0:00:02.000000',
'triggerMode: TooltipTriggerMode.manual',
'enableFeedback: true',
]);
});
testWidgets('Tooltip triggers on tap when trigger mode is tap', (WidgetTester tester) async {
await setWidgetForTooltipMode(tester, TooltipTriggerMode.tap);
final Finder tooltip = find.byType(Tooltip);
expect(find.text(tooltipText), findsNothing);
await testGestureTap(tester, tooltip);
expect(find.text(tooltipText), findsOneWidget);
});
testWidgets('Tooltip triggers on long press when mode is long press', (WidgetTester tester) async {
await setWidgetForTooltipMode(tester, TooltipTriggerMode.longPress);
final Finder tooltip = find.byType(Tooltip);
expect(find.text(tooltipText), findsNothing);
await testGestureTap(tester, tooltip);
expect(find.text(tooltipText), findsNothing);
await testGestureLongPress(tester, tooltip);
expect(find.text(tooltipText), findsOneWidget);
});
testWidgets('Tooltip does not trigger on tap when trigger mode is longPress', (WidgetTester tester) async {
await setWidgetForTooltipMode(tester, TooltipTriggerMode.longPress);
final Finder tooltip = find.byType(Tooltip);
expect(find.text(tooltipText), findsNothing);
await testGestureTap(tester, tooltip);
expect(find.text(tooltipText), findsNothing);
});
testWidgets('Tooltip does not trigger when trigger mode is manual', (WidgetTester tester) async {
await setWidgetForTooltipMode(tester, TooltipTriggerMode.manual);
final Finder tooltip = find.byType(Tooltip);
expect(find.text(tooltipText), findsNothing);
await testGestureTap(tester, tooltip);
expect(find.text(tooltipText), findsNothing);
await testGestureLongPress(tester, tooltip);
expect(find.text(tooltipText), findsNothing);
});
}
Future<void> setWidgetForTooltipMode(WidgetTester tester, TooltipTriggerMode triggerMode) async {
await tester.pumpWidget(
MaterialApp(
home: Tooltip(
message: tooltipText,
triggerMode: triggerMode,
child: const SizedBox(width: 100.0, height: 100.0),
),
),
);
}
Future<void> testGestureLongPress(WidgetTester tester, Finder tooltip) async {
final TestGesture gestureLongPress = await tester.startGesture(tester.getCenter(tooltip));
await tester.pump();
await tester.pump(kLongPressTimeout);
await gestureLongPress.up();
await tester.pump();
}
Future<void> testGestureTap(WidgetTester tester, Finder tooltip) async {
await tester.tap(tooltip);
await tester.pump(const Duration(milliseconds: 10));
}
SemanticsNode findDebugSemantics(RenderObject object) {
......
......@@ -48,6 +48,8 @@ void main() {
expect(theme.textStyle, null);
expect(theme.waitDuration, null);
expect(theme.showDuration, null);
expect(theme.triggerMode, null);
expect(theme.enableFeedback, null);
});
testWidgets('Default TooltipThemeData debugFillProperties', (WidgetTester tester) async {
......@@ -66,6 +68,8 @@ void main() {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const Duration wait = Duration(milliseconds: 100);
const Duration show = Duration(milliseconds: 200);
const TooltipTriggerMode triggerMode = TooltipTriggerMode.longPress;
const bool enableFeedback = true;
const TooltipThemeData(
height: 15.0,
padding: EdgeInsets.all(20.0),
......@@ -76,6 +80,8 @@ void main() {
textStyle: TextStyle(decoration: TextDecoration.underline),
waitDuration: wait,
showDuration: show,
triggerMode: triggerMode,
enableFeedback: enableFeedback,
).debugFillProperties(builder);
final List<String> description = builder.properties
......@@ -93,6 +99,8 @@ void main() {
'textStyle: TextStyle(inherit: true, decoration: TextDecoration.underline)',
'wait duration: ${wait.toString()}',
'show duration: ${show.toString()}',
'triggerMode: $triggerMode',
'enableFeedback: true',
]);
});
......@@ -985,6 +993,54 @@ void main() {
expect(find.text(tooltipText), findsNothing);
});
testWidgets('Tooltip triggerMode - ThemeData.triggerMode', (WidgetTester tester) async {
const TooltipTriggerMode triggerMode = TooltipTriggerMode.tap;
await tester.pumpWidget(
MaterialApp(
home: Theme(
data: ThemeData(
tooltipTheme: const TooltipThemeData(triggerMode: triggerMode),
),
child: const Center(
child: Tooltip(
message: tooltipText,
child: SizedBox(width: 100.0, height: 100.0),
),
),
),
),
);
final Finder tooltip = find.byType(Tooltip);
final TestGesture gesture = await tester.startGesture(tester.getCenter(tooltip));
await gesture.up();
await tester.pump();
expect(find.text(tooltipText), findsOneWidget); // Tooltip should show immediately after tap
});
testWidgets('Tooltip triggerMode - TooltipTheme', (WidgetTester tester) async {
const TooltipTriggerMode triggerMode = TooltipTriggerMode.tap;
await tester.pumpWidget(
const MaterialApp(
home: TooltipTheme(
data: TooltipThemeData(triggerMode: triggerMode),
child: Center(
child: Tooltip(
message: tooltipText,
child: SizedBox(width: 100.0, height: 100.0),
),
),
),
),
);
final Finder tooltip = find.byType(Tooltip);
final TestGesture gesture = await tester.startGesture(tester.getCenter(tooltip));
await gesture.up();
await tester.pump();
expect(find.text(tooltipText), findsOneWidget); // Tooltip should show immediately after tap
});
testWidgets('Semantics included by default - ThemeData.tooltipTheme', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
......
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