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

Add Semantics Property linkUrl #53507

Merged
merged 6 commits into from
Jul 3, 2024
Merged
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
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