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();