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

Reference Target #961

Closed
1 task done
dandclark opened this issue Jun 3, 2024 · 30 comments
Closed
1 task done

Reference Target #961

dandclark opened this issue Jun 3, 2024 · 30 comments
Assignees
Labels
a11y-tracker Group bringing to attention of a11y, or tracked by the a11y Group but not needing response. Focus: Accessibility (pending) Mode: breakout Work done during a time-limited breakout session Resolution: satisfied The TAG is satisfied with this design Topic: accessibility Topic: ARIA Accessible Rich Internet Applications Venue: WICG

Comments

@dandclark
Copy link

dandclark commented Jun 3, 2024

こんにちは TAG-さん!

I'm requesting a TAG review of Reference Target.

Reference Target is a feature that enables using IDREF attributes such as for and aria-labelledby to refer to elements inside a component's shadow DOM, while maintaining encapsulation of the internal details of the shadow DOM. The main goal of this feature is to enable ARIA to work across shadow root boundaries.

Further details:

You should also know that...

There have been a number of competing proposals in this area but little progress towards actually shipping solutions in browsers. See https://alice.pages.igalia.com/blog/how-shadow-dom-and-accessibility-are-in-conflict/.

For that reason this proposal is carefully scoped with the goal of identifying pieces of functionality with good prospects of reaching consensus such that they can be shipped and made available to developers, whose feedback will help inform additional work.

Hence, the proposal in the explainer is broken down into two phases. Phase 1 adds the ability to designate a single element as the target for all IDREF properties that refer to the host, while Phase 2 adds the ability to re-target specific properties individually. Breaking it down in this manner may allow us to reach consensus on Phase 1 while the more complex details of Phase 2 are still under discussion.

So if the TAG has feedback or concerns that apply to Phase 2 only, but is happy with Phase 1, it would be helpful for us if that is called out specifically so that Phase 1 can more easily move forward.

@LeaVerou LeaVerou self-assigned this Jul 23, 2024
@torgo torgo added Topic: accessibility a11y-tracker Group bringing to attention of a11y, or tracked by the a11y Group but not needing response. Focus: Accessibility (pending) Venue: WICG Mode: breakout Work done during a time-limited breakout session Topic: ARIA Accessible Rich Internet Applications and removed Progress: untriaged labels Aug 7, 2024
@torgo torgo added this to the 2024-08-12-week milestone Aug 7, 2024
@jyasskin
Copy link
Contributor

jyasskin commented Sep 4, 2024

This isn't a TAG opinion, but I wanted to point out that there's a known gap across the whole platform in the ability of IDREF attributes to refer to what authors want: w3ctag/gaps#2. I know that this particular gap in ARIA references has lingered for a very long time, and I don't want to delay getting it fixed, but it's also important to keep looking for a unified strategy to deal with all of these relation types.

@torgo torgo modified the milestones: 2024-09-02-week, 2024-09-09-week Sep 5, 2024
@jyasskin
Copy link
Contributor

We discussed in the TAG meetings this week, and while we haven't reviewed Phase 2 yet, we're in favor of you moving ahead with solving the piece of this (Phase 1) that you have general agreement on. We'll continue reviewing Phase 2 in future weeks.

@plinss plinss modified the milestones: 2024-09-09-week, Post TPAC Sep 16, 2024
@plinss
Copy link
Member

plinss commented Oct 14, 2024

Have you considered reversing the relationship of the shadow root to the target node? For example, rather than having the developer set the referenceTarget on the shadow node, introduce a aria-referencetarget (or some equivalent) attribute which is set on the target node? The shadow root can still have a referenceTarget attribute but it's read-only and can be a reference to the actual node.

I'm thinking of the developer ergonomics, for instance if I'm building a select-type control, in the current model I have to set some attribute (probably a class) on the currently selected node to indicate its selected state and trigger style changes, and then I have to also add an id and set the aria-activedescendant in the reference map on the shadow root. In my proposal, the developer would only need to set the aria-activedescendant attribute on the active node (which can also be used as the style trigger) and the reference map can update automatically. This also eliminates the need for ids on everything.

@dandclark
Copy link
Author

dandclark commented Oct 15, 2024

That sounds like this considered alternative: https://github.com/WICG/webcomponents/blob/gh-pages/proposals/reference-target-explainer.md#designate-target-elements-using-attributes-instead-of-idref, which @Westbrook proposed in more depth in https://github.com/Westbrook/cross-root-aria-reflection/blob/main/cross-root-aria-reflection.md.

IMO the developer ergonomics are similar in the use case you mentioned. They way I would handle the aria-activedescendant case with reference target is that I'd have the shadowRoot's referencetarget or referencetargetmap assign aria-activedescendant to a constant id, e.g. #active, which is also used to apply active styles to the node. And then when the activedescendant changes, I change which element has that ID.

<input role="combobox" aria-activedescendant="fancy-listbox"/>
<fancy-listbox id="fancy-listbox" >
  <template shadowrootmode="open" shadowrootreferencetarget="active">
    <style>
      #active { /* styles for the active element go here */ }
    </style>
    <option>One</option>
    <option id="active">Two</option>
    <option>Three</option>
  </template>
</fancy-listbox>

If the user selects another option, the only thing the dev needs to change is to clear the ID from option Two and add it to the new option. This is the same amount of state that would need to be changed if reference target was set using a property on the targeted element.

Semantically, it also feels to me that the choice of whether or not to delegate a given attribute is something that should be set on the shadow root, akin to delegatesfocus, which maybe is why @Westbrook's proposal used both a property on the shadow root and a property on the targeted element.

@Westbrook
Copy link

When discussing this at length with the AOM Working Group, the WCCG, and implementors, a primary shortcoming for an attribute marker being applied to an element within the shadow DOM is that in order for the API to scale over time to support mapping more than one attribute to more than one element you needed to inform the shadow root that it would be delegating a value and to which element the value would be delegated. If the delegation/reflection APIs were to stop at a one to one pass through, so no support for something like referenceTargetMap, which is a powerful part of this API (or the Level 2 of this API?), then it might make sense to have such a marker hold all the values applied from outside of the shadow root. However, many implementors were dissatisfied with API that could not scale in some way to support more complex situations, in fact a number of seemingly promising proposals in this area have been overrule for lack of this ability. This become more and more apropos as referenceTarget has expanded to properly support non-aria attributes like the Popover API, Invoker Commands, and other more recent attribute bound interactions which are even less likely to receive a "batch" handoff of attribute values.

@LeaVerou
Copy link
Member

@jyasskin @dandclark

@LeaVerou Check out how aria-labelledby and aria-describedby can say that an element is labeled or described by multiple other elements. It is an edge case, but it has to be supported somehow.

I know that, but it seems like as long as each of these ids is delegated properly, this should just work? Oh I see, you're saying you want to delegate each reference to the CE to multiple elements within its shadow root. In that case, the approach of specifying the attribute on the element seems to work even better: for attributes only taking a single idref the first one wins, for attributes taking multiple, all elements declaring a mapping are taken into account. You both seem to be saying that the order of ids is significant, but I can't find anything in the ARIA spec that says so, are we sure that's the case?

@LeaVerou
Copy link
Member

LeaVerou commented Oct 25, 2024

@Westbrook As a meta comment, I think we may have run into the classic big WC use case bifurcation, where some folks are talking about templating use cases and others about packaging up independent bits of functionality for broader reuse, and these use cases have vastly different characteristics so people are coming to these discussions with completely different contexts.

For example, wth most of my context being the latter, moving elements in and out of shadow roots is something I have never considered, since most components I work on are meant to be used more broadly than the pages that include them, and are managed by different people.

That different context might also explain this question:

Isn't requiring a new syntax to associate things accessibly just because they are inside a shadow root worse DX?

I could be wrong, but reading between the lines, it seems like to you this is mainly about overcoming a hurdle in how you define your ARIA attributes; since it's the same person/team writing the mappings for the light DOM and the component, the encapsulation is just a roadblock, they'd rather reference the shadow DOM element directly if they could. If that is correct, perhaps the real solution these use cases need is a general way to reduce encapsulation.

For the second set of use cases though, this is defining a way for the root element to specify which of its shadow DOM descendants certain things should be delegated to, and is largely independent of how exactly the mapping happened in the first place. E.g. if in the future we add an attribute that relates one element to another via a relative selector, the delegation should still work the same. So to me, it seems entirely separate than the idref mechanisms currently used, the actual design goal is to design a good syntax for delegation.

It doesn't help illustrate what @plinss and I are saying that in your code examples, elements already have ids, so it begs the question, why not just use the ids? However, what Peter and I were saying, is that ideally, you shouldn't need to assign ids if you don't need to for another purpose.

But regardless of the syntax we go with, I would fully expect more specific mappings to override more general mappings, rather than just resolving them based on order.

@Westbrook
Copy link

I'm not sure I see a practical difference between the two contexts that you're referencing @LeaVerou. While the code above is a very reduced reproduction, the workflow I outlined applies to the work I've seen or been a part of across a number of roles I've worked in. Whether building Spectrum Web Components where we commonly worked to translate examples like those found in ARIA APG into abstract reusable/customizable element or building interactive virtual classrooms, digital asset management or creativity tools that rely on larger custom element architectures, I see this approach being valuable in the way that it manages ID references much like they would be used without shadow DOM.

But, maybe I'm misunderstanding the context you're outlining. Would you be able to share some code outlining how that context would be different? Something that helped clarify how this context would benefit from managing ID references via this net new API surface?

@michaelwarren1106
Copy link

i don’t really have a strong preference for either api as long as something moves forward that helps. but i do think the perf aspect of the two approaches is important to consider.

if the map of els needing ids to shadow dom ids is established on the shadow root/host itself, then the browser doesn’t have to fully parse the shadow root template in order to finish calculating the references. the browser would have the complete map upfront as soon as the shadow root is constructed. likewise, the mapping being on the root doesn’t change the performance of calculating the map as the size of the shadow dom grows. the map will always be in a single spot which may mean that browsers can optimize for it since it’ll be known and predictable.

i do think that the attrs establishing IDREF mappings being on the SD child elements themselves is a more flexible api, but i would be interested in how big the perf tradeoff is for having to parse the whole SD template in order to assign refs.

possibly the perf impact would be mitigated by assigning each IDREF when each attr is encountered, and overwriting the assignment if a conflicting attr is encountered later on. but then i’d worry about the impact of the assignment changing. if i were the kind of dev writing uber-optimized framework code i might balk at references changing mid-parse?

@LeaVerou
Copy link
Member

@michaelwarren1106 This can be mitigated by simply using the first instance of the attribute (for references of the same scope). Even with ids the association can change — one can simply remove an element with that id and add a different one, or modify ids at runtime, so that doesn't seem different?

@michaelwarren1106
Copy link

@michaelwarren1106 This can be mitigated by simply using the first instance of the attribute (for references of the same scope). Even with ids the association can change — one can simply remove an element with that id and add a different one, or modify ids at runtime, so that doesn't seem different?

Perhaps, but I'm not sure I know of anything else that changes ref like that during tree parsing? Implementers would be better to weigh in on the pros/cons of that approach, but I would suspect that changing the ref during the parse is wholly different than changing the ref as a result of manipulating the dom.

And actually, I think that manipulating the dom is another perf lever where the "having to parse the whole dom in order for refs to settle out" is maybe not the best. What if the dom structure changes, but the ref map doesnt? If im moving elements around in the dom, but not changing their IDREF relationships to their for= attributes? If the mapping is on each individual element its going to need to be recalculated EVERY time the dom changes, not just the first time.

But if the ref/mapping is set on the shadow root itself, then that frees up the dom to be changed without the IDREFs needing to also be recalculated. And vice-versa, the IDREFs could be changed (potentially, I havent seen any imperative feature that would do that) without being dependent on the dom structure.

Having talked myself into it just now, it seems like disconnecting the actual DOM from the IDREF mapping metadata seems like a good thing to do even if its slightly worse DX? slightly worse DX for way better performance is a tradeoff many people would make I would think.

@nolanlawson
Copy link

One potential issue I can see with the non-id approach is that it makes changing the active descendant in a combobox two steps instead of one.

For example, in the current proposal (omitting some details for brevity):

<input role="combobox" aria-activedescendant="listbox">
<fancy-listbox id="listbox">
  <template shadowrootmode="open" shadowrootreferencetargetmap="aria-activedescendant: one">
    <div role="listbox">
      <div id="one" role="option">one</div>
      <div id="two" role="option">two</div>
    </div>
  </template>
</fancy-listbox>

To update the active descendant from one to two is just a matter of updating the shadowrootreferencetargetmap (or IDL equivalent) to point at two instead of one. This is similar to what you might do without shadow DOM – update aria-activedescendant to point to a different ID.

Whereas with the referenceTargetForAttributes counter-proposal:

<input role="combobox" aria-activedescendant="listbox">
<fancy-listbox id="listbox">
  <template shadowrootmode="open">
    <div role="listbox">
      <div role="option" referenceTargetForAttributes="aria-activedescendant">one</div>
      <div role="option">two</div>
    </div>
  </template>
</fancy-listbox>

Now the update would require 2 operations: 1) removing referenceTargetForAttributes from the first list option and 2) adding it to the second.

IMO this isn't a deal-breaker, but given that the first proposal is fewer operations and closer to the non-shadow DOM equivalent, the first would be my preference.

(BTW this combobox likely requires shadowrootreferencetargetmap rather than simply shadowrootreferencetarget because you'd likely also need an aria-controls pointing at the listbox.)

@Westbrook
Copy link

Would the use of defaultReferenceTarget or similar fail when moving from out "cannonical" example:

<label for="input">Label</label>
<my-input id="input">
  <template shadowrootmode="open">
    <input id="input" defaultReferenceTarget>
  </template>
</my-input>

To one that took multiple targets, as found here:

<form id="example-form">
  <input type="range" id="b" name="b" value="50" /> +
  <input type="number" id="a" name="a" value="10" /> =
  <output name="result" for="a b">60</output>
</form>

I will admit, it is a bit contrived, but were this factored for reusability via the following, would "simply using the first instance of the attribute" be enough to make this work as expected?

<form id="example-form">
  <calc-el>
    <template shadowrootmode="open">
      <input type="range" id="b" referenceTargetForAttributes="for" value="50" /> +
      <input type="number" id="a" referenceTargetForAttributes="for" value="10" /> =
    </template>
  </calc-el>
  <output name="result" for="a b">60</output>
</form>
  • is this not expected/intended to be covered?
  • did multiple targets via the map get covered?
  • could there be multiple default targets in either syntax? 👀
    <form id="example-form">
      <calc-el>
        <template shadowrootmode="open">
          <input type="range" id="b" defaultReferenceTarget value="50" /> +
          <input type="number" id="a" defaultReferenceTarget value="10" /> =
        </template>
      </calc-el>
      <output name="result" for="a b">60</output>
    </form>

@dandclark
Copy link
Author

dandclark commented Nov 1, 2024

Addressing @LeaVerou's comment above:

I know that, but it seems like as long as each of these ids is delegated properly, this should just work? Oh I see, you're saying you want to delegate each reference to the CE to multiple elements within its shadow root. In that case, the approach of specifying the attribute on the element seems to work even better: for attributes only taking a single idref the first one wins, for attributes taking multiple, all elements declaring a mapping are taken into account. You both seem to be saying that the order of ids is significant, but I can't find anything in the ARIA spec that says so, are we sure that's the case?

For things like aria-labelledby order does matter. See Step 2B of the accessible name computation steps where the IDs in the attribute are processed in order to build up the accessible name.

For example:

<input aria-labelledby="b a">
<div id="a">two</div>
<div id="b">one</div>

The accessible name of the <input> will be "one two". So developers are accustomed to being able to control order by reordering IDs in the attribute value, without depending on DOM order.

@jyasskin said:

@LeaVerou Check out how aria-labelledby and aria-describedby can say that an element is labeled or described by multiple other elements. It is an edge case, but it has to be supported somehow.

I'm tempted to say that it should be supported by letting those particular properties recurse, at least at the shadow root, and then you don't need anything like reference-target-map to deal with them. But I don't know enough about ARIA to be confident of that.

Changing the behavior of labelledby-describedby recursion within shadow roots seems like a recipe for confusion. There is some evidence that developers expect and depend on these properties not chaining. Rather than adding another element of complexity to labeledby/describedby by making them behave differently depending on context I'd prefer to just go with the version of ReferenceTarget that allows reordering these in the way that developers are already accustomed to, by ordering IDs in an attribute list.

@alice
Copy link

alice commented Nov 5, 2024

@Westbrook's example I think illustrates why this problem gets very hard for me to reason about from a general perspective when there's not a 1:1 mapping between the shadow host and the reference target.

Even leaving aside the syntax, I would struggle to reason about what should happen in a case like this:

<form>
  <label for="sum">Equation</label>
  <calc-el id="sum">
    <template shadowrootmode="open">
      <input type="range" referenceTargetForAttributes="for" value="50" /> +
      <input type="number" referenceTargetForAttributes="for" value="10" /> =
    </template>
  </calc-el>
  <output name="result" for="sum">60</output>
</form>

So, I've tried to think about specific cases instead. There are three types of non-contrived cases for this type of API I am reasonably confident I can get my head around (thanks to the folks who have patiently explained the latter two to me across multiple occasions):

  1. Something like<fancy-input>, where there is an <input> inside <fancy-input>'s shadow root which could reasonably substitute for the host <fancy-input>. (I'm using <input> as an example partly because of the number of ways <input>s can relate to other elements, but obviously it's not the only element which is or could be wrapped this way.)
  2. Needing aria-activedescendant (specifically) to refer to a list item which needs to be inside a shadow root in order to encapsulate certain implementation details, such as a recycler view and/or list items being loaded dynamically, when implementing an accessible combobox.
  3. Needing a component to have a computed name, for aria-labelledby/aria-describedby purposes, which doesn't include all of its contents, without excluding from the accessibility tree the content which should be excluded from the name. One example might be a contact chip including a profile picture, a name and a "remove" button, where the contact chip can be used as a label for something else: the profile picture and the remove button should be accessible, but not a part of its computed name. (And, in a slightly contrived case, you might like to list the surname first in the component, but have the computed name list the given name first; why not.)

For (1), I think the current referenceTarget design matches the way I think about the problem: the shadow root encloses some element which could generally substitute for the shadow host other than some encapsulated fanciness. It makes intuitive sense to me that the shadow root should be responsible for redirecting references to allow that substitution to occur whenever another element refers to the host.

That said, I don't necessarily have any objection to having an attribute on the substitute element instead; I can't see any major practical downside of either design for this case, particularly if you can retrieve the reference target from the shadow root for debugging.

For (2), we're arguably getting an upgrade on the status quo regardless of syntax, since the listbox no longer needs to coordinate with the <input> to tell it what its active descendant should be (although it's a bit unfortunate that the <input> still needs to have both an aria-activedescendant attribute and an aria-controls attribute which just point to the same element).

Since aria-activedescendant can only ever point to one element, there's not really a major downside to having an attribute on the element, since there's no ordering issue. So I think for this case, again, there's not a lot in between the two syntax options.

For (3), I do wonder like @jyasskin whether this is the best approach at all. I understand that there is a general expectation around how aria-labelledby will behave, but that expectation is already broken if you're pointing to the shadow host and getting something other than all of its contents as its computed name. So I do wonder whether we could allow aria-labelledby on the shadow root for this case.

And if we really can't come up with a case other than (2) for the reference map, I wonder likewise whether we could find a way to improve on this case in isolation.

@LeaVerou
Copy link
Member

LeaVerou commented Nov 5, 2024

If we go with the current id-based syntax, I wonder what ergonomics improvements we could make:

  • "referenceTarget" is an incredibly obscure name. We already have delegatesFocus. What about delegatesReferences, delegatesId or even just delegates?
  • Do we really need two attributes? What if the attribute syntax allowed for specifying a catch-all value, a map, or both? Then a single attribute contains all the information needed to delegate references.

@sorvell
Copy link

sorvell commented Nov 5, 2024

Not sure what I prefer but trying to think it through, I think this captures most of the points mentioned but happy to correct or amend as needed.

ARIA Info on Advantages Disadvantages
ShadowRoot 1. Centralizes new syntax/API 1. Hard to name things explicitly
2. Uses familiar ID syntax 2. Brittle to match names in two places
3. Easily supports ordering 3. Complex syntax for mapping
4. Dynamism might be simpler 4. Harder to solve general reference problem
Elements 1. Avoids need for explicit names 1. Unclear how to support ordering
2. Avoids need to coordinate names 2. Impact of introducing new global attributes
3. Easy to author changes 3. Novel solution may introduce cognitive load
4. Misses chance to solve general naming problem

@LeaVerou
Copy link
Member

LeaVerou commented Nov 5, 2024

Thanks for the summary @sorvell! I'd add to the disadvantages of the current approach that it makes it harder to solve the referencing problem in HTML.

@LeaVerou
Copy link
Member

LeaVerou commented Nov 5, 2024

One option that has not been explored: using relative selectors. It both solves the ordering issue and alleviates the need for naming. People who want to use ids can still use ids by just doing #foo, but people who would rather not manage ids, can just specify a selector describing their intent. Could this be the way to have our cake and eat it too?

@LeaVerou
Copy link
Member

Another issue we realized: Right now, the design for the whole feature is that an attribute specifies the defaults, and another one-off overrides on an attribute by attribute basis. If no default forwarding is specified, no forwarding happens (i.e. the reference resolves to the element itself). However, the pattern where everything gets delegated to a certain shadow DOM element but some attributes don't get forwarded at all is not supported at all with the id-based syntax. That is also something a selector-based syntax could address, as then :host could be a delegation target as well (= no delegation).


Also, from conversations with folks, the initial reaction for phrase 1 is "yes, forwarding everything to the same element makes sense, all our use cases are like that", then I remind them about the non-a11y idrefs such as popovertarget or form they are way less certain…

@alice
Copy link

alice commented Nov 18, 2024

One issue with using selectors in place of IDs is that selectors don't come with the same constraints as IDs. In particular, almost anything about the element can affect whether it matches a selector, and many selectors can match any number of elements.

This would be an issue for things like aria-activedescendant, which need to match exactly one element, and which need to trigger live updates to the accessibility tree. We could certainly specify that only the first element which matches the selector should be used (like we do in the case of multiple elements with the same ID), but triggering the live updates may become very costly, since whenever anything changes within the relevant tree, we'll essentially have to run querySelector() to check whether the active desendant has changed.

@matatk
Copy link

matatk commented Nov 18, 2024

Thank you for your time earlier in the week. As per our discussion, we are happy to see your work on phase 1 progress. It will be great to resolve this significant use case! Please let us know as and when we can provide input on phase 2. We do urge the authors to take the time to find a shorter, more broadly understandable name.

We also discussed the wider, and fundamental, issue of finding an alternative to IDREFs for expressing relationships between elements - we intend to explore this issue separately, and your feedback would be welcome on that thread. Should we set up a group to work on this issue, we will let you know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a11y-tracker Group bringing to attention of a11y, or tracked by the a11y Group but not needing response. Focus: Accessibility (pending) Mode: breakout Work done during a time-limited breakout session Resolution: satisfied The TAG is satisfied with this design Topic: accessibility Topic: ARIA Accessible Rich Internet Applications Venue: WICG
Projects
None yet
Development

No branches or pull requests