Skip to content
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

Encapsulation-preserving IDL Element reference attributes #195

Open
alice opened this issue Feb 24, 2023 · 10 comments
Open

Encapsulation-preserving IDL Element reference attributes #195

alice opened this issue Feb 24, 2023 · 10 comments

Comments

@alice
Copy link
Member

alice commented Feb 24, 2023

Background: @nolanlawson proposed on #192 that IDL attributes be allowed to refer into open shadow roots (through arbitrarily many layers of open shadow roots, as long as no closed shadow roots were involved). It was noted during that discussion that as currently specified, setting an IDL attribute to an element does have an effect, except the effect is only to set some data internal to the browser that can't be observed unless the DOM structure changes - trying to read back the IDL attribute simply returns null, as if no value had been set, and nothing is exposed to AT. The question was raised whether it might be possible to expose that data to assistive technology even though it wasn't exposed to the DOM API - so encapsulation might be preserved, but setting the attribute still have an observable (via AT/developer tools only) effect. This was dismissed as having developer ergonomics which were too bad to be workable. There was also concern around the idea of treating open shadow roots and closed shadow roots differently in this context.

Proposal:

I'd like to iterate on Nolan's proposal by proposing that

  • we allow references into open (and possibly even closed?) shadow roots, but preserve encapsulation by retargeting the element returned from the IDL attribute getter, the way we do for event targeting.
  • we provide an API to allow "undoing" the retargeting, to the extent that the caller already has access to each relevant shadow root. This API would allow authors to debug and test code which refers into shadow roots.

For example,

<!-- wants to refer into custom-optionlist's shadow root -->
<input role="combobox" id="input" 
       aria-owns="optlist" 
       aria-activedescendant> <!-- aria-activedescendant set via IDL attribute -->
<custom-optionlist id="optlist">
 #shadowRoot
 | <x-option id="opt1">221B Baker St</x-option>
 | <x-option id="opt2">29 Acacia Road</x-option>
 | <x-option id="opt3">724 Evergreen Terrace</x-option>
 #/shadowRoot
</custom-optionlist>
input.ariaActiveDescendantElement = opt1;
console.log(input.ariaActiveDescendantElement);  // logs <custom-optionlist id="optlist">
let deepActiveDescendant =
    input.getAttrAssociatedElement("aria-activedescendant", 
                                   {shadowRoots: [optlist.shadowRoot]});
console.log(deepActiveDescendant);               // logs <x-option id="opt1">221B Baker St</x-option>

This preserves encapsulation by requiring an explicit step (and access to the shadow root) to access elements inside shadow roots.

This proposal is inspired by several precedents:

@annevk
Copy link

annevk commented Feb 24, 2023

I think I need to see a clearer motivating example. For the case presented above we wouldn't want <input> to need to know about the implementation details of <custom-optionlist>. It would be rather unfortunate (and defeat the point of shadow trees) if each time <custom-optionlist> wanted to change the way it works it would also have to update all the places it's being used.

@nolanlawson
Copy link
Collaborator

The question of "why would the attr-associated element ever be anything not in a shadow-including ancestor" has come up a few times in the AOM group. A few examples I can think of:

  1. A "recycler" element (virtual list) which, in this case, would has the list options inside of a shadow root
  2. A combobox where, for styling or encapsulation reasons, the list options are in individual shadow roots separate from the shadow root of their container listbox

If #2 seems far-fetched, I can say that the two most prominent comboboxes in the Salesforce UI both use this pattern (our searchbox and our app picker). We have investigated moving from our current shadow DOM polyfill to native shadow DOM, and this was one of the issues we found. (Our polyfill covers styling, slotting, and most DOM APIs, but still allows ARIA references across shadows.)

@annevk
Copy link

annevk commented Mar 7, 2023

@nolanlawson I think these descriptions might be too abstract. It's not clear to me for instance if the host of the shadow root could fulfill the role of intermediary for these relationships. That's the solution space I would like to explore further.

@alice
Copy link
Member Author

alice commented Mar 13, 2023

@annevk I'm not entirely clear on what you're asking for - what do you mean about the shadow host being an intermediary? Are you proposing that instead of authors being able to set the relationship directly, that the shadow host be required to perform indirection (iterated through as many shadow roots as necessary)? Or is this a request for a more specific use case demonstrating how the shadow host might mediate the relationship?

@annevk
Copy link

annevk commented Mar 14, 2023

I might be proposing that, yes. I mean that something like https://github.com/leobalter/cross-root-aria-delegation/blob/main/explainer.md seems more reasonable, details TBD. And that if something like that isn't deemed feasible, I'd like to see more details.

@alice
Copy link
Member Author

alice commented Mar 15, 2023

I certainly don't mean that this is the only thing we need.

Unlike ARIA reflection/delegation, an API like this requires components which need to be associated across shadow roots to either:

  • have the shadow host have an explicit, bespoke mechanism to allow an element outside of its shadow root request to be connected to an element within it, or
  • have the shadow root explicitly pass a reference to an element within its shadow root out to some code which may not otherwise have access to it.

Also, ARIA delegation/reflection have important use cases which this proposal wouldn't solve, or at least not well - specifically that of honouring author provided attributes.

However, the benefits that having this more direct kind of connection possible as well would be that it allows a connection to be made in cases where an author does need to create a direct reference and does have the ability to do that (e.g. they're writing code where they are authoring all of the relevant components/page), but doing so using the reflection/delegation APIs would be either extremely inconvenient/verbose (potentially because it's crossing multiple shadow root boundaries which would each have to set up the correct reflection/delegation mechanisms) or impossible (because of the bottleneck effect, which is obviously more likely to be an issue when crossing multiple shadow root boundaries.)

@nolanlawson
Copy link
Collaborator

Just to put it out there: I really like this proposal.

  1. Aligns well with existing idioms around event retargeting, activeElement, etc.
  2. Gives maximum flexibility for authors to decide which elements to wire up with which ARIA attributes, which maps well to existing non-shadow-DOM development models (i.e. build your React components in an encapsulated way, then wire them up "out of band" using ARIA or provide hooks to manipulate ARIA)
  3. Doesn't suffer from the "bottleneck effect"
  4. Allows authors to be explicit about which ARIA attributes to expose (which can be useful if you want to limit your component's API surface)

The major downsides I see are 1) no SSR support, and 2) would require component authors to potentially expose implementation details they wouldn't otherwise want to expose*. Neither one is a dealbreaker to me, and would be addressed by either the semantic delegate or cross-root ARIA as nice additions on top of this API.

* To expand on (2): For example, today a React component might pass around string IDs to allow for ARIA wiring (e.g. downshift), whereas with this proposal, you would have to pass around the elements themselves (or a callback that someone else passes an element into). Either way, the entire element is exposed (in one direction or the other). To avoid this, authors would have to pass around a Proxy or some other clever technique. Which is a bit awkward, but not a dealbreaker to me.

@alice
Copy link
Member Author

alice commented Apr 7, 2023

For (1) there are a couple of ideas in https://gist.github.com/alice/54108d8037f865876702b07755f771a5#some-kind-of-id-syntax-to-allow-arbitrary-cross-shadow-root-id-references - I don't know that we have to commit to using either of them, or any other declarative option, in order to proceed with the proposal in this issue, but I would envision either working as a declarative version of this API if they did exist.

@keithamus
Copy link

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.

@nolanlawson
Copy link
Collaborator

This was discussed in today's cross-root ARIA F2F. One sticking point we discussed is that this API would expose internal details of the component. For example, the <custom-optionlist> would have to expose its private <x-option> so that the <input> could do:

input.ariaActiveDescendantElement = opt1;

One potential solution would be to have some kind of proxy/wrapper object that stands in for the element itself. For example:

const wrappedOpt1 = document.createAriaProxy(opt1);
input.ariaActiveDescendantElement = wrappedOpt1;

This proxy/wrapper object would be capable of being the target of an aria*Element[s] setter, but would serve no other purpose and be completely opaque.

This would allow the ARIA relationship to be set without exposing the private element itself.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants