Skip to content

Instantly share code, notes, and snippets.

@alice
Last active January 27, 2018 22:11
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save alice/c684bd031adcd0483d0306d95c32d0e9 to your computer and use it in GitHub Desktop.
Save alice/c684bd031adcd0483d0306d95c32d0e9 to your computer and use it in GitHub Desktop.

Table of Contents

Phase 1

ARIA properties

Setting ARIA properties on non-Custom Elements should just be ARIA. We should just make ARIA reflect in IDL.

el.ariaLabel = "banana";

el.ariaHidden = true;            // auto-converts to "true" since this is a string attribute

el.role = "button";              // no validation, this is just a string

el.ariaLabelledby = "an-idref";  // not an element ref here

Rather than add these directly to the Element interface, we propose this be implemented as a mixin interface:

interface mixin ARIAProperties {
  [Reflect] [Unscopable] attribute DOMString? ariaActiveDescendant;
            [Unscopable] attribute Element? ariaActiveDescendantElement;
  [Reflect] [Unscopable] attribute boolean? ariaAtomic;
  [Reflect] [Unscopable] attribute DOMString? ariaAutocomplete;
  [Reflect] [Unscopable] attribute boolean? ariaBusy;
  [Reflect] [Unscopable] attribute DOMString? ariaChecked;
  [Reflect] [Unscopable] attribute short? ariaColCount;
  // ...
  [Reflect] [Unscopable] attribute DOMString? role;
}

Element includes ARIAProperties;

Relationships

Other DOM relationships like htmlFor suffer the same issues as the ARIA relationships regarding ergonomics and Shadow DOM.

We propose that we have a common pattern for all non-tree relationships (including htmlFor):

el.htmlFor = 'id-for-my-input';
el.htmlForElement = myInput;  // overrides htmlFor

myCustomInput.ariaActiveDescendant = 'id-for-my-label';
myCustomInput.ariaActiveDescendantElement = myLabel;  // overrides ariaActiveDescendant

myCustomInput.ariaOwns = 'id-for-autocomplete-1';
myCustomInput.ariaOwnsElements = [autocomplete1, autocomplete2]; // overrides ariaOwns

All the ARIA relationship element properties would also be a part of the ARIAProperties mixin.

When using a property which may be set either via an IDREF or IDREF list string or via an element reference or list of elements, the element reference should take precedence.

For example, if both ariaLabelledBy and ariaLabelledByElements were set to refer to different non-null values, then ariaLabelledBy should be ignored in favour of ariaLabelledByElements.

If ariaLabelledByElements was explicitly set to null, or an empty list, or never set, then ariaLabelledBy should be used.

Phase 2

Event listeners

Instead of attaching event listeners to the AccessibleNode object, we make the "accessible" events available to Elements.

el.addEventListener('accessibleincrement', function() {
  el.value += 1;
});

el.addEventListener('accessibledecrement', function() {
  el.value -= 1;
});

We would need a way to prompt in an appropriate and timely manner for user permission to listen for assistive technology events.

Phase 3

Virtual nodes

// An AccessibleNode represents a virtual accessible node.
interface AccessibleNode {};
AccessibleNode includes ARIAProperties;

interface AccessibleRoot : AccessibleNode {}; // needed?
  • Calling attachAccessibleRoot() causes an AccessibleRoot to be associated with a Node.
    • The AccessibleRoot forms the root of a virtual accessibility tree.
    • The Node's DOM children are implicitly ignored for accessibility once an AccessibleRoot is attached - there is no mixing of DOM children and virtual accessible nodes.
  • Like ShadowRoot, an element may only have one associated AccessibleRoot.
  • Only AccessibleNodes may have AccessibleNodes as children, and AccessibleNodes may only have AccessibleNodes as children.
// Implementing a canvas-based spreadsheet's semantics
canvas.attachAccessibleRoot();
let table = canvas.accessibleRoot.appendChild(new AccessibleNode());
table.role = 'table';
table.ariaColCount = 10;
table.ariaRowcount = 100;
let headerRow = table.appendChild(new AccessibleNode());
headerRow.role = 'row';
headerRow.ariaRowindex = 0;
// etc. etc.

Using an AccessibleNode on ShadowRoot set default, non-reflected semantics

We would add an accessibleNode property on to the ShadowRoot interface, which would allow setting default semantics for the host node.

  class HowToCheckbox extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({mode: 'open'});
      this.shadowRoot.appendChild(template.content.cloneNode(true));

      // The default role of the host node should be 'checkbox'.
      this.shadowRoot.accessibleNode = 'checkbox';
    }
    // ...
   }


   window.customElements.define('howto-checkbox', HowToCheckbox);
<!-- ARIA role overrides AccessibleRoot role -->
<!-- If an author removes the ARIA role, the computed role reverts to "checkbox". -->
<howto-checkbox role="radio"></howto-checkbox>

Open questions

Which ARIA relationship properties should be exposed as element references and under what names?

  • Initial idea: begin only with aria-labelledby, aria-describedby and aria-activedescendant, and then assess.

How do we attach a virtual AccessibleNode subtree?

  • A "strawman" API is proposed above, but are there other proposals?

How does the virtual AccessibleNode subtree interact with the DOM tree?

  • Can DOM Elements be mixed in to the virtual subtree, like slots in a Shadow tree?
    • Simplest proposal: no, once a virtual subtree is attached, it overrides all DOM children of the host Element.

Can virtual AccessibleNodes be used in relationships with DOM elements?

  • To be consistent with not mixing the two trees, perhaps not?

Do we need a new API for collection types?

Why does a ShadowRoot need an accessibleNode for default semantics?

  • If AccessibleNode is primarily intended for building virtual accessibility trees, are we building a kind of virtual accessibility tree on the ShadowRoot? Is AccessibleNode a node or a bag of properties in this case?
  • Alternative: define accessible properties directly on ShadowRoot using the ARIA vocabulary instead.
@cookiecrook
Copy link

cookiecrook commented Nov 3, 2017

Note: this comment refers to an earlier revision


@ aria-* and @ role reflected in IDL on Element is fine with me, but that should just be part of an ARIA or DOM spec update.

However, I'm not convinced that reflected ARIA attrs should replace the existing Phase 1's Element.accessibleNode. Also, I'd prefer it not use the aria-prefixed property on accessibleNode because a) It seems to limit AOM to existing ARIA attrs, and b) Many of these pseudo-reflected relationships may be confusing to authors.

The code samples above are not reflected IDL, but a unique hybrid of reflection, inference, and validation. For example, I find this very confusing (quoting from above):

// If all Elements in barElements have IDs, bar is set to the corresponding IDREF list, e.g. "id1 id2 id3"
el.ariaOwnsElements = [elementWithIdref1, elementWithIdref2];
console.log(el.ariaOwns);  // "idref1 idref2"
console.log(el.getAttribute('aria-owns'));  // "idref1 idref2"

// otherwise, bar is set to an null and the attribute removed.
el.ariaOwnsElements = [elementWithIdref, elementWithoutIdref];
console.log(el.ariaOwns);  // null
console.log(el.getAttribute('aria-owns'));  // undefined

Doesn't this preclude the intended use of AOM Phase 1 to support relationships without IDREF, including across shadow root boundaries?

ARIA was, to some degree, a declarative workaround for the lack of a scripted interface into the browser's accessibility tree. I expect that trying to re-map declarative ARIA into a JavaScript API will be full of challenges. String-only reflection in IDL is fine (I would support this in the ARIA spec), but I would not want to design a JavaScript API that copies ARIA verbatim.

@cookiecrook
Copy link

cookiecrook commented Nov 3, 2017

@cookiecrook
Copy link

cookiecrook commented Nov 3, 2017

Note: this comment refers to an earlier revision


How would this intention work with a non-ARIA property? For example, we intended to expose hierarchical relationships in the later phases.

<main id="parent">
  <div role="none" id="ignored">
    <button id="child">label</button>
  </div>
</main>

How should one reference the accessible parent node (main#parent) rather than the DOM parentElement (div#ignored)?

let child = document.getElementById("child");
child.parentElement; // DOM parent aka div#ignored
child.accessibleNode.parent; // this was the original proposal for accessible parent

If the new Phase 1 removes Element.accessibleNode, how would you reference any non-ARIA property later?

child.ariaParent; // ???  :-0~
child.accessibleParentElement // ? What if was was not an element? 
child.accessibleParent; //? 

@alice
Copy link
Author

alice commented Nov 11, 2017

Note: this comment refers to an earlier revision


I'd prefer it not use the aria-prefixed property on accessibleNode because a) It seems to limit AOM to existing ARIA attrs,

As discussed elsewhere, the semantics which are within ARIA's domain to expose should be limited to ARIA (but we should work towards expanding that set as quickly as possible).

and b) Many of these pseudo-reflected relationships may be confusing to authors.

Yeah, we may need to refine how the relationship reflection works.

Doesn't this preclude the intended use of AOM Phase 1 to support relationships without IDREF, including across shadow root boundaries?

I think there's been a misunderstanding. You can (with this proposal) absolutely set a relationship property (aria- or otherwise) referring to an element without an IDREF - it just won't be reflected.

How should one reference the accessible parent node (main#parent) rather than the DOM parentElement (div#ignored)?

That is a computed property, and hence not available outside the phase 4 API.

@cookiecrook
Copy link

cookiecrook commented Dec 1, 2017

Note: this comment refers to an earlier revision


Pasting big response from the email thread. Questions and concerns in no particular order are as follows:

1. Usage and naming of Element.attachAccessibleRoot() and Element.accessibleRoot

This seems to be a convention reference to Element.attachShadow() and Element.shadowRoot. Is that correct? If so, let's consider changing the method and property names to Element.attachAccessibleShadow() and Element.accessibleShadowRoot. This should make the similarity more obvious to developers and avoid ambiguity between accessibleRoot-vs-accessibleNode.

I’m assuming that Element.accessibleShadowRoot will be null until Element.attachAccessibleShadow() is initiated. Is that corrrect? This would match the existing shadow DOM behavior.

Does there need to be a dictionary passed to attachAccessibleShadow() as there is with attachShadow({mode: 'open'})?

2. Node Inheritance

Can you further explain the need to have AccessibleNode inherit from Node? I don't remember what we’d gain from this change. There are some obvious downsides such as:

  • Crowding the namespace of Node with a bunch of accessibility-specific properties.
  • Inheriting methods that may have unintended consequences. For example, there is nothing stopping an author from passing a virtual node to Element.insertBefore.
  • Potential increase in memory consumption.

3. ARIA Reflection

I think most people are in agreement that ARIA property reflection makes sense but the proposal is mixed with several debatable details.

ARIA string value reflection could/should be defined in the ARIA spec or the HTML Spec. The ARIA change does not need to be associated with AOM.

Unless there is existing precedent, we should not try to reflect non-string values for AOM. The proposal mentioned reflection of string IDREFS into evaluated node lists. The implementation details of that approach are unknown, and a simple array usage would be sufficient: For example:

el.accessibleNode.labelElements = [one, two];

Clarifying: Is the goal of ARIA reflection to remove AccessibleNode entirely from Element? In other words, would AccessibleNode be used exclusively for virtual nodes, or would it remain available for DOM-backed nodes?

4. ARIA-prefixed naming conventions on accessibleNode properties

We believe using aria-prefixed naming conventions on accessibleNode properties will increase author confusion. It seems to limit AOM to ARIA-defined attributes, when we believe there will be outliers in AOM that are irrelevant to ARIA, and vice versa.

It also seems to limit the design of programmatic APIs to those able to be defined in declarative markup. In addition to the node list examples outlined previously, concepts like aria-invalid could be defined as enums or constants instead of strings. Concepts like aria-flowto and aria-live could be redesigned to better use capabilities of a programmatic API.

5. Precedence of ARIA over AOM

@rniwa proposed a new idea at TPAC that may make it acceptable to have ARIA on the shadow host override AOM on the shadow root. I believe this proposal resolves the Custom Elements use case we’ve been debating. To avoid muddying the threads, Ryosuke or I will raise this as a separate issue on the GitHub repo.

6. Accessible Events on Node versus AccessibleNode.

@minorninth wrote:

Accessible event listeners can be added to any Node - either a DOM element or an AccessibleNode, and they work the same way (that way it's not necessary to create an AccessibleNode just to listen for an event)

This justification seems minimal. To an author, isn’t referencing an accessibleNode (el.accessibleNode) equivalent to creating it? I don’t understand the cost you are trying to avoid incurring.

Thanks.

@alice
Copy link
Author

alice commented Dec 1, 2017

Note: this comment refers to an earlier revision


Copy/pasting replies to each section in turn 😁

1. Usage and naming of Element.attachAccessibleRoot() and Element.accessibleRoot

let's consider changing the method and property names to Element.attachAccessibleShadow() and Element.accessibleShadowRoot

I would be ok with that; curious what other think.

I’m assuming that Element.accessibleShadowRoot will be null until Element.attachAccessibleShadow() is initiated. Is that corrrect?

That is correct.

Does there need to be a dictionary passed to attachAccessibleShadow() as there is will attachShadow({mode: 'open’})?

Probably not - I don't imagine there being a "closed" mode. What do you think?

@alice
Copy link
Author

alice commented Dec 1, 2017

Note: this comment refers to an earlier revision


2. Node Inheritance

There are some obvious downsides such as:

  • Crowding the namespace of Node with a bunch of accessibility-specific properties.

That is not what's being proposed: https://gist.github.com/alice/c684bd031adcd0483d0306d95c32d0e9#aria-properties

Element implements ARIAProperties;

and then later

AccessibleNode implements ARIAProperties; 
  • Inheriting methods that may have unintended consequences. For example, there is nothing stopping an author from passing a virtual node to Element.insertBefore.

There are existing rules for insertion validity that we could amend: https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity

  • Potential increase in memory consumption.

Why would this be different vs. adding a new type?

@alice
Copy link
Author

alice commented Dec 1, 2017

Note: this comment refers to an earlier revision


3. ARIA Reflection

ARIA string value reflection could/should be defined in the ARIA spec or the HTML Spec. The ARIA change does not need to be associated with AOM.

Agreed, this is not part of the "AOM spec". However, it does have a potential bearing on it (see "4" below), and also provides a very small benefit (scripting) which AOM also potentially provides.

Unless there is existing precedent, we should not try to reflect non-string values for AOM. The proposal mentioned reflection of string IDREFS into evaluated node lists. The implementation details of that approach are unknown, and a simple array usage would be sufficient: For example:
el.accessibleNode.labelElements = [one, two];

This doesn't provide a solution for, e.g., htmlFor.

Also, it makes it unclear once again which should take precedence, since now we have the Accessible[ShadowRoot|Node] having a more expressive API than what is possible for Element.

Clarifying: Is the goal of ARIA reflection to remove AccessibleNode entirely from Element? In other words, would AccessibleNode be used exclusively for virtual nodes, or would it remain available for DOM-backed nodes?

An Accessible[Shadow]Root (or whatever it ends up being called) would allow you to set default semantics on an Element, similar to how a ShadowRoot allows you to define its host's style without adding a .style attribute to the host Element, and similar to how el.accessibleNode works in the current spec.

An AccessibleNode would then be purely a virtual node, but share mostly the same interface.

@alice
Copy link
Author

alice commented Dec 1, 2017

Note: this comment refers to an earlier revision


4. ARIA-prefixed naming conventions on accessibleNode properties

We believe using aria-prefixed naming conventions on accessibleNode properties will increase author confusion. It seems to limit AOM to ARIA-defined attributes, when we believe there will be outliers in AOM that are irrelevant to ARIA, and vice versa.

It is not intended to limit AOM to ARIA-defined attributes, but rather to

  1. provide a completely predictable mapping between ARIA attributes and reflected properties, and
  2. indicate that the interpretation for those properties is provided by the ARIA spec.

The ARIA properties would be a mixin shared by Element and AccessibleNode with the properties having the exact same meaning and usage in both places. So there should, hopefully, be less developer confusion, not more.

It also seems to limit the design of programmatic APIs to those able to be defined in declarative markup. In addition to the node list examples outlined previously, concepts like aria-invalid could be defined as enums or constants instead of strings. Concepts like aria-flowto and aria-live could be redesigned to better use capabilities of a programmatic API.

There is no reason why AccessibleNode should not have properties which are not defined on this mixin, and which are not defined by ARIA.

For example, as discussed elsewhere, we could design an API which would partially or completely "explain" aria-live, which would not share its name.

@alice
Copy link
Author

alice commented Dec 1, 2017

Note: this comment refers to an earlier revision


6. Accessible Events on Node versus AccessibleNode.

To an author, isn’t referencing an accessibleNode (el.accessibleNode) equivalent to creating it? I don’t understand the cost you are trying to avoid incurring.

It doesn't need to be created at all if you can attach the event listener directly to the Element, e.g.

el.addEventListener('accessibleincrement', function() {
  el.value += 1;
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment