diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/scrollable.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/scrollable.dart index 44beba5ae9..08e96af35d 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/scrollable.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/scrollable.dart @@ -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(); } } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart index 355b02d9ec..a9459d640a 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart @@ -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 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 dirtyNodes = {}; + 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 nodesWithDirtyPositionsAndSizes = {}; // 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) { diff --git a/engine/src/flutter/lib/web_ui/test/common/matchers.dart b/engine/src/flutter/lib/web_ui/test/common/matchers.dart index 7537b40cfd..e12d1a9025 100644 --- a/engine/src/flutter/lib/web_ui/test/common/matchers.dart +++ b/engine/src/flutter/lib/web_ui/test/common/matchers.dart @@ -274,7 +274,6 @@ class HtmlPatternMatcher extends Matcher { static bool _areTagsEqual(html.Element a, html.Element b) { const Map synonyms = { 'sem': 'flt-semantics', - 'sem-c': 'flt-semantics-container', 'sem-img': 'flt-semantics-img', 'sem-tf': 'flt-semantics-text-field', }; diff --git a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_multi_view_test.dart b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_multi_view_test.dart index 676c41d8b2..8fce7b62e9 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_multi_view_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_multi_view_test.dart @@ -99,15 +99,11 @@ Future testMain() async { // Test that each view renders its own semantics tree. expectSemanticsTree(view1.semantics, ''' - - '''); expectSemanticsTree(view2.semantics, ''' - - '''); diff --git a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart index 53d4806bb8..163489847c 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -161,10 +161,8 @@ void _testSemanticRole() { tester.expectSemantics(''' - - '''); tester.updateNode( @@ -178,10 +176,8 @@ void _testSemanticRole() { tester.expectSemantics(''' - - '''); tester.updateNode( @@ -196,11 +192,9 @@ void _testSemanticRole() { tester.expectSemantics(''' - - '''); }); } @@ -523,9 +517,7 @@ void _testEngineSemanticsOwner() { expectSemanticsTree(owner(), ''' - Hello - '''); // Update @@ -533,9 +525,7 @@ void _testEngineSemanticsOwner() { expectSemanticsTree(owner(), ''' - World - '''); // Remove @@ -543,9 +533,7 @@ void _testEngineSemanticsOwner() { expectSemanticsTree(owner(), ''' - - '''); semantics().semanticsEnabled = false; @@ -566,9 +554,7 @@ void _testEngineSemanticsOwner() { expectSemanticsTree(owner(), ''' - Hello - '''); // Update @@ -581,9 +567,7 @@ void _testEngineSemanticsOwner() { expect(tree[1]!.element.tagName.toLowerCase(), 'a'); expectSemanticsTree(owner(), ''' - Hello - '''); expect(existingParent, tree[1]!.element.parent); @@ -605,9 +589,7 @@ void _testEngineSemanticsOwner() { expectSemanticsTree(owner(), ''' - tooltip - '''); // Update @@ -615,9 +597,7 @@ void _testEngineSemanticsOwner() { expectSemanticsTree(owner(), ''' - tooltip\nHello - '''); // Remove @@ -625,9 +605,7 @@ void _testEngineSemanticsOwner() { expectSemanticsTree(owner(), ''' - - '''); semantics().semanticsEnabled = false; @@ -852,7 +830,7 @@ void _testHeader() { owner().updateSemantics(builder.build()); expectSemanticsTree(owner(), ''' -
+
'''); 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(), ''' - - - - -'''); + expectSemanticsTree(owner(), ''''''); 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(), ''' - - - - -'''); + expectSemanticsTree(owner(), ''''''); 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,63 +1010,103 @@ 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 { - semantics() - ..debugOverrideTimestampFunction(() => _testTime) - ..semanticsEnabled = true; + test( + 'child node transform compensates for parent rect offset when parent rect changed', + () async { + semantics() + ..debugOverrideTimestampFunction(() => _testTime) + ..semanticsEnabled = true; - final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder(); - updateNode( - builder, - transform: Matrix4.identity().toFloat64(), - rect: const ui.Rect.fromLTRB(0, 0, 20, 20), - childrenInHitTestOrder: Int32List.fromList([1]), - childrenInTraversalOrder: Int32List.fromList([1]), - ); - updateNode( - builder, - id: 1, - transform: Matrix4.identity().toFloat64(), - rect: const ui.Rect.fromLTRB(10, 10, 20, 20), - ); + final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder(); + updateNode( + builder, + transform: Matrix4.identity().toFloat64(), + rect: const ui.Rect.fromLTRB(10, 10, 20, 20), + childrenInHitTestOrder: Int32List.fromList([1]), + childrenInTraversalOrder: Int32List.fromList([1]), + ); + updateNode( + builder, + id: 1, + transform: Matrix4.identity().toFloat64(), + rect: const ui.Rect.fromLTRB(0, 0, 5, 5), + ); - owner().updateSemantics(builder.build()); - expectSemanticsTree(owner(), ''' - - - - -'''); + owner().updateSemantics(builder.build()); + expectSemanticsTree(owner(), ''''''); - final DomElement parentElement = owner().semanticsHost.querySelector('flt-semantics')!; - final DomElement container = owner().semanticsHost.querySelector('flt-semantics-container')!; + final DomElement parentElement = owner().semanticsHost.querySelector('flt-semantics')!; + 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'); - } else { - expect(parentElement.style.top, ''); - expect(parentElement.style.left, ''); - expect(container.style.top, ''); - expect(container.style.left, ''); - } - expect(parentElement.style.transform, ''); - expect(parentElement.style.transformOrigin, ''); - expect(container.style.transform, ''); - expect(container.style.transformOrigin, ''); + 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.transformOrigin, '0px 0px 0px'); + expect(childElement.style.transformOrigin, '0px 0px 0px'); + } + 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([1]), + childrenInTraversalOrder: Int32List.fromList([1]), + ); + + owner().updateSemantics(builder2.build()); + expectSemanticsTree(owner(), ''''''); + + 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(), ''' - - '''); } @@ -1141,12 +1147,10 @@ void _testContainer() { owner().updateSemantics(builder.build()); expectSemanticsTree(owner(), ''' - - '''); } @@ -1161,12 +1165,10 @@ void _testContainer() { owner().updateSemantics(builder.build()); expectSemanticsTree(owner(), ''' - - '''); } @@ -1181,12 +1183,10 @@ void _testContainer() { owner().updateSemantics(builder.build()); expectSemanticsTree(owner(), ''' - - '''); } @@ -1210,10 +1210,8 @@ void _testContainer() { owner().updateSemantics(builder.build()); expectSemanticsTree(owner(), ''' - - '''); final DomElement root = owner().semanticsHost.querySelector('#flt-semantic-node-0')!; @@ -1246,10 +1244,8 @@ void _testContainer() { owner().updateSemantics(builder.build()); expectSemanticsTree(owner(), ''' - - '''); 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(), ''' - - '''); final DomElement root = owner().semanticsHost.querySelector('#flt-semantic-node-0')!; @@ -1321,20 +1314,14 @@ void _testContainer() { owner().updateSemantics(builder.build()); expectSemanticsTree(owner(), ''' - - - - - - '''); expect( @@ -1361,15 +1348,11 @@ void _testContainer() { owner().updateSemantics(builder.build()); expectSemanticsTree(owner(), ''' - - - - '''); expect(owner().debugSemanticsTree!.keys.toList(), unorderedEquals([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([1, 2]), + childrenInHitTestOrder: Int32List.fromList([1, 2]), + ); + updateNode( + builder, + id: 1, + childrenInTraversalOrder: Int32List.fromList([3, 4]), + childrenInHitTestOrder: Int32List.fromList([3, 4]), + ); + updateNode( + builder, + id: 2, + childrenInTraversalOrder: Int32List.fromList([5, 6]), + childrenInHitTestOrder: Int32List.fromList([5, 6]), + ); + updateNode(builder, id: 3); + updateNode(builder, id: 4); + updateNode(builder, id: 5); + updateNode(builder, id: 6); + + owner().updateSemantics(builder.build()); + expectSemanticsTree(owner(), ''' + + + + + + + + + + '''); + + expect( + owner().debugSemanticsTree!.keys.toList(), + unorderedEquals([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([1, 2]), + childrenInHitTestOrder: Int32List.fromList([1, 2]), + ); + updateNode( + builder, + id: 1, + childrenInTraversalOrder: Int32List.fromList([3, 4]), + childrenInHitTestOrder: Int32List.fromList([3, 4]), + ); + updateNode( + builder, + id: 2, + role: ui.SemanticsRole.table, + childrenInTraversalOrder: Int32List.fromList([5, 6]), + childrenInHitTestOrder: Int32List.fromList([5, 6]), + ); + updateNode(builder, id: 3); + updateNode(builder, id: 4); + updateNode(builder, id: 5); + updateNode(builder, id: 6); + + owner().updateSemantics(builder.build()); + expectSemanticsTree(owner(), ''' + + + + + + + + + + '''); + + expect( + owner().debugSemanticsTree!.keys.toList(), + unorderedEquals([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([1, 2]), + childrenInHitTestOrder: Int32List.fromList([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([3]), + childrenInHitTestOrder: Int32List.fromList([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(), ''' + + + + + + '''); + + expect(owner().debugSemanticsTree!.keys.toList(), unorderedEquals([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([1]), + childrenInHitTestOrder: Int32List.fromList([1]), + ); + updateNode( + builder, + id: 1, + childrenInTraversalOrder: Int32List.fromList([3]), + childrenInHitTestOrder: Int32List.fromList([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(), ''' + + + + + '''); + + expect(owner().debugSemanticsTree!.keys.toList(), unorderedEquals([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(), ''' - - '''); final DomElement scrollable = findScrollable(owner()); @@ -1511,11 +1665,9 @@ void _testVerticalScrolling() { expectSemanticsTree(owner(), ''' - - '''); final DomElement scrollable = owner().debugSemanticsTree![0]!.element; @@ -1592,11 +1744,9 @@ void _testVerticalScrolling() { expectSemanticsTree(owner(), ''' - - '''); final DomElement scrollable = owner().debugSemanticsTree![0]!.element; @@ -1682,9 +1832,7 @@ void _testHorizontalScrolling() { expectSemanticsTree(owner(), ''' - - '''); final DomElement scrollable = findScrollable(owner()); @@ -1738,11 +1886,9 @@ void _testHorizontalScrolling() { expectSemanticsTree(owner(), ''' - - '''); final DomElement scrollable = findScrollable(owner()); @@ -2297,10 +2443,8 @@ void _testCheckables() { expectSemanticsTree(owner(), ''' - - '''); @@ -2398,11 +2542,9 @@ void _testSelectables() { expectSemanticsTree(owner(), ''' - - '''); @@ -2427,11 +2569,9 @@ void _testSelectables() { expectSemanticsTree(owner(), ''' - - '''); @@ -2496,11 +2636,9 @@ void _testExpandables() { expectSemanticsTree(owner(), ''' - - '''); @@ -2525,11 +2663,9 @@ void _testExpandables() { expectSemanticsTree(owner(), ''' - - '''); @@ -2783,9 +2919,7 @@ void _testTappable() { expectSemanticsTree(owner(), ''' - - '''); @@ -2876,9 +3010,7 @@ void _testImage() { expectSemanticsTree(owner(), ''' - - '''); semantics().semanticsEnabled = false; @@ -2928,9 +3060,7 @@ void _testImage() { expectSemanticsTree(owner(), ''' - - '''); semantics().semanticsEnabled = false; @@ -3147,11 +3277,9 @@ void _testPlatformView() { owner().updateSemantics(builder.build()); expectSemanticsTree(owner(), ''' - - '''); final DomElement root = owner().semanticsHost.querySelector('#flt-semantic-node-0')!; @@ -3245,7 +3373,7 @@ void _testGroup() { owner().updateSemantics(builder.build()); expectSemanticsTree(owner(), ''' - + '''); semantics().semanticsEnabled = false; @@ -3277,7 +3405,7 @@ void _testRoute() { owner().updateSemantics(builder.build()); expectSemanticsTree(owner(), ''' - + '''); expect(owner().debugSemanticsTree![0]!.semanticRole?.kind, EngineSemanticsRole.route); @@ -3316,7 +3444,7 @@ void _testRoute() { // But still sets the dialog role. expectSemanticsTree(owner(), ''' - + '''); expect(owner().debugSemanticsTree![0]!.semanticRole?.kind, EngineSemanticsRole.route); @@ -3348,13 +3476,9 @@ void _testRoute() { expectSemanticsTree(owner(), ''' - - $label - - '''); } @@ -3414,13 +3538,9 @@ void _testRoute() { expectSemanticsTree(owner(), ''' - - Hello - - '''); @@ -3597,16 +3717,12 @@ void _testRoute() { tester.expectSemantics(''' - - Heading Click me! - - '''); final DomElement span = owner().debugSemanticsTree![2]!.element.querySelectorAll('span').single; @@ -3706,13 +3822,9 @@ void _testDialogs() { expectSemanticsTree(owner(), ''' - - $label - - '''); } @@ -3871,9 +3983,7 @@ void _testFocusable() { expectSemanticsTree(owner(), ''' - focusable text - '''); @@ -4568,11 +4678,9 @@ void _testRequirable() { expectSemanticsTree(owner(), ''' - - '''); @@ -4597,11 +4705,9 @@ void _testRequirable() { expectSemanticsTree(owner(), ''' - - '''); @@ -4612,11 +4718,9 @@ void _testRequirable() { expectSemanticsTree(owner(), ''' - - '''); @@ -4671,6 +4775,7 @@ void updateNode( int headingLevel = 0, String? linkUrl, List? 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, diff --git a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_text_test.dart b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_text_test.dart index ccb7aec643..12cb5619f8 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_text_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_text_test.dart @@ -110,9 +110,7 @@ Future testMain() async { expectSemanticsTree(owner(), ''' - I am a child - '''); semantics().semanticsEnabled = false; @@ -159,9 +157,7 @@ Future testMain() async { expectSemanticsTree(owner(), ''' - I am a child - '''); }