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

Conversation

@yjbanov
Copy link
Contributor

@yjbanov yjbanov commented Apr 15, 2022

Fix traversal and hit test orders for web.

Problems with the previous approach:

  • The ARIA semantics tree was rendered behind the scene host. This was done to allow platform views be reachable using assistive technology. Putting semantics on top used to obscure the underlying platform views. However, now we ended up with the opposite problem: platform views obscure semantics. For example, a pointer interceptor that's meant to simply forward pointer events to the framework can frequently stretch across the whole screen, so no pointer events could land on the semantics tree at all. This is not a problem when users use accessibility shortcuts, but it is a problem when users use touch to explore UI.
    • Fix: move the semantics tree on top of the scene host. So the platform views are still reachable, make container nodes transparent hit-test wise using CSS pointer-events: none.
  • Child semantics nodes were always rendered in traversal order. Hit test order was ignored. This became problematic when the framework started customizing hit test order (e.g. Reverse the semantics order of modal barrier and modal scope flutter#59290).
    • Fix: use z-index CSS property to implement child hit test order.
  • We used aria-hidden: true on the <flt-scene-host> element, but because that property applies to the whole subtree and platform views are composited under it, platform views were hidden from accessibility too, even though they still received regular pointer and keyboard events.
    • Fix: do not hide <flt-scene-host>, but hide <flt-picture> elements instead. We are only interested in hiding DOM used to render pixels for widgets, so hiding just the pictures is sufficient.

Fixes flutter/flutter#100157

@flutter-dashboard flutter-dashboard bot added the platform-web Code specifically for the web engine label Apr 15, 2022
_accessibilityPlaceholder,
_sceneHostElement!,

// The semantic host goes last because hit-test order-wise we want it to
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: avoid we

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here and everywhere, we may refer to different entity based on the reader's perspective.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

// At this point we're guaranteed to have had a non-empty previous child list.
final List<SemanticsObject> previousChildrenInRenderOrder = _currentChildrenInRenderOrder!;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is goal for the rest of this function is too detach old children and attach new children? It feels quite complex as is? the intersectionIndicesNew can probably be removed since it is not used

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe just insert old list to a set to store old list, and loop through the new list and see if it is in the set. If it is not, attach the element. If it is, remove from the set. At the end just detach remaining element it the set.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The most complex part (starting with the "Both non-empty case" comment) tries to deal with the child order. When DOM nodes move unnecessarily ATs lose focus. So what this function does is compute the longest chain of existing nodes that did not move relative to each other. It keeps those nodes stationary, and moves/adds/removes the nodes around them. This covers most scenarios (scrolls in either direction, insertions, deletions, drag'n'drop, and more). I don't think this happens in native accessibility APIs because they can track moving nodes so long as their IDs are stable.

I am going to leave a comment explaining this. Would that help?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch about intersectionIndicesNew! It's unnecessary. Removed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah so the attach and detach order matter. SGTM then

Copy link
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I nitpicked some doc string styling, since they are not introduced in this pr, I will leave decision to you whether you want to fix them. Overall this looks good. just one concern about missing test for glassPaneElementHostNode element order

html.Element? get sceneElement => _sceneElement;
html.Element? _sceneElement;

/// This is state persistent across hot restarts that indicates what
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: doc string should start with a brief sentence, or convert this to //

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc comments under engine are not public (the dart:_engine library is private). We only use /// to get dartdoc linking (// will lose the links). We do not aim for public dartdoc level of quality. But thanks for setting a high standard!

}

/// We don't want to unnecessarily move DOM nodes around. If a DOM node is
/// Don't unnecessarily move DOM nodes around. If a DOM node is
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: same as above

}
}

/// The framework specifies semantics in physical pixels, but CSS uses
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: same as above

// DOM nodes created for semantics objects are positioned absolutely using
// transforms.
element.style.position = 'absolute';
element.setAttribute('id', 'flt-semantic-node-$id');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not a big deal: Is this test only? if so, it can be put into assert

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, accessibility node count tends to be small enough for this extra info to not matter in terms of performance. We also aggressively reuse accessibility nodes, so this constructor is only called once for the entire lifetime of a node with an ID. It is useful for debugging production apps, so I'd like to keep it. If this becomes a performance problem, it's easy enough to remove. It's not used for anything.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sg

}

// At this point we're guaranteed to have had a non-empty previous child list.
final List<SemanticsObject> previousChildrenInRenderOrder = _currentChildrenInRenderOrder!;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah so the attach and detach order matter. SGTM then

childrenInHitTestOrder: Int32List.fromList(<int>[1]),
childrenInTraversalOrder: Int32List.fromList(<int>[1]),
);
updateNode(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a comment about why this additional update is needed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was just a bug in the previous code. It pretended to have a child, but didn't send child info. I added more asserts in the SemanticsOwner, which uncovered the bug. I don't think a comment is necessary because this is now enforced everywhere (I'd have to write many comments like that, everywhere there are child nodes), and the assertion failure should make it obvious what's wrong if someone mistakenly removes it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sg

// elements transparent. This way, if a platform view appears among other
// interactive Flutter widgets, as long as those widgets do not intersect
// with the platform view, the platform view will be reachable.
semanticsHostElement,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a test coverage for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the past I could find a way to write a test that would emulate browser's hit test. But I just found this: https://developer.mozilla.org/en-US/docs/web/api/document/elementsfrompoint. I'm going to try.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

elementsFromPoint worked! I added a test. Also added CanvasKit mode to that test (since platform views are composited differently in CanvasKit), and while doing that discovered that offset was ignored in addPlatformView, so fixed that along the way.

Copy link
Member

@ditman ditman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is only ~20% scary, but the code makes sense, and the tests are logical. LGTM!

semanticsHostElement,
]);

// When debugging semantics, make the scene semi-transparent so that the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that the accessibility tree renders on top of the scene host, this is probably not required anymore :P

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Strictly speaking, no. But reducing the transparency of the scene makes the semantics overlay more visible, making it easier to work with. I'll keep the transparency, but I will update the comment.

@yjbanov yjbanov requested a review from harryterkelsen April 18, 2022 18:03
Copy link
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@yjbanov yjbanov force-pushed the dialog-a11y branch 2 times, most recently from 56b84e9 to 23ba1f8 Compare April 18, 2022 22:30
@yjbanov yjbanov merged commit bd93f6c into flutter:main Apr 18, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Apr 19, 2022
justinmc pushed a commit to justinmc/engine that referenced this pull request Apr 20, 2022
* [web:a11y] implement traversal and hit-test orders
* remove unused intersectionIndicesNew
* canvaskit: fix platform view offset and scene host initialization
* remove "we" in a bunch of comments
CaseyHillers pushed a commit to CaseyHillers/engine that referenced this pull request Apr 22, 2022
* [web:a11y] implement traversal and hit-test orders
* remove unused intersectionIndicesNew
* canvaskit: fix platform view offset and scene host initialization
* remove "we" in a bunch of comments
CaseyHillers pushed a commit that referenced this pull request Apr 22, 2022
…es (#32867)

* [web] do not allocate canvases just for text (#30804)

* [web:a11y] fix traversal and hit-test orders (#32712)

* [web:a11y] implement traversal and hit-test orders
* remove unused intersectionIndicesNew
* canvaskit: fix platform view offset and scene host initialization
* remove "we" in a bunch of comments

Co-authored-by: Yegor <[email protected]>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

accessibility platform-web Code specifically for the web engine

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[pointer_interceptor] Wrapped elements are not accessible.

4 participants