Note: I don't like these ideas very much. I might be able to make them less gross with some work, and that's what I'll end up doing if nobody comes up with a different approach. But I did want to throw them up here, in their current gross form, so that people could see where I was heading.
The basic idea is that when registering a custom element, you are optionally given the ability to manage its true ARIA roles and stoperties. This would probably be done via something similar to the revealing constructor pattern given to its createdCallback
. You would then opt-in to manually managing your ARIA roles and stoperties yourself.
Concretely, the registration would take as an arugment a customAria
boolean property. If it is truthy, the createdCallback
would be supplied with an ariaSetter
function, which you could invoke like ariaSetter("role", "separator")
or ariaSetter("aria-checked", "true")
. To modify the element's roles or stoperties later, you'd need to save ariaSetter
for later use within your scope.
It is also very convenient to provide an easy "default" behavior based on the arguments to attributeChangedCallback
, so that authors can fall back to the default in non-restricted cases. We expose this via ariaSetter.fromAttributeChanged(argumentsArrayLike)
. This would be a no-op if given arguments where the zeroth entry was not ARIA related (i.e. was not "role"
or "aria-something"
).
<hr>
elements have the strong native semantic of being a separator
.
var ariaSetters = new WeakMap();
class CustomHTMLHRElement extends HTMLElement {
createdCallback({ ariaSetter }) {
ariaSetter("role", "separator");
// store for later use
ariaSetters.set(this, ariaSetter);
}
attributeChangedCallback(name) {
// Don't allow them to override the role, but otherwise pass it through.
if (name !== "role") {
ariaSetters.get(this).fromAttributeChanged(arguments);
}
}
}
document.registerElement("custom-hr", CustomHTMLHRElement, { customAria: true });
<main>
elements have the default implicit ARIA semantic of the main role, and the role restriction of either document, application, or main.
var ariaSetters = new WeakMap();
class CustomHTMLMainElement extends HTMLElement {
createdCallback({ ariaSetter }) {
ariaSetter("role", "main");
ariaSetters.set(this, ariaSetter);
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === "role" &&
(newValue !== "document" && newValue !== "application" && newValue !== "main")) {
// Invalid role value; set back to default.
ariaSetters.get(this)("role", "main");
} else {
ariaSetters.get(this).fromAttributeChanged(arguments);
}
}
}
document.registerElement("custom-main", CustomHTMLMainElement, { customAria: true });
The above is pretty bare-to-the-metal. Can we come up with something more declarative-ish that still has the power necessary to be on par with native elements?
CustomHTMLHRElement.aria = {
role: {
default: "separator",
allowed: ["separator"]
}
};
CustomHTMLMainElement.aria = {
role: {
default: "main",
allowed: ["document", "application", "main"]
}
};
CustomHTMLNoScriptElement.aria = {
role: {
default: null,
allowed: [null]
},
"aria-hidden": "true"
}
CustomHTMLDetailsElement.aria = {
role: {
default: "group",
allowed: [/* put full list of roles that "support aria-expanded" here */]
},
"aria-expanded": function () {
return this.hasAttribute("open");
}
};
CustomHTMLDetailsElement.prototype.attributeChangedCallback = function () {
// ??? need some way to trigger the re-calculation of aria-expanded. or set it manually, bringing us back to before.
};