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

Clarify relationship between aria-hidden and aria-owns #1839

Open
wants to merge 15 commits into
base: main
Choose a base branch
from

Conversation

adampage
Copy link
Member

@adampage adampage commented Oct 21, 2022

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 adampage self-assigned this Oct 21, 2022
@adampage adampage added the clarification clarifying or correcting language that is either confusing, misleading or under-specified label Oct 21, 2022
@adampage
Copy link
Member Author

Here’s a Codepen with a few exploratory tests.

@Jym77
Copy link

Jym77 commented Nov 30, 2022

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 🤔

@pkra
Copy link
Member

pkra commented Nov 30, 2022

For the second point: #1150

@adampage
Copy link
Member Author

adampage commented Dec 6, 2022

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.

@Jym77
Copy link

Jym77 commented Dec 7, 2022

@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 🤔

@adampage
Copy link
Member Author

adampage commented Dec 7, 2022

This discussion has been super helpful, @Jym77, thank you. 😁 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!

@Jym77
Copy link

Jym77 commented Dec 8, 2022

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 👍

@scottaohara
Copy link
Member

scottaohara commented Dec 8, 2022

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 ?

@Jym77
Copy link

Jym77 commented Dec 8, 2022

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.

@scottaohara
Copy link
Member

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

@adampage
Copy link
Member Author

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.

@Jym77
Copy link

Jym77 commented Jan 12, 2023

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 👍

@spectranaut
Copy link
Contributor

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:

#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.

@adampage
Copy link
Member Author

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
Copy link
Member Author

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.

@spectranaut
Copy link
Contributor

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

omg this code pen is AMAZING.

index.html Outdated
@@ -11756,7 +11756,7 @@ <h2>Definitions of States and Properties (all aria-* attributes)</h2>
<sdef>aria-hidden</sdef>
<div class="state-description">
<p><a>Indicates</a> whether the <a>element</a> is exposed to an accessibility API. See related <sref>aria-disabled</sref>.</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>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 will be <a href="#tree_exclusion">excluded from the accessibility tree</a> if it or any of its <a>owning elements</a> are [=element/hidden=] or have their <code>aria-hidden</code> attribute value set to <code>true</code>.</p>
Copy link
Member

Choose a reason for hiding this comment

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

i think my only issue with this update is that the clarification about when to take aria-owns into account is so far removed from this initial mention of owning elements. As prior to reading the new paragraph added on line 12333 of this diff, i would have interpreted the use of "owning elements" to mean that "boo" should also be "hidden" because the hidden div "owns" boo... but for the fact on 12333 it is stated that's not true because the owning element is itself not rendered, so thus it can't own the "boo" element.

<div hidden aria-owns=boo>foo</div>
<div id=boo>boo</div>

i could see this clarified by either mentioning that if an element is an owning element due to usage of aria-owns, how this works is clarified later. Or that these bits of text are closer together in the spec, or that this paragraph remains just about ancestors being hidden, and owning elements / aria-owns headaches are talked about in one new paragraph/sets of paragraphs.

Copy link
Member Author

Choose a reason for hiding this comment

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

Heya @scottaohara, now that the accessibility parent/child language has been merged in, I updated this statement to reference accessibility parents instead of owning elements. I think this is now more accurate and also sidesteps the ambiguity you described here, at least until an author digs deeper into the aria-owns section? Here’s the full new sentence:

An element will be excluded from the accessibility tree if it or any of its accessibility parents are hidden or have their aria-hidden attribute value set to true.

@spectranaut, I’m seeing that this would be the spec’s first mention of accessibility parents plural. I re-read that term’s definition and it feels okay to pluralize it here to essentially mean ancestors. Do you agree?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ahhh actually no, there is only one "accessibility parent" -- I think the appropriate phrase here is "Accessibility ancestor". We don't have it defined, but I think it is a clear enough term? You could link to the accessibility tree section of the document: https://w3c.github.io/aria/#accessibility_tree

@cookiecrook
Copy link
Contributor

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 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.

index.html Outdated
@@ -11756,7 +11756,7 @@ <h2>Definitions of States and Properties (all aria-* attributes)</h2>
<sdef>aria-hidden</sdef>
<div class="state-description">
<p><a>Indicates</a> whether the <a>element</a> is exposed to an accessibility API. See related <sref>aria-disabled</sref>.</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>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 will be <a href="#tree_exclusion">excluded from the accessibility tree</a> if it or any of its <a>owning elements</a> are [=element/hidden=] or have their <code>aria-hidden</code> attribute value set to <code>true</code>.</p>
Copy link
Contributor

Choose a reason for hiding this comment

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

or have their <code>aria-hidden</code> attribute value set to <code>true</code>.

I know you didn't add this part, but I recall @aleventhal wanted a way to invert aria-hidden on an ancestor by setting aria-hidden=false on a rendered subtree. e.g. aria-hidden=true on the <body> and aria-hidden=false on a modal view (a dialog perhaps) ... This current wording excludes that I think.

It also might exclude inverting visibility: hidden; with visibility: visible; unless there is already an exception carved out in the ARIA definition of "hidden."

Copy link
Contributor

@cookiecrook cookiecrook Jun 28, 2023

Choose a reason for hiding this comment

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

This current wording excludes that I think.

The below text is terrible but something like:

  • if the current element is hidden/unrendered (def tbd), OR
  • (if the element does not have aria-hidden=true set AND its nearest ancestor with aria-hidden defined is (both aria-hidden=false and NOT hidden aka rendered))

Copy link
Contributor

Choose a reason for hiding this comment

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

I acknowledge that distinction is difficult to parse. Perhaps a Deep Dive (w./ Aaron) or Agenda+ for a weekly?

Copy link
Member Author

Choose a reason for hiding this comment

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

In anticipation of #2090 getting merged (in which we made aria-hidden="false" synonymous with undefined), I’m now thinking that this part can be left as is?

@pkra pkra added this to the ARIA 1.3 milestone Jun 28, 2023
@adampage
Copy link
Member Author

@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.

Copy link
Contributor

@spectranaut spectranaut left a comment

Choose a reason for hiding this comment

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

I think this looks good now, except for the switch from "accessibility parents" to "accessibility ancestors"! I'll mark as approving once that is done :)

@adampage
Copy link
Member Author

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

@adampage adampage requested a review from spectranaut May 29, 2024 04:42
Copy link
Contributor

@spectranaut spectranaut left a comment

Choose a reason for hiding this comment

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

Looks good!

@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
Labels
clarification clarifying or correcting language that is either confusing, misleading or under-specified spec:aria
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Should aria-hidden be effected by aria-owns?
6 participants