aom
aom copied to clipboard
linking elements cross boundaries is only possible imperatively and leads to anti-patterns
Disclosure: This issue is just to brainstorm on the proposed linking mechanism and maybe open other avenues.
A very common use-case is to have custom elements that should be described by sibling elements,
<template>
<span id="global-one">description for x-foo element</span>
<fancy-input aria-describedby="global-one">
</fancy-input>
</template>
Assuming that fancy-input
element contains some internal focusable elements, and itself is relying on delegate focus to facilitate the user interaction, e.g.:
<template>
<span id="global-one">description for x-foo element</span>
<fancy-input aria-describedby="global-one">
#shadow-dom (mode=closed, delegateFocus=true)
<label id="local-one">...</label>
<input aria-describedby="local-one" />
</fancy-input>
</template>
It is still impossible to connect those elements from outside (#global-one
) and from within (input
) without opening some sort of side-channel on fancy-input
element. I see two options to achieve this:
- expose a method on
<fancy-input>
custom element that when called with one argument (an element), it sets that element into the internal input'sariaDescribedByElements
collection. (with the corresponding guards and deduping). - expose a method on
<fancy-input>
custom element that when invoked without arguments return a reference to the internal input element.
Either way, it is going to be bad, error prompt or leaky, this is one of those situations when you have to take a poison.
Another option is to observe the aria attributes on the host, walk from the host all the way to the nearest root, and query for the ID from there to try to do some auto-wiring via imperative APIs. This should work fine, but the problem is that since you're in control of the situation as the author of the component, you will have to observe mutations to rewire when needed. This makes the situation very error prompt, while the option 1 and 2 outsource that responsibility to the consumer of the custom element.
Proposal
When discussing this with the team, we were wondering whether or not the delegate focus on the shadow root could be sufficient indication for some sort of compounding mechanism for some of these aria-*
attributes that reference IDs. In the example above, you can see that from the consumer perspective (the owner of the template), you can do the regular connecting between the two elements via IDs, they are in the same shadow after all.
But from the component's author perspective, just signaling that the root should receive the focus (via delegateFocus configuration), could be used to build the right tree under the hood that connects the input with both elements (#global-one
and #local-one
) without the user having to do so manually.
Pros:
- declarative works again
- very intuitive (you just don't need to know how the fancy-input works)
Cons:
- how to opt-out from this behavior? is it possible to opt-out?
cc @alice, @cookiecrook
I don't quite follow the proposal - could you possibly add some "strawman" code which illustrates what you mean?
@alice this proposal is focus on one premise: replacing <input>
with <fancy-input>
should be as easy as changing the tag name in your markup (if the both share the same API).
To provide an actual example:
class FancyInput extends HTMLElement {
constructor() {
super();
var shadow = this.attachShadow({
mode: 'open',
delegatesFocus: true,
});
shadow.innerHTML = `
<label id="fancy-label">internal label for input element</label>
<input aria-describedby="fancy-label" />
`;
}
}
customElements.define('fancy-input', FancyInput);
class MyComponent extends HTMLElement {
constructor() {
super();
var shadow = this.attachShadow({
mode: 'open',
});
shadow.innerHTML = `
<label id="my-label">my custom label for fancy-label element</label>
<fancy-input aria-describedby="my-label" />
`;
}
}
customElements.define('my-component', MyComponent);
If you declare those two components, and you insert <my-component></my-component>
in the page. What happen when you focus on fancy-input
? It will delegate the focus, but the aria-describedby
will not be compounded (SR will announce only the internal label "internal label for input element"
).
The author of MyComponent will be guessing why is the label specified on its markup doesn't get announced? Although, changing <fancy-input>
back to <input>
will work. Our suggestion here is to consider the ability to define compounding rules for aria attributes if the shadow root happens to have delegatesFocus: true,
,to match the behavior of the focus on those elements, which is already compounded.
It is important to notice that this is probably going to be one of the most common use-case. We can definitely provide some strawman, but first we will like to know if this was discussed in the past.
So you're suggesting that using delegateFocus
not only delegates away the focus to the focusable element inside the Shadow Root, but also delegates some attributes from the Custom Element to the focusable element inside, in an additive way?
Example:
class FancyInput extends HTMLElement {
constructor() {
super();
var shadow = this.attachShadow({
mode: 'open',
delegatesFocus: true,
});
shadow.innerHTML = `
<label for="fancy-input">internal label for input element</label>
<input id="fancy-input" aria-describedby="fancy-input-error" />
<span id="fancy-input-error">internal error message for the input element</span>
`;
}
}
customElements.define('fancy-input', FancyInput);
class MyComponent extends HTMLElement {
constructor() {
super();
var shadow = this.attachShadow({
mode: 'open',
});
shadow.innerHTML = `
<fancy-input aria-describedby="my-additional-error" />
<span id="my-additional-error">my custom error for fancy-input element</label>
`;
}
}
customElements.define('my-component', MyComponent);
<my-component></my-component>
would render in the DOM as
<my-component>
#shadow-root
| <fancy-input>
| #shadow-root
| | <label for="fancy-input">internal label for input element</label>
| | <input id="fancy-input" aria-describedby="fancy-input-error my-additional-error" />
| | <span id="fancy-input-error">internal error message for the input element</span>
| </fancy-input>
| <span id="my-additional-error">my custom error for fancy-input element</label>
</my-component>
Where my-additional-error
is added to the aria-describedby
of the fancy-input
input
element?
Feels super weird that delegateFocus
would be that vehicle. It also wouldn't account for things that potentially need referencing across shadow roots that aren't focusable. Consider Modal markup for example, I'd see if a fairly common that a modal component would be created as generic but the guts of the modal is in the modals shadow-root(s), but I want to label and describe the modal element by it's content.
<my-custom-modal role="dialog" aria-modal="true" aria-labelledby="id_of_visible_header" aria-describedby="id_of_modal_body">
<my-custom-modal-header>
#shadow-root
| <h2 id="id_of_visible_header">
| Modal header
| </h2>
</my-custom-modal-header>
<my-custom-modal-body>
#shadow-root
| <div id="id_of_modal_body">
| Random content provided by component consumer
| </div>
</my-custom-modal-body>
</my-custom-modal>
It's not clear to me how this would work with possible inward-pointing references (e.g. aria-activedescendant) in addition to the outward-pointing references like aria-labelledby.
delegatesFocus
was not designed exclusively for this type of "decorating" use case - it also covers cases where a custom element has multiple internal focusable elements. I don't think it's a good fit here.
This pattern of decorating built-in elements with custom elements has a number of issues like this one. I'm not sure what would be the best API to allow the behaviour you describe, but I agree it's desirable.
@SiTaggart
Feels super weird that delegateFocus would be that vehicle.
That's ok, maybe we can decouple this from focus, and just have a new configuration for shadow root that signals the auto-wiring of the accessibility descriptors. I should have started with it.
@cookiecrook
It's not clear to me how this would work with possible inward-pointing references (e.g. aria-activedescendant) in addition to the outward-pointing references like aria-labelledby.
that can works exactly as described above, where the internal element receiving the focus is auto-wired as the active descendant, without having to expose this to user-land. the entire proposal is to attack the most common use-cases so consumers of custom elements can thread them just like regular elements. I'm still missing the part where <label>something <fancy-input></fancy-input></label>
works.
@alice
it also covers cases where a custom element has multiple internal focusable elements
we have discussed this particular case, e.g. <input-location>
which contains latitude/longitude inputs, and we are not very clear on how that will work. That's why I was asking how to opt-out of this? It seems that if we use a different configuration, we could potentially opt-out by not providing that configuration in the shadow creation.
Again, the part that I want to stress here is that it is Not Okey to ask consumers of custom elements to do something different from what they normally do when using an input. It is also Not Okey to ask authors of custom elements to do very complicated gymnastics to do the right thing about accessibility, because we know where that leads.
Obviously we already made mistakes like not supporting labels in the same way they are supported for regular inputs, but we should be able to fix those and provide default behaviors that can cover a lot of grounds for the most common use-cases.
that can works exactly as described above, where the internal element receiving the focus is auto-wired as the active descendant
There are a number of IDREFS-valued ARIA attributes that have no direct relationship to the currently focused element. For example, aria-controls and aria-owns.
The issue with the "decorator" pattern is fundamentally that you end up with two (or more) nested elements trying to act like a single element - you want the decorated element to act like the single element for some purposes, and the decorator for other purposes.
FWIW the labeling issue is also being discussed here. https://github.com/w3c/webcomponents/issues/187
On Tue, Mar 6, 2018, 4:01 PM Alice [email protected] wrote:
The issue with the "decorator" pattern is fundamentally that you end up with two (or more) nested elements trying to act like a single element - you want the decorated element to act like the single element for some purposes, and the decorator for other purposes.
— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/WICG/aom/issues/107#issuecomment-370973394, or mute the thread https://github.com/notifications/unsubscribe-auth/ABBFDY4Ic8qMNtAG5VJ0y0ZBsVHoH82bks5tbyNEgaJpZM4SanSt .
...and here https://github.com/whatwg/html/issues/3219 - for imperative attaching of labelable element to the label element.
This is superseded by the proposal in https://github.com/WICG/aom/issues/169, which is a lot more coherent.