Add richMessage
parameter to the Tooltip
widget. (#88539)
This commit is contained in:
parent
f95e18ae8c
commit
15967669b2
74
examples/api/lib/material/tooltip/tooltip.2.dart
Normal file
74
examples/api/lib/material/tooltip/tooltip.2.dart
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// Template: dev/snippets/config/templates/stateless_widget_scaffold_center.tmpl
|
||||||
|
//
|
||||||
|
// Comment lines marked with "▼▼▼" and "▲▲▲" are used for authoring
|
||||||
|
// of samples, and may be ignored if you are just exploring the sample.
|
||||||
|
|
||||||
|
// Flutter code sample for Tooltip
|
||||||
|
//
|
||||||
|
//***************************************************************************
|
||||||
|
//* ▼▼▼▼▼▼▼▼ description ▼▼▼▼▼▼▼▼ (do not modify or remove section marker)
|
||||||
|
|
||||||
|
// This example shows a rich [Tooltip] that specifies the [richMessage]
|
||||||
|
// parameter instead of the [message] parameter (only one of these may be
|
||||||
|
// non-null. Any [InlineSpan] can be specified for the [richMessage] attribute,
|
||||||
|
// including [WidgetSpan].
|
||||||
|
|
||||||
|
//* ▲▲▲▲▲▲▲▲ description ▲▲▲▲▲▲▲▲ (do not modify or remove section marker)
|
||||||
|
//***************************************************************************
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
void main() => runApp(const MyApp());
|
||||||
|
|
||||||
|
/// This is the main application widget.
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
const MyApp({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
static const String _title = 'Flutter Code Sample';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
title: _title,
|
||||||
|
home: Scaffold(
|
||||||
|
appBar: AppBar(title: const Text(_title)),
|
||||||
|
body: const Center(
|
||||||
|
child: MyStatelessWidget(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is the stateless widget that the main application instantiates.
|
||||||
|
class MyStatelessWidget extends StatelessWidget {
|
||||||
|
const MyStatelessWidget({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
//********************************************************************
|
||||||
|
//* ▼▼▼▼▼▼▼▼ code ▼▼▼▼▼▼▼▼ (do not modify or remove section marker)
|
||||||
|
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Tooltip(
|
||||||
|
richMessage: TextSpan(
|
||||||
|
text: 'I am a rich tooltip. ',
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
children: <InlineSpan>[
|
||||||
|
TextSpan(
|
||||||
|
text: 'I am another span of this rich tooltip',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Text('Tap this text and hold down to show a tooltip.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//* ▲▲▲▲▲▲▲▲ code ▲▲▲▲▲▲▲▲ (do not modify or remove section marker)
|
||||||
|
//********************************************************************
|
||||||
|
|
||||||
|
}
|
@ -628,7 +628,7 @@ class FloatingActionButton extends StatelessWidget {
|
|||||||
|
|
||||||
if (tooltip != null) {
|
if (tooltip != null) {
|
||||||
result = Tooltip(
|
result = Tooltip(
|
||||||
message: tooltip!,
|
message: tooltip,
|
||||||
child: result,
|
child: result,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -323,7 +323,7 @@ class IconButton extends StatelessWidget {
|
|||||||
|
|
||||||
if (tooltip != null) {
|
if (tooltip != null) {
|
||||||
result = Tooltip(
|
result = Tooltip(
|
||||||
message: tooltip!,
|
message: tooltip,
|
||||||
child: result,
|
child: result,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -759,9 +759,7 @@ class _NavigationBarDestinationTooltip extends StatelessWidget {
|
|||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// The text that is rendered in the tooltip when it appears.
|
/// The text that is rendered in the tooltip when it appears.
|
||||||
///
|
final String message;
|
||||||
/// If [message] is null, no tooltip will be used.
|
|
||||||
final String? message;
|
|
||||||
|
|
||||||
/// The widget that, when pressed, will show a tooltip.
|
/// The widget that, when pressed, will show a tooltip.
|
||||||
final Widget child;
|
final Widget child;
|
||||||
@ -772,7 +770,7 @@ class _NavigationBarDestinationTooltip extends StatelessWidget {
|
|||||||
return child;
|
return child;
|
||||||
}
|
}
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
message: message!,
|
message: message,
|
||||||
// TODO(johnsonmh): Make this value configurable/themable.
|
// TODO(johnsonmh): Make this value configurable/themable.
|
||||||
verticalOffset: 42,
|
verticalOffset: 42,
|
||||||
excludeFromSemantics: true,
|
excludeFromSemantics: true,
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@ -55,6 +56,15 @@ import 'tooltip_theme.dart';
|
|||||||
/// ** See code in examples/api/lib/material/tooltip/tooltip.1.dart **
|
/// ** See code in examples/api/lib/material/tooltip/tooltip.1.dart **
|
||||||
/// {@end-tool}
|
/// {@end-tool}
|
||||||
///
|
///
|
||||||
|
/// {@tool dartpad --template=stateless_widget_scaffold_center}
|
||||||
|
/// This example shows a rich [Tooltip] that specifies the [richMessage]
|
||||||
|
/// parameter instead of the [message] parameter (only one of these may be
|
||||||
|
/// non-null. Any [InlineSpan] can be specified for the [richMessage] attribute,
|
||||||
|
/// including [WidgetSpan].
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/material/tooltip/tooltip.2.dart **
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * <https://material.io/design/components/tooltips.html>
|
/// * <https://material.io/design/components/tooltips.html>
|
||||||
@ -70,9 +80,12 @@ class Tooltip extends StatefulWidget {
|
|||||||
///
|
///
|
||||||
/// All parameters that are defined in the constructor will
|
/// All parameters that are defined in the constructor will
|
||||||
/// override the default values _and_ the values in [TooltipTheme.of].
|
/// override the default values _and_ the values in [TooltipTheme.of].
|
||||||
|
///
|
||||||
|
/// Only one of [message] and [richMessage] may be non-null.
|
||||||
const Tooltip({
|
const Tooltip({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.message,
|
this.message,
|
||||||
|
this.richMessage,
|
||||||
this.height,
|
this.height,
|
||||||
this.padding,
|
this.padding,
|
||||||
this.margin,
|
this.margin,
|
||||||
@ -86,11 +99,24 @@ class Tooltip extends StatefulWidget {
|
|||||||
this.child,
|
this.child,
|
||||||
this.triggerMode,
|
this.triggerMode,
|
||||||
this.enableFeedback,
|
this.enableFeedback,
|
||||||
}) : assert(message != null),
|
}) : assert((message == null) != (richMessage == null), 'Either `message` or `richMessage` must be specified'),
|
||||||
|
assert(
|
||||||
|
richMessage == null || textStyle == null,
|
||||||
|
'If `richMessage` is specified, `textStyle` will have no effect. '
|
||||||
|
'If you wish to provide a `textStyle` for a rich tooltip, add the '
|
||||||
|
'`textStyle` directly to the `richMessage` InlineSpan.',
|
||||||
|
),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
/// The text to display in the tooltip.
|
/// The text to display in the tooltip.
|
||||||
final String message;
|
///
|
||||||
|
/// Only one of [message] and [richMessage] may be non-null.
|
||||||
|
final String? message;
|
||||||
|
|
||||||
|
/// The rich text to display in the tooltip.
|
||||||
|
///
|
||||||
|
/// Only one of [message] and [richMessage] may be non-null.
|
||||||
|
final InlineSpan? richMessage;
|
||||||
|
|
||||||
/// The height of the tooltip's [child].
|
/// The height of the tooltip's [child].
|
||||||
///
|
///
|
||||||
@ -131,12 +157,13 @@ class Tooltip extends StatefulWidget {
|
|||||||
/// direction.
|
/// direction.
|
||||||
final bool? preferBelow;
|
final bool? preferBelow;
|
||||||
|
|
||||||
/// Whether the tooltip's [message] should be excluded from the semantics
|
/// Whether the tooltip's [message] or [richMessage] should be excluded from
|
||||||
/// tree.
|
/// the semantics tree.
|
||||||
///
|
///
|
||||||
/// Defaults to false. A tooltip will add a [Semantics] label that is set to
|
/// Defaults to false. A tooltip will add a [Semantics] label that is set to
|
||||||
/// [Tooltip.message]. Set this property to true if the app is going to
|
/// [Tooltip.message] if non-null, or the plain text value of
|
||||||
/// provide its own custom semantics label.
|
/// [Tooltip.richMessage] otherwise. Set this property to true if the app is
|
||||||
|
/// going to provide its own custom semantics label.
|
||||||
final bool? excludeFromSemantics;
|
final bool? excludeFromSemantics;
|
||||||
|
|
||||||
/// The widget below this widget in the tree.
|
/// The widget below this widget in the tree.
|
||||||
@ -241,7 +268,18 @@ class Tooltip extends StatefulWidget {
|
|||||||
@override
|
@override
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
super.debugFillProperties(properties);
|
super.debugFillProperties(properties);
|
||||||
properties.add(StringProperty('message', message, showName: false));
|
properties.add(StringProperty(
|
||||||
|
'message',
|
||||||
|
message,
|
||||||
|
showName: message == null,
|
||||||
|
defaultValue: message == null ? null : kNoDefaultValue,
|
||||||
|
));
|
||||||
|
properties.add(StringProperty(
|
||||||
|
'richMessage',
|
||||||
|
richMessage?.toPlainText(),
|
||||||
|
showName: richMessage == null,
|
||||||
|
defaultValue: richMessage == null ? null : kNoDefaultValue,
|
||||||
|
));
|
||||||
properties.add(DoubleProperty('height', height, defaultValue: null));
|
properties.add(DoubleProperty('height', height, defaultValue: null));
|
||||||
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
|
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
|
||||||
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('margin', margin, defaultValue: null));
|
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('margin', margin, defaultValue: null));
|
||||||
@ -290,6 +328,11 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
|||||||
late bool _isConcealed;
|
late bool _isConcealed;
|
||||||
late bool _forceRemoval;
|
late bool _forceRemoval;
|
||||||
|
|
||||||
|
/// The plain text message for this tooltip.
|
||||||
|
///
|
||||||
|
/// This value will either come from [widget.message] or [widget.richMessage].
|
||||||
|
String get _tooltipMessage => widget.message ?? widget.richMessage!.toPlainText();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -428,7 +471,7 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
|||||||
)!;
|
)!;
|
||||||
overlayState.insert(_entry!);
|
overlayState.insert(_entry!);
|
||||||
}
|
}
|
||||||
SemanticsService.tooltip(widget.message);
|
SemanticsService.tooltip(_tooltipMessage);
|
||||||
_controller.forward();
|
_controller.forward();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -487,7 +530,7 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
|||||||
final Widget overlay = Directionality(
|
final Widget overlay = Directionality(
|
||||||
textDirection: Directionality.of(context),
|
textDirection: Directionality.of(context),
|
||||||
child: _TooltipOverlay(
|
child: _TooltipOverlay(
|
||||||
message: widget.message,
|
richMessage: widget.richMessage ?? TextSpan(text: widget.message),
|
||||||
height: height,
|
height: height,
|
||||||
padding: padding,
|
padding: padding,
|
||||||
margin: margin,
|
margin: margin,
|
||||||
@ -507,7 +550,7 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
|||||||
_entry = OverlayEntry(builder: (BuildContext context) => overlay);
|
_entry = OverlayEntry(builder: (BuildContext context) => overlay);
|
||||||
_isConcealed = false;
|
_isConcealed = false;
|
||||||
overlayState.insert(_entry!);
|
overlayState.insert(_entry!);
|
||||||
SemanticsService.tooltip(widget.message);
|
SemanticsService.tooltip(_tooltipMessage);
|
||||||
if (_mouseIsConnected) {
|
if (_mouseIsConnected) {
|
||||||
// Hovered tooltips shouldn't show more than one at once. For example, a chip with
|
// Hovered tooltips shouldn't show more than one at once. For example, a chip with
|
||||||
// a delete icon shouldn't show both the delete icon tooltip and the chip tooltip
|
// a delete icon shouldn't show both the delete icon tooltip and the chip tooltip
|
||||||
@ -580,7 +623,7 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
|||||||
// 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
|
||||||
// the empty black container so just return the wrapped child as is or
|
// the empty black container so just return the wrapped child as is or
|
||||||
// empty container if child is not specified.
|
// empty container if child is not specified.
|
||||||
if (widget.message.isEmpty) {
|
if (_tooltipMessage.isEmpty) {
|
||||||
return widget.child ?? const SizedBox();
|
return widget.child ?? const SizedBox();
|
||||||
}
|
}
|
||||||
assert(Overlay.of(context, debugRequiredFor: widget) != null);
|
assert(Overlay.of(context, debugRequiredFor: widget) != null);
|
||||||
@ -629,7 +672,9 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
|||||||
onTap: (triggerMode == TooltipTriggerMode.tap) ? _handlePress : null,
|
onTap: (triggerMode == TooltipTriggerMode.tap) ? _handlePress : null,
|
||||||
excludeFromSemantics: true,
|
excludeFromSemantics: true,
|
||||||
child: Semantics(
|
child: Semantics(
|
||||||
label: excludeFromSemantics ? null : widget.message,
|
label: excludeFromSemantics
|
||||||
|
? null
|
||||||
|
: _tooltipMessage,
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -700,8 +745,8 @@ class _TooltipPositionDelegate extends SingleChildLayoutDelegate {
|
|||||||
class _TooltipOverlay extends StatelessWidget {
|
class _TooltipOverlay extends StatelessWidget {
|
||||||
const _TooltipOverlay({
|
const _TooltipOverlay({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.message,
|
|
||||||
required this.height,
|
required this.height,
|
||||||
|
required this.richMessage,
|
||||||
this.padding,
|
this.padding,
|
||||||
this.margin,
|
this.margin,
|
||||||
this.decoration,
|
this.decoration,
|
||||||
@ -714,7 +759,7 @@ class _TooltipOverlay extends StatelessWidget {
|
|||||||
this.onExit,
|
this.onExit,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final String message;
|
final InlineSpan richMessage;
|
||||||
final double height;
|
final double height;
|
||||||
final EdgeInsetsGeometry? padding;
|
final EdgeInsetsGeometry? padding;
|
||||||
final EdgeInsetsGeometry? margin;
|
final EdgeInsetsGeometry? margin;
|
||||||
@ -743,8 +788,8 @@ class _TooltipOverlay extends StatelessWidget {
|
|||||||
child: Center(
|
child: Center(
|
||||||
widthFactor: 1.0,
|
widthFactor: 1.0,
|
||||||
heightFactor: 1.0,
|
heightFactor: 1.0,
|
||||||
child: Text(
|
child: Text.rich(
|
||||||
message,
|
richMessage,
|
||||||
style: textStyle,
|
style: textStyle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1319,6 +1319,63 @@ void main() {
|
|||||||
expect(tip.size.height, equals(56.0));
|
expect(tip.size.height, equals(56.0));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Tooltip text displays with richMessage', (WidgetTester tester) async {
|
||||||
|
final GlobalKey key = GlobalKey();
|
||||||
|
const String textSpan1Text = 'I am a rich tooltip message. ';
|
||||||
|
const String textSpan2Text = 'I am another span of a rich tooltip message';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Tooltip(
|
||||||
|
key: key,
|
||||||
|
richMessage: const TextSpan(
|
||||||
|
text: textSpan1Text,
|
||||||
|
children: <InlineSpan>[
|
||||||
|
TextSpan(
|
||||||
|
text: textSpan2Text,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
width: 100.0,
|
||||||
|
height: 100.0,
|
||||||
|
color: Colors.green[500],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_ensureTooltipVisible(key);
|
||||||
|
await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
|
||||||
|
|
||||||
|
final RichText richText = tester.widget<RichText>(find.byType(RichText));
|
||||||
|
expect(richText.text.toPlainText(), equals('$textSpan1Text$textSpan2Text'));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Tooltip throws assertion error when both message and richMessage are specified', (WidgetTester tester) async {
|
||||||
|
expect(
|
||||||
|
() {
|
||||||
|
MaterialApp(
|
||||||
|
home: Tooltip(
|
||||||
|
message: 'I am a tooltip message.',
|
||||||
|
richMessage: const TextSpan(
|
||||||
|
text: 'I am a rich tooltip.',
|
||||||
|
children: <InlineSpan>[
|
||||||
|
TextSpan(
|
||||||
|
text: 'I am another span of a rich tooltip.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
width: 100.0,
|
||||||
|
height: 100.0,
|
||||||
|
color: Colors.green[500],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
throwsA(const TypeMatcher<AssertionError>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Haptic feedback', (WidgetTester tester) async {
|
testWidgets('Haptic feedback', (WidgetTester tester) async {
|
||||||
final FeedbackTester feedback = FeedbackTester();
|
final FeedbackTester feedback = FeedbackTester();
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
@ -1476,6 +1533,28 @@ void main() {
|
|||||||
'"message"',
|
'"message"',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
testWidgets('default Tooltip debugFillProperties with richMessage', (WidgetTester tester) async {
|
||||||
|
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
||||||
|
|
||||||
|
const Tooltip(
|
||||||
|
richMessage: TextSpan(
|
||||||
|
text: 'This is a ',
|
||||||
|
children: <InlineSpan>[
|
||||||
|
TextSpan(
|
||||||
|
text: 'richMessage',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).debugFillProperties(builder);
|
||||||
|
|
||||||
|
final List<String> description = builder.properties
|
||||||
|
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
|
||||||
|
.map((DiagnosticsNode node) => node.toString()).toList();
|
||||||
|
|
||||||
|
expect(description, <String>[
|
||||||
|
'"This is a richMessage"',
|
||||||
|
]);
|
||||||
|
});
|
||||||
testWidgets('Tooltip implements debugFillProperties', (WidgetTester tester) async {
|
testWidgets('Tooltip implements debugFillProperties', (WidgetTester tester) async {
|
||||||
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
||||||
|
|
||||||
|
@ -841,9 +841,10 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
|
|||||||
|
|
||||||
final Widget widget = element.widget;
|
final Widget widget = element.widget;
|
||||||
if (widget is Tooltip) {
|
if (widget is Tooltip) {
|
||||||
final Iterable<Element> matches = find.byTooltip(widget.message).evaluate();
|
final String message = widget.message ?? widget.richMessage!.toPlainText();
|
||||||
|
final Iterable<Element> matches = find.byTooltip(message).evaluate();
|
||||||
if (matches.length == 1) {
|
if (matches.length == 1) {
|
||||||
printToConsole(" find.byTooltip('${widget.message}')");
|
printToConsole(" find.byTooltip('$message')");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user