Skip to content

Commit

Permalink
Allow setting default accessibility semantics for custom elements
Browse files Browse the repository at this point in the history
  • Loading branch information
domenic committed Aug 28, 2020
1 parent c8d5ad3 commit e95cc26
Showing 1 changed file with 191 additions and 38 deletions.
229 changes: 191 additions & 38 deletions source
Original file line number Diff line number Diff line change
Expand Up @@ -3800,7 +3800,11 @@ a.setAttribute('href', 'https://example.com/'); // change the content attribute
<p>Finally, the following terms are defined <cite>ARIA</cite>: <ref spec=ARIA></p>

<ul class="brief">
<li><dfn data-x-href="https://w3c.github.io/aria/#dfn-role">role</dfn></li>
<li><dfn data-x-href="https://w3c.github.io/aria/#dfn-accessible-name" data-x="concept-accessible-name">accessible name</dfn></li>
<li>The <dfn data-x-href="https://w3c.github.io/aria/#ARIAMixin"><code>ARIAMixin</code></dfn> interface, with its associated
<dfn data-x-href="https://w3c.github.io/aria/#dfn-get-the-accessibility-idl-attribute">get the accessibility IDL attribute</dfn> and
<dfn data-x-href="https://w3c.github.io/aria/#dfn-set-the-accessibility-idl-attribute">set the accessibility IDL attribute</dfn> hooks</li>
</ul>
</dd>

Expand Down Expand Up @@ -12643,8 +12647,41 @@ interface <dfn>DOMStringMap</dfn> {
<h4 id="wai-aria">Requirements related to ARIA and to platform accessibility APIs</h4>

<p>User agent requirements for implementing Accessibility API semantics on <span>HTML
elements</span> are defined in <cite>HTML Accessibility API Mappings</cite>. <ref
spec=HTMLAAM></p>
elements</span> are defined in <cite>HTML Accessibility API Mappings</cite>. In addition to the
rules there, for a <span>custom element</span> <var>element</var>, the default ARIA role
semantics are determined as follows: <ref spec=HTMLAAM></p>

<ol>
<li><p>Let <var>map</var> be <var>element</var>'s <span>native accessibility semantics
map</span>.</p></li>

<li><p>If <var>map</var>["<code data-x="">role</code>"] <span data-x="map exists">exists</span>,
then return it.</p></li>

<li><p>Otherwise, return no role.</p></li>
</ol>

<p>Similarly, for a <span>custom element</span> <var>element</var>, the default ARIA state and
property semantics, for a state or property named <var>stateOrProperty</var>, are determined as
follows:</p>

<ol>
<li><p>Let <var>map</var> be <var>element</var>'s <span>native accessibility semantics
map</span>.</p></li>

<li><p>If <var>map</var>[<var>stateOrProperty</var>] <span data-x="map exists">exists</span>,
then return it.</p></li>

<li><p>Otherwise, return the default value for <var>stateOrProperty</var>.</p></li>
</ol>

<p class="note">The "default semantics" referred to here are sometimes also called "native",
"implicit", or "host language" semantics in <cite>ARIA</cite>. <ref spec=ARIA></p>

<p>For an example of this in action, see <a href="#custom-elements-accessibility-example">the
custom elements section</a>.</p>

<hr>

<p>Conformance checker requirements for checking use of ARIA <code
data-x="attr-aria-role">role</code> and <code data-x="attr-aria-*">aria-*</code> attributes on
Expand Down Expand Up @@ -65393,26 +65430,28 @@ document.body.appendChild(flagIcon)</code></pre>

<pre><code class="js">class MyCheckbox extends HTMLElement {
static get formAssociated() { return true; }
static get observedAttributes() { return ['checked']; }

constructor() {
super();
this._internals = this.attachInternals();
this._checked = false;
this.addEventListener('click', this._onClick.bind(this));
}

get form() { return this._internals.form; }
get name() { return this.getAttribute('name'); }
get type() { return this.localName; }

get checked() { return this._checked; }
set checked(flag) {
this._checked = !!flag;
this._internals.setFormValue(this._checked ? 'on' : null);
get checked() { return this.getAttribute('checked'); }
set checked(flag) { this.toggleAttribute('checked', Boolean(flag)); }

attributeChangedCallback(name, oldValue, newValue) {
// name will always be "checked" due to observedAttributes
this._internals.setFormValue(this.checked ? 'on' : null);
}

_onClick(event) {
this.checked = !this._checked;
this.checked = !this.checked;
}
}
customElements.define('my-checkbox', MyCheckbox);</code></pre>
Expand All @@ -65429,6 +65468,61 @@ customElements.define('my-checkbox', MyCheckbox);</code></pre>
&lt;/form>
</code></pre>

<h5 id="custom-elements-accessibility-example">Creating a custom element with default accessible roles, states, and properties</h5>

<!-- NON-NORMATIVE SECTION -->

<p>By using the appropriate properties of <code>ElementInternals</code>, your custom element can
have default accessibility semantics. The following code expands our form-associated checkbox from
the previous section to properly set its default role and checkedness, as viewed by accessibility
technology:</p>

<pre><code class="js" data-x="">class MyCheckbox extends HTMLElement {
static get formAssociated() { return true; }
static get observedAttributes() { return ['checked']; }

constructor() {
super();
this._internals = this.attachInternals();
this.addEventListener('click', this._onClick.bind(this));

<mark> this._internals.role = 'checkbox';
this._internals.ariaChecked = false;</mark>
}

get form() { return this._internals.form; }
get name() { return this.getAttribute('name'); }
get type() { return this.localName; }

get checked() { return this.getAttribute('checked'); }
set checked(flag) { this.toggleAttribute('checked', Boolean(flag)); }

attributeChangedCallback(name, oldValue, newValue) {
// name will always be "checked" due to observedAttributes
this._internals.setFormValue(this.checked ? 'on' : null);
<mark> this._internals.ariaChecked = this.checked;</mark>
}

_onClick(event) {
this.checked = !this.checked;
}
}
customElements.define('my-checkbox', MyCheckbox);</code></pre>

<p>Note that, like for built-in elements, these are only defaults, and can be overridden by the
page author using the <code data-x="attr-aria-role">role</code> and <code
data-x="attr-aria-*">aria-*</code> attributes:</p>

<pre class="bad"><code class="html" data-x="">&lt;!-- This markup is non-conforming -->
&lt;input type="checkbox" checked role="button" aria-checked="false"></code></pre>

<pre class="bad"><code class="html" data-x="">&lt;!-- This markup is probably not what the custom element author intended -->
&lt;my-checkbox role="button" checked aria-checked="false"></code></pre>

<p>Custom element authors are encouraged to state what aspects of their accessibility semantics
are strong native semantics, i.e., should not be overriden by users of the custom element. In our
example, the author of the <code data-x="">my-checkbox</code> element would state that its role
and aria-checked values are strong native semantics, thus discouraging code such as the above.</p>

<h5 id="custom-elements-customized-builtin-example">Creating a customized built-in element</h5>

Expand Down Expand Up @@ -65546,16 +65640,16 @@ console.log(plasticButton.outerHTML); // will output '&lt;button is="plastic-but
<code data-x="">taco-button</code> were to become logically disabled, the <code
data-x="attr-tabindex">tabindex</code> attribute would need to be removed.</p></li>

<li><p>The addition of various ARIA attributes helps convey semantics to accessibility
technology. For example, setting the <code data-x="attr-aria-role">role</code> attribute to
"<code data-x="attr-aria-role-button">button</code>" will convey the semantics that this is a
button, enabling users to successfully interact with the control using usual button-like
interactions in their accessibility technology. Setting the <code
data-x="attr-aria-label">aria-label</code> attribute is necessary to give the button an
<span data-x="concept-accessible-name">accessible name</span>, instead of having accessibility
technology traverse its child text nodes and announce them. And setting <code
data-x="attr-aria-disabled">aria-disabled</code> to "<code data-x="">true</code>" when the button
is logically disabled conveys to accessibility technology the button's disabled state.</p></li>
<li><p>The addition of an ARIA role and various ARIA states and properties helps convey semantics
to accessibility technology. For example, setting the <span>role</span> to "<code
data-x="attr-aria-role-button">button</code>" will convey the semantics that this is a button,
enabling users to successfully interact with the control using usual button-like interactions in
their accessibility technology. Setting the <code data-x="attr-aria-label">aria-label</code>
property is necessary to give the button an <span data-x="concept-accessible-name">accessible
name</span>, instead of having accessibility technology traverse its child text nodes and
announce them. And setting the <code data-x="attr-aria-disabled">aria-disabled</code> state to
"<code data-x="">true</code>" when the button is logically disabled conveys to accessibility
technology the button's disabled state.</p></li>

<li><p>The addition of event handlers to handle commonly-expected button behaviors helps convey
the semantics of the button to web browser users. In this case, the most relevant event handler
Expand All @@ -65579,9 +65673,11 @@ console.log(plasticButton.outerHTML); // will output '&lt;button is="plastic-but

constructor() {
super();
this._internals = this.attachInternals();
this._internals.role = "button";

this.addEventListener("keydown", e => {
if (e.keyCode === 32 || e.keyCode === 13) {
if (e.code === "Enter" || e.code === "Space") {
this.dispatchEvent(new MouseEvent("click", {
bubbles: true,
cancelable: true
Expand All @@ -65592,17 +65688,16 @@ console.log(plasticButton.outerHTML); // will output '&lt;button is="plastic-but
this.addEventListener("click", e => {
if (this.disabled) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
});

this._observer = new MutationObserver(() => {
this.setAttribute("aria-label", this.textContent);
this._internals.ariaLabel = this.textContent;
});
}

connectedCallback() {
this.setAttribute("role", "button");
this.setAttribute("tabindex", "0");

this._observer.observe(this, {
Expand All @@ -65619,33 +65714,29 @@ console.log(plasticButton.outerHTML); // will output '&lt;button is="plastic-but
get disabled() {
return this.hasAttribute("disabled");
}

set disabled(v) {
if (v) {
this.setAttribute("disabled", "");
} else {
this.removeAttribute("disabled");
}
set disabled(flag) {
this.toggleAttribute("disabled", Boolean(flag));
}

attributeChangedCallback() {
// only is called for the disabled attribute due to observedAttributes
attributeChangedCallback(name, oldValue, newValue) {
// name will always be "disabled" due to observedAttributes
if (this.disabled) {
this.removeAttribute("tabindex");
this.setAttribute("aria-disabled", "true");
this._internals.ariaDisabled = "true";
} else {
this.setAttribute("tabindex", "0");
this.setAttribute("aria-disabled", "false");
this._internals.ariaDisabled = "false";
}
}
}</code></pre>

<p>Even with this rather-complicated element definition, the element is not a pleasure to use for
consumers: it will be continually "sprouting" <code data-x="attr-tabindex">tabindex</code> and
<code data-x="attr-aria-*">aria-*</code> attributes of its own volition. This is because as of now
there is no way to specify default accessibility semantics or focus behavior for custom elements,
forcing the use of these attributes to do so (even though they are usually reserved for allowing
the consumer to override default behavior).</p>
consumers: it will be continually "sprouting" <code data-x="attr-tabindex">tabindex</code>
attributes of its own volition, and its choice of <code data-x="">tabindex="0"</code> focusability
behavior may not match the <code>button</code> behavior on the current platform. This is because
as of now there is no way to specify default focus behavior for custom elements, forcing the use
of the <code data-x="attr-tabindex">tabindex</code> attribute to do so (even though it is usually
reserved for allowing the consumer to override default behavior).</p>

<p>In contrast, a simple <span>customized built-in element</span>, as shown in the previous
section, would automatically inherit the semantics and behavior of the <code>button</code>
Expand Down Expand Up @@ -66988,6 +67079,8 @@ interface <dfn>ElementInternals</dfn> {
readonly attribute <span>NodeList</span> <span data-x="dom-ElementInternals-labels">labels</span>;
};

ElementInternals includes <span>ARIAMixin</span>;

dictionary <dfn>ValidityStateFlags</dfn> {
boolean valueMissing = false;
boolean typeMismatch = false;
Expand Down Expand Up @@ -67072,6 +67165,17 @@ dictionary <dfn>ValidityStateFlags</dfn> {
<dd><p>Returns a <code>NodeList</code> of all the <code>label</code> elements that
<var>internals</var>'s <span data-x="internals-target">target element</span> is associated
with.</p></dd>

<dt><var>internals</var> . <code data-x=""><a href="#dom-ElementInternals-accessibility-idl-get">role</a></code> [ = <var>value</var> ]</dt>
<dd><p>Sets or retrieves the default ARIA role for <var>internals</var>'s <span
data-x="internals-target">target element</span>, which will be used unless the page author
overrides it using the <code data-x="attr-aria-role">role</code> attribute.</p></dd>

<dt><var>internals</var> . <code data-x=""><a href="#dom-ElementInternals-accessibility-idl-get">aria*</a></code> [ = <var>value</var> ]</dt>
<dd><p>Sets or retrieves various default ARIA states or property values for
<var>internals</var>'s <span data-x="internals-target">target element</span>, which will be used
unless the page author overrides them using the <code data-x="attr-aria-*">aria-*</code>
attributes.</p></dd>
</dl>

<p>Each <code>ElementInternals</code> has a <dfn data-x="internals-target">target element</dfn>,
Expand Down Expand Up @@ -67281,6 +67385,55 @@ dictionary <dfn>ValidityStateFlags</dfn> {

</div>

<hr>

<p w-nohtml>By using the <code data-x="">role</code> and <code data-x="">aria*</code> properties
of <code>ElementInternals</code>, custom element can set default accessibile roles, states, and
property values for their custom element, similar to how native elements behave. See <a
href="#custom-elements-accessibility-example">the example above</a> for more details.</p>

<div w-nodev>

<p>Each <span>custom element</span> has a <dfn>native accessibility semantics map</dfn>, which is
a <span>map</span>, initially empty. See the <a href="#wai-aria">Requirements related to ARIA and
to platform accessibility APIs</a> section for information on how this impacts platform
accessibility APIs.</p>

<p><code>ElementInternals</code> includes the <code>ARIAMixin</code> mixin. The IDL attributes
provided by this mixin are used to manipulate the <span data-x="internals-target">target
element</span>'s <span>native accessibility semantics map</span>, as follows:</p>

<p id="dom-ElementInternals-accessibility-idl">To <span>get the accessibility IDL attribute</span>
for <code>ElementInternals</code>, given <var>internals</var>, <var>idlAttribute</var>, and
<var>contentAttribute</var>:</p>

<ol>
<li><p>Let <var>map</var> be <span>this</span>'s <span data-x="internals-target">target
element</span>'s <span>native accessibility semantics map</span>.</p></li>

<li><p>If <var>map</var>[<var>contentAttribute</var>] <span data-x="map exists">exists</span>,
then return it.</p></li>

<li><p>Return null.</p></li>
</ol>

<p>To <span>set the accessibility IDL attribute</span> for <code>ElementInternals</code>, given
<var>internals</var>, <var>idlAttribute</var>, <var>contentAttribute</var>, and
<var>value</var>:</p>

<ol>
<li><p>Let <var>map</var> be <span>this</span>'s <span data-x="internals-target">target
element</span>'s <span>native accessibility semantics map</span>.</p></li>

<li><p>If <var>value</var> is null, then <span data-x="map remove">remove</span>
<var>map</var>[<var>contentAttribute</var>].</p></li>

<li><p>Otherwise, <span data-x="map set">set</span> <var>map</var>[<var>contentAttribute</var>]
to <var>value</var>.</p></li>
</ol>

</div>

<h3 split-filename="semantics-other" id="common-idioms">Common idioms without dedicated elements</h3>

<h4 id="rel-up">Bread crumb navigation</h4>
Expand Down

0 comments on commit e95cc26

Please sign in to comment.