Skip to content

Instantly share code, notes, and snippets.

@nolanlawson
Last active June 2, 2022 18:06
Show Gist options
  • Save nolanlawson/4fe8b5d672cda3bcc4daf58079145202 to your computer and use it in GitHub Desktop.
Save nolanlawson/4fe8b5d672cda3bcc4daf58079145202 to your computer and use it in GitHub Desktop.
AOM descendant/ancestor restriction problem explainer

AOM descendant/ancestor restriction problem explainer

Summary

This is an overview of WICG/aom #192 and why it's a problem for web component authors using shadow DOM.

Consider the combobox

Let's consider the classic combobox pattern from the WAI ARIA Authoring Practices v1.2:

Screenshot of combobox, user has typed W and the dropdown shows four states starting with W and Wisconsin, the third one, is highlighted

Here's the HTML (some details have been omitted):

<div>
  <div>
    <input type="text" role="combobox"
           aria-autocomplete="list" aria-expanded="true"
           aria-controls="cb1-listbox"
           aria-activedescendant="lb1-wi">
  </div>
  <ul id="cb1-listbox" role="listbox" aria-label="States">
    <li id="lb1-wa" role="option">Washington</li>
    <li id="lb1-wv" role="option">West Virginia</li>
    <li id="lb1-wi" role="option" aria-selected="true">Wisconsin</li>
    <li id="lb1-wy" role="option">Wyoming</li>
  </ul>
</div>

Note the relationships:

  1. <input> has aria-controls pointing to the <ul>.
  2. <input> has aria-activedescendant pointing to the third <li>.

Enter shadow DOM

Now let's write the same thing, but using standard web component practices, including shadow DOM:

<fancy-input>
  #shadow-root
    <input type="text">
</fancy-input>
<fancy-listbox>
  #shadow-root
    <ul role="listbox">
      <fancy-option>
        #shadow-root
          <li role="option">Washington</li>
      </fancy-option>
      <fancy-option>
        #shadow-root
          <li role="option">West Virginia</li>
      </fancy-option>
      <fancy-option>
        #shadow-root
          <li role="option">Wisconsin</li>
      </fancy-option>
      <fancy-option>
        #shadow-root
          <li role="option">Wyoming</li>
      </fancy-option>
    </ul>
</fancy-listbox>

Using AOM element reflection, we would like to express the aria-activedescendant relationship:

input.ariaActiveDescendantElement = li3

However, this doesn't work. After running this code, the ariaActiveDescendantElement will be null:

console.log(input.ariaActiveDescendantElement) // null

Why? Because li3 is not a descendant of any of the input's shadow-including ancestors:

If element's explicitly set attr-element is a descendant of any of element's shadow-including ancestors, then return element's explicitly set attr-element. Otherwise, return null.

Why the restriction?

Why does this restriction exist? The main goal is to preserve encapsulation, especially for closed shadow roots. Consider this expando:

element1.customExpando = element2

In this case, anyone with access to element1 now has access to element2:

console.log(element1.customExpando) // element2

Without this expando, the only way to access element2 from element1 would be if element2 was in the same shadow root, or in an ancestor shadow root. This is because, even in closed shadow DOM, you can always walk up the tree as much as you want:

element1
  .getRootNode().host
  .getRootNode().host
  .getRootNode().host // etc

... but you cannot walk down the tree:

[...element1.getRootNode().querySelectorAll('*')]
  .map(_ => _.shadowRoot) // null for closed shadow

So the restriction on AOM element reflection preserves this same rule:

console.log(element.ariaActiveDescendantElement) // null if not in element's shadow-including ancestors

Why is the restriction a problem?

Let's go back to the combobox example above. To make the ariaActiveDescendantElement relationship work with AOM reflection, we would have to ensure that the <li> is in an ancestor shadow root of the <input>. This requires changing the structure of the DOM, and removing several shadow boundaries between the <input> and the <li>:

<fancy-input>
  #shadow-root
    <input type="text">
</fancy-input>
<fancy-listbox>
    <ul role="listbox">
      <fancy-option>
          <li role="option">Washington</li>
      </fancy-option>
      <fancy-option>
          <li role="option">West Virginia</li>
      </fancy-option>
      <fancy-option>
          <li role="option">Wisconsin</li>
      </fancy-option>
      <fancy-option>
          <li role="option">Wyoming</li>
      </fancy-option>
    </ul>
</fancy-listbox>

In short, <fancy-listbox> cannot have encapsulation from <fancy-input>. Nor can the individual <fancy-option>s have encapsulation from each other, or from the <fancy-listbox>. In effect, the developers of these components cannot use shadow DOM and must use light DOM as much as possible to minimize crossing shadow boundaries.

For developers of closed shadow root components, perhaps this restriction makes sense. But for developers of open shadow root components, this restriction does not match the existing semantics, since open shadow roots can be fully traversed in either direction.

Does cross-root ARIA delegation solve this?

No. Cross-root ARIA delegation allows nodes to delegate their aria-* attributes to their hosts.

In this case, the <input> being able to delegate aria-activedescendant to its host does not help, because the host still only has access to the ID of the <li> if both are in the same shadow root.

What is the proposed solution?

My proposal is to make AOM reflection follow the existing semantics for both open and closed shadow roots:

element1.ariaActiveDescendantElement = element2
Relationship between elements Shadow mode Allowed?
element1 and element2 are in the same shadow root Any
Any (sibling, cousin, ancestor, descendant, etc.) All shadow roots separating element1 from element2 are open
element2 is in a shadow-ancestor of element1 Any shadow root separating element1 from element2 is closed
element2 is not in a shadow-ancestor of element1 Any shadow root separating element1 from element2 is closed

In this system, the restriction would still exist for closed shadow roots, but it would not exist for open shadow roots.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment