Skip to content

Commit

Permalink
Add Semantics Property linkUrl (#53507)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mdebbar authored Jul 3, 2024
1 parent 31fd415 commit 388547a
Show file tree
Hide file tree
Showing 11 changed files with 93 additions and 10 deletions.
1 change: 1 addition & 0 deletions lib/ui/fixtures/ui_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ void sendSemanticsUpdate() {
childrenInHitTestOrder: childrenInHitTestOrder,
additionalActions: additionalActions,
headingLevel: 0,
linkUrl: '',
);
_semanticsUpdate(builder.build());
}
Expand Down
12 changes: 10 additions & 2 deletions lib/ui/semantics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,9 @@ abstract class SemanticsUpdateBuilder {
/// inclusive. This attribute is only used for Web platform, and it will have
/// no effect on other platforms.
///
/// The `linkUrl` describes the URI that this node links to. If the node is
/// not a link, this should be an empty string.
///
/// See also:
///
/// * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/heading_role
Expand Down Expand Up @@ -888,6 +891,7 @@ abstract class SemanticsUpdateBuilder {
required Int32List childrenInHitTestOrder,
required Int32List additionalActions,
int headingLevel = 0,
String linkUrl = '',
});

/// Update the custom semantics action associated with the given `id`.
Expand Down Expand Up @@ -959,6 +963,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1 implem
required Int32List childrenInHitTestOrder,
required Int32List additionalActions,
int headingLevel = 0,
String linkUrl = '',
}) {
assert(_matrix4IsValid(transform));
assert (
Expand Down Expand Up @@ -1003,6 +1008,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1 implem
childrenInHitTestOrder,
additionalActions,
headingLevel,
linkUrl,
);
}
@Native<
Expand Down Expand Up @@ -1044,7 +1050,8 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1 implem
Handle,
Handle,
Handle,
Int32)>(symbol: 'SemanticsUpdateBuilder::updateNode')
Int32,
Handle)>(symbol: 'SemanticsUpdateBuilder::updateNode')
external void _updateNode(
int id,
int flags,
Expand Down Expand Up @@ -1082,7 +1089,8 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1 implem
Int32List childrenInTraversalOrder,
Int32List childrenInHitTestOrder,
Int32List additionalActions,
int headingLevel);
int headingLevel,
String linkUrl);

@override
void updateCustomAction({required int id, String? label, String? hint, int overrideId = -1}) {
Expand Down
2 changes: 2 additions & 0 deletions lib/ui/semantics/semantics_node.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ struct SemanticsNode {
std::vector<int32_t> childrenInHitTestOrder;
std::vector<int32_t> customAccessibilityActions;
int32_t headingLevel = 0;

std::string linkUrl;
};

// Contains semantic nodes that need to be updated.
Expand Down
4 changes: 3 additions & 1 deletion lib/ui/semantics/semantics_update_builder.cc
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ void SemanticsUpdateBuilder::updateNode(
const tonic::Int32List& childrenInTraversalOrder,
const tonic::Int32List& childrenInHitTestOrder,
const tonic::Int32List& localContextActions,
int headingLevel) {
int headingLevel,
std::string linkUrl) {
FML_CHECK(scrollChildren == 0 ||
(scrollChildren > 0 && childrenInHitTestOrder.data()))
<< "Semantics update contained scrollChildren but did not have "
Expand Down Expand Up @@ -121,6 +122,7 @@ void SemanticsUpdateBuilder::updateNode(
nodes_[id] = node;

node.headingLevel = headingLevel;
node.linkUrl = std::move(linkUrl);
}

void SemanticsUpdateBuilder::updateCustomAction(int id,
Expand Down
3 changes: 2 additions & 1 deletion lib/ui/semantics/semantics_update_builder.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ class SemanticsUpdateBuilder
const tonic::Int32List& childrenInTraversalOrder,
const tonic::Int32List& childrenInHitTestOrder,
const tonic::Int32List& customAccessibilityActions,
int headingLevel);
int headingLevel,
std::string linkUrl);

void updateCustomAction(int id,
std::string label,
Expand Down
2 changes: 2 additions & 0 deletions lib/web_ui/lib/semantics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ class SemanticsUpdateBuilder {
required Int32List childrenInHitTestOrder,
required Int32List additionalActions,
int headingLevel = 0,
String? linkUrl,
}) {
if (transform.length != 16) {
throw ArgumentError('transform argument must have 16 entries.');
Expand Down Expand Up @@ -326,6 +327,7 @@ class SemanticsUpdateBuilder {
additionalActions: additionalActions,
platformViewId: platformViewId,
headingLevel: headingLevel,
linkUrl: linkUrl,
));
}

Expand Down
15 changes: 13 additions & 2 deletions lib/web_ui/lib/src/engine/semantics/link.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,23 @@ class Link extends PrimaryRoleManager {
@override
DomElement createElement() {
final DomElement element = domDocument.createElement('a');
// TODO(mdebbar): Fill in the real link once the framework sends entire uri.
// https://github.com/flutter/flutter/issues/150263.
element.style.display = 'block';
return element;
}

@override
void update() {
super.update();

if (semanticsObject.isLinkUrlDirty) {
if (semanticsObject.hasLinkUrl) {
element.setAttribute('href', semanticsObject.linkUrl!);
} else {
element.removeAttribute('href');
}
}
}

@override
bool focusAsRouteDefault() => focusable?.focusAsRouteDefault() ?? false;
}
25 changes: 25 additions & 0 deletions lib/web_ui/lib/src/engine/semantics/semantics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ class SemanticsNodeUpdate {
required this.childrenInHitTestOrder,
required this.additionalActions,
required this.headingLevel,
this.linkUrl,
});

/// See [ui.SemanticsUpdateBuilder.updateNode].
Expand Down Expand Up @@ -337,6 +338,9 @@ class SemanticsNodeUpdate {

/// See [ui.SemanticsUpdateBuilder.updateNode].
final int headingLevel;

/// See [ui.SemanticsUpdateBuilder.updateNode].
final String? linkUrl;
}

/// Identifies [PrimaryRoleManager] implementations.
Expand Down Expand Up @@ -1146,6 +1150,22 @@ class SemanticsObject {
_dirtyFields |= _identifierIndex;
}

/// See [ui.SemanticsUpdateBuilder.updateNode].
String? get linkUrl => _linkUrl;
String? _linkUrl;

/// Whether this object contains a non-empty link URL.
bool get hasLinkUrl => _linkUrl != null && _linkUrl!.isNotEmpty;

static const int _linkUrlIndex = 1 << 26;

/// Whether the [linkUrl] field has been updated but has not been
/// applied to the DOM yet.
bool get isLinkUrlDirty => _isDirty(_linkUrlIndex);
void _markLinkUrlDirty() {
_dirtyFields |= _linkUrlIndex;
}

/// A unique permanent identifier of the semantics node in the tree.
final int id;

Expand Down Expand Up @@ -1445,6 +1465,11 @@ class SemanticsObject {
_markPlatformViewIdDirty();
}

if (_linkUrl != update.linkUrl) {
_linkUrl = update.linkUrl;
_markLinkUrlDirty();
}

// Apply updates to the DOM.
_updateRoles();

Expand Down
24 changes: 24 additions & 0 deletions lib/web_ui/test/engine/semantics/semantics_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3603,6 +3603,28 @@ void _testLink() {
expect(object.element.tagName.toLowerCase(), 'a');
expect(object.element.hasAttribute('href'), isFalse);
});

test('link nodes with linkUrl set the href attribute', () {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;

SemanticsObject pumpSemantics() {
final SemanticsTester tester = SemanticsTester(owner());
tester.updateNode(
id: 0,
isLink: true,
linkUrl: 'https://flutter.dev',
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
);
tester.apply();
return tester.getSemanticsObject(0);
}

final SemanticsObject object = pumpSemantics();
expect(object.element.tagName.toLowerCase(), 'a');
expect(object.element.getAttribute('href'), 'https://flutter.dev');
});
}

/// A facade in front of [ui.SemanticsUpdateBuilder.updateNode] that
Expand Down Expand Up @@ -3645,6 +3667,7 @@ void updateNode(
Int32List? childrenInHitTestOrder,
Int32List? additionalActions,
int headingLevel = 0,
String? linkUrl,
}) {
transform ??= Float64List.fromList(Matrix4.identity().storage);
childrenInTraversalOrder ??= Int32List(0);
Expand Down Expand Up @@ -3685,6 +3708,7 @@ void updateNode(
childrenInHitTestOrder: childrenInHitTestOrder,
additionalActions: additionalActions,
headingLevel: headingLevel,
linkUrl: linkUrl,
);
}

Expand Down
2 changes: 2 additions & 0 deletions lib/web_ui/test/engine/semantics/semantics_tester.dart
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ class SemanticsTester {
Int32List? additionalActions,
List<SemanticsNodeUpdate>? children,
int? headingLevel,
String? linkUrl,
}) {
// Flags
if (hasCheckedState ?? false) {
Expand Down Expand Up @@ -313,6 +314,7 @@ class SemanticsTester {
childrenInHitTestOrder: childIds,
additionalActions: additionalActions ?? Int32List(0),
headingLevel: headingLevel ?? 0,
linkUrl: linkUrl,
);
_nodeUpdates.add(update);
return update;
Expand Down
13 changes: 9 additions & 4 deletions shell/platform/embedder/fixtures/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,8 @@ Future<void> a11y_main() async {
tooltip: 'tooltip',
textDirection: TextDirection.ltr,
additionalActions: Int32List(0),
headingLevel: 0
headingLevel: 0,
linkUrl: '',
)
..updateNode(
id: 84,
Expand Down Expand Up @@ -214,7 +215,8 @@ Future<void> a11y_main() async {
additionalActions: Int32List(0),
childrenInHitTestOrder: Int32List(0),
childrenInTraversalOrder: Int32List(0),
headingLevel: 0
headingLevel: 0,
linkUrl: '',
)
..updateNode(
id: 96,
Expand Down Expand Up @@ -250,7 +252,8 @@ Future<void> a11y_main() async {
tooltip: 'tooltip',
textDirection: TextDirection.ltr,
additionalActions: Int32List(0),
headingLevel: 0
headingLevel: 0,
linkUrl: '',
)
..updateNode(
id: 128,
Expand Down Expand Up @@ -286,7 +289,8 @@ Future<void> a11y_main() async {
textDirection: TextDirection.ltr,
childrenInHitTestOrder: Int32List(0),
childrenInTraversalOrder: Int32List(0),
headingLevel: 0
headingLevel: 0,
linkUrl: '',
)
..updateCustomAction(
id: 21,
Expand Down Expand Up @@ -384,6 +388,7 @@ Future<void> a11y_string_attributes() async {
textDirection: TextDirection.ltr,
additionalActions: Int32List(0),
headingLevel: 0,
linkUrl: '',
);

PlatformDispatcher.instance.views.first.updateSemantics(builder.build());
Expand Down

0 comments on commit 388547a

Please sign in to comment.