aom
aom copied to clipboard
Relationship between ARIA attributes, Accessible Properties and Computed Properties.
Given:
<span id="foo" role="button">
What should the interaction be between the AOM object and the ARIA DOM attributes? i.e.:
var foo = document.getElementById('foo');
/* 1. Should ARIA attributes be reflected to AOM object properties? */
console.log(foo.role); // What does this print?
/* 2. Should AOM object properties reflect back out to ARIA attributes */
foo.role = "checkbox"; // Does this affect the DOM or not?
And, what should the computed role of foo be in this instance?
- Should ARIA take precedence, i.e.
foo's role should staybutton? - Or, should AOM take precedence, i.e.
foo's role should becheckboxafter being set onfoo'saccessibleNode?
Should ARIA DOM attributes be reflected to AOM object properties?
| Pro | Con |
|---|---|
Can read ARIA attributes without using getAttribute(). |
May set expectation that this is getting the computed value (it won't be). |
| Can be used to validate/feature detect ARIA property values |
Should AOM object properties be reflected to ARIA DOM attributes?
| Pro | Con |
|---|---|
| More "backwards compatible", doesn't break expectations that ARIA tells the whole story. | Sprouting ARIA attributes is annoying. ARIA/AOM should be implementation details. |
| If things are styling based on ARIA, things still Just Work. | Means that this "shouldn't" be used in Custom Element constructors. |
| Improves ergonomics of using ARIA attributes. | Performance implications with changing attributes triggering style recalc/repaint. |
| Not always feasible e.g. for relationships. |
Should AOM or ARIA take precedence?
| AOM | ARIA |
|---|---|
Consistent with input where attributes represent only "initial" state and properties represent ground truth |
Consistent with philosophy of style attribute, where page author gets the final say via ARIA |
| Avoid forcing developers to unset ARIA properties to be sure AOM changes take effect | Avoid confusion with ARIA "mysteriously" not working, although this could be solved by best practices |
| Avoid diminishing returns when compared to a simple reflection of ARIA |
My opinion is that the safest solution is "no reflection".
One small thing we could do is that setting an AOM property could remove its associated ARIA attribute, to minimize developer confusion. I see no harm in that but I also think it'd be fine if we did nothing.
The next safest thing we could do is reflect ARIA DOM attributes to AOM object properties. That also seems safe, in that you could read ARIA attributes and validate them. Again, I think it'd be fine if we didn't, but it seems perfectly consistent and safe if we did.
Reflecting object properties back onto ARIA attributes seems like a bad idea to me. One additional Con is that it wouldn't work for all properties, like IDREFs, which could be confusing. What would you set the ARIA attribute to then?
My opinion is that the safest solution is "no reflection".
+1. Among other problems, reflections might cause repaints if selectors refer to them. If that behavior is desirable to the author, they can use standard DOM+ARIA interfaces, but I would not want there to be visible or rendering performance side effects of using AOM.
One small thing we could do is that setting an AOM property could remove its associated ARIA attribute, to minimize developer confusion.
I like this a little better, but it could still cause style recalculation or other side effects. Perhaps the spec could have an RFC-2119 that user agent dev tools MAY or SHOULD throw a warning to the console when a content attribute was change after the corresponding AOM property was defined.
The next safest thing we could do is reflect ARIA DOM attributes to AOM object properties. That also seems safe, in that you could read ARIA attributes and validate them. Again, I think it'd be fine if we didn't, but it seems perfectly consistent and safe if we did.
I remembered us agreeing that this was scoped to Stage 4.
Reflecting object properties back onto ARIA attributes seems like a bad idea to me. One additional Con is that it wouldn't work for all properties, like IDREFs, which could be confusing. What would you set the ARIA attribute to then?
+1
My position is that ARIA and AOM should be considered completely separate concerns which affect the same end result. i.e.:
el.setAttribute('role', 'grid');
console.log(el.getAttribute('role')); // grid
el.accessibleNode.role; // null
el.accessibleNode.role = 'treegrid';
console.log(el.accessibleNode.role); // treegrid
console.log(el.getAttribute('role')); // grid
The issue of confusion with out of date ARIA attributes I think is best addressed by having ARIA attributes take precedence when determining the computed property. This also means that people can continue to use ARIA without being aware of AOM and things will work as expected even if developers use AOM under the hood somewhere, such as with a custom element.
This also means that people can continue to use ARIA without being aware of AOM and things will work as expected even if developers use AOM under the hood somewhere, such as with a custom element.
That feels slippery to me. Potential ramp-up scenario:
- Junior Dev writes a content attribute in markup.
- shared framework overrides it in AOM (accidentally or otherwise)
- Junior Dev confused why it's not working. Overrides it via
setAttribute(think!important) - shared framework overrides it again at a later time?
- Junior Dev adds setAttribute throughout the app to keep it "working" (code now unmanageable)
One of the goals of AOM was to reduce the amount of DOM operations in large apps, but if I'm understanding you correctly, this will force engineers to set the AOM property and unset (via removeAttribute) any related ARIA attributes to ensure everything works.
Actually maybe I misunderstood your comment. Clarifying:
el.accessibleNode.role = 'treegrid';
el.setAttribute('role', 'grid');
console.log(el.accessibleNode.role); // treegrid
console.log(el.getAttribute('role')); // grid
I concur with your example above. I thought you were saying the later setAttribute call would affect the computedRole in the UA. My opinion is that the computed role should be treegrid because the AOM property value overrides the ARIA content attribute value.
Yes, I was saying that the computed role should be grid, because ARIA should take precedence.
It's not a last one wins scenario, I'm saying ARIA should always win. So in your case there's no confusion because the framework can never "override" it via AOM.
See my clarification PR #64
One of the goals of AOM was to reduce the amount of DOM operations in large apps, but if I'm understanding you correctly, this will force engineers to set the AOM property and unset (via removeAttribute) any related ARIA attributes to ensure everything works.
Ah, now I see your concern. I still think we gain more than we lose by having ARIA take precedence - see my PR for reasoning.
Having aria take precedence matches what we do for style ex.
<my-component style="color: red">
#shadow-root
<style>:host { color: blue; }</style>
This element is red, because we assume the consumer of the element adding the attribute knows better (they're in the future) as they're attempting to override what the author of the component did (in the past).
That makes sense to me here, where we're saying the consumer of the component can override the internal computed state with an attribute, while component authors should avoid using attributes and should use the AOM instead.
Perhaps I'm not following some part of the AOM model though and it makes sense to diverge?
I see these as being different from the style cascade. Ultimately having a directly settable API (and eventually a queryable API for computed values) will be more predictable and verifiable. If content attributes win, the author would need to double check DOM heuristics (or always unset them) to verify the AOM setters are going to be used.
Another goal of Phase 4 was to be able to check computed values (once privacy and/or perf concerns could be addressed). In the meantime, we discussed it would be sufficient to have accessibleNode respond like a simple dictionary in that you could always read back what you wrote into it. The author would be assured that the accessibleNode values would be the "truth" if and when accessibility was needed. If that dictionary is no longer the source of truth, is there much benefit to the nearly 1:1 ARIA property matching? Why not just add some reflecting DOM properties like the old Microsoft Element interfaces: el.ariaLabel, etc?
How would the "ARIA takes precedence" change work with a IDREF vs NodeRef conflict? Which one of these wins? Is it still ARIA? I'd lean towards no.
myDiv.setAttribute('aria-labelledby', 'foo'); // IDREF to <el id=foo>
myDiv.accessibleNode.labelElement = myNodeRef;
I'm open to the change (I think), but there may be more implications we haven't considered. It's possible the others I've listed here may be inconsequential.
I think we should walk through the use cases we have in mind where these situations might arise.
I think Elliott and I are both primarily thinking about a Custom Element type case, where a Custom Element author specifies the default semantics for their element via AOM. In this case, we really want web page authors to be able to override those default semantics using ARIA, for consistency with native elements.
Imagine if you did something like
<custom-toggle role="switch"></custom-toggle>
and custom-toggle's default role of checkbox was still applied - it would be a weird and frustrating experience.
It seems like the cases @cookiecrook has in mind are when things are less clear cut, but I don't have a good handle on why the desired semantics might be in conflict in the "Junior dev/shared framework" scenario.
I agree in that ARIA should have the last word. If AOM would take precedence, there'd be a need for another mechanism to implement default semantics for Custom Elements, adding even more complexity. That's not desirable.
I think attributes are useful for initial configuration, but beyond that you can't trust them and properties become the source of truth.
An example would be <input type="checkbox" checked>. If you select this element and say .checked=false, it will render an unchecked checkbox, though the (now stale) [checked] attribute remains in the DOM. .hasAttribute('checked') will say true.
This is one (among many) reasons why frameworks like React and Angular tend to prefer binding to properties instead of attributes.
Reflecting the ARIA attribute to the property sounds good to me. In fact I would really want this. But I wouldn't want or expect a change to the property to reflect back to the attribute, as that's not consistent with how other elements seem to work.
In the Custom Element case, the author should be checking for the presence of an attribute before they programmatically set the role:
if (!this.getAttribute('role')) {
this. accessibleNode.role = 'checkbox';
}
Again, this follows on from the idea of attributes for initial configuration.
Also, if someone were to call setAttribute('role', 'foo') I would expect the accessibleNode.role property to now equal 'foo'. That also seems consistent with how things work today, e.g. setAttribute('checked', '') also reflects to the property.
I think reflecting ARIA attributes to AOM properties sets the dangerous expectation that AOM properties reflect the computed properties. Getting the computed properties carries a potentially heavy performance penalty, so we have agreed to keep that totally separate.
Most reflected attributes reflect in both directions AFAIK - input is well known to be weird and confusing, so I'm not sure we want to follow that model.
However, I agree that it's also weird to have two different things with the same vocabulary and affecting the same outcome.
CSS stylesheets vs .style vs getComputedStyle() is about the only reasonable analogy I can come up with here - however, as @cookiecrook points out, AccessibleNodes don't live in a separate domain and match across a whole tree, but are directly attached to a single DOM Node.
I tried to sum up pro/con arguments again in the top post - feel free to edit there!
Note: the explainer currently reflects the "no reflection, ARIA wins" opinion, but we can update that if we come to a different conclusion here.
Ok, after talking with @robdodson a little more about this, here's something I think might work better:
The mental model is that AOM/accessibleNode reliably reflects the truth about the author-set accessibility properties.
So:
- If ARIA attributes have been used, are valid, and have not been overridden, they are reflected on the
accessibleNodeobject:
<div id="composeBtn" role="button">Valid role</div>
<div id="banana" role="banana">Invalid role</div>
<div id="toggle" aria-pressed="true">Valid aria-pressed value in invalid context</div>
console.log(composeBtn.accessibleNode.role); // "button"
console.log(banana.accessibleNode.role); // null
console.log(toggle.accessibleNode.pressed); // null
- If the author has set a valid property on the
accessibleNodeobject, it now becomes the source of truth as far as author-set properties go (AOM wins):
composeBtn.accessibleNode.role = "link";
console.log(composeBtn.accessibleNode.role); // "link"
- This does not reflect back to the ARIA attributes in question, for all the reasons discussed earlier:
console.log(composeBtn.getAttribute("role")); // "button"
So we do lose the "ARIA is a source of truth" guarantee, but at least you actually never need to use getAttribute() to check what ARIA has been set on a node, since you can get it via accessibleNode, and at the same time check whether it has been overridden.
And, as Rob pointed out, we don't really need to prevent web component authors from overriding web page authors' ARIA, even if it's something they shouldn't do. So a pattern for setting component semantics might look something like:
if (!this.accessibleNode.role)
this.accessibleNode.role = "checkbox";
Finally , the "author-provided properties" mental model explains why you don't get the computed attribute values. Furthermore, if you get a non-null value back from accessibleNode you can (I think) be fairly sure that will become the computed value, since it would be valid and thus override any implicit value.
One pro of this idea (reflect ARIA into AOM) is that AOM can be used to validate ARIA. It doesn't allow you to check the final computed properties, but it would allow you, for example, to determine if you tried to set an ARIA attribute to an invalid value, or one that the current browser doesn't yet support (feature detection).
I'm still okay either way, interested to hear what @cookiecrook thinks of this last argument before we make a consensus decision.
One pro of this idea (reflect ARIA into AOM) is that AOM can be used to validate ARIA.
Added to the Pro/Con table at the top.
Given that this stuff can be set via attributes, properties, but has a computed property, aligning with .style etc makes to me. That means:
- Attribute getters/setters work without any kind of validation.
- Property setters and getters are validated/corrected (as in, incorrect attributes won't be reflected).
- Both are different to computed values, meaning getting doesn't trigger a recalc/layout.
Here's a test case http://jsbin.com/kokemuv/edit?css,js,console.
Some thoughts:
Setting style properties does update the style attribute
div.style.color = 'green';
console.log(div.getAttribute('style'));
// logs "color: green;"
The alternative model is like the .value attribute on form fields, where the attribute is the source of truth until the DOM property is set or the user changes the value, in which case the DOM property becomes the source of truth. Demo: http://jsbin.com/koxizag/edit?js,console.
Is it worth being different here?
Unrelated values are validated on set
Given:
<div style="background: green; display: foo"></div>
div.style.display will return "" as the value is validated, but div.getAttribute('style') will return background: green; display: foo. If you execute div.style.background = 'blue', the whole attribute will be validated, so div.getAttribute('style') will now return just background: blue;.
This seems kinda weird, but if you're going for the same model it might be worth copying.
When is a value validated?
<div aria-labelledby="foo">…</div>
<div id="foo">…</div>
The above is valid ARIA, even though the aria-labelledby attribute is created by the parser before div#foo exists. Given that the properties return arrays of DOM elements, they work kinda differently:
<div aria-labelledby="foo">…</div>
<script>
const previousDiv = document.querySelector('[aria-labelledby]');
previousDiv.setAttribute('aria-labelledby', previousDiv.getAttribute('aria-labelledby'));
</script>
<div id="foo">…</div>
I assume this won't change anything - the attribute can be set to itself without an issue. However:
<div aria-labelledby="foo">…</div>
<script>
const previousDiv = document.querySelector('[aria-labelledby]');
previousDiv.accessibleNode.labelledBy = previousDiv.accessibleNode.labelledBy;
</script>
<div id="foo">…</div>
Will the above set labelledBy to an empty list? If validation occurs on 'getting' (as it does with .style) then previousDiv.accessibleNode.labelledBy will return an array of zero elements.
Setting style properties does update the style attribute ... Is it worth being different here?
I think the main issue is with relationships/IDREFs. You can set a relationship property via AOM that cannot be represented in the DOM, either for an element with no ID or for an element whose ID is in a different scope (e.g. Shadow DOM). Given we can't reflect everything, it seems better not to reflect anything (like value).
Will the above set labelledBy to an empty list?
Not sure whether it'll be an empty list or null, but yeah it'll override the previous value with an empty value.
Honestly I think this is ok, though. If you're trying to read things before they exist, you're always going to have a bad time, and I think people will figure this out. It'd be the same if you had a stylesheet at the end of the page and tried to set an element's style to its computed style in the middle of it.
Given we can't reflect everything, it seems better not to reflect anything (like value).
Yeah that's fair. +1 to following .value's weirdness rather than .style's weirdness in this case. It'll need something similar to the dirty value flag, which is set to change the source of truth.
Not sure whether it'll be an empty list or
null
I guess it depends if there's a difference between <div> and <div aria-labelledby="">.
It'll need something similar to the dirty value flag, which is set to change the source of truth.
Good point. @minorninth if we go in this direction we should probably copy that language for the spec.
@alice
Given we can't reflect everything, it seems better not to reflect anything (like value).
I agree, but I didn't follow you as far as "ARIA always wins over AOM." Perhaps Jake's point will resolve this discrepancy:
@jakearchibald
It'll need something similar to the dirty value flag
If I understand this correctly, this would mean we'd follow a "most recent setter wins" pattern, with no reflection back to the DOM. The AOM property is the source of truth for both validation and current value (set in either way), but changes to it are not reflected back into the content attributes.
el.setAttribute("role", "button");
el.accessibleNode.role; // "button" (DOM setter affects AOM)
el.accessibleNode.role = "link"; // role truth is now link, but not reflected back to @role
el.getAttribute("role"); // "button" but now _dirty_ b/c AOM role is "link"
el.setAttribute("role", "note"); // DOM setter now squashes both
el.accessibleNode.role; // "note" (AOM reflects any valid value set after the last time it was set)
el.setAttribute("role", "button link"); // space-separated tokens indicate fallback roles
el.accessibleNode.role; // "button" (first recognized, supported role token)
el.setAttribute("role", "switch checkbox");
el.accessibleNode.role; // "switch" where ARIA 1.1 switch role is supported; "checkbox" elsewhere.
If that's the gist of what you want, I'm on board, but what should happen here?
el.setAttribute("role", "foo");
el.accessibleNode.role; // "" (invalid value)
el.setAttribute("role", "button");
el.accessibleNode.role; // "button" (valid value from content attribute)
el.accessibleNode.role = "foo"; // invalid
el.accessibleNode.role; // is this "" (invalid AOM setter) or "button" (fallback value from content attr)?
@cookiecrook
el.setAttribute("role", "note"); // DOM setter now squashes both
.value works differently. With .value, changes to the attribute are ignored once the value has been altered via the DOM property or by the user, but they're honoured until that point. See http://jsbin.com/koxizag/edit?js,console
If I understand this correctly, this would mean we'd follow a "most recent setter wins" pattern, with no reflection back to the DOM.
Hm, this isn't what I had in mind. I was thinking more that once you set something on AOM, ARIA ceases to be effective. However, I can see that setting ARIA should potentially update the AccessibleNode as well.
The AOM property is the source of truth for both validation and current value (set in either way), but changes to it are not reflected back into the content attributes.
Yes, this is true.
If that's the gist of what you want, I'm on board, but what should happen here?
el.setAttribute("role", "foo");
el.accessibleNode.role; // "" (invalid value)
el.setAttribute("role", "button");
el.accessibleNode.role; // "button" (valid value from content attribute)
el.accessibleNode.role = "foo"; // invalid
el.accessibleNode.role; // is this "" (invalid AOM setter) or "button" (fallback value from content attr)?
This is still an open question.
I would expect that the invalid AOM property takes precedence, and we fall back on the default semantics of the element (and el.accessibleNode.role would return null rather than "").
Collided with @jakearchibald. It might be simpler to follow the .value pattern and have ARIA changes do nothing once the AccessibleNode has been written to.
@cookiecrook
el.accessibleNode.role = "foo"; // invalid el.accessibleNode.role; // is this "" (invalid AOM setter) or "button" (fallback value from content attr)?
In this case, .style falls back to the previous value. Demo: http://jsbin.com/zumivok/edit?js,console
@jakearchibald wrote:
…changes to the attribute are ignored once the value has been altered via the DOM property or by the user
My understanding is that "DOM property" refers to .value in this case, and "content attribute" refers to getAttribute("value"). Did you mean to use the term "content attribute" here? (I agree that the spec term "DOM property" should have a better name.)
Update: I think I understand and agree with "changes to the [content] attribute are ignored once the value has been altered via the DOM property (aka IDL attribute) or by the user (e.g. by typing a new value into a field)"
@jakearchibald
In this case, .style falls back to the previous value. Demo: http://jsbin.com/zumivok/edit?js,console
I'm not sure this pattern makes sense here, though, because you can think of .style as trying and failing to write to the attribute.
A related, maybe illustrative case:
<button id="el"></button>
el.setAttribute("role", "link"); // el's role is "link"
el.accessibleNode.role = "switch"; // el's role is "switch"
el.accessibleNode.role = null; // el's role is ... what now?
You could make arguments for either "link" or "button".
I think it boils down to: was the author trying to remove the AOM role, or remove both it and the ARIA role?
@alice wrote:
It might be simpler to follow the .value pattern and have ARIA changes do nothing once the AccessibleNode has been written to.
This is more like what I thought we had originally decided at the F2F. It seems the only addition is that content attribute now acts as the initial value setter for AOM, is that right?
Do we need a way to determine whether the value of el.accessibleNode.role is ~reflecting the content attribute, or has been set explicitly? e.g. Is there any way for an author to know whether setting the content property will work (if AOM has never been set) or be ignored (if AOM has been set)?
Previously, we solved this by only using the content attribute when the AOM property was null. AOM was always the source of truth until it was null, and then the markup became the source of truth.
If the only goal for this addition is value validation, we're still going to need to spin up accessibility backing code to perform that validation, even for non-accessibility users. Otherwise, we're going to have to move a lot of the backing accessibility code out of the accessibility-only runtime context. There is a non-zero implementation and performance cost for this, and there are potential implications for privacy and security, too.
Ultimately I want this, too, but we had agreed to wait until Phase 4 for computed values to ensure feasability/implementability of Phase 1. This change seems like it's trying to pull in little a bit of the Phase 4 scope, so I'm not convinced that's a good idea at this time.