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.
<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";
<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";
We want to implement <hr>
-like behavior of having a default separator
role.
class CustomHRElement extends HTMLElement {
createdCallback() {
this.implicitAria.role = "separator";
}
}
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
}
}
}
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?
}
}
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;
}
}
}
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)' }
});
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")
?
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".)