Reland #163662 [web][a11y]Delete _childContainerElement (#165434)

Reland #163662  with some changes 
1. when a node's role changes and reparenting it, append children to it
 2. Merge optimization from /pull/165352  


fix: https://github.com/flutter/flutter/issues/45205 
## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
Hannah Jin 2025-03-21 20:38:20 -07:00 committed by GitHub
parent 66df85a254
commit efa81a7af6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 354 additions and 268 deletions

View File

@ -70,6 +70,7 @@ class SemanticScrollable extends SemanticRole {
final bool doScrollForward = _domScrollPosition > _effectiveNeutralScrollPosition; final bool doScrollForward = _domScrollPosition > _effectiveNeutralScrollPosition;
_neutralizeDomScrollPosition(); _neutralizeDomScrollPosition();
semanticsObject.recomputePositionAndSize(); semanticsObject.recomputePositionAndSize();
semanticsObject.updateChildrenPositionAndSize();
final int semanticsId = semanticsObject.id; final int semanticsId = semanticsObject.id;
if (doScrollForward) { if (doScrollForward) {
@ -131,6 +132,7 @@ class SemanticScrollable extends SemanticRole {
semanticsObject.owner.addOneTimePostUpdateCallback(() { semanticsObject.owner.addOneTimePostUpdateCallback(() {
_neutralizeDomScrollPosition(); _neutralizeDomScrollPosition();
semanticsObject.recomputePositionAndSize(); semanticsObject.recomputePositionAndSize();
semanticsObject.updateChildrenPositionAndSize();
}); });
if (_scrollListener == null) { if (_scrollListener == null) {
@ -203,8 +205,8 @@ class SemanticScrollable extends SemanticRole {
// Read back because the effective value depends on the amount of content. // Read back because the effective value depends on the amount of content.
_effectiveNeutralScrollPosition = element.scrollTop.toInt(); _effectiveNeutralScrollPosition = element.scrollTop.toInt();
semanticsObject semanticsObject
..verticalContainerAdjustment = _effectiveNeutralScrollPosition.toDouble() ..verticalScrollAdjustment = _effectiveNeutralScrollPosition.toDouble()
..horizontalContainerAdjustment = 0.0; ..horizontalScrollAdjustment = 0.0;
} else { } else {
// Place the _scrollOverflowElement at the end of the content and // Place the _scrollOverflowElement at the end of the content and
// make sure that when we neutralize the scrolling position, // make sure that when we neutralize the scrolling position,
@ -219,8 +221,8 @@ class SemanticScrollable extends SemanticRole {
// Read back because the effective value depends on the amount of content. // Read back because the effective value depends on the amount of content.
_effectiveNeutralScrollPosition = element.scrollLeft.toInt(); _effectiveNeutralScrollPosition = element.scrollLeft.toInt();
semanticsObject semanticsObject
..verticalContainerAdjustment = 0.0 ..verticalScrollAdjustment = 0.0
..horizontalContainerAdjustment = _effectiveNeutralScrollPosition.toDouble(); ..horizontalScrollAdjustment = _effectiveNeutralScrollPosition.toDouble();
} }
} }

View File

@ -1383,33 +1383,6 @@ class SemanticsObject {
/// The dom element of this semantics object. /// The dom element of this semantics object.
DomElement get element => semanticRole!.element; DomElement get element => semanticRole!.element;
/// Returns the HTML element that contains the HTML elements of direct
/// children of this object.
///
/// The element is created lazily. When the child list is empty this element
/// is not created. This is necessary for "aria-label" to function correctly.
/// The browser will ignore the [label] of HTML element that contain child
/// elements.
DomElement? getOrCreateChildContainer() {
if (_childContainerElement == null) {
_childContainerElement = createDomElement('flt-semantics-container');
_childContainerElement!.style
..position = 'absolute'
// Ignore pointer events on child container so that platform views
// behind it can be reached.
..pointerEvents = 'none';
element.append(_childContainerElement!);
}
return _childContainerElement;
}
/// The element that contains the elements belonging to the child semantics
/// nodes.
///
/// This element is used to correct for [_rect] offsets. It is only non-`null`
/// when there are non-zero children (i.e. when [hasChildren] is `true`).
DomElement? _childContainerElement;
/// The parent of this semantics object. /// The parent of this semantics object.
/// ///
/// This value is not final until the tree is finalized. It is not safe to /// This value is not final until the tree is finalized. It is not safe to
@ -1677,12 +1650,6 @@ class SemanticsObject {
// Apply updates to the DOM. // Apply updates to the DOM.
_updateRole(); _updateRole();
// All properties that affect positioning and sizing are checked together
// any one of them triggers position and size recomputation.
if (isRectDirty || isTransformDirty || isScrollPositionDirty) {
recomputePositionAndSize();
}
if (semanticRole!.acceptsPointerEvents) { if (semanticRole!.acceptsPointerEvents) {
element.style.pointerEvents = 'all'; element.style.pointerEvents = 'all';
} else { } else {
@ -1710,22 +1677,15 @@ class SemanticsObject {
// Trivial case: remove all children. // Trivial case: remove all children.
if (_childrenInHitTestOrder == null || _childrenInHitTestOrder!.isEmpty) { if (_childrenInHitTestOrder == null || _childrenInHitTestOrder!.isEmpty) {
if (_currentChildrenInRenderOrder == null || _currentChildrenInRenderOrder!.isEmpty) { if (_currentChildrenInRenderOrder == null || _currentChildrenInRenderOrder!.isEmpty) {
// A container element must not have been created when child list is empty.
assert(_childContainerElement == null);
_currentChildrenInRenderOrder = null; _currentChildrenInRenderOrder = null;
return; return;
} }
// A container element must have been created when child list is not empty.
assert(_childContainerElement != null);
// Remove all children from this semantics object. // Remove all children from this semantics object.
final int len = _currentChildrenInRenderOrder!.length; final int len = _currentChildrenInRenderOrder!.length;
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
owner._detachObject(_currentChildrenInRenderOrder![i].id); owner._detachObject(_currentChildrenInRenderOrder![i].id);
} }
_childContainerElement!.remove();
_childContainerElement = null;
_currentChildrenInRenderOrder = null; _currentChildrenInRenderOrder = null;
return; return;
} }
@ -1734,7 +1694,6 @@ class SemanticsObject {
final Int32List childrenInTraversalOrder = _childrenInTraversalOrder!; final Int32List childrenInTraversalOrder = _childrenInTraversalOrder!;
final Int32List childrenInHitTestOrder = _childrenInHitTestOrder!; final Int32List childrenInHitTestOrder = _childrenInHitTestOrder!;
final int childCount = childrenInHitTestOrder.length; final int childCount = childrenInHitTestOrder.length;
final DomElement? containerElement = getOrCreateChildContainer();
assert(childrenInTraversalOrder.length == childrenInHitTestOrder.length); assert(childrenInTraversalOrder.length == childrenInHitTestOrder.length);
@ -1766,7 +1725,7 @@ class SemanticsObject {
// Trivial case: previous list was empty => just populate the container. // Trivial case: previous list was empty => just populate the container.
if (_currentChildrenInRenderOrder == null || _currentChildrenInRenderOrder!.isEmpty) { if (_currentChildrenInRenderOrder == null || _currentChildrenInRenderOrder!.isEmpty) {
for (final SemanticsObject child in childrenInRenderOrder) { for (final SemanticsObject child in childrenInRenderOrder) {
containerElement!.append(child.element); element.append(child.element);
owner._attachObject(parent: this, child: child); owner._attachObject(parent: this, child: child);
} }
_currentChildrenInRenderOrder = childrenInRenderOrder; _currentChildrenInRenderOrder = childrenInRenderOrder;
@ -1850,9 +1809,9 @@ class SemanticsObject {
final SemanticsObject child = childrenInRenderOrder[i]; final SemanticsObject child = childrenInRenderOrder[i];
if (!stationaryIds.contains(child.id)) { if (!stationaryIds.contains(child.id)) {
if (refNode == null) { if (refNode == null) {
containerElement!.append(child.element); element.append(child.element);
} else { } else {
containerElement!.insertBefore(child.element, refNode); element.insertBefore(child.element, refNode);
} }
owner._attachObject(parent: this, child: child); owner._attachObject(parent: this, child: child);
} else { } else {
@ -2030,9 +1989,10 @@ class SemanticsObject {
// Reparent element. // Reparent element.
if (previousElement != element) { if (previousElement != element) {
final DomElement? container = _childContainerElement; if (_currentChildrenInRenderOrder != null) {
if (container != null) { for (final child in _currentChildrenInRenderOrder!) {
element.append(container); element.append(child.element);
}
} }
final DomElement? parent = previousElement?.parent; final DomElement? parent = previousElement?.parent;
if (parent != null) { if (parent != null) {
@ -2127,32 +2087,53 @@ class SemanticsObject {
/// Indicates whether the node is currently expanded. /// Indicates whether the node is currently expanded.
bool get isExpanded => hasFlag(ui.SemanticsFlag.isExpanded); bool get isExpanded => hasFlag(ui.SemanticsFlag.isExpanded);
/// Role-specific adjustment of the vertical position of the child container. /// Role-specific adjustment of the vertical position of the children.
/// ///
/// This is used, for example, by the [SemanticScrollable] to compensate for the /// This is used, for example, by the [SemanticScrollable] to compensate for the
/// `scrollTop` offset in the DOM. /// `scrollTop` offset in the DOM.
/// ///
/// This field must not be null. /// This field must not be null.
double verticalContainerAdjustment = 0.0; double verticalScrollAdjustment = 0.0;
/// Role-specific adjustment of the horizontal position of the child /// Role-specific adjustment of the horizontal position of children.
/// container.
/// ///
/// This is used, for example, by the [SemanticScrollable] to compensate for the /// This is used, for example, by the [SemanticScrollable] to compensate for the
/// `scrollLeft` offset in the DOM. /// `scrollLeft` offset in the DOM.
/// ///
/// This field must not be null. /// This field must not be null.
double horizontalContainerAdjustment = 0.0; double horizontalScrollAdjustment = 0.0;
/// Computes the size and position of [element] and, if this element double verticalAdjustmentFromParent = 0.0;
/// [hasChildren], of [getOrCreateChildContainer]. double horizontalAdjustmentFromParent = 0.0;
/// If this element [hasChildren], computes the parent adjustment for each child.
void recomputeChildrenAdjustment(Set<SemanticsObject> dirtyNodes) {
if (!hasChildren) {
return;
}
// If this node has children, we need to compensate for the parent's rect and
// pass down the scroll adjustments.
final double translateX = -_rect!.left + horizontalScrollAdjustment;
final double translateY = -_rect!.top + verticalScrollAdjustment;
for (final childIndex in _childrenInTraversalOrder!) {
final child = owner._semanticsTree[childIndex]!;
if (child.horizontalAdjustmentFromParent != translateX ||
child.verticalAdjustmentFromParent != translateY) {
child.horizontalAdjustmentFromParent = translateX;
child.verticalAdjustmentFromParent = translateY;
dirtyNodes.add(child);
}
}
}
/// Computes the size and position of [element]
void recomputePositionAndSize() { void recomputePositionAndSize() {
element.style element.style
..width = '${_rect!.width}px' ..width = '${_rect!.width}px'
..height = '${_rect!.height}px'; ..height = '${_rect!.height}px';
final DomElement? containerElement = hasChildren ? getOrCreateChildContainer() : null;
final bool hasZeroRectOffset = _rect!.top == 0.0 && _rect!.left == 0.0; final bool hasZeroRectOffset = _rect!.top == 0.0 && _rect!.left == 0.0;
final Float32List? transform = _transform; final Float32List? transform = _transform;
final bool hasIdentityTransform = final bool hasIdentityTransform =
@ -2160,27 +2141,25 @@ class SemanticsObject {
if (hasZeroRectOffset && if (hasZeroRectOffset &&
hasIdentityTransform && hasIdentityTransform &&
verticalContainerAdjustment == 0.0 && verticalAdjustmentFromParent == 0.0 &&
horizontalContainerAdjustment == 0.0) { horizontalAdjustmentFromParent == 0.0) {
_clearSemanticElementTransform(element); _clearSemanticElementTransform(element);
if (containerElement != null) {
_clearSemanticElementTransform(containerElement);
}
return; return;
} }
late Matrix4 effectiveTransform; late Matrix4 effectiveTransform;
bool effectiveTransformIsIdentity = true; bool effectiveTransformIsIdentity = true;
if (!hasZeroRectOffset) {
final double left = _rect!.left + horizontalAdjustmentFromParent;
final double top = _rect!.top + verticalAdjustmentFromParent;
if (left != 0.0 || top != 0.0) {
if (transform == null) { if (transform == null) {
final double left = _rect!.left;
final double top = _rect!.top;
effectiveTransform = Matrix4.translationValues(left, top, 0.0); effectiveTransform = Matrix4.translationValues(left, top, 0.0);
effectiveTransformIsIdentity = left == 0.0 && top == 0.0; effectiveTransformIsIdentity = false;
} else { } else {
// Clone to avoid mutating _transform. // Clone to avoid mutating _transform.
effectiveTransform = effectiveTransform = Matrix4.fromFloat32List(transform).clone()..translate(left, top);
Matrix4.fromFloat32List(transform).clone()..translate(_rect!.left, _rect!.top);
effectiveTransformIsIdentity = effectiveTransform.isIdentity(); effectiveTransformIsIdentity = effectiveTransform.isIdentity();
} }
} else if (!hasIdentityTransform) { } else if (!hasIdentityTransform) {
@ -2195,19 +2174,15 @@ class SemanticsObject {
} else { } else {
_clearSemanticElementTransform(element); _clearSemanticElementTransform(element);
} }
}
if (containerElement != null) { /// Computes the size and position of children.
if (!hasZeroRectOffset || void updateChildrenPositionAndSize() {
verticalContainerAdjustment != 0.0 || final Set<SemanticsObject> dirtyNodes = <SemanticsObject>{};
horizontalContainerAdjustment != 0.0) { recomputeChildrenAdjustment(dirtyNodes);
final double translateX = -_rect!.left + horizontalContainerAdjustment;
final double translateY = -_rect!.top + verticalContainerAdjustment; for (final node in dirtyNodes) {
containerElement.style node.recomputePositionAndSize();
..top = '${translateY}px'
..left = '${translateX}px';
} else {
_clearSemanticElementTransform(containerElement);
}
} }
} }
@ -2769,7 +2744,7 @@ class EngineSemanticsOwner {
removals.add(node); removals.add(node);
} else { } else {
assert(node._parent == parent); assert(node._parent == parent);
assert(node.element.parentNode == parent._childContainerElement); assert(node.element.parentNode == parent.element);
} }
return true; return true;
}); });
@ -2872,14 +2847,29 @@ class EngineSemanticsOwner {
object.updateSelf(nodeUpdate); object.updateSelf(nodeUpdate);
} }
final Set<SemanticsObject> nodesWithDirtyPositionsAndSizes = <SemanticsObject>{};
// Second, fix the tree structure. This is moved out into its own loop, // Second, fix the tree structure. This is moved out into its own loop,
// because each object's own information must be updated first. // because each object's own information must be updated first.
for (final SemanticsNodeUpdate nodeUpdate in nodeUpdates) { for (final SemanticsNodeUpdate nodeUpdate in nodeUpdates) {
final SemanticsObject object = _semanticsTree[nodeUpdate.id]!; final SemanticsObject object = _semanticsTree[nodeUpdate.id]!;
object.updateChildren(); object.updateChildren();
if (object.isRectDirty ||
object.isTransformDirty ||
object.isScrollPositionDirty ||
object.isChildrenInTraversalOrderDirty) {
nodesWithDirtyPositionsAndSizes.add(object);
object.recomputeChildrenAdjustment(nodesWithDirtyPositionsAndSizes);
}
object._dirtyFields = 0; object._dirtyFields = 0;
} }
for (final node in nodesWithDirtyPositionsAndSizes) {
node.recomputePositionAndSize();
}
final SemanticsObject root = _semanticsTree[0]!; final SemanticsObject root = _semanticsTree[0]!;
if (_rootSemanticsElement == null) { if (_rootSemanticsElement == null) {
_rootSemanticsElement = root.element; _rootSemanticsElement = root.element;
@ -2913,9 +2903,6 @@ AFTER: $description
// Dirty fields should be cleared after the tree has been finalized. // Dirty fields should be cleared after the tree has been finalized.
assert(object._dirtyFields == 0); assert(object._dirtyFields == 0);
// Make sure a child container is created only when there are children.
assert(object._childContainerElement == null || object.hasChildren);
// Ensure child ID list is consistent with the parent-child // Ensure child ID list is consistent with the parent-child
// relationship of the semantics tree. // relationship of the semantics tree.
if (object._childrenInTraversalOrder != null) { if (object._childrenInTraversalOrder != null) {

View File

@ -274,7 +274,6 @@ class HtmlPatternMatcher extends Matcher {
static bool _areTagsEqual(html.Element a, html.Element b) { static bool _areTagsEqual(html.Element a, html.Element b) {
const Map<String, String> synonyms = <String, String>{ const Map<String, String> synonyms = <String, String>{
'sem': 'flt-semantics', 'sem': 'flt-semantics',
'sem-c': 'flt-semantics-container',
'sem-img': 'flt-semantics-img', 'sem-img': 'flt-semantics-img',
'sem-tf': 'flt-semantics-text-field', 'sem-tf': 'flt-semantics-text-field',
}; };

View File

@ -99,15 +99,11 @@ Future<void> testMain() async {
// Test that each view renders its own semantics tree. // Test that each view renders its own semantics tree.
expectSemanticsTree(view1.semantics, ''' expectSemanticsTree(view1.semantics, '''
<sem style="filter: opacity(0%); color: rgba(0, 0, 0, 0)"> <sem style="filter: opacity(0%); color: rgba(0, 0, 0, 0)">
<sem-c>
<sem flt-tappable="" role="button"></sem> <sem flt-tappable="" role="button"></sem>
</sem-c>
</sem>'''); </sem>''');
expectSemanticsTree(view2.semantics, ''' expectSemanticsTree(view2.semantics, '''
<sem style="filter: opacity(0%); color: rgba(0, 0, 0, 0)"> <sem style="filter: opacity(0%); color: rgba(0, 0, 0, 0)">
<sem-c>
<sem aria-label="d"><input aria-valuemax="1" aria-valuemin="1" aria-valuenow="1" aria-valuetext="" role="slider"></sem> <sem aria-label="d"><input aria-valuemax="1" aria-valuemin="1" aria-valuenow="1" aria-valuetext="" role="slider"></sem>
</sem-c>
</sem> </sem>
'''); ''');

View File

@ -161,10 +161,8 @@ void _testSemanticRole() {
tester.expectSemantics(''' tester.expectSemantics('''
<sem id="flt-semantic-node-0"> <sem id="flt-semantic-node-0">
<sem-c>
<sem id="flt-semantic-node-372"></sem> <sem id="flt-semantic-node-372"></sem>
<sem id="flt-semantic-node-599"></sem> <sem id="flt-semantic-node-599"></sem>
</sem-c>
</sem>'''); </sem>''');
tester.updateNode( tester.updateNode(
@ -178,10 +176,8 @@ void _testSemanticRole() {
tester.expectSemantics(''' tester.expectSemantics('''
<sem id="flt-semantic-node-0"> <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-372" flt-semantics-identifier="test-id-123"></sem>
<sem id="flt-semantic-node-599"></sem> <sem id="flt-semantic-node-599"></sem>
</sem-c>
</sem>'''); </sem>''');
tester.updateNode( tester.updateNode(
@ -196,11 +192,9 @@ void _testSemanticRole() {
tester.expectSemantics(''' tester.expectSemantics('''
<sem id="flt-semantic-node-0"> <sem id="flt-semantic-node-0">
<sem-c>
<sem id="flt-semantic-node-372"></sem> <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-599" flt-semantics-identifier="test-id-211"></sem>
<sem id="flt-semantic-node-612" flt-semantics-identifier="test-id-333"></sem> <sem id="flt-semantic-node-612" flt-semantics-identifier="test-id-333"></sem>
</sem-c>
</sem>'''); </sem>''');
}); });
} }
@ -523,9 +517,7 @@ void _testEngineSemanticsOwner() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem><span>Hello</span></sem> <sem><span>Hello</span></sem>
</sem-c>
</sem>'''); </sem>''');
// Update // Update
@ -533,9 +525,7 @@ void _testEngineSemanticsOwner() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem><span>World</span></sem> <sem><span>World</span></sem>
</sem-c>
</sem>'''); </sem>''');
// Remove // Remove
@ -543,9 +533,7 @@ void _testEngineSemanticsOwner() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem></sem> <sem></sem>
</sem-c>
</sem>'''); </sem>''');
semantics().semanticsEnabled = false; semantics().semanticsEnabled = false;
@ -566,9 +554,7 @@ void _testEngineSemanticsOwner() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem><span>Hello</span></sem> <sem><span>Hello</span></sem>
</sem-c>
</sem>'''); </sem>''');
// Update // Update
@ -581,9 +567,7 @@ void _testEngineSemanticsOwner() {
expect(tree[1]!.element.tagName.toLowerCase(), 'a'); expect(tree[1]!.element.tagName.toLowerCase(), 'a');
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<a style="display: block;">Hello</a> <a style="display: block;">Hello</a>
</sem-c>
</sem>'''); </sem>''');
expect(existingParent, tree[1]!.element.parent); expect(existingParent, tree[1]!.element.parent);
@ -605,9 +589,7 @@ void _testEngineSemanticsOwner() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem><span>tooltip</span></sem> <sem><span>tooltip</span></sem>
</sem-c>
</sem>'''); </sem>''');
// Update // Update
@ -615,9 +597,7 @@ void _testEngineSemanticsOwner() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem><span>tooltip\nHello</span></sem> <sem><span>tooltip\nHello</span></sem>
</sem-c>
</sem>'''); </sem>''');
// Remove // Remove
@ -625,9 +605,7 @@ void _testEngineSemanticsOwner() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem></sem> <sem></sem>
</sem-c>
</sem>'''); </sem>''');
semantics().semanticsEnabled = false; semantics().semanticsEnabled = false;
@ -852,7 +830,7 @@ void _testHeader() {
owner().updateSemantics(builder.build()); owner().updateSemantics(builder.build());
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<header aria-label="Header of the page"><sem-c><sem></sem></sem-c></header> <header aria-label="Header of the page"><sem></sem></header>
'''); ''');
semantics().semanticsEnabled = false; semantics().semanticsEnabled = false;
@ -959,7 +937,7 @@ label hint''');
} }
void _testContainer() { void _testContainer() {
test('container node has no transform when there is no rect offset', () async { test('child node has no transform when there is no rect offset', () async {
semantics() semantics()
..debugOverrideTimestampFunction(() => _testTime) ..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true; ..semanticsEnabled = true;
@ -976,35 +954,30 @@ void _testContainer() {
updateNode(builder, id: 1, transform: Matrix4.identity().toFloat64(), rect: zeroOffsetRect); updateNode(builder, id: 1, transform: Matrix4.identity().toFloat64(), rect: zeroOffsetRect);
owner().updateSemantics(builder.build()); owner().updateSemantics(builder.build());
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''<sem><sem></sem></sem>''');
<sem>
<sem-c>
<sem></sem>
</sem-c>
</sem>''');
final DomElement parentElement = owner().semanticsHost.querySelector('flt-semantics')!; final DomElement parentElement = owner().semanticsHost.querySelector('flt-semantics')!;
final DomElement container = owner().semanticsHost.querySelector('flt-semantics-container')!; final DomElement childElement = owner().semanticsHost.querySelector('#flt-semantic-node-1')!;
if (isMacOrIOS) { if (isMacOrIOS) {
expect(parentElement.style.top, '0px'); expect(parentElement.style.top, '0px');
expect(parentElement.style.left, '0px'); expect(parentElement.style.left, '0px');
expect(container.style.top, '0px'); expect(childElement.style.top, '0px');
expect(container.style.left, '0px'); expect(childElement.style.left, '0px');
} else { } else {
expect(parentElement.style.top, ''); expect(parentElement.style.top, '');
expect(parentElement.style.left, ''); expect(parentElement.style.left, '');
expect(container.style.top, ''); expect(childElement.style.top, '');
expect(container.style.left, ''); expect(childElement.style.left, '');
} }
expect(parentElement.style.transform, ''); expect(parentElement.style.transform, '');
expect(parentElement.style.transformOrigin, ''); expect(parentElement.style.transformOrigin, '');
expect(container.style.transform, ''); expect(childElement.style.transform, '');
expect(container.style.transformOrigin, ''); expect(childElement.style.transformOrigin, '');
semantics().semanticsEnabled = false; semantics().semanticsEnabled = false;
}); });
test('container node compensates for rect offset', () async { test('child node transform compensates for parent rect offset', () async {
semantics() semantics()
..debugOverrideTimestampFunction(() => _testTime) ..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true; ..semanticsEnabled = true;
@ -1021,19 +994,14 @@ void _testContainer() {
builder, builder,
id: 1, id: 1,
transform: Matrix4.identity().toFloat64(), transform: Matrix4.identity().toFloat64(),
rect: const ui.Rect.fromLTRB(10, 10, 20, 20), rect: const ui.Rect.fromLTRB(0, 0, 5, 5),
); );
owner().updateSemantics(builder.build()); owner().updateSemantics(builder.build());
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''<sem><sem></sem></sem>''');
<sem>
<sem-c>
<sem></sem>
</sem-c>
</sem>''');
final DomElement parentElement = owner().semanticsHost.querySelector('flt-semantics')!; final DomElement parentElement = owner().semanticsHost.querySelector('flt-semantics')!;
final DomElement container = owner().semanticsHost.querySelector('flt-semantics-container')!; final DomElement childElement = owner().semanticsHost.querySelector('#flt-semantic-node-1')!;
expect(parentElement.style.transform, 'matrix(1, 0, 0, 1, 10, 10)'); expect(parentElement.style.transform, 'matrix(1, 0, 0, 1, 10, 10)');
if (isSafari) { if (isSafari) {
@ -1042,63 +1010,103 @@ void _testContainer() {
parentElement.style.transformOrigin, parentElement.style.transformOrigin,
anyOf(contains('0px 0px 0px'), contains('0px 0px')), anyOf(contains('0px 0px 0px'), contains('0px 0px')),
); );
expect(
childElement.style.transformOrigin,
anyOf(contains('0px 0px 0px'), contains('0px 0px')),
);
} else { } else {
expect(parentElement.style.transformOrigin, '0px 0px 0px'); expect(parentElement.style.transformOrigin, '0px 0px 0px');
expect(childElement.style.transformOrigin, '0px 0px 0px');
} }
expect(container.style.top, '-10px'); expect(childElement.style.transform, 'matrix(1, 0, 0, 1, -10, -10)');
expect(container.style.left, '-10px'); expect(childElement.style.left == '0px' || childElement.style.left == '', isTrue);
expect(childElement.style.top == '0px' || childElement.style.top == '', isTrue);
semantics().semanticsEnabled = false; semantics().semanticsEnabled = false;
}); });
test('0 offsets are not removed for voiceover', () async { test(
semantics() 'child node transform compensates for parent rect offset when parent rect changed',
..debugOverrideTimestampFunction(() => _testTime) () async {
..semanticsEnabled = true; semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder(); final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
updateNode( updateNode(
builder, builder,
transform: Matrix4.identity().toFloat64(), transform: Matrix4.identity().toFloat64(),
rect: const ui.Rect.fromLTRB(0, 0, 20, 20), rect: const ui.Rect.fromLTRB(10, 10, 20, 20),
childrenInHitTestOrder: Int32List.fromList(<int>[1]), childrenInHitTestOrder: Int32List.fromList(<int>[1]),
childrenInTraversalOrder: Int32List.fromList(<int>[1]), childrenInTraversalOrder: Int32List.fromList(<int>[1]),
); );
updateNode( updateNode(
builder, builder,
id: 1, id: 1,
transform: Matrix4.identity().toFloat64(), transform: Matrix4.identity().toFloat64(),
rect: const ui.Rect.fromLTRB(10, 10, 20, 20), rect: const ui.Rect.fromLTRB(0, 0, 5, 5),
); );
owner().updateSemantics(builder.build()); owner().updateSemantics(builder.build());
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''<sem><sem></sem></sem>''');
<sem>
<sem-c>
<sem></sem>
</sem-c>
</sem>''');
final DomElement parentElement = owner().semanticsHost.querySelector('flt-semantics')!; final DomElement parentElement = owner().semanticsHost.querySelector('flt-semantics')!;
final DomElement container = owner().semanticsHost.querySelector('flt-semantics-container')!; final DomElement childElement = owner().semanticsHost.querySelector('#flt-semantic-node-1')!;
if (isMacOrIOS) { expect(parentElement.style.transform, 'matrix(1, 0, 0, 1, 10, 10)');
expect(parentElement.style.top, '0px'); if (isSafari) {
expect(parentElement.style.left, '0px'); // macOS 13 returns different values than macOS 12.
expect(container.style.top, '0px'); expect(
expect(container.style.left, '0px'); parentElement.style.transformOrigin,
} else { anyOf(contains('0px 0px 0px'), contains('0px 0px')),
expect(parentElement.style.top, ''); );
expect(parentElement.style.left, ''); expect(
expect(container.style.top, ''); childElement.style.transformOrigin,
expect(container.style.left, ''); anyOf(contains('0px 0px 0px'), contains('0px 0px')),
} );
expect(parentElement.style.transform, ''); } else {
expect(parentElement.style.transformOrigin, ''); expect(parentElement.style.transformOrigin, '0px 0px 0px');
expect(container.style.transform, ''); expect(childElement.style.transformOrigin, '0px 0px 0px');
expect(container.style.transformOrigin, ''); }
expect(childElement.style.transform, 'matrix(1, 0, 0, 1, -10, -10)');
expect(childElement.style.left == '0px' || childElement.style.left == '', isTrue);
expect(childElement.style.top == '0px' || childElement.style.top == '', isTrue);
semantics().semanticsEnabled = false; final ui.SemanticsUpdateBuilder builder2 = ui.SemanticsUpdateBuilder();
});
updateNode(
builder2,
transform: Matrix4.identity().toFloat64(),
rect: const ui.Rect.fromLTRB(33, 33, 20, 20),
childrenInHitTestOrder: Int32List.fromList(<int>[1]),
childrenInTraversalOrder: Int32List.fromList(<int>[1]),
);
owner().updateSemantics(builder2.build());
expectSemanticsTree(owner(), '''<sem><sem></sem></sem>''');
expect(parentElement.style.transform, 'matrix(1, 0, 0, 1, 33, 33)');
if (isSafari) {
// macOS 13 returns different values than macOS 12.
expect(
parentElement.style.transformOrigin,
anyOf(contains('0px 0px 0px'), contains('0px 0px')),
);
expect(
childElement.style.transformOrigin,
anyOf(contains('0px 0px 0px'), contains('0px 0px')),
);
} else {
expect(parentElement.style.transformOrigin, '0px 0px 0px');
expect(childElement.style.transformOrigin, '0px 0px 0px');
}
expect(childElement.style.transform, 'matrix(1, 0, 0, 1, -33, -33)');
expect(childElement.style.left == '0px' || childElement.style.left == '', isTrue);
expect(childElement.style.top == '0px' || childElement.style.top == '', isTrue);
semantics().semanticsEnabled = false;
},
);
test('renders in traversal order, hit-tests in reverse z-index order', () async { test('renders in traversal order, hit-tests in reverse z-index order', () async {
semantics() semantics()
@ -1121,12 +1129,10 @@ void _testContainer() {
owner().updateSemantics(builder.build()); owner().updateSemantics(builder.build());
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem style="z-index: 4"></sem> <sem style="z-index: 4"></sem>
<sem style="z-index: 2"></sem> <sem style="z-index: 2"></sem>
<sem style="z-index: 3"></sem> <sem style="z-index: 3"></sem>
<sem style="z-index: 1"></sem> <sem style="z-index: 1"></sem>
</sem-c>
</sem>'''); </sem>''');
} }
@ -1141,12 +1147,10 @@ void _testContainer() {
owner().updateSemantics(builder.build()); owner().updateSemantics(builder.build());
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem style="z-index: 4"></sem> <sem style="z-index: 4"></sem>
<sem style="z-index: 3"></sem> <sem style="z-index: 3"></sem>
<sem style="z-index: 2"></sem> <sem style="z-index: 2"></sem>
<sem style="z-index: 1"></sem> <sem style="z-index: 1"></sem>
</sem-c>
</sem>'''); </sem>''');
} }
@ -1161,12 +1165,10 @@ void _testContainer() {
owner().updateSemantics(builder.build()); owner().updateSemantics(builder.build());
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem style="z-index: 1"></sem> <sem style="z-index: 1"></sem>
<sem style="z-index: 3"></sem> <sem style="z-index: 3"></sem>
<sem style="z-index: 2"></sem> <sem style="z-index: 2"></sem>
<sem style="z-index: 4"></sem> <sem style="z-index: 4"></sem>
</sem-c>
</sem>'''); </sem>''');
} }
@ -1181,12 +1183,10 @@ void _testContainer() {
owner().updateSemantics(builder.build()); owner().updateSemantics(builder.build());
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem style="z-index: 2"></sem> <sem style="z-index: 2"></sem>
<sem style="z-index: 4"></sem> <sem style="z-index: 4"></sem>
<sem style="z-index: 1"></sem> <sem style="z-index: 1"></sem>
<sem style="z-index: 3"></sem> <sem style="z-index: 3"></sem>
</sem-c>
</sem>'''); </sem>''');
} }
@ -1210,10 +1210,8 @@ void _testContainer() {
owner().updateSemantics(builder.build()); owner().updateSemantics(builder.build());
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem style="z-index: 2"></sem> <sem style="z-index: 2"></sem>
<sem style="z-index: 1"></sem> <sem style="z-index: 1"></sem>
</sem-c>
</sem>'''); </sem>''');
final DomElement root = owner().semanticsHost.querySelector('#flt-semantic-node-0')!; final DomElement root = owner().semanticsHost.querySelector('#flt-semantic-node-0')!;
@ -1246,10 +1244,8 @@ void _testContainer() {
owner().updateSemantics(builder.build()); owner().updateSemantics(builder.build());
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem style="z-index: 2"></sem> <sem style="z-index: 2"></sem>
<sem style="z-index: 1"></sem> <sem style="z-index: 1"></sem>
</sem-c>
</sem>'''); </sem>''');
final DomElement root = owner().semanticsHost.querySelector('#flt-semantic-node-0')!; final DomElement root = owner().semanticsHost.querySelector('#flt-semantic-node-0')!;
@ -1257,7 +1253,6 @@ void _testContainer() {
semantics().semanticsEnabled = false; semantics().semanticsEnabled = false;
}); });
test('container can be opaque if it is a text field', () async { test('container can be opaque if it is a text field', () async {
semantics() semantics()
..debugOverrideTimestampFunction(() => _testTime) ..debugOverrideTimestampFunction(() => _testTime)
@ -1277,10 +1272,8 @@ void _testContainer() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<input> <input>
<sem-c>
<sem style="z-index: 2"></sem> <sem style="z-index: 2"></sem>
<sem style="z-index: 1"></sem> <sem style="z-index: 1"></sem>
</sem-c>
</sem>'''); </sem>''');
final DomElement root = owner().semanticsHost.querySelector('#flt-semantic-node-0')!; final DomElement root = owner().semanticsHost.querySelector('#flt-semantic-node-0')!;
@ -1321,20 +1314,14 @@ void _testContainer() {
owner().updateSemantics(builder.build()); owner().updateSemantics(builder.build());
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem style="z-index: 2"> <sem style="z-index: 2">
<sem-c>
<sem style="z-index: 2"></sem> <sem style="z-index: 2"></sem>
<sem style="z-index: 1"></sem> <sem style="z-index: 1"></sem>
</sem-c>
</sem> </sem>
<sem style="z-index: 1"> <sem style="z-index: 1">
<sem-c>
<sem style="z-index: 2"></sem> <sem style="z-index: 2"></sem>
<sem style="z-index: 1"></sem> <sem style="z-index: 1"></sem>
</sem-c>
</sem> </sem>
</sem-c>
</sem>'''); </sem>''');
expect( expect(
@ -1361,15 +1348,11 @@ void _testContainer() {
owner().updateSemantics(builder.build()); owner().updateSemantics(builder.build());
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem style="z-index: 2"> <sem style="z-index: 2">
<sem-c>
<sem style="z-index: 3"></sem> <sem style="z-index: 3"></sem>
<sem style="z-index: 2"></sem> <sem style="z-index: 2"></sem>
<sem style="z-index: 1"></sem> <sem style="z-index: 1"></sem>
</sem-c>
</sem> </sem>
</sem-c>
</sem>'''); </sem>''');
expect(owner().debugSemanticsTree!.keys.toList(), unorderedEquals(<int>[0, 1, 3, 4, 6])); expect(owner().debugSemanticsTree!.keys.toList(), unorderedEquals(<int>[0, 1, 3, 4, 6]));
@ -1377,6 +1360,179 @@ void _testContainer() {
semantics().semanticsEnabled = false; semantics().semanticsEnabled = false;
}); });
test('node updated with role change', () async {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
{
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
updateNode(
builder,
childrenInTraversalOrder: Int32List.fromList(<int>[1, 2]),
childrenInHitTestOrder: Int32List.fromList(<int>[1, 2]),
);
updateNode(
builder,
id: 1,
childrenInTraversalOrder: Int32List.fromList(<int>[3, 4]),
childrenInHitTestOrder: Int32List.fromList(<int>[3, 4]),
);
updateNode(
builder,
id: 2,
childrenInTraversalOrder: Int32List.fromList(<int>[5, 6]),
childrenInHitTestOrder: Int32List.fromList(<int>[5, 6]),
);
updateNode(builder, id: 3);
updateNode(builder, id: 4);
updateNode(builder, id: 5);
updateNode(builder, id: 6);
owner().updateSemantics(builder.build());
expectSemanticsTree(owner(), '''
<sem>
<sem style="z-index: 2">
<sem style="z-index: 2"></sem>
<sem style="z-index: 1"></sem>
</sem>
<sem style="z-index: 1">
<sem style="z-index: 2"></sem>
<sem style="z-index: 1"></sem>
</sem>
</sem>''');
expect(
owner().debugSemanticsTree!.keys.toList(),
unorderedEquals(<int>[0, 1, 2, 3, 4, 5, 6]),
);
}
// update node #2 with a new role
{
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
updateNode(
builder,
childrenInTraversalOrder: Int32List.fromList(<int>[1, 2]),
childrenInHitTestOrder: Int32List.fromList(<int>[1, 2]),
);
updateNode(
builder,
id: 1,
childrenInTraversalOrder: Int32List.fromList(<int>[3, 4]),
childrenInHitTestOrder: Int32List.fromList(<int>[3, 4]),
);
updateNode(
builder,
id: 2,
role: ui.SemanticsRole.table,
childrenInTraversalOrder: Int32List.fromList(<int>[5, 6]),
childrenInHitTestOrder: Int32List.fromList(<int>[5, 6]),
);
updateNode(builder, id: 3);
updateNode(builder, id: 4);
updateNode(builder, id: 5);
updateNode(builder, id: 6);
owner().updateSemantics(builder.build());
expectSemanticsTree(owner(), '''
<sem>
<sem style="z-index: 2">
<sem style="z-index: 2"></sem>
<sem style="z-index: 1"></sem>
</sem>
<sem style="z-index: 1">
<sem style="z-index: 2"></sem>
<sem style="z-index: 1"></sem>
</sem>
</sem>''');
expect(
owner().debugSemanticsTree!.keys.toList(),
unorderedEquals(<int>[0, 1, 2, 3, 4, 5, 6]),
);
}
semantics().semanticsEnabled = false;
});
test('reparented nodes have correct transform and position', () async {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
{
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
updateNode(
builder,
childrenInTraversalOrder: Int32List.fromList(<int>[1, 2]),
childrenInHitTestOrder: Int32List.fromList(<int>[1, 2]),
rect: const ui.Rect.fromLTRB(11, 11, 111, 111),
);
updateNode(builder, id: 1, rect: const ui.Rect.fromLTRB(10, 10, 20, 20));
updateNode(
builder,
id: 2,
childrenInTraversalOrder: Int32List.fromList(<int>[3]),
childrenInHitTestOrder: Int32List.fromList(<int>[3]),
rect: const ui.Rect.fromLTRB(22, 22, 222, 222),
);
updateNode(builder, id: 3);
owner().updateSemantics(builder.build());
final DomElement childElement = owner().semanticsHost.querySelector('#flt-semantic-node-3')!;
expectSemanticsTree(owner(), '''
<sem>
<sem style="z-index: 2"></sem>
<sem style="z-index: 1">
<sem></sem>
</sem>
</sem>''');
expect(owner().debugSemanticsTree!.keys.toList(), unorderedEquals(<int>[0, 1, 2, 3]));
expect(childElement.style.transform, 'matrix(1, 0, 0, 1, -22, -22)');
expect(childElement.style.left == '0px' || childElement.style.left == '', isTrue);
expect(childElement.style.top == '0px' || childElement.style.top == '', isTrue);
}
// Remove node #2 => expect nodes #2 to be removed and #3 reparented.
{
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
updateNode(
builder,
childrenInTraversalOrder: Int32List.fromList(<int>[1]),
childrenInHitTestOrder: Int32List.fromList(<int>[1]),
);
updateNode(
builder,
id: 1,
childrenInTraversalOrder: Int32List.fromList(<int>[3]),
childrenInHitTestOrder: Int32List.fromList(<int>[3]),
rect: const ui.Rect.fromLTRB(11, 11, 111, 111),
);
updateNode(builder, id: 3);
owner().updateSemantics(builder.build());
final DomElement childElement = owner().semanticsHost.querySelector('#flt-semantic-node-3')!;
expectSemanticsTree(owner(), '''
<sem>
<sem style="z-index: 2">
<sem ></sem>
</sem>
</sem>''');
expect(owner().debugSemanticsTree!.keys.toList(), unorderedEquals(<int>[0, 1, 3]));
expect(childElement.style.transform, 'matrix(1, 0, 0, 1, -11, -11)');
expect(childElement.style.left == '0px' || childElement.style.left == '', isTrue);
expect(childElement.style.top == '0px' || childElement.style.top == '', isTrue);
}
semantics().semanticsEnabled = false;
});
} }
void _testVerticalScrolling() { void _testVerticalScrolling() {
@ -1455,9 +1611,7 @@ void _testVerticalScrolling() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem style="touch-action: none; overflow-y: scroll"> <sem style="touch-action: none; overflow-y: scroll">
<flt-semantics-scroll-overflow></flt-semantics-scroll-overflow> <flt-semantics-scroll-overflow></flt-semantics-scroll-overflow>
<sem-c>
<sem></sem> <sem></sem>
</sem-c>
</sem>'''); </sem>''');
final DomElement scrollable = findScrollable(owner()); final DomElement scrollable = findScrollable(owner());
@ -1511,11 +1665,9 @@ void _testVerticalScrolling() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem style="touch-action: none; overflow-y: scroll"> <sem style="touch-action: none; overflow-y: scroll">
<flt-semantics-scroll-overflow></flt-semantics-scroll-overflow> <flt-semantics-scroll-overflow></flt-semantics-scroll-overflow>
<sem-c>
<sem style="z-index: 3"></sem> <sem style="z-index: 3"></sem>
<sem style="z-index: 2"></sem> <sem style="z-index: 2"></sem>
<sem style="z-index: 1"></sem> <sem style="z-index: 1"></sem>
</sem-c>
</sem>'''); </sem>''');
final DomElement scrollable = owner().debugSemanticsTree![0]!.element; final DomElement scrollable = owner().debugSemanticsTree![0]!.element;
@ -1592,11 +1744,9 @@ void _testVerticalScrolling() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem style="touch-action: none; overflow-y: scroll"> <sem style="touch-action: none; overflow-y: scroll">
<flt-semantics-scroll-overflow></flt-semantics-scroll-overflow> <flt-semantics-scroll-overflow></flt-semantics-scroll-overflow>
<sem-c>
<sem style="z-index: 3"></sem> <sem style="z-index: 3"></sem>
<sem style="z-index: 2"></sem> <sem style="z-index: 2"></sem>
<sem style="z-index: 1"></sem> <sem style="z-index: 1"></sem>
</sem-c>
</sem>'''); </sem>''');
final DomElement scrollable = owner().debugSemanticsTree![0]!.element; final DomElement scrollable = owner().debugSemanticsTree![0]!.element;
@ -1682,9 +1832,7 @@ void _testHorizontalScrolling() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem style="touch-action: none; overflow-x: scroll"> <sem style="touch-action: none; overflow-x: scroll">
<flt-semantics-scroll-overflow></flt-semantics-scroll-overflow> <flt-semantics-scroll-overflow></flt-semantics-scroll-overflow>
<sem-c>
<sem></sem> <sem></sem>
</sem-c>
</sem>'''); </sem>''');
final DomElement scrollable = findScrollable(owner()); final DomElement scrollable = findScrollable(owner());
@ -1738,11 +1886,9 @@ void _testHorizontalScrolling() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem style="touch-action: none; overflow-x: scroll"> <sem style="touch-action: none; overflow-x: scroll">
<flt-semantics-scroll-overflow></flt-semantics-scroll-overflow> <flt-semantics-scroll-overflow></flt-semantics-scroll-overflow>
<sem-c>
<sem style="z-index: 3"></sem> <sem style="z-index: 3"></sem>
<sem style="z-index: 2"></sem> <sem style="z-index: 2"></sem>
<sem style="z-index: 1"></sem> <sem style="z-index: 1"></sem>
</sem-c>
</sem>'''); </sem>''');
final DomElement scrollable = findScrollable(owner()); final DomElement scrollable = findScrollable(owner());
@ -2297,10 +2443,8 @@ void _testCheckables() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem role="radiogroup"> <sem role="radiogroup">
<sem-c>
<sem aria-checked="false"></sem> <sem aria-checked="false"></sem>
<sem aria-checked="true"></sem> <sem aria-checked="true"></sem>
</sem-c>
</sem> </sem>
'''); ''');
@ -2398,11 +2542,9 @@ void _testSelectables() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem></sem> <sem></sem>
<sem aria-selected="false"></sem> <sem aria-selected="false"></sem>
<sem aria-selected="true"></sem> <sem aria-selected="true"></sem>
</sem-c>
</sem> </sem>
'''); ''');
@ -2427,11 +2569,9 @@ void _testSelectables() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem></sem> <sem></sem>
<sem aria-selected="true"></sem> <sem aria-selected="true"></sem>
<sem aria-selected="false"></sem> <sem aria-selected="false"></sem>
</sem-c>
</sem> </sem>
'''); ''');
@ -2496,11 +2636,9 @@ void _testExpandables() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem></sem> <sem></sem>
<sem aria-expanded="false"></sem> <sem aria-expanded="false"></sem>
<sem aria-expanded="true"></sem> <sem aria-expanded="true"></sem>
</sem-c>
</sem> </sem>
'''); ''');
@ -2525,11 +2663,9 @@ void _testExpandables() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem></sem> <sem></sem>
<sem aria-expanded="true"></sem> <sem aria-expanded="true"></sem>
<sem aria-expanded="false"></sem> <sem aria-expanded="false"></sem>
</sem-c>
</sem> </sem>
'''); ''');
@ -2783,9 +2919,7 @@ void _testTappable() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem flt-tappable role="button"> <sem flt-tappable role="button">
<sem-c>
<sem flt-tappable role="button"></sem> <sem flt-tappable role="button"></sem>
</sem-c>
</sem> </sem>
'''); ''');
@ -2876,9 +3010,7 @@ void _testImage() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-img role="img" aria-label="Test Image Label"></sem-img> <sem-img role="img" aria-label="Test Image Label"></sem-img>
<sem-c>
<sem></sem> <sem></sem>
</sem-c>
</sem>'''); </sem>''');
semantics().semanticsEnabled = false; semantics().semanticsEnabled = false;
@ -2928,9 +3060,7 @@ void _testImage() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-img role="img"></sem-img> <sem-img role="img"></sem-img>
<sem-c>
<sem></sem> <sem></sem>
</sem-c>
</sem>'''); </sem>''');
semantics().semanticsEnabled = false; semantics().semanticsEnabled = false;
@ -3147,11 +3277,9 @@ void _testPlatformView() {
owner().updateSemantics(builder.build()); owner().updateSemantics(builder.build());
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem style="z-index: 3"></sem> <sem style="z-index: 3"></sem>
<sem style="z-index: 2" aria-owns="flt-pv-0"></sem> <sem style="z-index: 2" aria-owns="flt-pv-0"></sem>
<sem style="z-index: 1"></sem> <sem style="z-index: 1"></sem>
</sem-c>
</sem>'''); </sem>''');
final DomElement root = owner().semanticsHost.querySelector('#flt-semantic-node-0')!; final DomElement root = owner().semanticsHost.querySelector('#flt-semantic-node-0')!;
@ -3245,7 +3373,7 @@ void _testGroup() {
owner().updateSemantics(builder.build()); owner().updateSemantics(builder.build());
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem role="group" aria-label="this is a label for a group of elements"><sem-c><sem></sem></sem-c></sem> <sem role="group" aria-label="this is a label for a group of elements"><sem></sem></sem>
'''); ''');
semantics().semanticsEnabled = false; semantics().semanticsEnabled = false;
@ -3277,7 +3405,7 @@ void _testRoute() {
owner().updateSemantics(builder.build()); owner().updateSemantics(builder.build());
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem role="dialog" aria-label="this is a route label"><sem-c><sem></sem></sem-c></sem> <sem role="dialog" aria-label="this is a route label"><sem></sem></sem>
'''); ''');
expect(owner().debugSemanticsTree![0]!.semanticRole?.kind, EngineSemanticsRole.route); expect(owner().debugSemanticsTree![0]!.semanticRole?.kind, EngineSemanticsRole.route);
@ -3316,7 +3444,7 @@ void _testRoute() {
// But still sets the dialog role. // But still sets the dialog role.
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem role="dialog" aria-label=""><sem-c><sem></sem></sem-c></sem> <sem role="dialog" aria-label=""><sem></sem></sem>
'''); ''');
expect(owner().debugSemanticsTree![0]!.semanticRole?.kind, EngineSemanticsRole.route); expect(owner().debugSemanticsTree![0]!.semanticRole?.kind, EngineSemanticsRole.route);
@ -3348,13 +3476,9 @@ void _testRoute() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem role="dialog" aria-describedby="flt-semantic-node-2"> <sem role="dialog" aria-describedby="flt-semantic-node-2">
<sem-c>
<sem> <sem>
<sem-c>
<sem><span>$label</span></sem> <sem><span>$label</span></sem>
</sem-c>
</sem> </sem>
</sem-c>
</sem> </sem>
'''); ''');
} }
@ -3414,13 +3538,9 @@ void _testRoute() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem> <sem>
<sem-c>
<sem><span>Hello</span></sem> <sem><span>Hello</span></sem>
</sem-c>
</sem> </sem>
</sem-c>
</sem> </sem>
'''); ''');
@ -3597,16 +3717,12 @@ void _testRoute() {
tester.expectSemantics(''' tester.expectSemantics('''
<flt-semantics> <flt-semantics>
<flt-semantics-container>
<flt-semantics> <flt-semantics>
<flt-semantics-container>
<flt-semantics id="flt-semantic-node-2"> <flt-semantics id="flt-semantic-node-2">
<span tabindex="-1">Heading</span> <span tabindex="-1">Heading</span>
</flt-semantics> </flt-semantics>
<flt-semantics role="button" tabindex="0" flt-tappable="">Click me!</flt-semantics> <flt-semantics role="button" tabindex="0" flt-tappable="">Click me!</flt-semantics>
</flt-semantics-container>
</flt-semantics> </flt-semantics>
</flt-semantics-container>
</flt-semantics>'''); </flt-semantics>''');
final DomElement span = owner().debugSemanticsTree![2]!.element.querySelectorAll('span').single; final DomElement span = owner().debugSemanticsTree![2]!.element.querySelectorAll('span').single;
@ -3706,13 +3822,9 @@ void _testDialogs() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem role="dialog" aria-describedby="flt-semantic-node-2"> <sem role="dialog" aria-describedby="flt-semantic-node-2">
<sem-c>
<sem> <sem>
<sem-c>
<sem><span>$label</span></sem> <sem><span>$label</span></sem>
</sem-c>
</sem> </sem>
</sem-c>
</sem> </sem>
'''); ''');
} }
@ -3871,9 +3983,7 @@ void _testFocusable() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem><span>focusable text</span></sem> <sem><span>focusable text</span></sem>
</sem-c>
</sem> </sem>
'''); ''');
@ -4568,11 +4678,9 @@ void _testRequirable() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem></sem> <sem></sem>
<sem aria-required="false"></sem> <sem aria-required="false"></sem>
<sem aria-required="true"></sem> <sem aria-required="true"></sem>
</sem-c>
</sem> </sem>
'''); ''');
@ -4597,11 +4705,9 @@ void _testRequirable() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem></sem> <sem></sem>
<sem aria-required="true"></sem> <sem aria-required="true"></sem>
<sem aria-required="false"></sem> <sem aria-required="false"></sem>
</sem-c>
</sem> </sem>
'''); ''');
@ -4612,11 +4718,9 @@ void _testRequirable() {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem> <sem>
<sem-c>
<sem></sem> <sem></sem>
<sem></sem> <sem></sem>
<sem></sem> <sem></sem>
</sem-c>
</sem> </sem>
'''); ''');
@ -4671,6 +4775,7 @@ void updateNode(
int headingLevel = 0, int headingLevel = 0,
String? linkUrl, String? linkUrl,
List<String>? controlsNodes, List<String>? controlsNodes,
ui.SemanticsRole role = ui.SemanticsRole.none,
}) { }) {
transform ??= Float64List.fromList(Matrix4.identity().storage); transform ??= Float64List.fromList(Matrix4.identity().storage);
childrenInTraversalOrder ??= Int32List(0); childrenInTraversalOrder ??= Int32List(0);
@ -4678,6 +4783,7 @@ void updateNode(
additionalActions ??= Int32List(0); additionalActions ??= Int32List(0);
builder.updateNode( builder.updateNode(
id: id, id: id,
role: role,
flags: flags, flags: flags,
actions: actions, actions: actions,
maxValueLength: maxValueLength, maxValueLength: maxValueLength,

View File

@ -110,9 +110,7 @@ Future<void> testMain() async {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem aria-label="I am a parent" role="group"> <sem aria-label="I am a parent" role="group">
<sem-c>
<sem><span>I am a child</span></sem> <sem><span>I am a child</span></sem>
</sem-c>
</sem>'''); </sem>''');
semantics().semanticsEnabled = false; semantics().semanticsEnabled = false;
@ -159,9 +157,7 @@ Future<void> testMain() async {
expectSemanticsTree(owner(), ''' expectSemanticsTree(owner(), '''
<sem aria-label="I am a parent" role="group"> <sem aria-label="I am a parent" role="group">
<sem-c>
<sem><span>I am a child</span></sem> <sem><span>I am a child</span></sem>
</sem-c>
</sem>'''); </sem>''');
} }