From e094ce6f340ea84ee18be82300482da4ef705407 Mon Sep 17 00:00:00 2001 From: chunhtai <47866232+chunhtai@users.noreply.github.com> Date: Fri, 1 Oct 2021 11:18:02 -0700 Subject: [PATCH] Add web support for tooltip (#28966) --- .../src/engine/semantics/label_and_value.dart | 9 ++- .../lib/src/engine/semantics/semantics.dart | 23 +++++++- .../test/engine/semantics/semantics_test.dart | 56 ++++++++++++++++++- 3 files changed, 84 insertions(+), 4 deletions(-) diff --git a/lib/web_ui/lib/src/engine/semantics/label_and_value.dart b/lib/web_ui/lib/src/engine/semantics/label_and_value.dart index a9fab80ce7820..e7229b89ba00b 100644 --- a/lib/web_ui/lib/src/engine/semantics/label_and_value.dart +++ b/lib/web_ui/lib/src/engine/semantics/label_and_value.dart @@ -52,17 +52,24 @@ class LabelAndValue extends RoleManager { void update() { final bool hasValue = semanticsObject.hasValue; final bool hasLabel = semanticsObject.hasLabel; + final bool hasTooltip = semanticsObject.hasTooltip; // If the node is incrementable the value is reported to the browser via // the respective role manager. We do not need to also render it again here. final bool shouldDisplayValue = hasValue && !semanticsObject.isIncrementable; - if (!hasLabel && !shouldDisplayValue) { + if (!hasLabel && !shouldDisplayValue && !hasTooltip) { _cleanUpDom(); return; } final StringBuffer combinedValue = StringBuffer(); + if (hasTooltip) { + combinedValue.write(semanticsObject.tooltip); + if (hasLabel || shouldDisplayValue) { + combinedValue.write('\n'); + } + } if (hasLabel) { combinedValue.write(semanticsObject.label); if (shouldDisplayValue) { diff --git a/lib/web_ui/lib/src/engine/semantics/semantics.dart b/lib/web_ui/lib/src/engine/semantics/semantics.dart index fbd06b16fdafa..db59176cfa0d1 100644 --- a/lib/web_ui/lib/src/engine/semantics/semantics.dart +++ b/lib/web_ui/lib/src/engine/semantics/semantics.dart @@ -600,6 +600,22 @@ class SemanticsObject { _dirtyFields |= _additionalActionsIndex; } + /// See [ui.SemanticsUpdateBuilder.updateNode]. + String? get tooltip => _tooltip; + String? _tooltip; + + /// Whether this object contains a non-empty tooltip. + bool get hasTooltip => _tooltip != null && _tooltip!.isNotEmpty; + + static const int _tooltipIndex = 1 << 22; + + /// Whether the [tooltip] field has been updated but has not been + /// applied to the DOM yet. + bool get isTooltipDirty => _isDirty(_tooltipIndex); + void _markTooltipDirty() { + _dirtyFields |= _tooltipIndex; + } + /// A unique permanent identifier of the semantics node in the tree. final int id; @@ -812,6 +828,11 @@ class SemanticsObject { _markDecreasedValueDirty(); } + if (_tooltip != update.tooltip) { + _tooltip = update.tooltip; + _markTooltipDirty(); + } + if (_textDirection != update.textDirection) { _textDirection = update.textDirection; _markTextDirectionDirty(); @@ -882,7 +903,7 @@ class SemanticsObject { /// Detects the roles that this semantics object corresponds to and manages /// the lifecycles of [SemanticsObjectRole] objects. void _updateRoles() { - _updateRole(Role.labelAndValue, (hasLabel || hasValue) && !isTextField && !isVisualOnly); + _updateRole(Role.labelAndValue, (hasLabel || hasValue || hasTooltip) && !isTextField && !isVisualOnly); _updateRole(Role.textField, isTextField); final bool shouldUseTappableRole = diff --git a/lib/web_ui/test/engine/semantics/semantics_test.dart b/lib/web_ui/test/engine/semantics/semantics_test.dart index d3ba3c33c6faf..a4a7299b77f52 100644 --- a/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -131,7 +131,7 @@ void _testEngineSemanticsOwner() { expect(placeholder.isConnected, isFalse); }); - void renderLabel(String label) { + void renderSemantics({String? label, String? tooltip}) { final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder(); updateNode( builder, @@ -148,13 +148,18 @@ void _testEngineSemanticsOwner() { id: 1, actions: 0, flags: 0, - label: label, + label: label ?? '', + tooltip: tooltip ?? '', transform: Matrix4.identity().toFloat64(), rect: const ui.Rect.fromLTRB(0, 0, 20, 20), ); semantics().updateSemantics(builder.build()); } + void renderLabel(String label) { + renderSemantics(label: label); + } + test('produces an aria-label', () async { semantics().semanticsEnabled = true; @@ -202,6 +207,53 @@ void _testEngineSemanticsOwner() { semantics().semanticsEnabled = false; }); + test('tooltip is part of label', () async { + semantics().semanticsEnabled = true; + + // Create + renderSemantics(tooltip: 'tooltip'); + + final Map tree = semantics().debugSemanticsTree!; + expect(tree.length, 2); + expect(tree[0]!.id, 0); + expect(tree[0]!.element.tagName.toLowerCase(), 'flt-semantics'); + expect(tree[1]!.id, 1); + expect(tree[1]!.tooltip, 'tooltip'); + + expectSemanticsTree(''' + + + + tooltip + + +'''); + + // Update + renderSemantics(label: 'Hello', tooltip: 'tooltip'); + + expectSemanticsTree(''' + + + + tooltip\nHello + + +'''); + + // Remove + renderSemantics(); + + expectSemanticsTree(''' + + + + +'''); + + semantics().semanticsEnabled = false; + }); + test('clears semantics tree when disabled', () { expect(semantics().debugSemanticsTree, isEmpty); semantics().semanticsEnabled = true;