Skip to content

Instantly share code, notes, and snippets.

@JanMiksovsky
Created February 10, 2021 19:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JanMiksovsky/b080f70a6046beeb2fe1bcf6a65af009 to your computer and use it in GitHub Desktop.
Save JanMiksovsky/b080f70a6046beeb2fe1bcf6a65af009 to your computer and use it in GitHub Desktop.
Exploring indirect AOM element references to bridge light/shadow DOM

Indirect ARIA element references

This illustrates the possibility of indirecting AOM element references so that they work across shadows. The main ideas are:

  1. Use custom element code to coordinate references light DOM and shadow DOM.
  2. Ensure that AOM references can be indirected: if element A references B for a label, and B references C for a label, then A ends up getting its label from C.
  3. Let the custom element host serve as an indirection point between the light and shadow DOM.
  4. Define fallback AOM properties on elementInternals that are used if the corresponding properties are not set on the host. This lets a host reference elements in its own shadow without exposing those references.

Adapting a combo box example from Alice Boxhall.

<my-label for="customComboBox">
    #shadow-root
      <label><slot></slot></label>
  Name
</my-label>
<custom-combobox id="customComboBox">
    #shadow-root
      <input role="combobox"></input>
      <slot></slot>
  <custom-optionlist aria-activedescendant="opt1">
    <x-option id="opt1">Option 1</x-option>
    <x-option id="opt2">Option 2</x-option>
    <x-option id="opt3">Option 3</x-option>
 </custom-optionlist>
</custom-combobox>

In the source for my-label:

/* ARIA wiring in constructor for my-label */
const label = this.shadowRoot.querySelector("label");
label.ariaForElement = this;

In the source for custom-combobox:

/* ARIA wiring in constructor for custom-combobox */
/* Input gets its ARIA properties from the host. */
const input = this.shadowRoot.querySelector("input");
input.ariaLabelledByElement = this;
input.ariaActiveDescendantElement = this;
input.ariaControlsElement = this;

/* ARIA wiring in custom-combobox slotchange handler */
/* Host delegates its ARIA list aspects to a light DOM list */
const slot = this.shadowRoot.querySelector("slot");
const list = slot.assignedElements({ deep: true })[0];
this.ariaActiveDescendantElement = list;
this.ariaControlsElement = list;

The custom-combobox host acts as a bridge between the input in the shadow DOM and the list in the light DOM. A shadow element like the input can reference the host; the host in turn can reference light DOM elements.

This forms a chain for the ARIA label:

  1. The custom-combobox's input gets its ARIA label from its host (via script).
  2. The custom-combobox host gets its label from the my-label host (via for).
  3. The my-label host gets its label from a shadow label (via script).
  4. The shadow label gets its content from the slotted light DOM content.

References into shadow

This particular example happens to avoid a case where the host needs to reference an element in its own shadow. If the DOM arrangement here were inverted, so that the input was in the light DOM and the list in the shadow, the host would need some way to reference the shadow list — without leaking that reference to the outside world.

<custom-combobox id="customComboBox">
    #shadow-root
      <custom-optionlist aria-activedescendant="opt1">
        <x-option id="opt1">Option 1</x-option>
        <x-option id="opt2">Option 2</x-option>
        <x-option id="opt3">Option 3</x-option>
    </custom-optionlist>
  <input role="combobox"></input>
</custom-combobox>

One possibility would be to have fallback ARIA properties on elementInternals that are used if no corresponding ARIA properties are set on the host itself.

/* ARIA wiring in constructor for custom-combobox */
const internals = this.attachInternals();
const list = this.shadowRoot.querySelector("custom-optionlist");
internals.ariaActiveDescendantElement = list;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment