-
Notifications
You must be signed in to change notification settings - Fork 59
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
Content attribute to import/export IDs across shadow boundaries #169
Comments
As discussed, this seems more general than ARIA, so the name |
CC @annevk. This idea was developed in response to the concerns about components depending on the outer tree with element reflection references from inner to outer scopes (whatwg/html#6063). One question is whether this should actually impact the id map (e.g. getElementById) in the destination scope or whether these "ids" should only be used for the purposes of ARIA. While it would seem to be intuitive that they do impact destination id maps, I'm concerned that would allow outer scopes to mutate and walk the inner tree. Take the second example above (modified slightly for clarity):
If id maps are affected, the outer scope can now do: I'm also not sure how the browser will reliably figure out which are imports and which are exports. |
We clearly can't expose IDs in the sense of making |
Similarly So the ID forwarding mechanism is limited in its impacts. What if |
Similarly what?
Not sure what do you mean by this. We can certainly make
I'd imagine there is no change in the behavior of I do think it's a bit weird that |
Thanks for the initiative! Let me share a real world use case that we have in our components library.
<vaadin-text-field label="Login"></vaadin-text-field>
<vaadin-form-item>
<label id="outer">Label</label>
</vaadin-form-item>
<vaadin-text-field aria-maps="label-1:outer">
#shadow
<label part="label" id="label-1"></label>
<input id="input-1" aria-labelledby="label-1">
</vaadin-text-field-element> I'm wondering how to handle both use cases. So far I see the following options:
The question is whether |
Oops, that's what happens when I try to comment when exhausted. I mean similarly to <label for="input">
<custom-combobox aria-maps="input: inner_input">
# shadowRoot
<input id="inner_input">
</custom-combobox>
Just that the ID forwarding only affects declarative attributes, but not script, as you said. I think it might be worth exploring having script-based APIs return the element that the forwarding is declared on - much like how we return the light DOM ancestor of the active element for e.g. for @jcsteh's example: <label-element aria-maps="inner_my_label:outer_radio_label">
#shadow
<div id="inner_my_label">My radio label</div>
<div id="inner_sibling">...</div>
</label-element>
<ul role="radiogroup" aria-labelledby="outer_radio_label">
...
</ul> console.log($('ul[role=radiogroup]').ariaLabelledByElement); // logs <label-element>
document.getElementById('inner_my_label'); // same, or maybe we need a forwarding-aware version? I recall at some point we talked about having a mechanism to pass in a shadow root to some kind of accessor method, but I've forgotten the context. Would some kind of shadow DOM aware accessor be helpful here? |
I'm very excited about this one, thanks @rniwa for taking the time to address this. We raised that concern in #107 more than a year ago about programatic access to elements from another shadow (reflection of id and elements), as it breaks the encapsulation, making it impossible for us to use such feature. My proposal at that time was similar to this, but obviously this is much more nicer. The declarative aspect of this proposal aligns very well for us, and the parts precedent makes me think that this could work very well. I do not think we need a specific imperative API, developers can simply rely on the DOM apis for attributes, plus the traversal mechanism of a shadow to discover those elements in the shadows are open. |
That's an interesting idea. For selection, what we concluded was that we want to make a method take a set of shadow roots and disclose nodes if nodes are either not in any shadow root or is in one of the shadow roots being passed in the argument. So for the above example, we would have the behavior like this: document.getElementById('outer_radio_label');
// returns null
document.getElementById('outer_radio_label', {shadowRoots: [shadowRootOfLabelElement]});
// returns the div inside the shadow root
document.querySelector('ul[role=radiogroup]').ariaLabelledByElement;
// returns null
document.querySelector('ul[role=radiogroup]').getAriaLabelledByElement({shadowRoots: [shadowRootOfLabelElement]});
// returns the div inside the shadow root |
Right, that was the concern Mozilla raised as well, so we decided to take a stab at it, and it seems like this API might work quite well.
Right, what I like most is the parity with I think the only annoyance here is that you'd have to forward IDs at each level of shadow boundary but that might be actually a benefit depending on how you look at it. It would make the relationship between different (shadow) trees more explicit. |
You might be thinking of the |
This is a bit tangential but that API can't possibly just take closed shadow roots. It needs to take both modes of shadow roots since it would violate the encapsulation either way. The fact open mode allows a deliberate access isn't an excuse for adding new APIs that break encapsulation. |
As you said, this is definitely tangential. Let’s discuss on the declarative SD thread. But I don’t understand how this “breaks” the encapsulation of open shadow roots. They’re already accessible via JS, via element.shadowRoot. |
I think this is great. This matches what James and I discussed, but it's actually concrete rather than a conceptual model. 😊 Kudos. And yeah, it seems that something like |
@annevk : What do you feel about expanding this for all other IDs? For things like |
That seems reasonable, but we probably need an upfront enumeration of the affected places so we can update them all to use this new primitive. I rather not repeat the shadow tree whack-a-mole that's still ongoing. |
Just to get a feel for what it might be like to use this kind of API, I had a go at writing out some more complex examples. I took the liberty of assuming that like Referring to sibling shadow roots, and to slotted elements in outer DOM: <my-label id-maps="target-input:combobox-input">
#shadowRoot
| <label for="target-input"><slot></slot></label>
#/shadowRoot
Name
</my-label>
<custom-combobox id-maps="opt1, opt2, opt3, inner-input:combobox-input">
#shadowRoot
| <input id="inner-input" aria-activedescendant="opt1"></input>
| <slot></slot>
#/shadowRoot
<custom-optionlist>
<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> // assume code here to set up appropriate variables pointing to elements with equivalent IDs
innerInput.ariaActiveDescendantElement = opt2;
console.log(innerInput.getAttribute("aria-activedescendant")); // logs "opt2", because the ID is mapped Referring up two levels of Shadow DOM: <template id="component-template">...</template>
<my-app id-maps="component-template">
#shadowRoot
| <my-section id-maps="component-template">
| #shadowRoot
| | <my-component template="component-template"></my-component>
| </my-section>
</my-app> Does that match what you envisioned, @rniwa, @annevk, @jcsteh, @mfreed ..? |
We probably need a mechanism for a shadow tree for opt-in maybe. @alice raised a concern / commentary that people seem to find that the ID mapping does bidirectional mapping was confusing for some people. We could consider making them two separate attributes like |
It would also be helpful to have a programmatic way to add/remove things from |
I can see how the syntax is somewhat confusing. From looking at just the <my-custom-element id-maps="inner : outer"></my-custom-element> From context you might infer that the <input aria-labelledby="outer"></input>
<my-custom-element id-maps="inner : outer"></my-custom-element> But then we come across a new line, and have to instead infer that the <label id="outer">outer label</label> You can imagine that this bidirectionality may lead to subtle and hard to find author errors <my-custom-element id-maps="inner : outer">
# shadowRoot
| <label id="inner">inner label</label>
</my-custom-element>
<label id="outer">outer label</label>
<input aria-labelledby="outer"></input> It can also lead to unclear code <my-custom-element id-maps="foo : outer"></my-custom-element>
<my-other-custom-element id-maps="bar : outer"></my-other-custom-element>
<!-- where does my label come from? -->
<input aria-labelledby="outer"></input> |
Yes, I was one of those people who told @alice I found this very confusing and shared a few examples that I thought kind of made my head spin. I suggested that separate attributes <!-- the host exposes the idref available to it to the rest of the tree as b -->
<x-div exportids="a:b">
#shadow (closed)
| <div id="a" exportid>
| inner A is made available to the host for mapping
| </div>
/shadow
D
</x-div> This lets you have a map similar <input id="foo">
<!-- the host exposes the idref foo to shadow tree as bar -->
<x-div importids="foo:bar">
#shadow (closed)
| <label for="bar">Label thing outside</div>
/shadow
</x-div> This seems much clearer to me and a couple of people I checked with. The one thing about this (either way I guess) that is kind of weird is that the exportid thing is kind of a two-step that matches the way most import/export things we are familiar with would work... The inner tree exposes something as 'available' and the outer tree asks for that and maps it to something itself. However, with import there isn't a similar 'ask for the thing made available from the outer tree and expose it to me. This seems to imply that the outer tree can kind of just shove ids into my inner tree space which seems just very weird and unfortunate and prone to bugs. Short of some kind of way use something like Separately, the answer to how should things like |
What we do for |
@annevk : Do you think someone from Mozilla can drive this feature & propose the refined version given Mozilla raised the original concern with regards to crossing shadow boundaries? |
@rniwa I assume you're asking @annevk about this issue? whatwg/html#6063 |
These are the current open issues here, as far as I can tell: (Element Reflection)
(ID forwarding)
Edit to add
|
Reworking the example in #169 (comment) using separate imports and exports: <!-- using from:to order for both imports and exports -->
<!-- this means that import is outer:inner, *not* inner:outer order -->
<my-label id-import="combobox-input:target-input">
#shadowRoot
| <!-- should this opt-in to importing? -->
| <label for="target-input"><slot></slot></label>
#/shadowRoot
Name
</my-label>
<!-- still using id as equivalent to id:id -->
<custom-combobox id-imports="opt1 opt2 opt3" id-exports="inner-input:combobox-input">
#shadowRoot
| <!-- opt in to exporting ID -->
| <input id="inner-input" aria-activedescendant="opt1" export-id></input>
| <slot></slot>
#/shadowRoot
<custom-optionlist>
<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> |
Discussed in today's meeting:
|
Yeah, it should be space-separated, just like the |
Thanks @annevk. Did you have any thoughts on any of the other open questions? |
Not particularly, except that I would suggest to keep the initial version as simple as possible. So if something can be added later, opt for that. |
Thanks for those examples @alice - it's really helping me wrap my brain around this. I think one reason I was struggling is that, when I look at the following, I don't see "import an id" but rather "export a
@bkardell's earlier comment also rang true with me:
With that in mind, what if we had the following:
To further iterate on the example: <!-- using outer:inner order for mappings -->
<my-label remap-ids="combobox-input:target-input">
#shadowRoot
| <!-- opt in to exporting element -->
| <label for="target-input" exported><slot></slot></label>
#/shadowRoot
Name
</my-label>
<!-- outer document can set active descendant via id map instead of reaching into inner document -->
<custom-combobox remap-ids="opt1:inner-activedescendant combobox-input:inner-input">
#shadowRoot
| <!-- no need to export this element anymore -->
| <input id="inner-input" aria-activedescendant="inner-activedescendant"></input>
| <slot></slot>
#/shadowRoot
<custom-optionlist>
<!-- the outer document exports these elements to the inner document. -->
<x-option id="opt1" exported>Option 1</x-option>
<x-option id="opt2" exported>Option 2</x-option>
<x-option id="opt3" exported>Option 3</x-option>
</custom-optionlist>
</custom-combobox> Hmm. Having written that out, one thing that feels missing is control over where an element is exported to. If the whole markup above were contained within another level of shadow DOM, we would want to export opt1..opt3 in the "inwards" direction but probably not the "outwards" direction. |
Sorry for not noticing this issue earlier. I wanted to mention that the original proposal here by @rniwa following the September 2020 meeting is a little different than what I’d proposed at that meeting. As captured, my original suggestion had tried to avoid using shadow element IDs outside the shadow where they appear. Instead, the idea was that an element could use element internals to programmatically delegate its participation in ARIA relationships to its own shadow elements. The outer element could be referenced by ID by elements in the same tree as usual, but the IDs of the interior shadow elements would never be exposed outside the element. The example in that comment shows some made-up syntax for delegating responsibility if an element wants to act as an <label-element id="label">
#shadow-root
<div id="my_label">My radio label</div>
</label-element>
<my-element aria-labelledby="label">
#shadow-root
<ul role="radiogroup">
<li role="radio">Item #1</li>
<li role="radio">Item #2</li>
<li role="radio">Item #3</li>
</ul>
</my-element> /* In constructor for label-element */
const root = this.attachShadow(...);
const internals = this.attachInternals();
internals.labelSource = root.getElementById("my_label");
/* In constructor for my-element */
const root = this.attachShadow(...);
const internals = this.attachInternals();
internals.labelTarget = root.getElementById("radiogroup"); Essentially, any element used as the source of a label (like label-element above) can elect to specify where in its shadow the label should come from. That label source would be used with On the receiving side of the label, an element that’s being labeled (above, my-element) can indicate a target in the shadow which should receive the label. That label target would apply if my-element were labeled via Some benefits:
This isn’t a full proposal, just pointing out that maybe we can avoid relying on IDs as the main connectors. |
@JanMiksovsky Could you show how that proposal might apply to the example I wrote out above? And, I assume Finally, it would be good to have a declarative option for use with declarative Shadow DOM. I wonder if @mfreed7 has any plans for a declarative version of |
If I have a combobox UI deep within a number of shadow root and I need to throw the listbox element of the pattern to the end of the |
@alice I've posted a gist that develops the idea a bit further, and applies it to your combo box example. I've also tried to clean up the property names a bit based on my understanding of AOM, but this is still just a napkin sketch. There's probably a lot to work here — would be happy to set up a real-time discussion with you and other interested parties to hash this out a bit more. |
WCCG had their spring F2F in which this was discussed. You can read the full notes of the discussion (WICG/webcomponents#978 (comment)) in which this was discussed, heading entitled "ARIA Mixin & Cross Root ARIA" - where this issue was specifically discussed. In the meeting, present members of WCCG reached a consensus to discuss further in breakout sessions. I'd like to call out that WICG/webcomponents#1005 is the tracking issue for that breakout, in which this will likely be discussed. |
During AOM sync up on 10/20, we came up with the idea of using content attribute to map ID across shadow boundaries like
exportparts
.The idea here is to use
innerIdent: outerIdent
pairs to denote mapping of inner tree's ID with outer tree'd ID. For example, in the following example,radio_label
defined in the shadow tree ofmy-element
is exported as list in the outer tree, and ul is labeled by the div in the outer tree.In the following example, we export my_label in the shadow tree in label-element's shadow tree as radio_label and makes use of it by ul in the outer tree.
Combining these two things together, we can export a label from one shadow tree and use it in another shadow tree:
The text was updated successfully, but these errors were encountered: