Skip to content

Commit

Permalink
[web] Add 'flt-semantics-identifier' attribute to semantics nodes (#5…
Browse files Browse the repository at this point in the history
…3278)

Make [`Semantics(identifier: '...')`](https://api.flutter.dev/flutter/semantics/SemanticsProperties/identifier.html) useful on the web. This PR plugs the Semantics `identifier` property as an HTML attribute `semantics-identifier` onto semantics elements.

This is useful in some scenarios:
- In testing to check if a certain semantics node has made it to the page ([example](flutter/flutter#97455)).
- In apps and/or packages to be able to lookup the DOM element that corresponds to a certain semantics node ([example](flutter/packages#6711)).

Fixes flutter/flutter#97455
  • Loading branch information
mdebbar committed Jun 20, 2024
1 parent 1ad7953 commit 241f8ea
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 0 deletions.
32 changes: 32 additions & 0 deletions lib/web_ui/lib/src/engine/semantics/semantics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,18 @@ abstract class PrimaryRoleManager {
for (final RoleManager secondaryRole in secondaryRoles) {
secondaryRole.update();
}

if (semanticsObject.isIdentifierDirty) {
_updateIdentifier();
}
}

void _updateIdentifier() {
if (semanticsObject.hasIdentifier) {
setAttribute('flt-semantics-identifier', semanticsObject.identifier!);
} else {
removeAttribute('flt-semantics-identifier');
}
}

/// Whether this role manager was disposed of.
Expand Down Expand Up @@ -1119,6 +1131,21 @@ class SemanticsObject {
_dirtyFields |= _headingLevelIndex;
}

/// See [ui.SemanticsUpdateBuilder.updateNode].
String? get identifier => _identifier;
String? _identifier;

bool get hasIdentifier => _identifier != null && _identifier!.isNotEmpty;

static const int _identifierIndex = 1 << 25;

/// 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 @@ -1278,6 +1305,11 @@ class SemanticsObject {
_markFlagsDirty();
}

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

if (_value != update.value) {
_value = update.value;
_markValueDirty();
Expand Down
65 changes: 65 additions & 0 deletions lib/web_ui/test/engine/semantics/semantics_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ void runSemanticsTests() {
group('longestIncreasingSubsequence', () {
_testLongestIncreasingSubsequence();
});
group(PrimaryRoleManager, () {
_testPrimaryRoleManager();
});
group('Role managers', () {
_testRoleManagerLifecycle();
});
Expand Down Expand Up @@ -107,6 +110,68 @@ void runSemanticsTests() {
});
}

void _testPrimaryRoleManager() {
test('Sets id and flt-semantics-identifier on the element', () {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;

final SemanticsTester tester = SemanticsTester(owner());
tester.updateNode(
id: 0,
children: <SemanticsNodeUpdate>[
tester.updateNode(id: 372),
tester.updateNode(id: 599),
],
);
tester.apply();

tester.expectSemantics('''
<sem id="flt-semantic-node-0">
<sem-c>
<sem id="flt-semantic-node-372"></sem>
<sem id="flt-semantic-node-599"></sem>
</sem-c>
</sem>''');

tester.updateNode(
id: 0,
children: <SemanticsNodeUpdate>[
tester.updateNode(id: 372, identifier: 'test-id-123'),
tester.updateNode(id: 599),
],
);
tester.apply();

tester.expectSemantics('''
<sem id="flt-semantic-node-0">
<sem-c>
<sem id="flt-semantic-node-372" flt-semantics-identifier="test-id-123"></sem>
<sem id="flt-semantic-node-599"></sem>
</sem-c>
</sem>''');

tester.updateNode(
id: 0,
children: <SemanticsNodeUpdate>[
tester.updateNode(id: 372),
tester.updateNode(id: 599, identifier: 'test-id-211'),
tester.updateNode(id: 612, identifier: 'test-id-333'),
],
);
tester.apply();

tester.expectSemantics('''
<sem id="flt-semantic-node-0">
<sem-c>
<sem id="flt-semantic-node-372"></sem>
<sem id="flt-semantic-node-599" flt-semantics-identifier="test-id-211"></sem>
<sem id="flt-semantic-node-612" flt-semantics-identifier="test-id-333"></sem>
</sem-c>
</sem>''');
});
}

void _testRoleManagerLifecycle() {
test('Secondary role managers are added upon node initialization', () {
semantics()
Expand Down

0 comments on commit 241f8ea

Please sign in to comment.