Reland "Add TooltipVisibility widget" (#92090)
This commit is contained in:
parent
8474f41e90
commit
502edd48c9
@ -158,6 +158,7 @@ export 'src/material/toggle_buttons_theme.dart';
|
|||||||
export 'src/material/toggleable.dart';
|
export 'src/material/toggleable.dart';
|
||||||
export 'src/material/tooltip.dart';
|
export 'src/material/tooltip.dart';
|
||||||
export 'src/material/tooltip_theme.dart';
|
export 'src/material/tooltip_theme.dart';
|
||||||
|
export 'src/material/tooltip_visibility.dart';
|
||||||
export 'src/material/typography.dart';
|
export 'src/material/typography.dart';
|
||||||
export 'src/material/user_accounts_drawer_header.dart';
|
export 'src/material/user_accounts_drawer_header.dart';
|
||||||
export 'widgets.dart';
|
export 'widgets.dart';
|
||||||
|
@ -14,6 +14,7 @@ import 'colors.dart';
|
|||||||
import 'feedback.dart';
|
import 'feedback.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
import 'tooltip_theme.dart';
|
import 'tooltip_theme.dart';
|
||||||
|
import 'tooltip_visibility.dart';
|
||||||
|
|
||||||
/// A material design tooltip.
|
/// A material design tooltip.
|
||||||
///
|
///
|
||||||
@ -69,6 +70,7 @@ import 'tooltip_theme.dart';
|
|||||||
///
|
///
|
||||||
/// * <https://material.io/design/components/tooltips.html>
|
/// * <https://material.io/design/components/tooltips.html>
|
||||||
/// * [TooltipTheme] or [ThemeData.tooltipTheme]
|
/// * [TooltipTheme] or [ThemeData.tooltipTheme]
|
||||||
|
/// * [TooltipVisibility]
|
||||||
class Tooltip extends StatefulWidget {
|
class Tooltip extends StatefulWidget {
|
||||||
/// Creates a tooltip.
|
/// Creates a tooltip.
|
||||||
///
|
///
|
||||||
@ -327,6 +329,7 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
|||||||
late bool enableFeedback;
|
late bool enableFeedback;
|
||||||
late bool _isConcealed;
|
late bool _isConcealed;
|
||||||
late bool _forceRemoval;
|
late bool _forceRemoval;
|
||||||
|
late bool _visible;
|
||||||
|
|
||||||
/// The plain text message for this tooltip.
|
/// The plain text message for this tooltip.
|
||||||
///
|
///
|
||||||
@ -352,6 +355,12 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
|||||||
GestureBinding.instance!.pointerRouter.addGlobalRoute(_handlePointerEvent);
|
GestureBinding.instance!.pointerRouter.addGlobalRoute(_handlePointerEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_visible = TooltipVisibility.of(context);
|
||||||
|
}
|
||||||
|
|
||||||
// https://material.io/components/tooltips#specs
|
// https://material.io/components/tooltips#specs
|
||||||
double _getDefaultTooltipHeight() {
|
double _getDefaultTooltipHeight() {
|
||||||
final ThemeData theme = Theme.of(context);
|
final ThemeData theme = Theme.of(context);
|
||||||
@ -483,8 +492,11 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
|||||||
|
|
||||||
/// Shows the tooltip if it is not already visible.
|
/// Shows the tooltip if it is not already visible.
|
||||||
///
|
///
|
||||||
/// Returns `false` when the tooltip was already visible.
|
/// Returns `false` when the tooltip shouldn't be shown or when the tooltip
|
||||||
|
/// was already visible.
|
||||||
bool ensureTooltipVisible() {
|
bool ensureTooltipVisible() {
|
||||||
|
if (!_visible)
|
||||||
|
return false;
|
||||||
_showTimer?.cancel();
|
_showTimer?.cancel();
|
||||||
_showTimer = null;
|
_showTimer = null;
|
||||||
_forceRemoval = false;
|
_forceRemoval = false;
|
||||||
@ -671,27 +683,31 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
|||||||
triggerMode = widget.triggerMode ?? tooltipTheme.triggerMode ?? _defaultTriggerMode;
|
triggerMode = widget.triggerMode ?? tooltipTheme.triggerMode ?? _defaultTriggerMode;
|
||||||
enableFeedback = widget.enableFeedback ?? tooltipTheme.enableFeedback ?? _defaultEnableFeedback;
|
enableFeedback = widget.enableFeedback ?? tooltipTheme.enableFeedback ?? _defaultEnableFeedback;
|
||||||
|
|
||||||
Widget result = GestureDetector(
|
Widget result = Semantics(
|
||||||
behavior: HitTestBehavior.opaque,
|
label: excludeFromSemantics
|
||||||
onLongPress: (triggerMode == TooltipTriggerMode.longPress) ?
|
? null
|
||||||
_handlePress : null,
|
: _tooltipMessage,
|
||||||
onTap: (triggerMode == TooltipTriggerMode.tap) ? _handlePress : null,
|
child: widget.child,
|
||||||
excludeFromSemantics: true,
|
|
||||||
child: Semantics(
|
|
||||||
label: excludeFromSemantics
|
|
||||||
? null
|
|
||||||
: _tooltipMessage,
|
|
||||||
child: widget.child,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Only check for hovering if there is a mouse connected.
|
// Only check for gestures if tooltip should be visible.
|
||||||
if (_mouseIsConnected) {
|
if (_visible) {
|
||||||
result = MouseRegion(
|
result = GestureDetector(
|
||||||
onEnter: (_) => _handleMouseEnter(),
|
behavior: HitTestBehavior.opaque,
|
||||||
onExit: (_) => _handleMouseExit(),
|
onLongPress: (triggerMode == TooltipTriggerMode.longPress) ?
|
||||||
|
_handlePress : null,
|
||||||
|
onTap: (triggerMode == TooltipTriggerMode.tap) ? _handlePress : null,
|
||||||
|
excludeFromSemantics: true,
|
||||||
child: result,
|
child: result,
|
||||||
);
|
);
|
||||||
|
// Only check for hovering if there is a mouse connected.
|
||||||
|
if (_mouseIsConnected) {
|
||||||
|
result = MouseRegion(
|
||||||
|
onEnter: (_) => _handleMouseEnter(),
|
||||||
|
onExit: (_) => _handleMouseExit(),
|
||||||
|
child: result,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -242,6 +242,10 @@ class TooltipThemeData with Diagnosticable {
|
|||||||
/// )
|
/// )
|
||||||
/// ```
|
/// ```
|
||||||
/// {@end-tool}
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [TooltipVisibility], which can be used to visually disable descendant [Tooltip]s.
|
||||||
class TooltipTheme extends InheritedTheme {
|
class TooltipTheme extends InheritedTheme {
|
||||||
/// Creates a tooltip theme that controls the configurations for
|
/// Creates a tooltip theme that controls the configurations for
|
||||||
/// [Tooltip].
|
/// [Tooltip].
|
||||||
|
63
packages/flutter/lib/src/material/tooltip_visibility.dart
Normal file
63
packages/flutter/lib/src/material/tooltip_visibility.dart
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class _TooltipVisibilityScope extends InheritedWidget {
|
||||||
|
const _TooltipVisibilityScope({
|
||||||
|
Key? key,
|
||||||
|
required Widget child,
|
||||||
|
required this.visible,
|
||||||
|
}) : super(key: key, child: child);
|
||||||
|
|
||||||
|
final bool visible;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(_TooltipVisibilityScope old) {
|
||||||
|
return old.visible != visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Overrides the visibility of descendant [Tooltip] widgets.
|
||||||
|
///
|
||||||
|
/// If disabled, the descendant [Tooltip] widgets will not display a tooltip
|
||||||
|
/// when tapped, long-pressed, hovered by the mouse, or when
|
||||||
|
/// `ensureTooltipVisible` is called. This only visually disables tooltips but
|
||||||
|
/// continues to provide any semantic information that is provided.
|
||||||
|
class TooltipVisibility extends StatelessWidget {
|
||||||
|
/// Creates a widget that configures the visibility of [Tooltip].
|
||||||
|
///
|
||||||
|
/// Both arguments must not be null.
|
||||||
|
const TooltipVisibility({
|
||||||
|
Key? key,
|
||||||
|
required this.visible,
|
||||||
|
required this.child,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// The widget below this widget in the tree.
|
||||||
|
///
|
||||||
|
/// The entire app can be wrapped in this widget to globally control [Tooltip]
|
||||||
|
/// visibility.
|
||||||
|
///
|
||||||
|
/// {@macro flutter.widgets.ProxyWidget.child}
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
/// Determines the visibility of [Tooltip] widgets that inherit from this widget.
|
||||||
|
final bool visible;
|
||||||
|
|
||||||
|
/// The [visible] of the closest instance of this class that encloses the
|
||||||
|
/// given context. Defaults to `true` if none are found.
|
||||||
|
static bool of(BuildContext context) {
|
||||||
|
final _TooltipVisibilityScope? visibility = context.dependOnInheritedWidgetOfExactType<_TooltipVisibilityScope>();
|
||||||
|
return visibility?.visible ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return _TooltipVisibilityScope(
|
||||||
|
visible: visible,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
223
packages/flutter/test/material/tooltip_visibility_test.dart
Normal file
223
packages/flutter/test/material/tooltip_visibility_test.dart
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void _ensureTooltipVisible(GlobalKey key) {
|
||||||
|
// This function uses "as dynamic" to defeat the static analysis. In general
|
||||||
|
// you want to avoid using this style in your code, as it will cause the
|
||||||
|
// analyzer to be unable to help you catch errors.
|
||||||
|
//
|
||||||
|
// In this case, we do it because we are trying to call internal methods of
|
||||||
|
// the tooltip code in order to test it. Normally, the state of a tooltip is a
|
||||||
|
// private class, but by using a GlobalKey we can get a handle to that object
|
||||||
|
// and by using "as dynamic" we can bypass the analyzer's type checks and call
|
||||||
|
// methods that we aren't supposed to be able to know about.
|
||||||
|
//
|
||||||
|
// It's ok to do this in tests, but you really don't want to do it in
|
||||||
|
// production code.
|
||||||
|
// ignore: avoid_dynamic_calls
|
||||||
|
(key.currentState as dynamic).ensureTooltipVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
const String tooltipText = 'TIP';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('Tooltip does not build MouseRegion when mouse is detected and in TooltipVisibility with visibility = false', (WidgetTester tester) async {
|
||||||
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||||
|
addTearDown(() async {
|
||||||
|
if (gesture != null)
|
||||||
|
return gesture.removePointer();
|
||||||
|
});
|
||||||
|
await gesture.addPointer();
|
||||||
|
await gesture.moveTo(const Offset(1.0, 1.0));
|
||||||
|
await tester.pump();
|
||||||
|
await gesture.moveTo(Offset.zero);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const MaterialApp(
|
||||||
|
home: TooltipVisibility(
|
||||||
|
visible: false,
|
||||||
|
child: Tooltip(
|
||||||
|
message: tooltipText,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 100.0,
|
||||||
|
height: 100.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.descendant(of: find.byType(Tooltip), matching: find.byType(MouseRegion)), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Tooltip does not show when hovered when in TooltipVisibility with visible = false', (WidgetTester tester) async {
|
||||||
|
const Duration waitDuration = Duration.zero;
|
||||||
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||||
|
addTearDown(() async {
|
||||||
|
if (gesture != null)
|
||||||
|
return gesture.removePointer();
|
||||||
|
});
|
||||||
|
await gesture.addPointer();
|
||||||
|
await gesture.moveTo(const Offset(1.0, 1.0));
|
||||||
|
await tester.pump();
|
||||||
|
await gesture.moveTo(Offset.zero);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const MaterialApp(
|
||||||
|
home: Center(
|
||||||
|
child: TooltipVisibility(
|
||||||
|
visible: false,
|
||||||
|
child: Tooltip(
|
||||||
|
message: tooltipText,
|
||||||
|
waitDuration: waitDuration,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 100.0,
|
||||||
|
height: 100.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Finder tooltip = find.byType(Tooltip);
|
||||||
|
await gesture.moveTo(Offset.zero);
|
||||||
|
await tester.pump();
|
||||||
|
await gesture.moveTo(tester.getCenter(tooltip));
|
||||||
|
await tester.pump();
|
||||||
|
// Wait for it to appear.
|
||||||
|
await tester.pump(waitDuration);
|
||||||
|
expect(find.text(tooltipText), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Tooltip shows when hovered when in TooltipVisibility with visible = true', (WidgetTester tester) async {
|
||||||
|
const Duration waitDuration = Duration.zero;
|
||||||
|
TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||||
|
addTearDown(() async {
|
||||||
|
if (gesture != null)
|
||||||
|
return gesture.removePointer();
|
||||||
|
});
|
||||||
|
await gesture.addPointer();
|
||||||
|
await gesture.moveTo(const Offset(1.0, 1.0));
|
||||||
|
await tester.pump();
|
||||||
|
await gesture.moveTo(Offset.zero);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const MaterialApp(
|
||||||
|
home: Center(
|
||||||
|
child: TooltipVisibility(
|
||||||
|
visible: true,
|
||||||
|
child: Tooltip(
|
||||||
|
message: tooltipText,
|
||||||
|
waitDuration: waitDuration,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 100.0,
|
||||||
|
height: 100.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Finder tooltip = find.byType(Tooltip);
|
||||||
|
await gesture.moveTo(Offset.zero);
|
||||||
|
await tester.pump();
|
||||||
|
await gesture.moveTo(tester.getCenter(tooltip));
|
||||||
|
await tester.pump();
|
||||||
|
// Wait for it to appear.
|
||||||
|
await tester.pump(waitDuration);
|
||||||
|
expect(find.text(tooltipText), findsOneWidget);
|
||||||
|
|
||||||
|
// Wait for it to disappear.
|
||||||
|
await gesture.moveTo(Offset.zero);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await gesture.removePointer();
|
||||||
|
gesture = null;
|
||||||
|
expect(find.text(tooltipText), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Tooltip does not build GestureDetector when in TooltipVisibility with visibility = false', (WidgetTester tester) async {
|
||||||
|
await setWidgetForTooltipMode(tester, TooltipTriggerMode.tap, false);
|
||||||
|
|
||||||
|
expect(find.byType(GestureDetector), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Tooltip triggers on tap when trigger mode is tap and in TooltipVisibility with visible = true', (WidgetTester tester) async {
|
||||||
|
await setWidgetForTooltipMode(tester, TooltipTriggerMode.tap, true);
|
||||||
|
|
||||||
|
final Finder tooltip = find.byType(Tooltip);
|
||||||
|
expect(find.text(tooltipText), findsNothing);
|
||||||
|
|
||||||
|
await testGestureTap(tester, tooltip);
|
||||||
|
expect(find.text(tooltipText), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Tooltip does not trigger manually when in TooltipVisibility with visible = false', (WidgetTester tester) async {
|
||||||
|
final GlobalKey key = GlobalKey();
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: TooltipVisibility(
|
||||||
|
visible: false,
|
||||||
|
child: Tooltip(
|
||||||
|
key: key,
|
||||||
|
message: tooltipText,
|
||||||
|
child: const SizedBox(width: 100.0, height: 100.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_ensureTooltipVisible(key);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text(tooltipText), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Tooltip triggers manually when in TooltipVisibility with visible = true', (WidgetTester tester) async {
|
||||||
|
final GlobalKey key = GlobalKey();
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: TooltipVisibility(
|
||||||
|
visible: true,
|
||||||
|
child: Tooltip(
|
||||||
|
key: key,
|
||||||
|
message: tooltipText,
|
||||||
|
child: const SizedBox(width: 100.0, height: 100.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_ensureTooltipVisible(key);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text(tooltipText), findsOneWidget);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setWidgetForTooltipMode(WidgetTester tester, TooltipTriggerMode triggerMode, bool visibility) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: TooltipVisibility(
|
||||||
|
visible: visibility,
|
||||||
|
child: Tooltip(
|
||||||
|
message: tooltipText,
|
||||||
|
triggerMode: triggerMode,
|
||||||
|
child: const SizedBox(width: 100.0, height: 100.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> testGestureTap(WidgetTester tester, Finder tooltip) async {
|
||||||
|
await tester.tap(tooltip);
|
||||||
|
await tester.pump(const Duration(milliseconds: 10));
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user