Skip to content

Commit e95cc26

Browse files
committed
Allow setting default accessibility semantics for custom elements
1 parent c8d5ad3 commit e95cc26

File tree

1 file changed

+191
-38
lines changed

1 file changed

+191
-38
lines changed

source

+191-38
Original file line numberDiff line numberDiff line change
@@ -3800,7 +3800,11 @@ a.setAttribute('href', 'https://example.com/'); // change the content attribute
38003800
<p>Finally, the following terms are defined <cite>ARIA</cite>: <ref spec=ARIA></p>
38013801

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

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

1264512649
<p>User agent requirements for implementing Accessibility API semantics on <span>HTML
12646-
elements</span> are defined in <cite>HTML Accessibility API Mappings</cite>. <ref
12647-
spec=HTMLAAM></p>
12650+
elements</span> are defined in <cite>HTML Accessibility API Mappings</cite>. In addition to the
12651+
rules there, for a <span>custom element</span> <var>element</var>, the default ARIA role
12652+
semantics are determined as follows: <ref spec=HTMLAAM></p>
12653+
12654+
<ol>
12655+
<li><p>Let <var>map</var> be <var>element</var>'s <span>native accessibility semantics
12656+
map</span>.</p></li>
12657+
12658+
<li><p>If <var>map</var>["<code data-x="">role</code>"] <span data-x="map exists">exists</span>,
12659+
then return it.</p></li>
12660+
12661+
<li><p>Otherwise, return no role.</p></li>
12662+
</ol>
12663+
12664+
<p>Similarly, for a <span>custom element</span> <var>element</var>, the default ARIA state and
12665+
property semantics, for a state or property named <var>stateOrProperty</var>, are determined as
12666+
follows:</p>
12667+
12668+
<ol>
12669+
<li><p>Let <var>map</var> be <var>element</var>'s <span>native accessibility semantics
12670+
map</span>.</p></li>
12671+
12672+
<li><p>If <var>map</var>[<var>stateOrProperty</var>] <span data-x="map exists">exists</span>,
12673+
then return it.</p></li>
12674+
12675+
<li><p>Otherwise, return the default value for <var>stateOrProperty</var>.</p></li>
12676+
</ol>
12677+
12678+
<p class="note">The "default semantics" referred to here are sometimes also called "native",
12679+
"implicit", or "host language" semantics in <cite>ARIA</cite>. <ref spec=ARIA></p>
12680+
12681+
<p>For an example of this in action, see <a href="#custom-elements-accessibility-example">the
12682+
custom elements section</a>.</p>
12683+
12684+
<hr>
1264812685

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

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

6539765435
constructor() {
6539865436
super();
6539965437
this._internals = this.attachInternals();
65400-
this._checked = false;
6540165438
this.addEventListener('click', this._onClick.bind(this));
6540265439
}
6540365440

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

65408-
get checked() { return this._checked; }
65409-
set checked(flag) {
65410-
this._checked = !!flag;
65411-
this._internals.setFormValue(this._checked ? 'on' : null);
65445+
get checked() { return this.getAttribute('checked'); }
65446+
set checked(flag) { this.toggleAttribute('checked', Boolean(flag)); }
65447+
65448+
attributeChangedCallback(name, oldValue, newValue) {
65449+
// name will always be "checked" due to observedAttributes
65450+
this._internals.setFormValue(this.checked ? 'on' : null);
6541265451
}
6541365452

6541465453
_onClick(event) {
65415-
this.checked = !this._checked;
65454+
this.checked = !this.checked;
6541665455
}
6541765456
}
6541865457
customElements.define('my-checkbox', MyCheckbox);</code></pre>
@@ -65429,6 +65468,61 @@ customElements.define('my-checkbox', MyCheckbox);</code></pre>
6542965468
&lt;/form>
6543065469
</code></pre>
6543165470

65471+
<h5 id="custom-elements-accessibility-example">Creating a custom element with default accessible roles, states, and properties</h5>
65472+
65473+
<!-- NON-NORMATIVE SECTION -->
65474+
65475+
<p>By using the appropriate properties of <code>ElementInternals</code>, your custom element can
65476+
have default accessibility semantics. The following code expands our form-associated checkbox from
65477+
the previous section to properly set its default role and checkedness, as viewed by accessibility
65478+
technology:</p>
65479+
65480+
<pre><code class="js" data-x="">class MyCheckbox extends HTMLElement {
65481+
static get formAssociated() { return true; }
65482+
static get observedAttributes() { return ['checked']; }
65483+
65484+
constructor() {
65485+
super();
65486+
this._internals = this.attachInternals();
65487+
this.addEventListener('click', this._onClick.bind(this));
65488+
65489+
<mark> this._internals.role = 'checkbox';
65490+
this._internals.ariaChecked = false;</mark>
65491+
}
65492+
65493+
get form() { return this._internals.form; }
65494+
get name() { return this.getAttribute('name'); }
65495+
get type() { return this.localName; }
65496+
65497+
get checked() { return this.getAttribute('checked'); }
65498+
set checked(flag) { this.toggleAttribute('checked', Boolean(flag)); }
65499+
65500+
attributeChangedCallback(name, oldValue, newValue) {
65501+
// name will always be "checked" due to observedAttributes
65502+
this._internals.setFormValue(this.checked ? 'on' : null);
65503+
<mark> this._internals.ariaChecked = this.checked;</mark>
65504+
}
65505+
65506+
_onClick(event) {
65507+
this.checked = !this.checked;
65508+
}
65509+
}
65510+
customElements.define('my-checkbox', MyCheckbox);</code></pre>
65511+
65512+
<p>Note that, like for built-in elements, these are only defaults, and can be overridden by the
65513+
page author using the <code data-x="attr-aria-role">role</code> and <code
65514+
data-x="attr-aria-*">aria-*</code> attributes:</p>
65515+
65516+
<pre class="bad"><code class="html" data-x="">&lt;!-- This markup is non-conforming -->
65517+
&lt;input type="checkbox" checked role="button" aria-checked="false"></code></pre>
65518+
65519+
<pre class="bad"><code class="html" data-x="">&lt;!-- This markup is probably not what the custom element author intended -->
65520+
&lt;my-checkbox role="button" checked aria-checked="false"></code></pre>
65521+
65522+
<p>Custom element authors are encouraged to state what aspects of their accessibility semantics
65523+
are strong native semantics, i.e., should not be overriden by users of the custom element. In our
65524+
example, the author of the <code data-x="">my-checkbox</code> element would state that its role
65525+
and aria-checked values are strong native semantics, thus discouraging code such as the above.</p>
6543265526

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

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

65549-
<li><p>The addition of various ARIA attributes helps convey semantics to accessibility
65550-
technology. For example, setting the <code data-x="attr-aria-role">role</code> attribute to
65551-
"<code data-x="attr-aria-role-button">button</code>" will convey the semantics that this is a
65552-
button, enabling users to successfully interact with the control using usual button-like
65553-
interactions in their accessibility technology. Setting the <code
65554-
data-x="attr-aria-label">aria-label</code> attribute is necessary to give the button an
65555-
<span data-x="concept-accessible-name">accessible name</span>, instead of having accessibility
65556-
technology traverse its child text nodes and announce them. And setting <code
65557-
data-x="attr-aria-disabled">aria-disabled</code> to "<code data-x="">true</code>" when the button
65558-
is logically disabled conveys to accessibility technology the button's disabled state.</p></li>
65643+
<li><p>The addition of an ARIA role and various ARIA states and properties helps convey semantics
65644+
to accessibility technology. For example, setting the <span>role</span> to "<code
65645+
data-x="attr-aria-role-button">button</code>" will convey the semantics that this is a button,
65646+
enabling users to successfully interact with the control using usual button-like interactions in
65647+
their accessibility technology. Setting the <code data-x="attr-aria-label">aria-label</code>
65648+
property is necessary to give the button an <span data-x="concept-accessible-name">accessible
65649+
name</span>, instead of having accessibility technology traverse its child text nodes and
65650+
announce them. And setting the <code data-x="attr-aria-disabled">aria-disabled</code> state to
65651+
"<code data-x="">true</code>" when the button is logically disabled conveys to accessibility
65652+
technology the button's disabled state.</p></li>
6555965653

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

6558065674
constructor() {
6558165675
super();
65676+
this._internals = this.attachInternals();
65677+
this._internals.role = "button";
6558265678

6558365679
this.addEventListener("keydown", e => {
65584-
if (e.keyCode === 32 || e.keyCode === 13) {
65680+
if (e.code === "Enter" || e.code === "Space") {
6558565681
this.dispatchEvent(new MouseEvent("click", {
6558665682
bubbles: true,
6558765683
cancelable: true
@@ -65592,17 +65688,16 @@ console.log(plasticButton.outerHTML); // will output '&lt;button is="plastic-but
6559265688
this.addEventListener("click", e => {
6559365689
if (this.disabled) {
6559465690
e.preventDefault();
65595-
e.stopPropagation();
65691+
e.stopImmediatePropagation();
6559665692
}
6559765693
});
6559865694

6559965695
this._observer = new MutationObserver(() => {
65600-
this.setAttribute("aria-label", this.textContent);
65696+
this._internals.ariaLabel = this.textContent;
6560165697
});
6560265698
}
6560365699

6560465700
connectedCallback() {
65605-
this.setAttribute("role", "button");
6560665701
this.setAttribute("tabindex", "0");
6560765702

6560865703
this._observer.observe(this, {
@@ -65619,33 +65714,29 @@ console.log(plasticButton.outerHTML); // will output '&lt;button is="plastic-but
6561965714
get disabled() {
6562065715
return this.hasAttribute("disabled");
6562165716
}
65622-
65623-
set disabled(v) {
65624-
if (v) {
65625-
this.setAttribute("disabled", "");
65626-
} else {
65627-
this.removeAttribute("disabled");
65628-
}
65717+
set disabled(flag) {
65718+
this.toggleAttribute("disabled", Boolean(flag));
6562965719
}
6563065720

65631-
attributeChangedCallback() {
65632-
// only is called for the disabled attribute due to observedAttributes
65721+
attributeChangedCallback(name, oldValue, newValue) {
65722+
// name will always be "disabled" due to observedAttributes
6563365723
if (this.disabled) {
6563465724
this.removeAttribute("tabindex");
65635-
this.setAttribute("aria-disabled", "true");
65725+
this._internals.ariaDisabled = "true";
6563665726
} else {
6563765727
this.setAttribute("tabindex", "0");
65638-
this.setAttribute("aria-disabled", "false");
65728+
this._internals.ariaDisabled = "false";
6563965729
}
6564065730
}
6564165731
}</code></pre>
6564265732

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

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

67082+
ElementInternals includes <span>ARIAMixin</span>;
67083+
6699167084
dictionary <dfn>ValidityStateFlags</dfn> {
6699267085
boolean valueMissing = false;
6699367086
boolean typeMismatch = false;
@@ -67072,6 +67165,17 @@ dictionary <dfn>ValidityStateFlags</dfn> {
6707267165
<dd><p>Returns a <code>NodeList</code> of all the <code>label</code> elements that
6707367166
<var>internals</var>'s <span data-x="internals-target">target element</span> is associated
6707467167
with.</p></dd>
67168+
67169+
<dt><var>internals</var> . <code data-x=""><a href="#dom-ElementInternals-accessibility-idl-get">role</a></code> [ = <var>value</var> ]</dt>
67170+
<dd><p>Sets or retrieves the default ARIA role for <var>internals</var>'s <span
67171+
data-x="internals-target">target element</span>, which will be used unless the page author
67172+
overrides it using the <code data-x="attr-aria-role">role</code> attribute.</p></dd>
67173+
67174+
<dt><var>internals</var> . <code data-x=""><a href="#dom-ElementInternals-accessibility-idl-get">aria*</a></code> [ = <var>value</var> ]</dt>
67175+
<dd><p>Sets or retrieves various default ARIA states or property values for
67176+
<var>internals</var>'s <span data-x="internals-target">target element</span>, which will be used
67177+
unless the page author overrides them using the <code data-x="attr-aria-*">aria-*</code>
67178+
attributes.</p></dd>
6707567179
</dl>
6707667180

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

6728267386
</div>
6728367387

67388+
<hr>
67389+
67390+
<p w-nohtml>By using the <code data-x="">role</code> and <code data-x="">aria*</code> properties
67391+
of <code>ElementInternals</code>, custom element can set default accessibile roles, states, and
67392+
property values for their custom element, similar to how native elements behave. See <a
67393+
href="#custom-elements-accessibility-example">the example above</a> for more details.</p>
67394+
67395+
<div w-nodev>
67396+
67397+
<p>Each <span>custom element</span> has a <dfn>native accessibility semantics map</dfn>, which is
67398+
a <span>map</span>, initially empty. See the <a href="#wai-aria">Requirements related to ARIA and
67399+
to platform accessibility APIs</a> section for information on how this impacts platform
67400+
accessibility APIs.</p>
67401+
67402+
<p><code>ElementInternals</code> includes the <code>ARIAMixin</code> mixin. The IDL attributes
67403+
provided by this mixin are used to manipulate the <span data-x="internals-target">target
67404+
element</span>'s <span>native accessibility semantics map</span>, as follows:</p>
67405+
67406+
<p id="dom-ElementInternals-accessibility-idl">To <span>get the accessibility IDL attribute</span>
67407+
for <code>ElementInternals</code>, given <var>internals</var>, <var>idlAttribute</var>, and
67408+
<var>contentAttribute</var>:</p>
67409+
67410+
<ol>
67411+
<li><p>Let <var>map</var> be <span>this</span>'s <span data-x="internals-target">target
67412+
element</span>'s <span>native accessibility semantics map</span>.</p></li>
67413+
67414+
<li><p>If <var>map</var>[<var>contentAttribute</var>] <span data-x="map exists">exists</span>,
67415+
then return it.</p></li>
67416+
67417+
<li><p>Return null.</p></li>
67418+
</ol>
67419+
67420+
<p>To <span>set the accessibility IDL attribute</span> for <code>ElementInternals</code>, given
67421+
<var>internals</var>, <var>idlAttribute</var>, <var>contentAttribute</var>, and
67422+
<var>value</var>:</p>
67423+
67424+
<ol>
67425+
<li><p>Let <var>map</var> be <span>this</span>'s <span data-x="internals-target">target
67426+
element</span>'s <span>native accessibility semantics map</span>.</p></li>
67427+
67428+
<li><p>If <var>value</var> is null, then <span data-x="map remove">remove</span>
67429+
<var>map</var>[<var>contentAttribute</var>].</p></li>
67430+
67431+
<li><p>Otherwise, <span data-x="map set">set</span> <var>map</var>[<var>contentAttribute</var>]
67432+
to <var>value</var>.</p></li>
67433+
</ol>
67434+
67435+
</div>
67436+
6728467437
<h3 split-filename="semantics-other" id="common-idioms">Common idioms without dedicated elements</h3>
6728567438

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

0 commit comments

Comments
 (0)