From 241f8ea5eb96be99dcf565fc496bf31b2dacac06 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Thu, 20 Jun 2024 19:23:15 -0400 Subject: [PATCH] [web] Add 'flt-semantics-identifier' attribute to semantics nodes (#53278) 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](https://github.com/flutter/flutter/issues/97455)). - In apps and/or packages to be able to lookup the DOM element that corresponds to a certain semantics node ([example](https://github.com/flutter/packages/pull/6711)). Fixes https://github.com/flutter/flutter/issues/97455 --- .../lib/src/engine/semantics/semantics.dart | 32 +++++++++ .../test/engine/semantics/semantics_test.dart | 65 +++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/lib/web_ui/lib/src/engine/semantics/semantics.dart b/lib/web_ui/lib/src/engine/semantics/semantics.dart index 0fbd8596e9f00..c48851d9836a2 100644 --- a/lib/web_ui/lib/src/engine/semantics/semantics.dart +++ b/lib/web_ui/lib/src/engine/semantics/semantics.dart @@ -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. @@ -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; @@ -1278,6 +1305,11 @@ class SemanticsObject { _markFlagsDirty(); } + if (_identifier != update.identifier) { + _identifier = update.identifier; + _markIdentifierDirty(); + } + if (_value != update.value) { _value = update.value; _markValueDirty(); diff --git a/lib/web_ui/test/engine/semantics/semantics_test.dart b/lib/web_ui/test/engine/semantics/semantics_test.dart index c89472bbfa826..64fa0a54fee44 100644 --- a/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -48,6 +48,9 @@ void runSemanticsTests() { group('longestIncreasingSubsequence', () { _testLongestIncreasingSubsequence(); }); + group(PrimaryRoleManager, () { + _testPrimaryRoleManager(); + }); group('Role managers', () { _testRoleManagerLifecycle(); }); @@ -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: [ + tester.updateNode(id: 372), + tester.updateNode(id: 599), + ], + ); + tester.apply(); + + tester.expectSemantics(''' + + + + + +'''); + + tester.updateNode( + id: 0, + children: [ + tester.updateNode(id: 372, identifier: 'test-id-123'), + tester.updateNode(id: 599), + ], + ); + tester.apply(); + + tester.expectSemantics(''' + + + + + +'''); + + tester.updateNode( + id: 0, + children: [ + tester.updateNode(id: 372), + tester.updateNode(id: 599, identifier: 'test-id-211'), + tester.updateNode(id: 612, identifier: 'test-id-333'), + ], + ); + tester.apply(); + + tester.expectSemantics(''' + + + + + + +'''); + }); +} + void _testRoleManagerLifecycle() { test('Secondary role managers are added upon node initialization', () { semantics()