diff --git a/lib/web_ui/lib/src/engine/semantics/incrementable.dart b/lib/web_ui/lib/src/engine/semantics/incrementable.dart index 12d4468cd2047..35d4e12547db3 100644 --- a/lib/web_ui/lib/src/engine/semantics/incrementable.dart +++ b/lib/web_ui/lib/src/engine/semantics/incrementable.dart @@ -28,7 +28,10 @@ class Incrementable extends PrimaryRoleManager { // the one being focused on, but the internal `` element. addLiveRegion(); addRouteName(); - addLabelAndValue(preferredRepresentation: LabelRepresentation.ariaLabel); + addLabelAndValue( + preferredRepresentation: LabelRepresentation.ariaLabel, + labelSources: _incrementableLabelSources, + ); append(_element); _element.type = 'range'; @@ -60,6 +63,12 @@ class Incrementable extends PrimaryRoleManager { _focusManager.manage(semanticsObject.id, _element); } + // The incrementable role manager has custom reporting of the semantics value. + // We do not need to also render it as a label. + static final Set _incrementableLabelSources = LabelAndValue + .allLabelSources + .difference({LabelSource.value}); + @override bool focusAsRouteDefault() { _element.focus(); 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 9601074785b21..e96441c7d3cc6 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 @@ -425,14 +425,33 @@ final class SizedSpanRepresentation extends LabelRepresentationBehavior { DomElement get focusTarget => _domText; } +/// The source of the label attribute for a semantic node. +enum LabelSource { + /// The label is provided by the [SemanticsObject.label] property. + label, + + /// The label is provided by the [SemanticsObject.value] property. + value, + + /// The label is provided by the [SemanticsObject.hint] property. + hint, + + /// The label is provided by the [SemanticsObject.tooltip] property. + tooltip, +} + /// Renders [SemanticsObject.label] and/or [SemanticsObject.value] to the semantics DOM. /// /// The value is not always rendered. Some semantics nodes correspond to /// interactive controls. In such case the value is reported via that element's /// `value` attribute rather than rendering it separately. class LabelAndValue extends RoleManager { - LabelAndValue(SemanticsObject semanticsObject, PrimaryRoleManager owner, { required this.preferredRepresentation }) - : super(Role.labelAndValue, semanticsObject, owner); + LabelAndValue( + SemanticsObject semanticsObject, + PrimaryRoleManager owner, { + required this.preferredRepresentation, + this.labelSources = allLabelSources, + }) : super(Role.labelAndValue, semanticsObject, owner); /// The preferred representation of the label in the DOM. /// @@ -443,6 +462,17 @@ class LabelAndValue extends RoleManager { /// instead. LabelRepresentation preferredRepresentation; + /// The sources of the label that are allowed to be used. + final Set labelSources; + + /// All possible sources of the label. + static const Set allLabelSources = { + LabelSource.label, + LabelSource.value, + LabelSource.hint, + LabelSource.tooltip, + }; + @override void update() { final String? computedLabel = _computeLabel(); @@ -477,20 +507,34 @@ class LabelAndValue extends RoleManager { return representation; } + String? get _label => + labelSources.contains(LabelSource.label) && semanticsObject.hasLabel + ? semanticsObject.label + : null; + + String? get _value => + labelSources.contains(LabelSource.value) && semanticsObject.hasValue + ? semanticsObject.value + : null; + + String? get _tooltip => + labelSources.contains(LabelSource.tooltip) && semanticsObject.hasTooltip + ? semanticsObject.tooltip + : null; + + String? get _hint => + labelSources.contains(LabelSource.hint) ? semanticsObject.hint : null; + /// Computes the final label to be assigned to the node. /// /// The label is a concatenation of tooltip, label, hint, and value, whichever - /// combination is present. + /// combination is present and allowed by [labelSources]. String? _computeLabel() { - // 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 = !semanticsObject.isIncrementable && semanticsObject.hasValue; - return computeDomSemanticsLabel( - tooltip: semanticsObject.hasTooltip ? semanticsObject.tooltip : null, - label: semanticsObject.hasLabel ? semanticsObject.label : null, - hint: semanticsObject.hint, - value: shouldDisplayValue ? semanticsObject.value : null, + tooltip: _tooltip, + label: _label, + hint: _hint, + value: _value, ); } diff --git a/lib/web_ui/lib/src/engine/semantics/link.dart b/lib/web_ui/lib/src/engine/semantics/link.dart index 87a2a47a0759c..0f41695f52326 100644 --- a/lib/web_ui/lib/src/engine/semantics/link.dart +++ b/lib/web_ui/lib/src/engine/semantics/link.dart @@ -11,16 +11,22 @@ class Link extends PrimaryRoleManager { PrimaryRole.link, semanticsObject, preferredLabelRepresentation: LabelRepresentation.domText, + labelSources: _linkLabelSources, ) { addTappable(); } + // The semantics value is consumed by the [Link] role manager, so there's no + // need to also render it as a label. + static final Set _linkLabelSources = LabelAndValue.allLabelSources + .difference({LabelSource.value}); + @override DomElement createElement() { final DomElement element = domDocument.createElement('a'); - // TODO(chunhtai): Fill in the real link once the framework sends entire uri. - // https://github.com/flutter/flutter/issues/102535. - element.setAttribute('href', '#'); + if (semanticsObject.hasValue) { + element.setAttribute('href', semanticsObject.value!); + } element.style.display = 'block'; return element; } diff --git a/lib/web_ui/lib/src/engine/semantics/semantics.dart b/lib/web_ui/lib/src/engine/semantics/semantics.dart index 505531afcc080..8381fd739c329 100644 --- a/lib/web_ui/lib/src/engine/semantics/semantics.dart +++ b/lib/web_ui/lib/src/engine/semantics/semantics.dart @@ -434,12 +434,20 @@ abstract class PrimaryRoleManager { /// /// If `labelRepresentation` is true, configures the [LabelAndValue] role with /// [LabelAndValue.labelRepresentation] set to true. - PrimaryRoleManager.withBasics(this.role, this.semanticsObject, { required LabelRepresentation preferredLabelRepresentation }) { + PrimaryRoleManager.withBasics( + this.role, + this.semanticsObject, { + required LabelRepresentation preferredLabelRepresentation, + Set labelSources = LabelAndValue.allLabelSources, + }) { element = _initElement(createElement(), semanticsObject); addFocusManagement(); addLiveRegion(); addRouteName(); - addLabelAndValue(preferredRepresentation: preferredLabelRepresentation); + addLabelAndValue( + preferredRepresentation: preferredLabelRepresentation, + labelSources: labelSources, + ); } /// Initializes a blank role for a [semanticsObject]. @@ -480,6 +488,10 @@ abstract class PrimaryRoleManager { ..overflow = 'visible'; element.setAttribute('id', 'flt-semantic-node-${semanticsObject.id}'); + if (semanticsObject.hasIdentifier) { + element.setAttribute('semantic-identifier', semanticsObject.identifier!); + } + // The root node has some properties that other nodes do not. if (semanticsObject.id == 0 && !configuration.debugShowSemanticsNodes) { // Make all semantics transparent. Use `filter` instead of `opacity` @@ -562,8 +574,16 @@ abstract class PrimaryRoleManager { LabelAndValue? _labelAndValue; /// Adds generic label features. - void addLabelAndValue({ required LabelRepresentation preferredRepresentation }) { - addSecondaryRole(_labelAndValue = LabelAndValue(semanticsObject, this, preferredRepresentation: preferredRepresentation)); + void addLabelAndValue({ + required LabelRepresentation preferredRepresentation, + Set labelSources = LabelAndValue.allLabelSources, + }) { + addSecondaryRole(_labelAndValue = LabelAndValue( + semanticsObject, + this, + preferredRepresentation: preferredRepresentation, + labelSources: labelSources, + )); } /// Adds generic functionality for handling taps and clicks. @@ -1097,6 +1117,21 @@ class SemanticsObject { _dirtyFields |= _platformViewIdIndex; } + /// See [ui.SemanticsUpdateBuilder.updateNode]. + String? get identifier => _identifier; + String? _identifier; + + bool get hasIdentifier => _identifier != null && _identifier!.isNotEmpty; + + static const int _identifierIndex = 1 << 24; + + /// Whether the [identifier] field has been updated but has not been + /// applied to the DOM yet. + bool get isIdentifierDirty => _isDirty(_identifierIndex); + void _markIdentifierDirty() { + _dirtyFields |= _identifierIndex; + } + /// A unique permanent identifier of the semantics node in the tree. final int id; @@ -1253,6 +1288,11 @@ class SemanticsObject { _markFlagsDirty(); } + if (_identifier != update.identifier) { + _identifier = update.identifier; + _markIdentifierDirty(); + } + if (_value != update.value) { _value = update.value; _markValueDirty();