Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expectations for aria-hidden and focused elements #2181

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

scottaohara
Copy link
Member

@scottaohara scottaohara commented May 20, 2024

This PR closes #1765 and is related to work that was done in #2037, but scoped only to the original issue I filed.

The intent of this PR is to identify not only how user agents would need to handle focusable elements that are aria-hidden (explicitly or due to being a descendant of an aria-hidden container) - but for the case where a focusable element is within an aria-hidden container, that the entire subtree would need to be re-exposed so that any other relevant information to the user could be made available. (e.g., so as to not just expose a "learn more" link, with no way to determine what someone would be learning about)

a simple example being like:

<div aria-hidden=true>
  <h3>Something or other</h3>
   some details about said something, or other.
  <a href=#>Learn more!</a>
</div>
  • Related Core AAM Issue/PR:
  • Related AccName Issue/PR:
  • Any other dependent changes?

Test, Documentation and Implementation tracking

Once this PR and all related PRs have been approved by the working group, tests
should be written and issues should be opened on browsers. Add N/A and check when not
applicable.

  • Related APG Issue/PR:
  • MDN Issue/PR:
  • "author MUST" tests:
  • "user agent MUST" tests:
  • Browser implementations (link to issue or when done, link to commit):
    • WebKit:
    • Gecko:
    • Blink:
  • Does this need AT implementations?

Preview | Diff

This PR closes #1765 and is related to work that was done in #2037, but scoped only to the original issue I filed.

The intent of this PR is to identify not only how user agents would need to handle focusable elements that are aria-hidden (explicitly or due to being a descendant of an aria-hidden container) - but for the case where a focusable element is within an aria-hidden container, that the entire subtree would need to be re-exposed so that any other relevant information to the user could be made available.  (e.g., so as to not just expose a "learn more" link, with no way to determine what someone would be learning about)

a simple example being like:

```
<div aria-hidden=true>
  <h3>Something or other</h3>
   some details about said something, or other.
  <a href=#>Learn more!</a>
</div>
```
@@ -11604,6 +11604,7 @@ <h2>Definitions of States and Properties (all aria-* attributes)</h2>
<p><a>Indicates</a>, when set to <code>true</code>, that an <a>element</a> and its entire subtree are hidden from assistive technology, regardless of whether it is visibly rendered.</p>
<p>User agents determine an element's [=element/hidden=] status based on whether it is rendered, and the rendering is usually controlled by CSS. For example, an element whose <code>display</code> property is set to <code>none</code> is not rendered. An element is considered [=element/hidden=] if it, or any of its ancestors are not rendered or have their <code>aria-hidden</code> attribute value set to <code>true</code>.</p>
<p>Authors MAY, with caution, use aria-hidden to hide visibly rendered content from assistive technologies <em>only</em> if the act of hiding this content is intended to improve the experience for users of assistive technologies by removing redundant or extraneous content. Authors using aria-hidden to hide visible content MUST ensure that identical or equivalent meaning and functionality is exposed to assistive technologies.</p>
<p>Additionally, authors SHOULD ensure that any elements that are accessibility descendants of an aria-hidden element, or any elements which have been marked as aria-hidden themselves, are prevented from receiving focus. If an aria-hidden element or a descendant of an aria-hidden ancestor receives focus, User Agents MUST ignore the aria-hidden state of any ancestor to the focused element, resulting in the entire subtree to be exposed to assistive technologies from that point forward, even once the element loses focus or is no longer focusable.</p>
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks very good. Obviously if the subtree were to later be marked display:none, it would not be exposed. So it's a subtle thing, but perhaps small edits will fix it, like:
"User Agents MUST ignore the aria-hidden state of any ancestor to the focused element from that point forward, resulting in the entire subtree to be exposed to assistive technologies. The ancestor aria-hidden markup will continue to be ignored, even once the element loses focus or is no longer focusable.

Copy link
Member Author

Choose a reason for hiding this comment

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

i know this section is already note-heavy, but i was debating on whether it was worth mentioning that authors SHOULD consider using host language features, like inert or display: none to properly hide content and thus mitigate needing to worry about this at all.

Copy link
Contributor

@rahimabdi rahimabdi May 21, 2024

Choose a reason for hiding this comment

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

+1, section 7.1 lists these techniques as well if it's valuable to link to them: https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion:

Elements, including their descendent elements, that have host language semantics specifying that the element is not displayed, such as CSS display:none, visibility:hidden, or the HTML hidden attribute.

@aleventhal
Copy link
Contributor

aleventhal commented May 20, 2024 via email

@rahimabdi
Copy link
Contributor

This may be WPT-testable; should I create a separate issue if you'd like tests for this (the MUST around the subtree being re-exposed)?

@scottaohara
Copy link
Member Author

talking with @smhigley about this... but should this clarify that if an element is already focused and its container becomes aria-hidden, then the aria-hidden state should remain unless focus is then moved to another element within that hidden subtree.

for instance, think of a custom modal dialog that was built where the order of events are:

  • button invokes dialog (focus is on button)
  • element containing button set to aria-hidden=true
  • dialog is rendered (outside aria-hidden=true container)
  • focus moves to dialog

since focus moving is the last thing that occurs, we do not want browsers to immediately undo the aria-hidden that was dynamically added (though focus is still on the button inside the aria-hidden container)

@spectranaut spectranaut requested a review from adampage May 23, 2024 17:07
@jnurthen jnurthen self-requested a review May 23, 2024 17:08
aarongable pushed a commit to chromium/chromium that referenced this pull request May 28, 2024
Using aria-hidden on content that is actually visible can cause problems
if a node inside receives focus. In the past, nothing was announced,
and the fix was to expose all focusable nodes inside of an aria-hidden,
but with the invisible state. This gave the AT a chance to decide to
make the announcement. However, this fix did not work with VoiceOver
or NVDA, and only partially worked with JAWS.

The new approach is to assume the aria-hidden is correct and to
not expose anything in the subtree, unless the user somehow is able
to focus something in subtree. In that case, it is assumed to be
an authoring error and misuse of aria-hidden. As such, browsers will
now consider the bad aria-hidden on the containing element
to be invalid and ignore it from that point forward, causing the
entire subtree to now be exposed as visible. See this PR for
the ARIA spec: w3c/aria#2181.

This also helps enable a downstream CL that removes 125 lines.

Fixed: 40833533
Change-Id: Ib0d7eed10fada5ae7799c5af1257e5a66fb0c034
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5544894
Reviewed-by: David Tseng <dtseng@chromium.org>
Commit-Queue: Aaron Leventhal <aleventhal@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1306971}
@scottaohara
Copy link
Member Author

need to add to this that user agents need to make this repair if the element focused is in the same document - but say if the element that is focused is within an iframe, but the iframe is hidden from an aria-hidden surrounding it in the parent doc, then browsers may not be able to reasonably make this repair.

@smhigley
Copy link
Contributor

I have one other question, but mostly for browser implementors; I'm not sure if this amount of detail can be in the spec --

Would the removal of aria-hidden occur after the focus event on an element, or after document.activeElement is set to point to a hidden element? I'm making the distinction because in a lot of our focus management logic for things like dialogs, we look for a focus event on a hidden element (either a focus bumper, or looking for focus moving to the background page), and immediately send focus back into the dialog in the event handler. The result is that document.activeElement never points to a hidden element, but the focus event does fire. If the change happened as a result of the event, it would break quite a lot of things at least in our library, and IIRC in others as well 😅.

If that's not the case, and if the scenario that @scottaohara already mentioned is also handled, I think I wouldn't have any more concerns about this.

@aleventhal
Copy link
Contributor

aleventhal commented May 28, 2024 via email

@smhigley
Copy link
Contributor

smhigley commented May 29, 2024

@aleventhal I think I may not have communicated my use case well enough 😅.

The distinction between doing a .removeAttribute() and ignoring the attribute through some other method wasn't really material to the question I had in mind, though bringing it up does make me worry that this may make it difficult for developers to debug when an element hidden in markup is exposed.

For my original question, I made this JS Fiddle to help: https://jsfiddle.net/sjc89htb/. It uses logic similar to what we use at Microsoft, and what I've seen for focus traps elsewhere, where focus is immediately sent elsewhere when it lands on a hidden element. In practice this works perfectly in all ATs I've tested up till now. Would you be able to clarify whether it will continue working with this change, or whether the change will result in the hidden content being exposed?

Thanks for taking the time to investigate and weigh in!

@@ -11604,6 +11604,7 @@ <h2>Definitions of States and Properties (all aria-* attributes)</h2>
<p><a>Indicates</a>, when set to <code>true</code>, that an <a>element</a> and its entire subtree are hidden from assistive technology, regardless of whether it is visibly rendered.</p>
<p>User agents determine an element's [=element/hidden=] status based on whether it is rendered, and the rendering is usually controlled by CSS. For example, an element whose <code>display</code> property is set to <code>none</code> is not rendered. An element is considered [=element/hidden=] if it, or any of its ancestors are not rendered or have their <code>aria-hidden</code> attribute value set to <code>true</code>.</p>
<p>Authors MAY, with caution, use aria-hidden to hide visibly rendered content from assistive technologies <em>only</em> if the act of hiding this content is intended to improve the experience for users of assistive technologies by removing redundant or extraneous content. Authors using aria-hidden to hide visible content MUST ensure that identical or equivalent meaning and functionality is exposed to assistive technologies.</p>
<p>Additionally, authors SHOULD ensure that any elements that are accessibility descendants of an aria-hidden element, or any elements which have been marked as aria-hidden themselves, are prevented from receiving focus. If an aria-hidden element or a descendant of an aria-hidden ancestor receives focus, User Agents MUST ignore the aria-hidden state of any ancestor to the focused element, resulting in the entire subtree to be exposed to assistive technologies from that point forward, even once the element loses focus or is no longer focusable.</p>
Copy link
Member

@adampage adampage May 29, 2024

Choose a reason for hiding this comment

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

Suggested change
<p>Additionally, authors SHOULD ensure that any elements that are accessibility descendants of an aria-hidden element, or any elements which have been marked as aria-hidden themselves, are prevented from receiving focus. If an aria-hidden element or a descendant of an aria-hidden ancestor receives focus, User Agents MUST ignore the aria-hidden state of any ancestor to the focused element, resulting in the entire subtree to be exposed to assistive technologies from that point forward, even once the element loses focus or is no longer focusable.</p>
<p>Additionally, authors SHOULD prevent a focusable element from receiving focus if it or any of its accessibility ancestors have been <a href="#tree_exclusion">excluded from the accessibility tree</a>. If a focusable element receives focus while [=element/hidden=], User Agents MUST re-expose it and each of its [=element/hidden=] ancestors to the accessibility tree, and persist that exposure even after the focused element loses focus or is no longer focusable.</p>

For the first sentence, here’s an editorial suggestion that reads a little more clearly to me:

Additionally, authors SHOULD prevent a focusable element from receiving focus if it or any of its accessibility ancestors have been excluded from the accessibility tree.

In the second sentence, “entire subtree” sounds greedy. In this contrived example, if #link receives focus, I’m supposing we’d expect #a to get exposed (and therefore also #b and #link), but not #c:

<div id="a" aria-hidden="true">
  <p id="b">Some stuff that is only hidden by virtue of its hidden parent.</p>
  <a id="link" href="#">I’m hidden but focusable</button>
  <p id="c" aria-hidden="true">A sibling to #link that is coincidentally but explicitly hidden.</p>
</div>

If that’s true, then how about this rephrasing?

If a focusable element receives focus while hidden, User Agents MUST re-expose it and each of its hidden ancestors to the accessibility tree, and persist that exposure even after the focused element loses focus or is no longer focusable.

Copy link
Contributor

Choose a reason for hiding this comment

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

We don't just expose the ancestor chain, but rather the entire subtree under the aria-hidden (basically the aria-hidden markup on the ancestor is marked as problematic and ignored by the browser, which can affect the entire subtree). Of course, there could be additional markup in some parts of the subtree that also hide things (display:none, etc.).

Copy link
Member Author

Choose a reason for hiding this comment

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

i'm not a fan of switching between saying "excluded from the accessibility tree" and then switching to "hidden" (though i realize 'hidden' in this spec doesn't necessarily mean "completely hidden").

beyond what aaron mentioned (though maybe you updated the text since then) i think that's my only thing i'm not sure about with this rewording. it uses the terminology correctly, but i had to read it a number of times to figure out if it still meant the same thing. maybe that's just my problem though.

@aleventhal
Copy link
Contributor

@aleventhal I think I may not have communicated my use case well enough 😅.

Thanks for explaining... good chance the issue was on the receiver's end :)

The distinction between doing a .removeAttribute() and ignoring the attribute through some other method wasn't really material to the question I had in mind, though bringing it up does make me worry that this may make it difficult for developers to debug when an element hidden in markup is exposed.

We could log something like an error to the JS console, warning the developer that bad aria-hidden markup had been discarded. I thought we had similar guidance for other user agent repairs, but I can't find that in the spec after a quick search.

For my original question, I made this JS Fiddle to help: https://jsfiddle.net/sjc89htb/. It uses logic similar to what we use at Microsoft, and what I've seen for focus traps elsewhere, where focus is immediately sent elsewhere when it lands on a hidden element. In practice this works perfectly in all ATs I've tested up till now. Would you be able to clarify whether it will continue working with this change, or whether the change will result in the hidden content being exposed?

It seems to work! Our proposed change is currently implemented in Chrome Canary from this morning, version 127.0.6508.0. I ran with and without the focus trap. In the version without the focus trap, once you tab into the button, the button is revealed in the a11y tree (I used the devtools Inspector full page a11y view for the test). In the version with the focus trap, focus never goes to the button, and the aria-hidden is never discarded. I think this is an important test that we should add to WPT tests. @scottaohara maybe we can update the spec text to say if focus is not forwarded somewhere else? Perhaps it should mentioned as a valuable technique, although maybe authors should be using inert here.

Anyway, go ahead and play with Canary if you like. I'll wait to hear back what people think of a console message to developers. Maybe something like, console.error("The following element had aria-hidden markup that was discarded, because focus went to it or an element within it. See WAI-ARIA rules on aria-hidden:", bad_aria_hidden_element);

@smhigley
Copy link
Contributor

smhigley commented May 29, 2024

@aleventhal awesome! I love both the idea of putting that scenario in WPT, and the idea of logging a warning to the console.

I'm all in on this now, thanks again for investigating!

@aleventhal
Copy link
Contributor

aleventhal commented May 30, 2024

Landing a patch to Chromium that adds this console error:

  element.AddConsoleMessage(
      mojom::blink::ConsoleMessageSource::kRendering,
      mojom::blink::ConsoleMessageLevel::kError,
      String::Format(
          "Blocked aria-hidden on a <%s> element because the element that just "
          "received focus must not be hidden from assistive technology users. "
          "Avoid using aria-hidden on a focused element or its ancestor. "
          "Consider using the inert attribute instead, which will also prevent "
          "focus. For more details, see the aria-hidden section of the "
          "WAI-ARIA specification at https://w3c.github.io/aria/#aria-hidden.",
          element.TagQName().ToString().Ascii().c_str()));

Should we add text to this PR saying that user agents may provide a helpful console error message?
Also, can we update this PR to say how developers can fix the issue? (Either by forwarding focus via focusin, by not putting aria-hidden on focusable objects, by using inert etc.)

@pkra pkra added the spec:aria label Jun 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Consider further calling out aria-hidden expectations for focusable elements
6 participants