Open mrego opened 2 years ago
It looks like #920 replaced the string reflection (e.g. ariaActiveDescendant
) with element reflection (e.g. ariaActiveDescendantElement
) and then https://github.com/w3c/aria/pull/1260 removed the element reflection (by commenting it out).
From our (Salesforce Lightning Web Components) perspective, it seems sensible to support both the element reflection and the string reflection. The element reflection is necessary to support relationships that cross shadow root boundaries. Whereas the string reflection resolves some inconsistencies in how property reflection works across aria-*
attributes. (Currently, element.ariaLabel
can be used as a substitute for element.getAttribute('aria-label')
, but element.ariaLabelledBy
does not work as a substitute for element.getAttribute('aria-labelledby')
.)
Both the element and the string reflection can be supported simultaneously, thanks to the fact that the element reflection always has an Element
or Elements
suffix.
JFTR, there have been previous discussions around the possibility of having both string and element reflection in https://github.com/whatwg/html/issues/3515#issuecomment-413716944
Thanks for the context. My proposal seems to be "option # 1" from Alice's comment.
Just to summarize my preference (since https://github.com/whatwg/html/issues/3515 has a lot of discussion, much of from before element reflection took its current shape), I would prefer for these two to be equivalent in every way:
element.setAttribute('aria-labelledby', 'foo')
element.ariaLabelledBy = 'foo'
And these two as well:
console.log(element.getAttribute('aria-labelledby'))
console.log(element.ariaLabelledBy)
In other words, I'm proposing for properties like ariaLabelledBy
to not attempt to solve any thorny issues with ID refs (e.g. massaging them, changing when the element ID changes), but simply to be a shorthand for setAttribute()
and getAttribute()
. As far as I can tell, this defines them the same way as the other string attribute reflections (e.g. aria-label
/ariaLabel
).
This gives us consistency across the board at the expense of maybe allowing the properties to provide some better ergonomics than simply setting/getting the attribute.
Sending back to @jnurthen b/c he already has a PR linked.
Would it be more clear if it were ariaActiveDescendantID and ariaActiveDescendantElement ?
What are the use cases for having both, rather than just the element reference? Want to make sure the complexity is necessary
A common use case for this is a custom element framework that massages properties into attributes (or vice versa). For instance, here is a template in Preact:
export default function App() {
return (
<input type="text" ariaLabel="foo" ariaDescribedBy="bar" />
);
}
This renders (incorrectly) as:
<input type="text" aria-label="foo" ariadescribedby="bar">
(Note that the aria-label
renders fine, but the aria-describedby
does not – it's missing a hyphen.)
And here is a similar example with Vue:
<template>
<input type="text" ariaLabel="foo" ariaDescribedBy="bar">
</template>
...which also renders incorrectly:
<input type="text" aria-label="foo" ariadescribedby="bar">
The reason this is happening is that both Preact and Vue try to intelligently convert propertiesLikeThis
into attributes-like-this
, especially for custom elements, which may support only the property format. They check if ariaLabel
is a valid property on the element (it is), so they set it. Whereas when they check if ariaDescribedBy
is a valid property, it's not, so they revert to calling setAttribute()
(without guessing that it should be aria-describedby
, with the hyphen).
Of course, for <input>
, the component author could use the kebab-cased attribute format instead (<input type="text" aria-label="foo" aria-describedby="bar">
). But they get may accustomed to writing everything in the camel-cased property format, since it "just works" most of the time. For properties like ariaDescribedBy
and ariaLabelledBy
, though, they would have to remember that these ones don't work, whereas ariaLabel
and ariaDescription
do. It feels inconsistent.
Another alternative is for frameworks like Vue and Preact to contain a hard-coded list of ARIA attributes that have special property formats, and to use setAttribute()
for those instead. But this would add fragile bloat to those frameworks.
For this reason, I prefer ariaActiveDescendant
rather than ariaActiveDescendantID
(especially because the attribute value may be empty, malformed, point to a non-existent element, etc., in which case it's not really an ID).
Also, note that in no case would it really make sense for the framework (or the component author) to use the *Element
or *Elements
property, since it's unergonomic if you just want to set an ID ref:
// Doesn't work
element.ariaDescribedBy = 'foo'
// Works, but awkward
element.ariaDescribedByElements = [element.getRootNode().getElementById('foo')]
I agree with @nolanlawson that having ID or IDS suffix can be confusing.
I guess the idea is that if you have an HTML attribute like aria-activedescendant
you would expect to have a reflecting property on the JavaScript side that is called ariaActiveDescendant
. That one would be just a DOMString
reflection, like ariaLabel
for example.
In addition you will have the element reference reflection properties like ariaActiveDescendantElement
, so you can reference elements directly instead of IDs.
I guess one concern might be that the HTML attribute and the JavaScript element reference reflection property might get out of sync. For example if you do:
div.ariaActiveDescendantElement = foo;
console.log(div.ariaActiveDescendant); // foo
console.log(div.ariaActiveDescendantElement.id); // foo
// Modify element id.
foo.id = "bar";
console.log(div.ariaActiveDescendant); // foo
console.log(div.ariaActiveDescendantElement.id); // bar
But that's already happening if you use getAttribute()
, so div.ariaActiveDescendant
will match div.getAttribute("aria-activedescendant")
.
When setting it, div.ariaActiveDescendantElement
will be properly updated:
div.ariaActiveDescendant = "baz"; // This is equivalent to: div.setAttribute("aria-activedescendant", "baz");
console.log(div.ariaActiveDescendant); // baz
console.log(div.ariaActiveDescendantElement.id); // baz
If baz
element doesn't exist you'll get null
in div.ariaActiveDescendantElement
, but again that's the same thing that happens with setAttribute()
.
ARIA IDL still doesn't include attributes that refer to elements, but AOM IDL doesn't use any suffix for those attributes (e.g.
ariaActiveDescendant
orariaDescribedBy
).However Chromium implementation and the AOM explainer use suffix
Element
orElements
for them (e.g.ariaActiveDescendantElement
orariaDescribedByElements
).What's the expected naming for these attributes? Thanks!