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) {
|
||||
result = Tooltip(
|
||||
message: tooltip!,
|
||||
message: tooltip,
|
||||
child: result,
|
||||
);
|
||||
}
|
||||
|
@ -323,7 +323,7 @@ class IconButton extends StatelessWidget {
|
||||
|
||||
if (tooltip != null) {
|
||||
result = Tooltip(
|
||||
message: tooltip!,
|
||||
message: tooltip,
|
||||
child: result,
|
||||
);
|
||||
}
|
||||
|
@ -759,9 +759,7 @@ class _NavigationBarDestinationTooltip extends StatelessWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
/// The text that is rendered in the tooltip when it appears.
|
||||
///
|
||||
/// If [message] is null, no tooltip will be used.
|
||||
final String? message;
|
||||
final String message;
|
||||
|
||||
/// The widget that, when pressed, will show a tooltip.
|
||||
final Widget child;
|
||||
@ -772,7 +770,7 @@ class _NavigationBarDestinationTooltip extends StatelessWidget {
|
||||
return child;
|
||||
}
|
||||
return Tooltip(
|
||||
message: message!,
|
||||
message: message,
|
||||
// TODO(johnsonmh): Make this value configurable/themable.
|
||||
verticalOffset: 42,
|
||||
excludeFromSemantics: true,
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/rendering.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 **
|
||||
/// {@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:
|
||||
///
|
||||
/// * <https://material.io/design/components/tooltips.html>
|
||||
@ -70,9 +80,12 @@ class Tooltip extends StatefulWidget {
|
||||
///
|
||||
/// All parameters that are defined in the constructor will
|
||||
/// override the default values _and_ the values in [TooltipTheme.of].
|
||||
///
|
||||
/// Only one of [message] and [richMessage] may be non-null.
|
||||
const Tooltip({
|
||||
Key? key,
|
||||
required this.message,
|
||||
this.message,
|
||||
this.richMessage,
|
||||
this.height,
|
||||
this.padding,
|
||||
this.margin,
|
||||
@ -86,11 +99,24 @@ class Tooltip extends StatefulWidget {
|
||||
this.child,
|
||||
this.triggerMode,
|
||||
this.enableFeedback,
|
||||
}) : assert(message != null),
|
||||
super(key: key);
|
||||
}) : 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);
|
||||
|
||||
/// 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].
|
||||
///
|
||||
@ -131,12 +157,13 @@ class Tooltip extends StatefulWidget {
|
||||
/// direction.
|
||||
final bool? preferBelow;
|
||||
|
||||
/// Whether the tooltip's [message] should be excluded from the semantics
|
||||
/// tree.
|
||||
/// Whether the tooltip's [message] or [richMessage] should be excluded from
|
||||
/// the semantics tree.
|
||||
///
|
||||
/// 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
|
||||
/// provide its own custom semantics label.
|
||||
/// [Tooltip.message] if non-null, or the plain text value of
|
||||
/// [Tooltip.richMessage] otherwise. Set this property to true if the app is
|
||||
/// going to provide its own custom semantics label.
|
||||
final bool? excludeFromSemantics;
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
@ -241,7 +268,18 @@ class Tooltip extends StatefulWidget {
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder 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(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, 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 _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
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -428,7 +471,7 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
||||
)!;
|
||||
overlayState.insert(_entry!);
|
||||
}
|
||||
SemanticsService.tooltip(widget.message);
|
||||
SemanticsService.tooltip(_tooltipMessage);
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@ -487,7 +530,7 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
||||
final Widget overlay = Directionality(
|
||||
textDirection: Directionality.of(context),
|
||||
child: _TooltipOverlay(
|
||||
message: widget.message,
|
||||
richMessage: widget.richMessage ?? TextSpan(text: widget.message),
|
||||
height: height,
|
||||
padding: padding,
|
||||
margin: margin,
|
||||
@ -507,7 +550,7 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
||||
_entry = OverlayEntry(builder: (BuildContext context) => overlay);
|
||||
_isConcealed = false;
|
||||
overlayState.insert(_entry!);
|
||||
SemanticsService.tooltip(widget.message);
|
||||
SemanticsService.tooltip(_tooltipMessage);
|
||||
if (_mouseIsConnected) {
|
||||
// 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
|
||||
@ -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
|
||||
// the empty black container so just return the wrapped child as is or
|
||||
// empty container if child is not specified.
|
||||
if (widget.message.isEmpty) {
|
||||
if (_tooltipMessage.isEmpty) {
|
||||
return widget.child ?? const SizedBox();
|
||||
}
|
||||
assert(Overlay.of(context, debugRequiredFor: widget) != null);
|
||||
@ -629,7 +672,9 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
||||
onTap: (triggerMode == TooltipTriggerMode.tap) ? _handlePress : null,
|
||||
excludeFromSemantics: true,
|
||||
child: Semantics(
|
||||
label: excludeFromSemantics ? null : widget.message,
|
||||
label: excludeFromSemantics
|
||||
? null
|
||||
: _tooltipMessage,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
@ -700,8 +745,8 @@ class _TooltipPositionDelegate extends SingleChildLayoutDelegate {
|
||||
class _TooltipOverlay extends StatelessWidget {
|
||||
const _TooltipOverlay({
|
||||
Key? key,
|
||||
required this.message,
|
||||
required this.height,
|
||||
required this.richMessage,
|
||||
this.padding,
|
||||
this.margin,
|
||||
this.decoration,
|
||||
@ -714,7 +759,7 @@ class _TooltipOverlay extends StatelessWidget {
|
||||
this.onExit,
|
||||
}) : super(key: key);
|
||||
|
||||
final String message;
|
||||
final InlineSpan richMessage;
|
||||
final double height;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final EdgeInsetsGeometry? margin;
|
||||
@ -743,8 +788,8 @@ class _TooltipOverlay extends StatelessWidget {
|
||||
child: Center(
|
||||
widthFactor: 1.0,
|
||||
heightFactor: 1.0,
|
||||
child: Text(
|
||||
message,
|
||||
child: Text.rich(
|
||||
richMessage,
|
||||
style: textStyle,
|
||||
),
|
||||
),
|
||||
|
@ -1319,6 +1319,63 @@ void main() {
|
||||
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 {
|
||||
final FeedbackTester feedback = FeedbackTester();
|
||||
await tester.pumpWidget(
|
||||
@ -1476,6 +1533,28 @@ void main() {
|
||||
'"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 {
|
||||
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
||||
|
||||
|
@ -841,9 +841,10 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
|
||||
|
||||
final Widget widget = element.widget;
|
||||
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) {
|
||||
printToConsole(" find.byTooltip('${widget.message}')");
|
||||
printToConsole(" find.byTooltip('$message')");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user