Skip to content

Instantly share code, notes, and snippets.

@domenic
Created August 27, 2014 23:41
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save domenic/4ddde9bdcb673c48f237 to your computer and use it in GitHub Desktop.
Save domenic/4ddde9bdcb673c48f237 to your computer and use it in GitHub Desktop.
ARIA possible solutions scratchwork

ARIA + Custom Elements Solution Scratchwork

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.

Proposed Solution

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

Sample Usage

<hr>

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

<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 });

Alternate Solution

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.
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment