e Web Components and the AOM

Web Components and the AOM

Léonie Watson

Web Components and the AOM

JSConf, Singapore June 2019

Léonie Watson, TetraLogical

Browser mechanics

HTML > DOM > Accessibility Tree > User Interface

HTML: the <button> element

<button>Play</button>

DOM: the <button> element

<button>Play</button>

Acc tree: the <button> element

UI: the <button> element

Button image

Custom Element: customised

class CustomButton extends HTMLButtonElement {
    constructor() {
        super();
    }
}

customElements.define("custom-button", CustomButton, { extends: "button" });

connectedCallback()

connectedCallback() {
    this.addEventListener("click", doSomething);
}

HTML: the is attribute

<button is="custom-button">Play</button>

DOM: the is attribute

<button is="custom-button">Play</button>

Acc tree: the is attribute

Demo: the is="custom-button" attribute

Custom Element: autonomous

class ToggleButton extends HTMLElement {
    constructor() {
        super();
    }
}

customElements.define("toggle-button", ToggleButton);

connectedCallback()

connectedCallback() {
    this.setAttribute("role", "button");
    this.setAttribute("tabindex", "0");
    this.setAttribute("aria-pressed", "false");

    this.addEventListener("click", togglePressed);
    this.addEventListener("keydown", function (e) {
        if (e.keyCode == 13 || e.keyCode == 32) {
            togglePressed();
        }
    });
}

Function: the <toggle-button> element

function togglePressed() {
    if (this.getAttribute("aria-pressed") == "false") {
        this.setAttribute("aria-pressed", "true");
    } else {
        this.setAttribute("aria-pressed", "false");
    }
}

HTML: the <toggle-button> element

<toggle-button>Play</toggle-button>

DOM: the <toggle-button> element

<toggle-button role="button" tabindex="0" aria-pressed="false">Play</toggle-button>

Acc tree: the <toggle-button> element

Demo: the <toggle-button> element

Accessibility Object Model (AOM)

W3C

Web Platform Incubator Community Group (WICG)
https://wicg.github.io/aom

Phase 1a: ARIA reflection

Will enable reflection of:

Reflecting ARIA attributes

Adds two interface mixins to ARIA 1.2

connectedCallback()

connectedCallback() {
    this.role = "button";
    this.tabIndex = "0";
    this.ariaPressed = "false";

    ...
}

Reflecting element references ⚠️

connectedCallback() {
    this.ariaDescribedByElements = [description1];
    ...
}

Phase 1a: Web Platform Tests

https://w3c-test.org/wai-aria/idlharness.window.html

Chrome Edge Firefox Safari
Chrome Edge (Chromium beta) Firefox Safari
65/95 65/95 1/95 79/95

Phase 1b: Custom element semantics ⚠️

Will enable semantics to be provided in two ways:

Default semantics ⚠️

class ToggleButton extends HTMLElement {
    constructor() {
        super();
    }
}

customElements.define("toggle-button", ToggleButton, { role: "button", tabIndex: "0" });

Per instance semantics ⚠️

class ToggleButton extends HTMLElement {
    var internals = null;
	constructor() {
        super();
        this.internals = customElements.createInternals(this);
        this.internals.ariaPressed = "false";
    }
}

customElements.define("toggle-button", ToggleButton, { role: "button", tabIndex: "0" });

Phase 2: User action events ⚠️

Will enable listening for Assistive Technology (AT) events

Synthesized DOM events ⚠️

To protect AT user privacy, AT events will trigger synthesized DOM events

New AT events ⚠️

New AT events will be introduced:

Phase 3: Virtual accessibility nodes ⚠️

Will allow virtual nodes to be added to the accessibility tree

Phase 4: Computed tree introspection ⚠️

Will provide access to:

HTML: the <speak-content> element

<speak-content>
    <span slot="control">Speak this</span>
    <div slot="speak">1 tequila... 2 tequila... 3 tequila... floor!</div>
</speak-content>

HTML template + the shadow DOM

class SpeakContent extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: "open" });
        this.shadowRoot.appendChild(template.content.cloneNode(true));
    }
}
const template = document.createElement("template");
template.innerHTML = `<style> ... </style>
                      <button id="control">
                        <slot name="control"></slot>
                      </button>
                      <slot name="speak" id="speak"></slot>`;

customElements.define("speak-content", SpeakContent);

connectedCallback()

   connectedCallback() {
        var control = this.shadowRoot.querySelector("button");
        control.setAttribute("aria-pressed", "false");
        var speak = this.querySelector("div");
        var speakContent = speak.textContent;
        control.addEventListener("click", speakThis);
    }

speakThis()

    function speakThis(e) {
        if (control.getAttribute("aria-pressed") == "false") {
            control.setAttribute("aria-pressed", "true");

            var utterance = new SpeechSynthesisUtterance();
            utterance.text = speakContent;
            window.speechSynthesis.speak(utterance);

            setTimeout(resetPressedState, 500);
        }
    }

resetPressedState()

    function resetPressedState() {
        if (window.speechSynthesis.speaking === false) {
            control.setAttribute("aria-pressed", "false");
        }
        else {
            setTimeout(resetPressedState, 500);
        }
    }

CSS attribute selector

const template = document.createElement("template");
template.innerHTML = 
    `<style>
    ...
    #control[aria-pressed="true"] {
        color: #fff;
        padding: 11px 12px 9px 16px;
        text-shadow: 0 -1px 0 #444, 0 0 5px #ffd, 0 0 8px #fff;
        box-shadow: 0 1px 0 #666, 0 2px 0 #444, 0 2px 2px rgba(0, 0, 0, 0.9);
    }
    ...
    </style>`;

Demo: the <speak-content> element

With thanks to

Thank you!

Léonie Watson, TetraLogical