aria icon indicating copy to clipboard operation
aria copied to clipboard

Clarify relationship between `aria-hidden` and `aria-owns`

Open adampage opened this issue 3 years ago • 19 comments

Closes #1818.

The definition of Owning Element is already inclusive of both DOM ancestry and ownership via aria-owns:

An 'owning element' is any DOM ancestor of the element, or any element with an aria-owns attribute which references the ID of the element.

Because of this, I’m thinking it’ll be sufficient to swap out “ancestors” for “owning elements” in aria-hidden’s description.

I also editorially removed what I believe is an unhelpful comma.


Preview | Diff

adampage avatar Oct 21 '22 02:10 adampage

Two points to mention:


I'm encountering a problem similar to #1818 but the other way around, and I'm not sure this PR fully handles it 🤔

The case in #1818 was:

  <ul aria-owns="li"></ul>
  <div aria-hidden="true">
    <li id="li">Child node</li>
  </div>

That is, a not-hidden element aria-owns an element with a hidden DOM ancestor. In that case, this PR says that the li#li element should also be hidden because it has an "owning element" (here, DOM ancestor) which is hidden.

What happens in the reverse case:

<div aria-hidden="true">
  <div id="owner" aria-owns="li" />
</div>
<ul> <li id="li">Item</li> </ul>

In that case, a hidden element aria-owns an element with no hidden ancestor. The clarification in this PR also means that li#li should be hidden because it has an "owning element" (here, an element with aria-owns) which is hidden. Quick tests, it seems that browsers ignore aria-owns in this case. So, mostly raising the point to make sure this is the intention of the PR.


Second point is that the language in §7.1 Excluding Elements from the Accessibility Tree uses a lot of "descendant" and other tree-related wording, notably for the case concerning this PR:

user agents SHOULD NOT include the following elements in the accessibility tree:

  • Elements, including their descendants, that have aria-hidden set to true.

Should there be a precision that "descendants" here mean "either descendant in the DOM tree, or descendant in the accessibility tree where aria-owns relations are taken into account"? Most of this Section would benefit from such clarification. Maybe it's a topic for another PR, though 🤔

Jym77 avatar Nov 30 '22 12:11 Jym77

For the second point: https://github.com/w3c/aria/issues/1150

pkra avatar Nov 30 '22 12:11 pkra

In that case, a hidden element aria-owns an element with no hidden ancestor.

Mmm, thanks for poking at this, @Jym77. I hadn’t considered that reverse case.

“Owning element” is currently defined as “any DOM ancestor of the element, or any element with an aria-owns attribute which references the ID of the element.

In this reverse scenario, <div aria-hidden="true"> is neither a DOM ancestor of li#li nor does it use aria-owns to reference li#li directly. It feels a bit lawyerly 🤔, but I think we could then say that <div aria-hidden="true"> is not an “owning element” of li#li. div#owner is hidden because its DOM ancestor is aria-hidden, so its use of aria-owns is no longer exposed to the accessibility API. This leaves li#li in its original location, owned by its parent ul.

adampage avatar Dec 06 '22 18:12 adampage

@adampage That actually makes sense 😄 This PR seems to change the behaviour in the related simplified case:

<div aria-hidden="true" aria-owns="li"></div>

<ul> <li id="li">Item</li> </ul>

In that case, the current behaviour is to expose li#li as it has no ancestor with aria-hidden (and Chrome and FF do that), but the new behaviour will be to hide it since it has a owning element with aria-hidden.

I guess it's a bit of a question which of aria-owns or aria-hidden "acts faster". The current behaviour is that aria-hidden hides away the owner before it has time to own anything else, but the new behaviour is that aria-owns is tweaking the tree before the owner gets hidden 🤔


Can get weirder with a case like:

<div aria-hidden="true" aria-owns="foo"></div>
<div id="foo" aria-owns="bar"></div>
<div id="bar"></div>

Now, #foo has a owning element with aria-hidden, so it is hidden. But since it itself does not have aria-hidden, it still owns #bar, which is also not hidden in any way. I feel that is at best confusing and might explode in weird ways 🤔

Jym77 avatar Dec 07 '22 08:12 Jym77

This discussion has been super helpful, @Jym77, thank you. :grin: I think you hit the nail on the head with the question of which attribute “acts faster”.

Let’s say that this PR now includes some additional text (somewhere appropriate in the spec) to the effect of:

Changes to ownership are resolved in DOM order. If both aria-hidden and aria-owns are specified on the same element, aria-hidden is processed first and aria-owns will not take effect.

Now let’s apply that to the four scenarios that have come up. In the initial #1818 example:

  <ul aria-owns="li"></ul>
  <div aria-hidden="true">
    <li id="li">Child node</li>
  </div>

...the ul steals ownership of #li from div[aria-hidden="true"] because it’s encountered first in DOM order. #li becomes not hidden because ul is not hidden. By the time div[aria-hidden="true"] is encountered, it no longer has a descendant to be affected by its use of aria-hidden. It hides itself, full stop.

In the reverse case:

<div aria-hidden="true">
  <div id="owner" aria-owns="li" />
</div>
<ul> <li id="li">Item</li> </ul>

...div[aria-hidden="true"] is encountered first, so it excludes itself and its descendants from the accessibility API, leaving #owner[aria-owns="li"] altogether ignored. #li continues to be naturally owned by ul and not hidden.

In the follow-up simplified case:

<div aria-hidden="true" aria-owns="li"></div>
<ul> <li id="li">Item</li> </ul>

...div[aria-hidden="true"] is encountered, and it resolves aria-hidden before aria-owns. This prevents the change in ownership of #li. #li continues to be naturally owned by ul, and not hidden.

In the follow-up weirder case:

<div aria-hidden="true" aria-owns="foo"></div>
<div id="foo" aria-owns="bar"></div>
<div id="bar"></div>

...the previous story is essentially true again. div[aria-hidden="true"] is encountered, and it resolves aria-hidden before aria-owns. This prevents the change in ownership for #foo. #foo is then encountered, but is not hidden so its ownership of #bar is resolved and both remain not hidden.


The original intention of this spec clarification was to align with the current Chrome & Firefox behavior, so I built out all four of these scenarios in Codepen and tested them with macOS + VoiceOver + Chrome. Each outcome I described above is correctly demonstrated by that AT combo.

Whew. Having said all that, would you agree that the gist of my additional spec proposal is helpful & accurate? And that it would reasonably lead to the interpretations I described for each of those four scenarios? Are there yet other ambiguous scenarios lurking out there? 😅

Thanks again!

adampage avatar Dec 07 '22 23:12 adampage

Changes to ownership are resolved in DOM order. If both aria-hidden and aria-owns are specified on the same element, aria-hidden is processed first and aria-owns will not take effect.

Whew. Having said all that, would you agree that the gist of my additional spec proposal is helpful & accurate? And that it would reasonably lead to the interpretations I described for each of those four scenarios? Are there yet other ambiguous scenarios lurking out there? 😅

Yes, I think that clarification works. Maybe we need to add that change of ownership is only initiated by non-hidden node (that's more or less the second sentence but making it clear that one should stop looking for aria-owns upon encountering a aria-hidden, i.e. the way case 2 works)¹ From a graph-theory point of view, we'd walk the DOM tree in DOM order, stop the current traversal on aria-hidden or already traversed node, and jump to follow aria-owns (which does trigger the "already traversed node" in the previous check).

This also makes it clear that aria-hidden affects the accessibility tree while display: none affects the DOM tree (so replacing aria-hidden by display: none in case 2 does not un-hide the li, it is still not rendered, therefore hidden).

Great clarification 👍

Jym77 avatar Dec 08 '22 12:12 Jym77

was following along with this until:

This also makes it clear that aria-hidden affects the accessibility tree while display: none affects the DOM tree (so replacing aria-hidden by display: none in case 2 does not un-hide the li, it is still not rendered, therefore hidden).

this statement has made me wonder if this actually makes sense or not. since display none does modify the a11y tree... and then i'm not sure what case 2 refers to? the second of Adam's examples in his last comment, or the second odd case you brought up, @Jym77 ?

scottaohara avatar Dec 08 '22 14:12 scottaohara

Yep, poor writing on my side 🙈 and messing up things badly…

  <ul aria-owns="li"></ul>
  <div style="display: none">
    <li id="li">Child node</li>
  </div>

case 1 not 2 in Adam's list with aria-hidden replaced by display: none.

This does not expose li#li since it hits the "Elements, including their descendent elements, that have host language semantics specifying that the element is not displayed" condition.

That is of course linked to non-interference with the host language (aria-owns cannot expose an element that HTML is hiding, but can expose an element that ARIA is trying to hide (aria-hidden)). Which is more or less what I was trying to say by putting display: none at the DOM level and aria-hidden at the accessibility tree level. Of course, display: none affects the accessibility tree, but indirectly because it acts on the DOM tree in way that accessibility tree building is concerned, that's why I was writing that it affects the DOM tree.

Jym77 avatar Dec 08 '22 14:12 Jym77

Thanks for the correction/clarification. Yah that makes sense again now.

scottaohara avatar Dec 08 '22 16:12 scottaohara

So, I reacquainted myself with the spec’s definition of “hidden:

Indicates that the element is not visible, perceivable, or interactive to any user. An element is considered hidden if it or any one of its ancestor elements is not rendered or is explicitly hidden.

Based on that, I took another pass at the aria-hidden section with the goal of disambiguating its use of the term “hidden” by leveraging the existing “Excluding Elements from the Accessibility Tree” section.

Beyond that, I wordsmithed the clarifications we discussed earlier in this thread and added them to the aria-owns section.

adampage avatar Jan 11 '23 03:01 adampage

I feel there is still a small clarification needed to say that aria-owns breaks DOM parent/child relation in addition to create new ones (i.e. the result is indeed an accessibility tree, not a DAG).

Owning element states:

An 'owning element' is any DOM ancestor of the element, or any element with an aria-owns attribute which references the ID of the element.

Because of the "or", and no mention of unicity, in case 1 above

<ul aria-owns="li"></ul>
  <div aria-hidden="true">
    <li id="li">Child node</li>
  </div>

Both the ul and the div are "owning element" for the li (or at least the definition doesn't really tells which to use), and the definition of aria-owns doesn't really answer that question either. (and since any DOM ancestor is a "owning element", it is actually not a problem to have more than one owning element).

So, without disambiguating that, the li has an aria-hidden "owning element" (the div) and should be hidden, which is precisely what we want to avoid.

Should we change the definition of "owning element" to state something like

An 'owning element' is any element with an aria-owns attribute which references the ID of the element or, if none, any DOM ancestor of the element.

Otherwise, I think this is correct and expresses what is needed 👍

Jym77 avatar Jan 12 '23 12:01 Jym77

I just want to note that @jym77's recommended definition of owned is nice, and, that there is also a 1.3-blocking issue related to inconsistent use the term "owned by", just like how hidden was just revisited:

https://github.com/w3c/aria/issues/1161

The "owned by inconsistencies" can be addressed before of after this PR, but it might also lead to a term change or term update.

spectranaut avatar Feb 09 '23 23:02 spectranaut

Thanks @spectranaut and @Jym77, I’ve just passed along your suggested clarification for “owning element” in PR #1454. I think it will be helpful to address it in that context.

If that goes through, then I’d plan to update this PR by leveraging @spectranaut’s recently clarified definition of “hidden”, the improved definition of “owning element”, and the new “accessibility parent” term. My first instinct would be to update the aria-hidden state definition with something like:

<p>
  ...An element is considered [=element/hidden=] if:
</p>
<ul>
  <li>
    It or any of its <a>accessibility parents</a> are not rendered
  </li>
  <li>
    It or any of its <a>owning elements</a> are not rendered or have their
    <code>aria-hidden</code> attribute value set to <code>true</code>
  </li>
</ul>

Could I trouble you to first take a look at my comment in #1454, and then consider my proposal here?

adampage avatar Feb 15 '23 02:02 adampage

I’ve created a new Codepen to describe & test assorted scenarios involving aria-owns and hidden content.

We originally decided to align the spec with the current behavior of Chrome & Firefox, but my testing showed some interesting inconsistencies for Chrome 110 on macOS vs Windows. Firefox 110 seemed to behave consistently with our expectations everywhere.

Once #1454 is finalized, I’ll take another whack at the spec language for this PR, orienting it around Firefox’s demonstration of our intent.

adampage avatar Feb 16 '23 01:02 adampage

I’ve created a new Codepen to describe & test assorted scenarios involving aria-owns and hidden content.

omg this code pen is AMAZING.

Agreed. Most useful CodePen I think I've ever seen.

@AdamPage, you are planning to turn some of this into a series of WPT tests for https://github.com/web-platform-tests/interop-2023-accessibility-testing/issues/32 correct? Are you planning to file the implementation bugs you've discovered? Trying to determine how best to cross-reference the trackers from the issue, etc.

cookiecrook avatar Jun 28 '23 06:06 cookiecrook

@adampage, you are planning to turn some of this into a series of WPT tests for web-platform-tests/interop-accessibility#32 correct? Are you planning to file the implementation bugs you've discovered? Trying to determine how best to cross-reference the trackers from the issue, etc.

Heya @cookiecrook, I just turned all of these tests into comparable (I hope) WPT tests: PR #44822. Using your latest tip, I’ve marked that file as .tentative, but will definitely plan to file any legit implementation bugs once this ARIA spec PR (and that WPT PR, if you think it deserves to be in the test suite) is accepted and merged.

I’d be grateful for your thoughts on my approach for testing this spec change within WPT’s capabilities. My original Codepen tests weren’t accname-focused; I just rendered a bunch of generics and went looking in the a11y tree to see how they all got exposed as static text nodes.

With WPT, I chose to use the test driver’s get_computed_label() function and translated all my test markup scenarios into <button> elements relying on a name from content accname assembly. I’m hoping this is roughly equivalent, but the ARIA spec clarification I’m attempting to make in this PR is bigger than accname.

adampage avatar Feb 28 '24 03:02 adampage

Thanks, @spectranaut. I’ve swapped in “accessibility ancestors for “accessibility parents” and resolved the merge conflict.

adampage avatar May 29 '24 04:05 adampage