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;
_neutralizeDomScrollPosition();
semanticsObject.recomputePositionAndSize();
semanticsObject.updateChildrenPositionAndSize();
final int semanticsId = semanticsObject.id;
if (doScrollForward) {
@ -131,6 +132,7 @@ class SemanticScrollable extends SemanticRole {
semanticsObject.owner.addOneTimePostUpdateCallback(() {
_neutralizeDomScrollPosition();
semanticsObject.recomputePositionAndSize();
semanticsObject.updateChildrenPositionAndSize();
});
if (_scrollListener == null) {
@ -203,8 +205,8 @@ class SemanticScrollable extends SemanticRole {
// Read back because the effective value depends on the amount of content.
_effectiveNeutralScrollPosition = element.scrollTop.toInt();
semanticsObject
..verticalContainerAdjustment = _effectiveNeutralScrollPosition.toDouble()
..horizontalContainerAdjustment = 0.0;
..verticalScrollAdjustment = _effectiveNeutralScrollPosition.toDouble()
..horizontalScrollAdjustment = 0.0;
} else {
// Place the _scrollOverflowElement at the end of the content and
// 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.
_effectiveNeutralScrollPosition = element.scrollLeft.toInt();
semanticsObject
..verticalContainerAdjustment = 0.0
..horizontalContainerAdjustment = _effectiveNeutralScrollPosition.toDouble();
..verticalScrollAdjustment = 0.0
..horizontalScrollAdjustment = _effectiveNeutralScrollPosition.toDouble();
}
}

View File

@ -1383,33 +1383,6 @@ class SemanticsObject {
/// The dom element of this semantics object.
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.
///
/// 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.
_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) {
element.style.pointerEvents = 'all';
} else {
@ -1710,22 +1677,15 @@ class SemanticsObject {
// Trivial case: remove all children.
if (_childrenInHitTestOrder == null || _childrenInHitTestOrder!.isEmpty) {
if (_currentChildrenInRenderOrder == null || _currentChildrenInRenderOrder!.isEmpty) {
// A container element must not have been created when child list is empty.
assert(_childContainerElement == null);
_currentChildrenInRenderOrder = null;
return;
}
// A container element must have been created when child list is not empty.
assert(_childContainerElement != null);
// Remove all children from this semantics object.
final int len = _currentChildrenInRenderOrder!.length;
for (int i = 0; i < len; i++) {
owner._detachObject(_currentChildrenInRenderOrder![i].id);
}
_childContainerElement!.remove();
_childContainerElement = null;
_currentChildrenInRenderOrder = null;
return;
}
@ -1734,7 +1694,6 @@ class SemanticsObject {
final Int32List childrenInTraversalOrder = _childrenInTraversalOrder!;
final Int32List childrenInHitTestOrder = _childrenInHitTestOrder!;
final int childCount = childrenInHitTestOrder.length;
final DomElement? containerElement = getOrCreateChildContainer();
assert(childrenInTraversalOrder.length == childrenInHitTestOrder.length);
@ -1766,7 +1725,7 @@ class SemanticsObject {
// Trivial case: previous list was empty => just populate the container.
if (_currentChildrenInRenderOrder == null || _currentChildrenInRenderOrder!.isEmpty) {
for (final SemanticsObject child in childrenInRenderOrder) {
containerElement!.append(child.element);
element.append(child.element);
owner._attachObject(parent: this, child: child);
}
_currentChildrenInRenderOrder = childrenInRenderOrder;
@ -1850,9 +1809,9 @@ class SemanticsObject {
final SemanticsObject child = childrenInRenderOrder[i];
if (!stationaryIds.contains(child.id)) {
if (refNode == null) {
containerElement!.append(child.element);
element.append(child.element);
} else {
containerElement!.insertBefore(child.element, refNode);
element.insertBefore(child.element, refNode);
}
owner._attachObject(parent: this, child: child);
} else {
@ -2030,9 +1989,10 @@ class SemanticsObject {
// Reparent element.
if (previousElement != element) {
final DomElement? container = _childContainerElement;
if (container != null) {
element.append(container);
if (_currentChildrenInRenderOrder != null) {
for (final child in _currentChildrenInRenderOrder!) {
element.append(child.element);
}
}
final DomElement? parent = previousElement?.parent;
if (parent != null) {
@ -2127,32 +2087,53 @@ class SemanticsObject {
/// Indicates whether the node is currently expanded.
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
/// `scrollTop` offset in the DOM.
///
/// This field must not be null.
double verticalContainerAdjustment = 0.0;
double verticalScrollAdjustment = 0.0;
/// Role-specific adjustment of the horizontal position of the child
/// container.
/// Role-specific adjustment of the horizontal position of children.
///
/// This is used, for example, by the [SemanticScrollable] to compensate for the
/// `scrollLeft` offset in the DOM.
///
/// 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
/// [hasChildren], of [getOrCreateChildContainer].
double verticalAdjustmentFromParent = 0.0;
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() {
element.style
..width = '${_rect!.width}px'
..height = '${_rect!.height}px';
final DomElement? containerElement = hasChildren ? getOrCreateChildContainer() : null;
final bool hasZeroRectOffset = _rect!.top == 0.0 && _rect!.left == 0.0;
final Float32List? transform = _transform;
final bool hasIdentityTransform =
@ -2160,27 +2141,25 @@ class SemanticsObject {
if (hasZeroRectOffset &&
hasIdentityTransform &&
verticalContainerAdjustment == 0.0 &&
horizontalContainerAdjustment == 0.0) {
verticalAdjustmentFromParent == 0.0 &&
horizontalAdjustmentFromParent == 0.0) {
_clearSemanticElementTransform(element);
if (containerElement != null) {
_clearSemanticElementTransform(containerElement);
}
return;
}
late Matrix4 effectiveTransform;
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) {
final double left = _rect!.left;
final double top = _rect!.top;
effectiveTransform = Matrix4.translationValues(left, top, 0.0);
effectiveTransformIsIdentity = left == 0.0 && top == 0.0;
effectiveTransformIsIdentity = false;
} else {
// Clone to avoid mutating _transform.
effectiveTransform =
Matrix4.fromFloat32List(transform).clone()..translate(_rect!.left, _rect!.top);
effectiveTransform = Matrix4.fromFloat32List(transform).clone()..translate(left, top);
effectiveTransformIsIdentity = effectiveTransform.isIdentity();
}
} else if (!hasIdentityTransform) {
@ -2195,19 +2174,15 @@ class SemanticsObject {
} else {
_clearSemanticElementTransform(element);
}
if (containerElement != null) {
if (!hasZeroRectOffset ||
verticalContainerAdjustment != 0.0 ||
horizontalContainerAdjustment != 0.0) {
final double translateX = -_rect!.left + horizontalContainerAdjustment;
final double translateY = -_rect!.top + verticalContainerAdjustment;
containerElement.style
..top = '${translateY}px'
..left = '${translateX}px';
} else {
_clearSemanticElementTransform(containerElement);
}
/// Computes the size and position of children.
void updateChildrenPositionAndSize() {
final Set<SemanticsObject> dirtyNodes = <SemanticsObject>{};
recomputeChildrenAdjustment(dirtyNodes);
for (final node in dirtyNodes) {
node.recomputePositionAndSize();
}
}
@ -2769,7 +2744,7 @@ class EngineSemanticsOwner {
removals.add(node);
} else {
assert(node._parent == parent);
assert(node.element.parentNode == parent._childContainerElement);
assert(node.element.parentNode == parent.element);
}
return true;
});
@ -2872,14 +2847,29 @@ class EngineSemanticsOwner {
object.updateSelf(nodeUpdate);
}
final Set<SemanticsObject> nodesWithDirtyPositionsAndSizes = <SemanticsObject>{};
// Second, fix the tree structure. This is moved out into its own loop,
// because each object's own information must be updated first.
for (final SemanticsNodeUpdate nodeUpdate in nodeUpdates) {
final SemanticsObject object = _semanticsTree[nodeUpdate.id]!;
object.updateChildren();
if (object.isRectDirty ||
object.isTransformDirty ||
object.isScrollPositionDirty ||
object.isChildrenInTraversalOrderDirty) {
nodesWithDirtyPositionsAndSizes.add(object);
object.recomputeChildrenAdjustment(nodesWithDirtyPositionsAndSizes);
}
object._dirtyFields = 0;
}
for (final node in nodesWithDirtyPositionsAndSizes) {
node.recomputePositionAndSize();
}
final SemanticsObject root = _semanticsTree[0]!;
if (_rootSemanticsElement == null) {
_rootSemanticsElement = root.element;
@ -2913,9 +2903,6 @@ AFTER: $description
// Dirty fields should be cleared after the tree has been finalized.
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
// relationship of the semantics tree.
if (object._childrenInTraversalOrder != null) {

View File

@ -274,7 +274,6 @@ class HtmlPatternMatcher extends Matcher {
static bool _areTagsEqual(html.Element a, html.Element b) {
const Map<String, String> synonyms = <String, String>{
'sem': 'flt-semantics',
'sem-c': 'flt-semantics-container',
'sem-img': 'flt-semantics-img',
'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.
expectSemanticsTree(view1.semantics, '''
<sem style="filter: opacity(0%); color: rgba(0, 0, 0, 0)">
<sem-c>
<sem flt-tappable="" role="button"></sem>
</sem-c>
</sem>''');
expectSemanticsTree(view2.semantics, '''
<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-c>
</sem>
''');

View File

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

View File

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