Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[web] Set the correct href on semantic links #52720

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion lib/web_ui/lib/src/engine/semantics/incrementable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ class Incrementable extends PrimaryRoleManager {
// the one being focused on, but the internal `<input>` element.
addLiveRegion();
addRouteName();
addLabelAndValue(preferredRepresentation: LabelRepresentation.ariaLabel);
addLabelAndValue(
preferredRepresentation: LabelRepresentation.ariaLabel,
labelSources: _incrementableLabelSources,
);

append(_element);
_element.type = 'range';
Expand Down Expand Up @@ -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<LabelSource> _incrementableLabelSources = LabelAndValue
.allLabelSources
.difference(<LabelSource>{LabelSource.value});

@override
bool focusAsRouteDefault() {
_element.focus();
Expand Down
66 changes: 55 additions & 11 deletions lib/web_ui/lib/src/engine/semantics/label_and_value.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -443,6 +462,17 @@ class LabelAndValue extends RoleManager {
/// instead.
LabelRepresentation preferredRepresentation;

/// The sources of the label that are allowed to be used.
final Set<LabelSource> labelSources;

/// All possible sources of the label.
static const Set<LabelSource> allLabelSources = <LabelSource>{
LabelSource.label,
LabelSource.value,
LabelSource.hint,
LabelSource.tooltip,
};

@override
void update() {
final String? computedLabel = _computeLabel();
Expand Down Expand Up @@ -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,
);
}

Expand Down
12 changes: 9 additions & 3 deletions lib/web_ui/lib/src/engine/semantics/link.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<LabelSource> _linkLabelSources = LabelAndValue.allLabelSources
.difference(<LabelSource>{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;
}
Expand Down
48 changes: 44 additions & 4 deletions lib/web_ui/lib/src/engine/semantics/semantics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<LabelSource> 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].
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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<LabelSource> labelSources = LabelAndValue.allLabelSources,
}) {
addSecondaryRole(_labelAndValue = LabelAndValue(
semanticsObject,
this,
preferredRepresentation: preferredRepresentation,
labelSources: labelSources,
));
}

/// Adds generic functionality for handling taps and clicks.
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -1253,6 +1288,11 @@ class SemanticsObject {
_markFlagsDirty();
}

if (_identifier != update.identifier) {
_identifier = update.identifier;
_markIdentifierDirty();
}

if (_value != update.value) {
_value = update.value;
_markValueDirty();
Expand Down