- Leo Balter [email protected]
- Manuel Rego [email protected]
It is critically important that content on the web be accessible. When native elements are not the solution, ARIA is the standard which allows us to describe accessible relationships among elements. Its mechanism for this is based on IDREFS, which unfortunately cannot express relationships between different DOM trees, thus creating a problem for applying these relationships across a shadow root.
Today, because of that, authors are left with only incomplete and undesirable choices:
- Observe and move ARIA-related attributes across elements (for role, etc.).
- Use non-standard attributes for ARIA features, in order to apply them to elements in a shadow root.
- Require usage of custom elements to wrap/slot elements so that ARIA attributes can be placed directly on them. This gets very complicated as the number of slotted inputs and levels of shadow root nesting increase.
- Duplicating nodes across shadow root boundaries.
- Abandoning Shadow DOM.
- Abandoning accessibility.
There are two different kind of problems that are very related but are not exactly the same thing. Initially they can be seen as two sides of the same coin but there are some differences when looking into them.
For example, we have a label
and a custom input x-input
that has some content and a regular input
in the shadow tree.
We want to associate the inner input
to the external label
, we can set aria-labelledby
in the x-input
, but how can we determine which element in the shadow tree should set the relationship?
<label id="label">My outer label</label>
<x-input aria-labelledby="label">
<template shadowroot="closed">
<span>Before</span>
<input/>
<span>After</span>
</template>
</x-input>
We'd need a way to specify that the inner input
is labelled by the external label
.
One way to do that would be in JavaScript using ARIA attribute reflection setting ariaLabelledByElements
in the inner input
directly. But that's not possible using Declarative Shadow DOM.
In this case we're looking into a way to import attributes into the shadow tree that could be used for both string (e.g. aria-label
) and IDREF attributes (e.g. aria-labelledby
).
On a similar fashion we could have a custom label x-label
with some content and a regular label
in the shadow tree.
In this case we want to associate the external input
to the inner label
, we can set aria-labelledby
in the input
, but again how can we determine to which element in the shadow tree set the relationship?
<x-label id="label">
<template shadowroot="closed">
<span>Before</span>
<label>My inner label</label>
<span>After</span>
</template>
</x-label>
<input aria-labelledby="label" />
We'd need a way to specify that the inner label
is somehow exposed out of the shadow tree and can be referenced by the external input
.
Here we're looking into a way to expose some elements from the shadow tree, so they can be referenced from the outside. This is only useful for IDREF attributes (e.g. aria-labelledby
) as this won't be needed for string ones.
There could be cases where you could need both things at the same time.
Example:
<fancy-input aria-controls="the-listbox" aria-activedescendant="the-listbox">
<template shadowroot="closed">
<input>
</template>
</fancy-input>
<fancy-listbox id="the-listbox">
<template shadowroot="closed">
<div role="listbox">
<div role="option">One</div>
<div role="option">Two</div>
</div>
</template>
</fancy-listbox>
On one side we want to import the ARIA attributes from fancy-input
into the inner input
. Then we want set a relationship between that inner input
and the inner elements of fancy-listbox
.
As there are two different problems, we should look into two different proposals. But as they are so related we believe it's worth discussing them together.
For this problem there's the Cross-root ARIA Delegation proposal. This proposal introduces a delegation API which would allow ARIA attributes set on a custom element to be forwarded to elements inside of its shadow root.
This will add new option to attachShadow()
called delegatesAriaAttributes
similar to delegatesFocus
. While introducing a new content attribute delegatedariaattributes
to be used in the shadow tree elements.
This will also work on declarative Sadow DOM by adding a new shadowrootdelegatesariaattributes
content attribute for the template
element, similar to shadowrootdelegatesfocus
.
Using declarative Shadow DOM like in the previous examples:
<span id="foo">Description!</span>
<x-foo aria-label="Hello!" aria-describedby="foo">
<template shadowroot="closed" shadowrootdelegatesariaattributes="aria-label aria-describedby">
<input id="input" delegatedariaattributes="aria-label aria-describedby" />
<span delegatedariaattributes="aria-label">Another target</span>
</template>
</x-foo>
And using imperative Shadow DOM:
<span id="foo">Description!</span>
<template id="template1">
<input id="input" delegatedariaattributes="aria-label aria-describedby" />
<span delegatedariaattributes="aria-label">Another target</span>
</template>
<x-foo aria-label="Hello!" aria-describedby="foo"></x-foo>
const template = document.getElementById('template1');
class XFoo extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open", delegatesAriaAttributes: "aria-label aria-describedby" });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
}
customElements.define("x-foo", XFoo);
In the examples above, the ARIA attributes assigned in the host x-foo
are delegated to inner elements inside the custom element's shadow tree. Today, custom code can reflect this application but synthetically applying the aria attributes and their effects to both the host x-foo
and its inner elements.
Note: See issue #22.
This proposal was originally named Cross-root ARIA Delegation, as it's somehow based in the same concept than delegatesFocus
. We could discuss about the final name and think if there are better alternatives for this case. Maybe "import" or maybe other words we can think about.
Apart from that the current attribute for the template
element is very long shadowrootdelegatesariaattributes
, maybe it could be simplified to delegatesariaattributes
.
Note: See issue #24.
We'll need to determine what happens when one of the delegated attributes gets modified and when we're going to set the underneath relationships in the shadow tree elements. Probably we need to couple this with Custom elements reactions and the attributeChangedCallback
.
Having the ARIA attributes in the shadow host and setting them also in the inner elements in the shadow tree might cause that a screen reader reads it twice.
Example:
<x-input aria-label="One label">
<template shadowroot="closed" shadowrootdelegatesariaattributes="aria-label">
<input delegatedariaattributes="aria-label"/>
</template>
</x-input>
Should we somehow "remove/ignore" the delegated attributes from the shadow host? Not removing them directly, but like ignoring them from the screen reader.
If we have a custom element with two input
s and we want to import different aria-labelledby
elements for each of them, we cannot achieve that with this proposal.
Example:
<label id="label1">Label one</label>
<label id="label2">Label two</label>
<x-input>
<template shadowroot="closed">
<input id="input1"/>
<input id="input2"/>
</template>
</x-input>
We could not specify that input1
is labelled by label1
and input2
by label2
. In this proposal we're setting aria-labelledby
in the custom element x-input
directly, so we could set aria-labelledby="label1 label2"
there. But we cannot then set the exact relationship in the inner elements.
Not sure if there are actual use cases affected by this issue. Should this be added as non-goal?
This proposal should work if we have nested shadow trees.
Example:
<label id="label">My outer label</label>
<x-input aria-labelledby="label">
<template shadowroot="closed" shadowrootdelegatesariaattributes="aria-labelledby">
<y-input delegatedariaattributes="aria-labelledby">
<template shadowroot="closed" shadowrootdelegatesariaattributes="aria-labelledby">
<input delegatedariaattributes="aria-labelledby" />
</template>
</y-input>
</template>
</x-input>
Note: See issue #23.
There could be different approaches to deal with this issue.
One approach is to do something similar to Cross-root ARIA Delegation, that's what Cross-root ARIA Reflection proposal does.
Example:
<span aria-controls="foo" aria-activedescendant="foo">Description</span>
<x-foo id="foo">
<template shadowroot="open" shadowrootreflectsariaattributes="aria-controls aria-activedescendant">
<ul reflectedariaattributes="aria-controls">
<li>Item 1</li>
<li reflectedariaattributes="aria-activedescendant">Item 2</li>
<li>Item 3</li>
</ul>
</template>
</x-foo>
Another approach would be reusing somehow CSS Shadow Parts.
From the CSS Shadow Parts spec introduction:
This specification defines the
::part()
pseudo-element on shadow hosts, allowing shadow hosts to selectively expose chosen elements from their shadow tree to the outside page for styling purposes.
So this specs already provide a mechanism to expose certain elements from the shadow tree to the outside world so they can be styled. We're looking for something similar so those exposed elements can be referenced by IDREF attributes from the outside.
To expose those elements CSS Shadow Parts uses the part
attribute, if there are nested Shadow DOM then exportparts
is also used.
Example:
<span aria-controls="foo" aria-activedescendant="foo">Description</span>
<x-foo id="foo">
<template shadowroot="open">
<ul part="aria-controls">
<li>Item 1</li>
<li part="aria-activedescendant">Item 2</li>
<li>Item 3</li>
</ul>
</template>
</x-foo>
Maybe this doesn't make a lot of sense, as part
is a way to expose things for styling them outside. A different approach could be use a new attribute with a different name. For example exportfor
, and set it directly on the elements indicating for which ARIA attributes the element is exported:
<span aria-controls="foo" aria-activedescendant="foo">Description</span>
<x-foo id="foo">
<template shadowroot="open">
<ul exportfor="aria-controls">
<li>Item 1</li>
<li exportfor="aria-activedescendant">Item 2</li>
<li>Item 3</li>
</ul>
</template>
</x-foo>
On a similar fashion, but using a new attribute we could define something like a exportids
attribute similar to exportparts
, so we could do things like:
<span aria-controls="foo-innner-list" aria-activedescendant="foo-innner-item">Description</span>
<x-foo id="foo" exportids="list: foo-innner-list, item: foo-innner-item">
<template shadowroot="open">
<ul id="list">
<li>Item 1</li>
<li id="item">Item 2</li>
<li>Item 3</li>
</ul>
</template>
</x-foo>
One problem with this idea is that exportids
is defined in the shadow host (like exportparts
). When using the custom element we'd need to know the internal ids, similar to how we need to know the available parts for styling. Another issue is that each time we use the custom element we have to set this, which might be not nice.
When you start dealing with nested shadow trees things get more complicated. We have to review the proposals so they can work in those cases too.
Note: See issue #14.
Once we have an agreement about the proposals for each problem it would be nice to see how they can work together in some more complex use cases.
Note: See issue #13.
There are more IDREF attributes apart from ARIA ones.
For example the for
attribute in label
element.
Example:
<label id="label" for="custom-input">My outer label</label>
<x-input id="custom-input">
<template shadowroot="closed">
<span>Before</span>
<input/>
<span>After</span>
</template>
</x-input>
Here we cannot set the relationship between the external label
and the inner input
element. A similar thing can happen in the opposite direction with a custom label referencing an input outside the shadow tree.
There are other cases that also reference to other elements:
list
attribute ininput
.- Pop Up proposal by Open UI, that adds some new attributes
popuptoggletarget
,popupshowtarget
andpopuphidetarget
. - SVG
<use>
and thehref
attribute @font-face
and thefont-family
CSS property
Should we look for a solution that is generic enough to cover these cases too?