Skip to content

Instantly share code, notes, and snippets.

@domenic
Last active Aug 29, 2015
Embed
What would you like to do?
ARIA solution

Proposed ARIA API

I propose adding an implicitAria property to all elements that exposes and allows modification of their implicit ARIA roles, states and properties, i.e. the values that are used when no HTML attributes are present to override them. "No role" manifests as null in JavaScript.

In what follows, let el.[[role]] and el.[[aria-expanded]] and the like be a fictional syntax for getting the screen-reader exposed ARIA values for an element. We could consider, as a separate proposal, exposing an API for these.

Examples with Normal Elements

HTMLHRElement

<hr> elements have the separator role by default.

var el = document.createElement("hr");
el.getAttribute("role") === null;
el.implicitAria.role === "separator";
el.[[role]] === "separator";

el.setAttribute("role", "menuitem");
el.implitAria.role === "separator";
el.[[role]] === "menuitem";

el.removeAttribute("role");
el.implitAria.role === "separator";
el.[[role]] === "separator";

el.implicitAria.role = "button";
el.getAttribute("role") === null;
el.[[role]] === "button";

el.setAttribute("role", "menuitem");
el.implicitAria.role === "button";
el.[[role]] === "menuitem";

HTMLAnchorElement

<a> elements have a default role of link if and only if they have a href attribute.

var el = document.createElement("a");
el.implicitAria.role === null;
el.[[role]] === null;

el.setAttribute("role", "separator");
el.implicitAria.role === null;
el.[[role]] === "separator";

el.href = "http://example.com/";
el.implicitAria.role === "link";
el.[[role]] === "separator";

el.removeAttribute("role");
el.implicitAria.role === "link";
el.[[role]] === "link";

el.implicitAria.role = "menuitem";
el.[[role]] === "menuitem";

el.removeAttribute("href");
el.implicitAria.role === null; // resets even though we customized it
el.[[role]] === null;

el.href = "http://example.com/";
el.implicitAria.role === "link"; // set back to link; menuitem not remembered
el.[[role]] === "link";

Using to Build Custom Elements

CustomHRElement

We want to implement <hr>-like behavior of having a default separator role.

class CustomHRElement extends HTMLElement {
    createdCallback() {
        this.implicitAria.role = "separator";
    }
}

CustomAnchorElement

We want to implement <a>-like behavior of switching our role depending on the presence or absence of the href attribute.

class CustomAnchorElement extends HTMLElement {
    createdCallback() {
        this.implicitAria.role = this.hasAttribute("href") ? "link" : null;
    }

    attributeChangedCallback(name, oldValue, newValue) {
        if (name === "href") {
            this.implicitAria.role = newValue !== null; // implicit string conversion
        }
    }
}

CustomMetaElement

We want to implement <meta>-like behavior of having aria-hidden set to "true" by default.

class CustomMetaElement extends HTMLElement {
    createdCallback() {
        this.implicitAria["aria-hidden"] = "true";
        // Bikeshed potential: `.ariaHidden` instead/also?
    }
}

CustomDetailsElement

We want to implement <details>-like behavior of having aria-expanded's default set depending on the presence or absence of the open attribute. While we're at it we'll set a default role of group.

class CustomDetailsElement extends HTMLElement {
    createdCallback() {
        this.implicitAria.role = "group";
        this.implicitAria["aria-expanded"] = this.hasAttribute("open");
    }

    attributeChangedCallback(name, oldValue, newValue) {
        if (name === "open") {
            this.implicitAria["aria-expanded"] = newValue !== null;
        }
    }
}

Potential Library Sugar

A library like Polymer could easily use this lower-level building block to build a declarative API for the common cases. For example, you might imagine

Polymer('custom-hr', {
    implicitAria: { role: 'separator' }
});

Polymer('custom-details', {
    implicitAria: { role: 'group', 'aria-expanded': 'attr(open)' }
});

Details to Bikeshed

Is element.implicitAria the right name? Perhaps element.aria.implicit to leave room for a different ARIA API later?

Should it be el.implicitAria["aria-expanded"] or el.implicitAria.ariaExpanded or el.implicitAria.get("aria-expanded")?

Restricting Access

One downside of this proposal, as shown in the opening examples, is that it allows arbitrary author code to change the accessibility characteristics of HTML elements. For example, it allows creating a <hr> element (not a <custom-hr>, just a <hr>) that has implicit ARIA role button. Or, it could allow creating a <details> element which has an open attribute present, but is exposed to accessibility tech as aria-expanded="false".

This can be sidestepped via a slightly more awkward API, that only allows the author of the element access to the ability to change its implicit ARIA roles. An example of this in action would be

class CustomHRElement extends HTMLElement {
    createdCallback({ setAria }) {
        setAria("role", "separator");
    }
}
var privates = new WeakMap();

class CustomAnchorElement extends HTMLElement {
    createdCallback({ setAria }) {
        setAria("role", this.hasAttribute("href") ? "link" : null);

        privates.set(this, { setAria });
    }

    attributeChangedCallback(name, oldValue, newValue) {
        if (name === "href") {
            privates.get(this).setAria("role", newValue !== null); // implicit string conversion
        }
    }
}

In these examples, there is no implicitAria property that anyone can access on every element, but instead the capability to set implicit ARIA roles and stoperties is done via the setAria function passed to the createdCallback in the element definition.

(Note: the use of a WeakMap as a side-table for storing private per-instance state is a fairly common ES6 idiom; see e.g. "Private instance members with weakmaps in JavaScript".)

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