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

Relationship between ARIA attributes, Accessible Properties and Computed Properties. #60

Closed
alice opened this issue Mar 14, 2017 · 91 comments

Comments

@alice
Copy link
Member

alice commented Mar 14, 2017

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 stay button?
  • Or, should AOM take precedence, i.e. foo's role should be checkbox after being set on foo's accessibleNode?

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
@minorninth
Copy link
Collaborator

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?

@cookiecrook
Copy link
Collaborator

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

@alice
Copy link
Member Author

alice commented Mar 16, 2017

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.

@cookiecrook
Copy link
Collaborator

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.

@cookiecrook
Copy link
Collaborator

cookiecrook commented Mar 16, 2017

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.

@alice
Copy link
Member Author

alice commented Mar 16, 2017

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.

@alice
Copy link
Member Author

alice commented Mar 16, 2017

See my clarification PR #64

@alice
Copy link
Member Author

alice commented Mar 16, 2017

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.

@esprehn
Copy link

esprehn commented Mar 16, 2017

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?

@cookiecrook
Copy link
Collaborator

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.

@alice
Copy link
Member Author

alice commented Mar 16, 2017

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.

@CountOrlok
Copy link
Contributor

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.

@robdodson
Copy link
Contributor

robdodson commented Mar 16, 2017

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.

@alice alice changed the title Reflecting to/from ARIA attributes? Relationship between ARIA attributes, Accessible Properties and Computed Properties. Mar 17, 2017
@alice
Copy link
Member Author

alice commented Mar 17, 2017

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!

@alice
Copy link
Member Author

alice commented Mar 17, 2017

Note: the explainer currently reflects the "no reflection, ARIA wins" opinion, but we can update that if we come to a different conclusion here.

@alice
Copy link
Member Author

alice commented Mar 19, 2017

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 accessibleNode object:
<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 accessibleNode object, 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.

@minorninth
Copy link
Collaborator

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.

@alice
Copy link
Member Author

alice commented Mar 20, 2017

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.

@jakearchibald
Copy link

jakearchibald commented Mar 20, 2017

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.

@alice
Copy link
Member Author

alice commented Mar 20, 2017

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.

@jakearchibald
Copy link

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="">.

@alice
Copy link
Member Author

alice commented Mar 21, 2017

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.

@cookiecrook
Copy link
Collaborator

@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)?

@jakearchibald
Copy link

@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

@alice
Copy link
Member Author

alice commented Mar 21, 2017

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

@alice
Copy link
Member Author

alice commented Mar 21, 2017

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.

@jakearchibald
Copy link

@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

@cookiecrook
Copy link
Collaborator

cookiecrook commented Mar 21, 2017

@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)"

@alice
Copy link
Member Author

alice commented Mar 21, 2017

@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?

@domenic
Copy link

domenic commented Apr 26, 2017

Ah, OK. Then my thoughts are that the author should not be setting aria-checked on the custom element, just like they wouldn't be setting it on a <input type="checkbox">, unless they are sure they want to override the custom element/checkbox's accessible checked state.

@alice
Copy link
Member Author

alice commented Apr 26, 2017

Hm, ok then.

Would you then have ARIA and AOM be completely separate systems, other than ARIA taking precedence? i.e. no reflection at all?

@domenic
Copy link

domenic commented Apr 26, 2017

I think that makes sense to me, but now that we're outside of a specific scenario I have to be more cautious about stating my position as a "good" one, since I didn't get a chance to read through this whole thread and see all the motivations involved. I feel that .value-like is quite bad, but maybe there are ways of making them interact between .value-like and no-interaction.

I can't really see the downsides of no interaction from where I'm standing, though.

@alice
Copy link
Member Author

alice commented Apr 26, 2017

My instinct at this point is that we have either .value-like, or no interaction between them other than ARIA taking precedence, since full two-way reflection isn't an option.

@alice
Copy link
Member Author

alice commented Apr 26, 2017

I think the downside of "no reflection" is that these things share a vocabulary, so it would be reasonable to expect that they were different expressions of the same thing. I think it might be confusing and annoying to authors that there is still no programmatic way beyond setAttribute() and getAttribute() to set ARIA, which has the final word in semantics.

@robdodson makes this point indirectly here: #60 (comment)

@minorninth
Copy link
Collaborator

The scenario would be something like:

Author sets aria-checked on a custom element
User takes some kind of action causing the checked state to toggle
Custom element code wants to update the accessible checked state, but needs to remove or update the ARIA attribute first.

Alice, I also don't find this example very compelling, because it's not clear why you wouldn't want the page author to win. In general we should want custom elements to work well by default, and encapsulate all of their internal state as possible, but we also want page authors to be able to override them as much as possible, too.

Also, I think it's different from the argument Rob was trying to make earlier.

I think I'd prefer AOM win versus ARIA because that's consistent with how attributes in custom elements work today. For example . When my element boots up I already have to check if there's a disabled attribute, otherwise I'd incorrectly set a default value of this.disabled = false. Similarly I should check if the user has applied a role before applying my default one.

I think Rob's argument was that the custom element author may want to change its behavior, such as its initial state, based on the attributes provided by the page author.

However, I'm not sure that they're the same. When the author sets the disabled attribute on x-btn, that's part of x-btn's public API. The author specifically wants the x-btn to change its visual appearance and stop responding to input events. When the author says role="switch", though, they're not really asking the x-btn to change its appearance or semantics.

So I guess that's an argument in favor of ARIA taking precedence over AOM.

It's a bit unfortunate because I did like the idea of reflection. I liked the idea of reflecting ARIA attributes into accessibleNode properties, to show their correspondence and provide some nice discoverability and exploration.

However, purely from the perspective of intended use cases and not for testing, validation, feature detection, and exploration - I suspect "ARIA wins" is more useful, in that it balances the power reasonably well between component authors and page authors.

I think part of the problem here is that without the last phase, the ergonomics are poor because there's no easy way to figure out what wins. It's like having "style" without getComputedStyle.

Here's an idea: what if we added support for getting computed properties back to the spec now - back in Phase 1 or 2 - but we specifically exclude properties that depend on things like layout.

Role seems safe, for example.

So what I'm thinking is that we define exactly what it'd look like to expose the computed role in the AOM, along with things like the computed "checked" state and many others. Then we defer things like exposing the computed name and parents and children because there are lots of issues around those.

I wonder if we did this and we had clear, nice-looking semantics for querying the computed role, then it might be fine if there was no reflection.

To be very explicit, we could use "source" as Alex suggested:

element.accessibleNode.source.role = "button";
element.accessibleNode.computed.role;  // returns "button"

element.setAttribute("role", "switch");
element.accessibleNode.computed.role;  // returns "switch"

@alice
Copy link
Member Author

alice commented Apr 27, 2017

I think layout could affect computed role.

For example:

<div id="parent" role="tree">
   <div id="child" role="checkbox">Checkbox can't be a direct child of a tree, this is just text</div>
</div>

What should child.accessibleNode.computed.role be?

@alice
Copy link
Member Author

alice commented Apr 27, 2017

it's not clear why you wouldn't want the page author to win.

In this case, my argument was that because this is a dynamic state, the fact that the state has changed means that the page author-provided value is now out of date.

However, I agree that this scenario shouldn't really happen in practice - custom elements should provide a non-ARIA API for setting that type of state.

I think it's different from the argument Rob was trying to make earlier.

I actually meant this comment (I think we have timing issues with deep-linking and loading causing the page to scroll; it would be nice if GitHub would provide a strong visual indication of the deep linked comment!):

Reflecting the ARIA attribute to the property sounds good to me. In fact I would really want this.

However, purely from the perspective of intended use cases and not for testing, validation, feature detection, and exploration - I suspect "ARIA wins" is more useful, in that it balances the power reasonably well between component authors and page authors.

Let's not forget that page authors can also use AOM. I think it's more a question of static, load-time semantics vs dynamic updates.

For example, there is a developing responsive pattern where a particular role is specified in markup, and then overridden in script when the script loads and provides fuller interactivity. There is a good demonstration in howto-tabs:

  • The demo page hard-codes a role of heading for tabs and a role of section for tabpanels, meaning that if the script never loads or executes, the content can be understood as a series of sections and headings.
  • The connectedCallback hook in the custom element code rewrites the roles to tab and tabpanel to reflect the interactive version's semantics.

In this case, it's not a matter of "overriding" the page author's wishes, but that the tab/tabpanel roles only make sense when the script has loaded.

Obviously, this could be implemented by removing or rewriting the ARIA, but the outcome is still the same.

I think part of the problem here is that without the last phase, the ergonomics are poor because there's no easy way to figure out what wins. It's like having "style" without getComputedStyle.

Modulo strong native semantics (which I think we should leave aside) and layout-dependent issues like in the above comment, I think it's still "easy", just clumsy:

let role = el.hasAttribute('role') ? el.getAttribute('role') : el.accessibleNode.source.role;

I'm still opposed to using computed values for any run-time logic.

@alice
Copy link
Member Author

alice commented Apr 28, 2017

What if it was last one wins, but accessibleNode (or accessibleNode.source) always reflects the current value?

i.e.

<div id="composeBtn" role="button">Compose<div>
composeBtn.accessibleNode.role; // button
composeBtn.accessibleNode.role = "switch";
composeBtn.accessibleNode.role; // switch
composeBtn.setAttribute("role", "checkbox")
composeBtn.accessibleNode.role;  // checkbox

@domenic
Copy link

domenic commented Apr 28, 2017

That seems like it might work. However, I'm not sure what the underlying model is. Is it something like this?

  • Every accessible node has a [[role]] internal slot
  • Every time the role="" content attribute changes, we override [[role]] to contain the attribute's new value. (Or, only if it's a valid role?)
  • The accessibleNode.role setter also overrides [[role]] to equal the given value. (Or, only if it's a valid role?)
  • The accessibleNode.role getter returns [[role]].
  • Through some process un-specced here, [[role]] feeds into the a11y machinery to determine the "computed role" exposed to the AT user.

(I recognize the validity question is being discussed in a separate thread, and we don't have to get into it here, but I wanted to highlight that a model like this gives you two separate places you need to decide the validity question, not just one.)

I think this model is OK, but it does have some drawbacks, such as providing two ways to do the same thing. It also might be a bit confusing for authors that the content attribute affects the IDL attribute, but not in the usual two-way manner that we see with content attribute <-> IDL attribute reflection.

Plus of course it loses the ability to have the two-tier structure of underlying-role and role-provided-by-DOM-attributes, which I so desperately want so I can emulate native element behavior with custom elements. But I understand that's not this API's priority.

Overall I think it's more powerful to have the two different APIs operate on different levels. Your suggestion makes them operate on the same level, which means we aren't exposing new capabilities so much as providing a new syntax for doing the same thing. But, this new syntax has benefits like being able to refer to other elements without idrefs, and avoid sprouting attributes, so maybe that's enough of a benefit. I can understand if the extra power necessary to emulate <input type="checkbox"> behavior is considered out of scope.

@domenic
Copy link

domenic commented Apr 28, 2017

To be a bit more concrete, one of the downsides of the above model is the following code:

composeBtn.accessibleNode.role; // button
composeBtn.accessibleNode.role = "switch";
composeBtn.accessibleNode.role; // switch

composeBtn.getAttribute("role"); // button <--- maybe unexpected

composeBtn.removeAttribute("role");
composeBtn.accessibleNode.role; // empty string <-- pretty unexpected IMO; what happened to "switch"?

@alice
Copy link
Member Author

alice commented Apr 29, 2017

@domenic Thanks for writing that example out. I hadn't considered the removing attribute case - I agree that's weird (although I think every option we can come up with has some kind of weirdness; it's a matter of which type of weirdness we want to live with).

Regarding your notes on the underlying model - that seems right.

There were a few parts of your comment I didn't quite follow:

Regarding validity, I think we have the same issue if ARIA doesn't reflect but still takes precedence; does invalid ARIA still "override" a valid AOM property? i.e. <my-element id="el" role="🍌">, el.accessibleNode.role = 'button'; - what would you expect the computed role here to be? Presumably "none" or null, but I'm not clear on why this constitutes fewer places to decide the validity question.

I don't quite follow that you say you don't want two ways to do the same thing, but you don't mind having two totally separate systems with the same vocabulary affecting the same outcome - that seems like two ways to do the same thing to me:

<my-element id="el">
el.setAttribute('role', 'switch');
// or, and equally valid and having the same effect on the computed accessibility tree
el.accessibleNode.role = 'switch';

I also don't follow this:

I can understand if the extra power necessary to emulate <input type="checkbox"> behavior is considered out of scope.

What specific power were you referring to?

@domenic
Copy link

domenic commented May 1, 2017

Regarding validity, I think we have the same issue if ARIA doesn't reflect but still takes precedence; does invalid ARIA still "override" a valid AOM property?

Yeah, although I wouldn't call it the same issue, this is indeed an issue to be discussed. I'd suggest going with whatever the behavior is right now for e.g. <input type="checkbox" role="banana">.

I don't quite follow that you say you don't want two ways to do the same thing, but you don't mind having two totally separate systems with the same vocabulary affecting the same outcome - that seems like two ways to do the same thing to me:

I'm confused by your confusion. You stated the difference quite well: it's "two ways to do the same thing" (operating on a single underlying system) vs. "two separate systems affecting the same outcome". Notably those code examples don't do the same thing, because you can vary each independently to achieve useful effects (again, like those of native elements).

What specific power were you referring to?

The ability to have a set of underlying semantics, not controlled by content attributes, but able to be overridden by them. (And then fall back to the underlying semantics if the content attributes are removed.)

That's exactly what built-in elements have today, and what you would get with an ARIA-layers-on-top-of-AOM model, but could not get with the last-one-wins model.

@minorninth
Copy link
Collaborator

What I like the most about what's in the explainer now is reflection. I think that's great for discoverability. I also liked the limited amount of validation / type-checking we had originally, I thought that was a great fit.

It doesn't look like we're going to get validation. Mozilla isn't supportive, and Apple says it will be more work for dubious benefit. Also I don't think any of the core use cases depend on it.

Out of the approaches discussed, I don't really see how one of them is "two ways to do the same thing" but the other is not. With any of the approaches we're talking about, there's a DOM attribute and an AccessibleNode property, they can have different values, the property is richer, and they both affect one underlying computed property that's communicated to assistive technology. What's different is how the attribute and property relate to one another, but in any of the approaches they're distinct.

Consider what happens with the "dirty value flag" model when you consider not only what the AccessibleNode property returns, but the actual computed value too.

element = document.createElement('a');
element.accessibleNode.role;  // returns null
// Step 1. Computed role is "link"

element.setAttribute('role', 'button');
element.accessibleNode.role;  // returns "button"
// Step 2. Computed role is "button"

element.accessibleNode.role = 'checkbox';
element.accessibleNode.role;  // returns "checkbox"
// Step 3. Computed role is "checkbox"

element.accessibleNode.role = null;
element.accessibleNode.role;  // returns null
// Step 4. Computed role is now "link" even though the ARIA role is "button"

This just seems really bad to me, because from the perspective of the author, the state of the world at step 2 and step 4 are indistinguishable. Not only is there a magic hidden flag that you can't see, you can actually get into a state where it's impossible to predict the outcome by examining all of the properties and attributes you can access from JavaScript.

Correct me if I'm wrong, but that's worse than the input "value" attribute, and I think the key difference is that "value" isn't nullable.

If you set the value attribute, that's reflected in the value property. So far so good.

If you then change the value property, the value property now takes precedence. Changing the value of the attribute no longer matters. That's maybe a bit confusing, but the value property still reflects reality. You can't set it to null. In fact, it validates - so if you try to set it to something invalid, you'll get back something slightly different.

The bottom line is, you can always know the value of an input element by checking the value property.

As currently written, the same isn't true for AOM, and that seems like worse than other approaches that have different tradeoffs.

At first I tried to convince myself there was an approach where AOM properties could override ARIA attributes, but they were clearable, as a way to avoid that problem. But then you can't have reflection, unless you also have validation.

If we have validation, then setting an AOM property to null means to clear any extra semantics from the AccessibleNode, and revert back to as if it hadn't been overridden, and getting an AOM property always means get me the latest validated value from either the property, or from the corresponding ARIA attribute. However, making this work means buying into validation, which I don't think we can do.

I think that unfortunately what that means is that we have to give up reflection.

If we give up reflection, then either AOM properties or ARIA attributes can take precedence and both are equally consistent and don't lead to any weird cases. I think I've always leaned towards ARIA attributes taking precedence, all other things being equal, and Domenic's arguments above reinforce that. I was okay with AOM taking precedence if we could make reflection work, but now I think I'm convinced that that's going to be inferior.

To summarize, I think I'm going back to ARIA attributes overriding AOM properties, with no reflection or validation either way. It's not perfect but it's consistent and not at all surprising once you understand it, and in a later phase we'll provide a way to get computed values.

@cookiecrook
Copy link
Collaborator

cookiecrook commented May 3, 2017

@minornight wrote:

To summarize, I think I'm going back to ARIA attributes overriding AOM properties,

I've been opposed to ARIA overriding AOM all along. I think @asurkov has been too. I may have missed (or forgotten) a reason listed in the giant thread above, but IIRC, the core needs were 1) author discoverability, and 2) a novice/intermediate author's ability to override.

1. Author discoverability. I understand this case, but I think improvements to dev tools are the only good solution to this problem. Don't design an API based on the convenience of using it with the current tools. Design the tools to make it convenient to use the best API you can envision. Regarding tools: WebKit and Edge have engine-computed values in the dev tools, and @alice has an outstanding Chromium patch to allow something similar. Firefox has an inspector with computed values but it seems targeted at implementors (by Alex, for Alex) rather than web developers. To my knowledge, all the add-on accessibility extensions (including the shipping Chrome accessibility extension) vend accessibility property info based on assumptions of DOM parsing rather than computed values in the engine. @minorninth @alice @asurkov Is that accurate and fair? If so, do you see improving the dev tools as an adequate solution to this problem?

2) A novice/intermediate author's ability to override AOM by setting an ARIA attr. If I understand this argument correctly (I assume I'm missing something), the story is that someone might need to override a framework that uses AOM, but they don't know how to modify the AOM? If that's an accurate portrayal, I don't give this argument any credence. (Analogy: Most CSS novices learn about !important but it's better to learn and rely on the nuances of the cascade.) Any author in the situation should learn what is necessary to override AOM with AOM.

Like ARIA overrides native role semantics and most properties, I hoped AOM would override ARIA:

  • AOM overrides ARIA.
  • ARIA overrides host language (e.g. HTML). [Implemented today.]

Also:

  • Nulling an AOM value falls back to ARIA.
  • Nulling an ARIA value (removeAttribute) falls back to the host language. [Implemented today.]

It feels really backward to me to have a JavaScript interface that defers to (or can be overridden by) markup/DOM at some times (role and aria-* attrs) but not at other times (tagname, alt attr, required attr).

To summarize, I think I'm going back to […snip…] no reflection or validation either way.

I'm also back around to "no reflection" because, as you seem to imply below, the real reason for reflection is to get something closer computed values. If we can't get real computed values yet, let's wait until we can, and not complicate the path to get there.

It's not perfect but it's consistent and not at all surprising once you understand it, and in a later phase we'll provide a way to get computed values.

+1

@cookiecrook
Copy link
Collaborator

In summary, my vote is: AOM overrides ARIA with no reflection.

@minorninth
Copy link
Collaborator

@cookiecrook my only disagreement is really with your characterization of reason #2 as "A novice/intermediate author's ability to override AOM by setting an ARIA attr". I don't think it has to do with being novice, it's the idea that setting an ARIA attribute would be a natural way to override a custom control that has native semantics.

So if somebody implemented , and I wanted to use it as a scroll bar, I'd love to just write:

<custom-slider role="scrollbar">

If AOM overrides ARIA, it means the author of the custom element would need to check for the attribute and maybe not set the AccessibleNode property if it's already set. I guess that isn't so bad, it'd just be nice if they didn't have to remember to do that. It'd be a best practice.

Anyway, that's just clarifying one argument, but I can live with it either way.

Alice, how are you feeling about this now?

@domenic
Copy link

domenic commented May 3, 2017

If AOM overrides ARIA, it means the author of the custom element would need to check for the attribute and maybe not set the AccessibleNode property if it's already set. I guess that isn't so bad, it'd just be nice if they didn't have to remember to do that. It'd be a best practice.

It's a little worse than that. They'd need to listen for all changes to every ARIA attribute that applies, and if the attribute is added or changed, update the corresponding property via AOM. And if the attribute is removed, then restore the control's default role/property/state.

This is a significant amount of boilerplate to write, and easy to get wrong, very similar to the easy-to-get-wrong boilerplate for combining custom elements and ARIA today.

So yeah, it's workable, but it's definitely worse for custom control authors.

@alice
Copy link
Member Author

alice commented May 4, 2017

I can live with more or less any of the options discussed here.

As Dominic put it, I'm disappointed not to have reflection; in my case because I'd like AOM to feel more like a system underpinning ARIA than a completely different system.

Dominic and I chatted today and agreed that basically implementing James' proposal (which seems to be the common ground among implementers), and getting it in front of developers, is probably going to be the quickest way to shake out how painful or otherwise this will be for developers.

@robdodson
Copy link
Contributor

Dominic and I chatted today and agreed that basically implementing James' proposal (which seems to be the common ground among implementers), and getting it in front of developers, is probably going to be the quickest way to shake out how painful or otherwise this will be for developers.

I'd be happy to try the various suggested approaches and give feedback on them. I've been working with some teammates to create a set of Custom Elements which implement all of the ARIA Authoring Practices Guide recommendations. I think we'd be able to point out any roadblocks that the larger developer community would encounter.

@surma
Copy link

surma commented May 4, 2017

Just caught up on this! I think I agree with the current decision. I think. I can’t quite tell. I am gonna state my thoughts and y’all can feel free to ignore them.

I tried to form an informed opinion on what exactly “ARIA reflects to AOM” should entail and I am a bit confused, esp. after reading @domenic’s example:

<el role="button"></el>
<script>
el.getAttribute('role'); // 'button'
el.accessibleNode.role; // 'button'
el.accessibleNode.role = 'list';
el.getAttribute('role'); // 'button'
el.accessibleNode.role; // 'list'
el.setAttribute('role', 'heading');
el.accessibleNode.role; // (1) ???
el.removeAttribute('role')
el.accessibleNode.role; // (2) ???
</script>

My expectation is for (1) to return heading, and (2) to return "" or whatever the appropriate value for “unset” is. So when we say “ARIA reflects to AOM”, I’d expect that to always happen, not only once at the start. So I actually disagree that @domenic’s example code shows unexpected behavior.

However, taking a look at how <input> and .value behave, both (1) and (2) should return list. I am not sure what to make of that, I am honestly fine with either at this point.

I do however think that ARIA reflecting to AOM and having AOM be the source of truth (not requiring developers to remove ARIA attributes first to be able to start using AOM) is the right decision in terms of ergonomics. I don’t think we have anything on the platform that behaves this way.

As @robdodson said, we’re happy to do write some practical examples using different approaches to AOM and see how it affects developer ergonomnics.

@minorninth
Copy link
Collaborator

Based on Alex's earlier support, we've made a consensus decision to go with: AOM overrides ARIA with no reflection.

This is not set in stone. But we're going to set aside this issue for now, work on implementations, and tighten up the existing spec, hopefully start working on next phases while this one bakes.

@joanmarie
Copy link

At the end of Section 2.2 AccessibleNode interface, it says:

When both an AccessibleNode property and its equivalent ARIA attribute are both present, the ARIA attribute overrides the AccessibleNode property.

Should that be updated? (I see @minorninth's "not set in stone" and "set aside this issue for now" comments, but the spec and explainer have conflicting statements.)

@alice alice closed this as completed Jul 15, 2024
@alice alice reopened this Jul 15, 2024
@alice alice closed this as not planned Won't fix, can't repro, duplicate, stale Jul 15, 2024
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