whatwg / html

HTML Standard
https://html.spec.whatwg.org/multipage/
Other
8.16k stars 2.69k forks source link

Discussing how to focus navigate display: contents elements that are focusable in CSS reading-flow #10533

Open dizhang168 opened 3 months ago

dizhang168 commented 3 months ago

What is the issue with the HTML Standard?

The CSS Working Group has resolved to add the new reading-flow property (https://github.com/w3c/csswg-drafts/issues/8589, spec) to enable focus navigation in visual order for layout items that might not be displayed in source order (such as grid, flex and masonry items). Chromium is implementing the new property and opened a proposal on the needed HTML spec change. The explainer can be found here.

One remaining question is about display:contents elements in this context. display: contents elements are elements that do not have a rendered style or layout. They can still be focusable if the tabindex attribute is set, per https://github.com/w3c/csswg-drafts/issues/2632 and https://github.com/w3c/csswg-drafts/issues/9230#issuecomment-2165098385. How display: contents elements should be navigated is not decided yet (https://github.com/w3c/csswg-drafts/issues/9230).

This issue discusses a few possibilities for handling display:contents elements with reading-flow.

Option 1: Visit all items of a reading flow container together

All reading flow items are visited in the order they are defined by the layout algorithm for CSS reading-flow. Afterward, we visit all descendant display: contents elements, in case they are focusable. Only one focus scope is created per grid/flex container.

Spec change

Update steps of find the next item in reading order.

  1. Let reading flow layout items be the list of reading flow items owned by a reading flow scope, sorted in reading flow.
  2. Let display contents elements be the list of display: contents descendants of the reading flow container, as they appear in source order.
  3. Let reading flow items be the concatenation of _reading flow layout items and display contents elements._
  4. Visit step 2-4 of the algorithm to find the next item in reading order.

Pro: The elements are mostly visited in visual order.

Con: This would result in a mismatch with the Accessibility tree, which expects visiting siblings together and doesn’t want to combine descendants of different parents together.

Option 2: A reading flow item with display: contents is a focus scope owner

Here, we update the definition of a reading flow container such that a container with display: contents will result in a new focus scope. Within it, we will visit all the direct children of this container starting with the reading flow items and then the display: contents elements. If within those elements, a display: contents element is found, a new focus scope will be created.

Spec change

Add to definition of a reading flow container:

Replace step 1 of find the next item in reading order.

  1. Let reading flow layout items be the list of reading flow items owned by scope, sorted in reading flow order.
    1. Filter out any item whose parent is not the reading flow container.
  2. Let display contents elements be the list of display: contents direct children of scope, as they appear in source order.
  3. Let reading flow items be the concatenation of _reading flow layout items and display contents elements._

Pro: This would match with the Accessibility tree.

Con: We are more likely to get a mismatch between the visual and the reading flow order (see Example 2).

Examples

Example 1: An item has display: contents and has no element child

<div style="display:flex; reading-flow: flex-visual">
 <div id=A style="display:contents; order: 2">A</div>
 <button id=B style="order: 3">B</button>
 <button id=C style="order: 1">C</button>
</div>

This will be displayed as:

image

However, neither div nor text node A is not recognized as a flex item and the order value is not used. Additionally, A is not focusable.

The visit order should be: C -> B.

Example 2: An item has display: contents, is focusable and has no child

<div style="display:flex; reading-flow: flex-visual">
 <div id=A style="display:contents; order: 2" tabindex=0>A</div>
 <button id=B style="order: 3">B</button>
 <button id=C style="order: 1">C</button>
</div>

This will be displayed as:

image

However, text node A is not recognized as a flex item and the order value is not used. The visit order should be: C -> B -> A.

Now, the tricky part when the display contents element has children.

Example 3: An item has display: contents, is focusable and has children

<div style="display:flex; reading-flow: flex-visual">
 <div id=d1 style="display: contents" tabindex="0">
   <button style="order: 3" id="C">C</button>
   <button style="order: 1" id="A">A</button>
   <div id=d2 style="display: contents" tabindex=0>
     <button style="order: 4" id="D">D</button>
     <button style="order: 2" id="B">B</button>
   </div>
 </div>
</div>

The rendered output:

image

Given option 1, we would visit in the order A -> B -> C -> D -> d1 -> d2. Given option 2, we would visit in the order div1 -> A -> C -> div2 -> B -> D.

Example 4: A Slot is focusable and has children

<div>
 <template shadowrootmode="open">
   <style>
   .wrapper {
     display: flex;
     reading-flow: flex-visual;
   }
   </style>
   <div class="wrapper">
     <button style="order: 4" id=D>D</button>
     <!-- Because slot has display: contents, the order value doesn't get used -->
     <slot style="order: 10" tabindex=0 id="slot"></slot>
     <button style="order: 2" id=B>B</button>
   </div>
 </template>
 <button style="order: 1" id=A>A</button>
 <button style="order: 3" id=C>C</button>
</div>

The rendered output:

image

Given option 1, we would visit in the order B -> D -> slot -> A -> C. This is because it recognizes the slot as a scope owner and its children should not be visited until that scope is created. Given option 2, we would visit in the order B -> D -> slot -> A -> C.

Note that we have already requested the feedback from Accessibility implementer @aleventhal. They prefer Option 2 because of how the accessibility tree is constructed. They also don’t think it is common to have display: contents mixed with non display: contents flex/grid items so this issue should not be common. However, just because a case is uncommon doesn’t mean we shouldn’t consider it. Especially because we want this feature to work well with web components. We have opened a blog post to request developer feedback.

dizhang168 commented 3 months ago

We would like to discuss and resolve this issue at the next WHATNOT meeting. In particular, we would like to get HTML editors and implementers feedback on how to approach the display:contents issue, such as:

cc: @emilio, @mfreed7

domenic commented 3 months ago
  • Should display: contents specifications be included in HTML standard?

What do you mean by this in more detail? Is it about the sequential focus order specifically? If so, then I think the answer is yes, because HTML defines sequential focus order in general. Or is it about something else?

dizhang168 commented 3 months ago

The question is specifically for focus navigation (although the same implementation should be used for accessibility tree and screen reader reading). Awesome, I also believe this should be included in HTML.

aleventhal commented 3 months ago

Regarding the a11y tree, the specific concern is reading order items are not DOM siblings of each other. If they are still siblings, then there is no issue with having intervening display:contents ancestors between the reordered items and the container. However, if they are not siblings, it can wreak havoc with the a11y tree logic because the display:contents parent elements may need to be in the a11y tree structure. In that case we can't always both keep things correct -- either the reordering must be restricted to DOM siblings, or the parent in the a11y tree would need to be changed from what it was in the DOM (this is hard to implement and I would argue is unexpected).

dizhang168 commented 3 months ago

Summary from 2024/08/01 meeting: Display: contents being focusable are pretty rare cases to begin with. Given there are currently no preference nor use cases from web developers, we should prioritize what is easiest to implement and what wouldn't break existing API. Option 2 is the preferred option because, as mentioned above, this would avoid making huge/complex changes to the accessibility tree.

annevk commented 1 month ago

@aleventhal say you have two sibling nodes A and B, in that order. B has two child nodes, B1, and B2, in that order. Then with CSS you make the visual order B2 A B1 and you use display: contents on B.

What is the specific problem with making the focus order for that case be B B2 A B1? (At least I get the impression you are arguing for B B2 B1 A for reasons I don't quite understand.)

aleventhal commented 1 month ago

Hi Anne, thanks for example. I've written it as

Grandparent
   Parent A
   Parent B   display:contents
      Child B1
      Child B2

The problem starts with the a11y tree, not the focus order. And the problem for the a11y tree is that Parent B could be exposed if it has any interesting attributes. It doesn't need to be focusable. Even something like a role or aria-live could cause it to be part of the a11y tree. But, it also needs to be the parent of B1 and B2. So how could you have the order of B2 A B1? The parent would end up parenting the first and third child, but not the second, which is obviously a problem. It may be helpful to know that the a11y tree is built with a version of the flat DOM tree that includes shadow subtrees and pseudo elements.

Because of the a11y tree problem, now the focus order also becomes a problem, because the focus ordwer needs to match the a11y tree order or it will seriously be confusing for AT users.

Before we consider any complex new tree building logic we need to consider whether it's worth it, if there are broken use cases with my recommended approach, which is to restrict what reader order can do: siblings of the flat DOM tree can be reordered relative to each other within the a11y tree and focus navigation system, but other reordering is not possible. This still allows intervening display:contents ancestors between the reading order container and reading items, which I do believe is an important use case. Therefore the following can work sensibly:

Grandparent
   Parent display:contents
      Even another display:contents
          Item 1
          Item 2
          Item 3
          ...

But regarding your example, I've been asking if there is an example where that's really needed, because I don't see a "KISS" way that doesn't causing paradoxes for the a11y tree.

dizhang168 commented 1 month ago

This topic was discussed at TPAC during the Joint meeting WHATWG + CSS WG. There was a lot of great feedback and the meeting log can be found here: https://github.com/w3c/csswg-drafts/issues/9230#issuecomment-2369780005

@aleventhal, could you help clarify the following:

  1. Is there a strong documentation that we can point to about the rules of the Accessibility Tree?
  2. Can you expand on how tabindex works for the accessibility tree? Our understanding is that it is ignored. If so, it means the focus navigation order and the accessibility tree elements order is already mismatched. Maybe it is ok for reading-flow to mismatch as well?
aleventhal commented 1 month ago

This topic was discussed at TPAC during the Joint meeting WHATWG + CSS WG. There was a lot of great feedback and the meeting log can be found here: w3c/csswg-drafts#9230 (comment)

@aleventhal, could you help clarify the following:

  1. Is there a strong documentation that we can point to about the rules of the Accessibility Tree?

No, but for this discussion the important thing to know is that in Blink we use LayoutTreeBuilderTraversal, which is the DOM traversal that layout uses to build its tree. It's flat traversal + pseudo elements. Any DOM node can end up in the tree.

  1. Can you expand on how tabindex works for the accessibility tree? Our understanding is that it is ignored. If so, it means the focus navigation order and the accessibility tree elements order is already mismatched. Maybe it is ok for reading-flow to mismatch as well?

This is why positive values of tabindex are harmful. The only values developers should use are 0 and -1. Tabindex is an exception only because it is too late to change.

No, IMO it's not ok for the a11y tree order to mismatch the tab order. This should be a feature that benefits a11y not harms. @jcsteh maybe you want to add your opinion.

tabatkins commented 1 month ago

Pulling over my comment summarizing the TPAC discussion to centralize the topics:

Currently, the reading-flow PR specifies that if a reading flow item is display:contents, then two things happen:

  1. The item itself is treated as "not participating", so it gets put at the end of the reading order, arranged in DOM order with other non-participating things.
  2. The item is treated as a reading flow container itself, so its children follow it to the end of the order, and are arranged in the visual order specified by the parent reading flow container.

There was significant discussion on this behavior in the joint CSS/WHATWG meeting at TPAC about making this work better in several cases. Notably, it was considered bad if adding a "useless" 'reading-flow' value (applied to an element where DOM order and visual order are already consistent) could cause the reading order to now mismatch the visual order, because some of the items are in a display:contents subtree. For example, this markup:

<div class=flex>
  <button class=item>A</button>
  <div class="item container">
    <button class=item>B1</button>
    <button class=item>B2</button>
  </div>
  <button class=item>C</button>
</div>
<style>
.flex { display: flex; }
.container { display: contents; }
</style>

Currently, has consistent DOM, visual, and tabbing order: A, B1, B2, C. But if we added .flex { reading-order: flex-flow; }, the tabbing order would change to A, C, B1, B2, despite the DOM order already matching the "flex-flow" order!

Conclusions reached during the meeting:

aleventhal commented 1 month ago

In the interests of coming to a resolution, I'm here until Friday midday. Maybe we can all get together to chat.

Food for thought: can you describe the concerns from your side? Are we just talking actual broken use cases, or only about purity / consistency with the idea that CSS always applies?

Can we have a description of what we need to solve for that we aren't yet? I will try to help do that.

tabatkins commented 1 month ago

The use-case we're worried about is a display:contents element where, due to some layout shenanigans, its children are interleaved with other items. In my example, imagine the author used 'order' to make the buttons "B1, A, C, B2" visually.

If they then used reading-order: flow-visual, they would get a tab order of B1, A, C, B2 (matching the visual order, as intended) if all the buttons were direct children. But if display:contents is a strict grouping construct, the order would end up "B1, B2, A, C".

But an author could have used tabindex (instead of reading-order) to manually achieve the B1, A, C, B2 order. So our question was just: is that terrible in practice and something we shouldn't emulate? Is it worth banning that, if the result is that the tabbing order cannot match the visual order in some cases? (And tbf there are

(Note that we're pretty sure this case - interleaving of display:contents children with other items outside the display:contents thing - is a rare situation. Per @rachelandrew, most display:contents usage is just removing a wrapper, so its children stay together in the order anyway. So this might very well be something we just don't need to care about very much.)

ericwbailey commented 1 month ago

If it is helpful, tabbing to interactive items/items assigned atabindex value is not representative of the main way a lot of assistive technology users navigate. It would be via a virtual cursor for a screen reader (which uses the Accessibility Tree, informed by DOM order), or visual scanning of the page for voice control. Apologies if y'all are already aware of this.

With that said, I do want to re-emphasize:

This is why positive values of tabindex are harmful. The only values developers should use are 0 and -1. Tabindex is an exception only because it is too late to change.

Focus order deviating from reading order can be incredibly confusing for a wide range of disability types attempting to use a digital experience.

I have also observed that manually curated > 0 tabindex values often get skipped over when experiences are updated by engineers, as they are often unfamiliar with the attribute, or accessibility best practices in general.

I am also nebulously very worried about display: contents being used here, given its long, storied, and unreliable behavior.

LJWatson commented 1 month ago

Screen reader users switch between navigation modes for a variety of reasons. As a screen reader user who cannot see, if I initially Tab through some content, then switch to using screen reader commands only to discover the content is no longer in the order it was first presented, it's a horrible experience.

It's also worth mentioning that not all screen reader users are unable to see. Somewhere between 5% and 12% of screen reader users are not blind or low vision. For people in this group the experience is worse when the Tab order and accessibility tree don't match because the visual order will only be consistent with one mode of navigation, not both.

The use of tabindex with positive values has been discouraged, and warned against because it'sdisruptive for as long as I can remember.

rachelandrew commented 1 month ago

I am also nebulously very worried about display: contents being used here, given its long, storied, and unreliable behavior.

We're not "using" display: contents we're working out what to do if a developer has used display: contents in combination with reading-flow. We need to define what happens in that case. I've struggled to actually find use cases after posting about the issue, so this is possibly an edge case, but we still need to make sure whatever we do doesn't cause a problem, and is interoperable.

tabatkins commented 1 month ago

@LJWatson Apologies, but the tabindex/display:contents question is hard to convey properly, so you're actually responding to something different. ^_^

Here's the issue we're (slightly) worried about. Say an author has a flexbox with three children, A, B, and C. The B child is focusable on its own, but also has display:contents and two focusable children, B1 and B2. Now, assume that the author, thru the use of order or something, sets the visual order of the elements to be B1, A, C, B2.

If reading-flow: flex-visual is applied, should the tabbing order match the visual order? You, and @ericwbailey, and multiple other people we've polled, seem to feel very strongly that the answer is "yes", and we agree with you. However, that means that the tabbing order doesn't match the a11y tree order - the B descendants are visited first and last, but you jump out to unrelated elements between them. (Specifically, the tabbing order would be "B, B1, A, C, B2", grouping the B element with its first visible child.)

We mention tabindex in this context because the desired tabbing order can be achieved with tabindex, assigning positive numbers as appropriate. Our question was if the result of that back-and-forth jumping is bad in and of itself (ignoring the other reasons using tabindex is bad). If it's acceptable, then we can just say that visual order is maintained, and make everyone happy. But if jumping across the a11y tree is bad and should never be done, then we need to make some compromises.

If we should never jump around the a11y tree, then the best tabbing order we can get is "B, B1, B2, A, C", which does not match the visual order - the B2 element is in the wrong place.

So which sin is worse for us to commit? Jumping back and forth between elements of the a11y tree, or not matching the visual order?

LJWatson commented 1 month ago

Thanks for clarifying and further conversation yesterday @tabatkins .

If there is really no way to align the accessibility tree with the Tab/visual order, and there is no alternative except to choose one experience over the other, and given @rachelandrew's thinking that this is an edge case, I'd probably favour making the Tab/visual order match, as opposed to the Tab/acc tree - on the basis that there are more sighted keyboard users than not-sighted screen reader users.

mfreed7 commented 1 month ago

Thanks for clarifying and further conversation yesterday @tabatkins .

If there is really no way to align the accessibility tree with the Tab/visual order, and there is no alternative except to choose one experience over the other, and given @rachelandrew's thinking that this is an edge case, I'd probably favour making the Tab/visual order match, as opposed to the Tab/acc tree - on the basis that there are more sighted keyboard users than not-sighted screen reader users.

I will definitely go with the consensus of folks on this thread. But I wanted to ask about going that route. It seems that in this (bad, discouraged, corner-) case where display:contents is used and contains "split" positioned children, one group of folks is going to get a sub-optimal result:

  1. If we make the tab order match the visual order, and therefore not match the a11y tree order, then screen reader users will have a confusing experience, as you detailed in your comment.
  2. If we make the tab order match the a11y tree order, and therefore not the visual order, then sighted users will have a confusing experience, since the focus will appear to bounce around on the screen.

Of these two, I think I'd prefer #2, for two important reasons:

  1. Developers are very likely not to check or understand the a11y tree at all, and therefore not even know there's a problem with their HTML/CSS, if we go with option #1. If the tab order if visually out-of-whack (option #2), they're more likely to notice it and perhaps stop doing the bad display:contents thing.
  2. At least part, if not a significant part, of the reading-flow-items feature is geared toward improving the accessibility of out-of-order content on the web. It would seem unfortunate if we make a decision that biases the other way.

Again, just my feelings, and we're happy to go with the consensus.

jcsteh commented 1 month ago
1. Developers are very likely **not to check or understand the a11y tree** at all, and therefore not even know there's a problem with their HTML/CSS, if we go with option #1. If the tab order if visually out-of-whack (option #2), they're more likely to notice it and perhaps stop doing the bad `display:contents` thing.

I tend to agree with this. It arguably impacts more users, but it's also a great deal more likely to be noticed and prioritised, rather than the smaller group of users always losing out.

2. At least part, if not a significant part, of the `reading-flow-items` feature is geared toward improving the accessibility of out-of-order content on the web. It would seem unfortunate if we make a decision that biases the other way.

"Accessibility" covers both the accessibility tree (e.g. screen reader users) and tab order (e.g. sighted non-screen reader users), though. So I'm not convinced either approach really biases against improving accessibility. It's just a matter of which kind of accessibility you're asking about. :)