Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 388547a

Browse files
authored
Add Semantics Property linkUrl (#53507)
The new property allows the user to specify a URI for their semantics link node. It's plumbed through for both web and non-web engines, but it's only used in the web engine currently. It sets the `href` of the anchor element associated with semantics node. This is going to unlock better semantics support in the Link widget on web ([PR](flutter/packages#6711)). Framework counterpart: flutter/flutter#150639 Part of flutter/flutter#150263
1 parent 31fd415 commit 388547a

File tree

11 files changed

+93
-10
lines changed

11 files changed

+93
-10
lines changed

lib/ui/fixtures/ui_test.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ void sendSemanticsUpdate() {
233233
childrenInHitTestOrder: childrenInHitTestOrder,
234234
additionalActions: additionalActions,
235235
headingLevel: 0,
236+
linkUrl: '',
236237
);
237238
_semanticsUpdate(builder.build());
238239
}

lib/ui/semantics.dart

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,9 @@ abstract class SemanticsUpdateBuilder {
849849
/// inclusive. This attribute is only used for Web platform, and it will have
850850
/// no effect on other platforms.
851851
///
852+
/// The `linkUrl` describes the URI that this node links to. If the node is
853+
/// not a link, this should be an empty string.
854+
///
852855
/// See also:
853856
///
854857
/// * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/heading_role
@@ -888,6 +891,7 @@ abstract class SemanticsUpdateBuilder {
888891
required Int32List childrenInHitTestOrder,
889892
required Int32List additionalActions,
890893
int headingLevel = 0,
894+
String linkUrl = '',
891895
});
892896

893897
/// Update the custom semantics action associated with the given `id`.
@@ -959,6 +963,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1 implem
959963
required Int32List childrenInHitTestOrder,
960964
required Int32List additionalActions,
961965
int headingLevel = 0,
966+
String linkUrl = '',
962967
}) {
963968
assert(_matrix4IsValid(transform));
964969
assert (
@@ -1003,6 +1008,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1 implem
10031008
childrenInHitTestOrder,
10041009
additionalActions,
10051010
headingLevel,
1011+
linkUrl,
10061012
);
10071013
}
10081014
@Native<
@@ -1044,7 +1050,8 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1 implem
10441050
Handle,
10451051
Handle,
10461052
Handle,
1047-
Int32)>(symbol: 'SemanticsUpdateBuilder::updateNode')
1053+
Int32,
1054+
Handle)>(symbol: 'SemanticsUpdateBuilder::updateNode')
10481055
external void _updateNode(
10491056
int id,
10501057
int flags,
@@ -1082,7 +1089,8 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1 implem
10821089
Int32List childrenInTraversalOrder,
10831090
Int32List childrenInHitTestOrder,
10841091
Int32List additionalActions,
1085-
int headingLevel);
1092+
int headingLevel,
1093+
String linkUrl);
10861094

10871095
@override
10881096
void updateCustomAction({required int id, String? label, String? hint, int overrideId = -1}) {

lib/ui/semantics/semantics_node.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ struct SemanticsNode {
144144
std::vector<int32_t> childrenInHitTestOrder;
145145
std::vector<int32_t> customAccessibilityActions;
146146
int32_t headingLevel = 0;
147+
148+
std::string linkUrl;
147149
};
148150

149151
// Contains semantic nodes that need to be updated.

lib/ui/semantics/semantics_update_builder.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ void SemanticsUpdateBuilder::updateNode(
6767
const tonic::Int32List& childrenInTraversalOrder,
6868
const tonic::Int32List& childrenInHitTestOrder,
6969
const tonic::Int32List& localContextActions,
70-
int headingLevel) {
70+
int headingLevel,
71+
std::string linkUrl) {
7172
FML_CHECK(scrollChildren == 0 ||
7273
(scrollChildren > 0 && childrenInHitTestOrder.data()))
7374
<< "Semantics update contained scrollChildren but did not have "
@@ -121,6 +122,7 @@ void SemanticsUpdateBuilder::updateNode(
121122
nodes_[id] = node;
122123

123124
node.headingLevel = headingLevel;
125+
node.linkUrl = std::move(linkUrl);
124126
}
125127

126128
void SemanticsUpdateBuilder::updateCustomAction(int id,

lib/ui/semantics/semantics_update_builder.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ class SemanticsUpdateBuilder
6666
const tonic::Int32List& childrenInTraversalOrder,
6767
const tonic::Int32List& childrenInHitTestOrder,
6868
const tonic::Int32List& customAccessibilityActions,
69-
int headingLevel);
69+
int headingLevel,
70+
std::string linkUrl);
7071

7172
void updateCustomAction(int id,
7273
std::string label,

lib/web_ui/lib/semantics.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ class SemanticsUpdateBuilder {
287287
required Int32List childrenInHitTestOrder,
288288
required Int32List additionalActions,
289289
int headingLevel = 0,
290+
String? linkUrl,
290291
}) {
291292
if (transform.length != 16) {
292293
throw ArgumentError('transform argument must have 16 entries.');
@@ -326,6 +327,7 @@ class SemanticsUpdateBuilder {
326327
additionalActions: additionalActions,
327328
platformViewId: platformViewId,
328329
headingLevel: headingLevel,
330+
linkUrl: linkUrl,
329331
));
330332
}
331333

lib/web_ui/lib/src/engine/semantics/link.dart

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,23 @@ class Link extends PrimaryRoleManager {
1818
@override
1919
DomElement createElement() {
2020
final DomElement element = domDocument.createElement('a');
21-
// TODO(mdebbar): Fill in the real link once the framework sends entire uri.
22-
// https://github.com/flutter/flutter/issues/150263.
2321
element.style.display = 'block';
2422
return element;
2523
}
2624

25+
@override
26+
void update() {
27+
super.update();
28+
29+
if (semanticsObject.isLinkUrlDirty) {
30+
if (semanticsObject.hasLinkUrl) {
31+
element.setAttribute('href', semanticsObject.linkUrl!);
32+
} else {
33+
element.removeAttribute('href');
34+
}
35+
}
36+
}
37+
2738
@override
2839
bool focusAsRouteDefault() => focusable?.focusAsRouteDefault() ?? false;
2940
}

lib/web_ui/lib/src/engine/semantics/semantics.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ class SemanticsNodeUpdate {
234234
required this.childrenInHitTestOrder,
235235
required this.additionalActions,
236236
required this.headingLevel,
237+
this.linkUrl,
237238
});
238239

239240
/// See [ui.SemanticsUpdateBuilder.updateNode].
@@ -337,6 +338,9 @@ class SemanticsNodeUpdate {
337338

338339
/// See [ui.SemanticsUpdateBuilder.updateNode].
339340
final int headingLevel;
341+
342+
/// See [ui.SemanticsUpdateBuilder.updateNode].
343+
final String? linkUrl;
340344
}
341345

342346
/// Identifies [PrimaryRoleManager] implementations.
@@ -1146,6 +1150,22 @@ class SemanticsObject {
11461150
_dirtyFields |= _identifierIndex;
11471151
}
11481152

1153+
/// See [ui.SemanticsUpdateBuilder.updateNode].
1154+
String? get linkUrl => _linkUrl;
1155+
String? _linkUrl;
1156+
1157+
/// Whether this object contains a non-empty link URL.
1158+
bool get hasLinkUrl => _linkUrl != null && _linkUrl!.isNotEmpty;
1159+
1160+
static const int _linkUrlIndex = 1 << 26;
1161+
1162+
/// Whether the [linkUrl] field has been updated but has not been
1163+
/// applied to the DOM yet.
1164+
bool get isLinkUrlDirty => _isDirty(_linkUrlIndex);
1165+
void _markLinkUrlDirty() {
1166+
_dirtyFields |= _linkUrlIndex;
1167+
}
1168+
11491169
/// A unique permanent identifier of the semantics node in the tree.
11501170
final int id;
11511171

@@ -1445,6 +1465,11 @@ class SemanticsObject {
14451465
_markPlatformViewIdDirty();
14461466
}
14471467

1468+
if (_linkUrl != update.linkUrl) {
1469+
_linkUrl = update.linkUrl;
1470+
_markLinkUrlDirty();
1471+
}
1472+
14481473
// Apply updates to the DOM.
14491474
_updateRoles();
14501475

lib/web_ui/test/engine/semantics/semantics_test.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3603,6 +3603,28 @@ void _testLink() {
36033603
expect(object.element.tagName.toLowerCase(), 'a');
36043604
expect(object.element.hasAttribute('href'), isFalse);
36053605
});
3606+
3607+
test('link nodes with linkUrl set the href attribute', () {
3608+
semantics()
3609+
..debugOverrideTimestampFunction(() => _testTime)
3610+
..semanticsEnabled = true;
3611+
3612+
SemanticsObject pumpSemantics() {
3613+
final SemanticsTester tester = SemanticsTester(owner());
3614+
tester.updateNode(
3615+
id: 0,
3616+
isLink: true,
3617+
linkUrl: 'https://flutter.dev',
3618+
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
3619+
);
3620+
tester.apply();
3621+
return tester.getSemanticsObject(0);
3622+
}
3623+
3624+
final SemanticsObject object = pumpSemantics();
3625+
expect(object.element.tagName.toLowerCase(), 'a');
3626+
expect(object.element.getAttribute('href'), 'https://flutter.dev');
3627+
});
36063628
}
36073629

36083630
/// A facade in front of [ui.SemanticsUpdateBuilder.updateNode] that
@@ -3645,6 +3667,7 @@ void updateNode(
36453667
Int32List? childrenInHitTestOrder,
36463668
Int32List? additionalActions,
36473669
int headingLevel = 0,
3670+
String? linkUrl,
36483671
}) {
36493672
transform ??= Float64List.fromList(Matrix4.identity().storage);
36503673
childrenInTraversalOrder ??= Int32List(0);
@@ -3685,6 +3708,7 @@ void updateNode(
36853708
childrenInHitTestOrder: childrenInHitTestOrder,
36863709
additionalActions: additionalActions,
36873710
headingLevel: headingLevel,
3711+
linkUrl: linkUrl,
36883712
);
36893713
}
36903714

lib/web_ui/test/engine/semantics/semantics_tester.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ class SemanticsTester {
112112
Int32List? additionalActions,
113113
List<SemanticsNodeUpdate>? children,
114114
int? headingLevel,
115+
String? linkUrl,
115116
}) {
116117
// Flags
117118
if (hasCheckedState ?? false) {
@@ -313,6 +314,7 @@ class SemanticsTester {
313314
childrenInHitTestOrder: childIds,
314315
additionalActions: additionalActions ?? Int32List(0),
315316
headingLevel: headingLevel ?? 0,
317+
linkUrl: linkUrl,
316318
);
317319
_nodeUpdates.add(update);
318320
return update;

shell/platform/embedder/fixtures/main.dart

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,8 @@ Future<void> a11y_main() async {
178178
tooltip: 'tooltip',
179179
textDirection: TextDirection.ltr,
180180
additionalActions: Int32List(0),
181-
headingLevel: 0
181+
headingLevel: 0,
182+
linkUrl: '',
182183
)
183184
..updateNode(
184185
id: 84,
@@ -214,7 +215,8 @@ Future<void> a11y_main() async {
214215
additionalActions: Int32List(0),
215216
childrenInHitTestOrder: Int32List(0),
216217
childrenInTraversalOrder: Int32List(0),
217-
headingLevel: 0
218+
headingLevel: 0,
219+
linkUrl: '',
218220
)
219221
..updateNode(
220222
id: 96,
@@ -250,7 +252,8 @@ Future<void> a11y_main() async {
250252
tooltip: 'tooltip',
251253
textDirection: TextDirection.ltr,
252254
additionalActions: Int32List(0),
253-
headingLevel: 0
255+
headingLevel: 0,
256+
linkUrl: '',
254257
)
255258
..updateNode(
256259
id: 128,
@@ -286,7 +289,8 @@ Future<void> a11y_main() async {
286289
textDirection: TextDirection.ltr,
287290
childrenInHitTestOrder: Int32List(0),
288291
childrenInTraversalOrder: Int32List(0),
289-
headingLevel: 0
292+
headingLevel: 0,
293+
linkUrl: '',
290294
)
291295
..updateCustomAction(
292296
id: 21,
@@ -384,6 +388,7 @@ Future<void> a11y_string_attributes() async {
384388
textDirection: TextDirection.ltr,
385389
additionalActions: Int32List(0),
386390
headingLevel: 0,
391+
linkUrl: '',
387392
);
388393

389394
PlatformDispatcher.instance.views.first.updateSemantics(builder.build());

0 commit comments

Comments
 (0)