w3c / aria

Accessible Rich Internet Applications (WAI-ARIA)
https://w3c.github.io/aria/
Other
645 stars 125 forks source link

Using suffix Element/s for IDL attributes referring to elements #1732

Open mrego opened 2 years ago

mrego commented 2 years ago

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 or ariaDescribedBy).

However Chromium implementation and the AOM explainer use suffix Element or Elements for them (e.g. ariaActiveDescendantElement or ariaDescribedByElements).

What's the expected naming for these attributes? Thanks!

nolanlawson commented 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.

mrego commented 2 years ago

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

nolanlawson commented 2 years ago

Thanks for the context. My proposal seems to be "option # 1" from Alice's comment.

nolanlawson commented 2 years ago

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.

cookiecrook commented 2 years ago

Sending back to @jnurthen b/c he already has a PR linked.

cyns commented 2 years ago

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

nolanlawson commented 2 years ago

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')]
mrego commented 2 years ago

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().