Skip to content

Commit f7a3dcc

Browse files
authored
Allow setting default accessibility semantics for custom elements
1 parent 140dd67 commit f7a3dcc

File tree

1 file changed

+198
-38
lines changed

1 file changed

+198
-38
lines changed

source

+198-38
Original file line numberDiff line numberDiff line change
@@ -3787,6 +3787,7 @@ a.setAttribute('href', 'https://example.com/'); // change the content attribute
37873787
attributes are defined in <cite>ARIA</cite>: <ref spec=ARIA></p>
37883788

37893789
<ul class="brief">
3790+
<li><dfn data-x-href="https://w3c.github.io/aria/#aria-checked"><code data-x="attr-aria-checked">aria-checked</code></dfn></li>
37903791
<li><dfn data-x-href="https://w3c.github.io/aria/#aria-describedby"><code data-x="attr-aria-describedby">aria-describedby</code></dfn></li>
37913792
<li><dfn data-x-href="https://w3c.github.io/aria/#aria-disabled"><code data-x="attr-aria-disabled">aria-disabled</code></dfn></li>
37923793
<li><dfn data-x-href="https://w3c.github.io/aria/#aria-label"><code data-x="attr-aria-label">aria-label</code></dfn></li>
@@ -3795,7 +3796,11 @@ a.setAttribute('href', 'https://example.com/'); // change the content attribute
37953796
<p>Finally, the following terms are defined <cite>ARIA</cite>: <ref spec=ARIA></p>
37963797

37973798
<ul class="brief">
3799+
<li><dfn data-x-href="https://w3c.github.io/aria/#dfn-role">role</dfn></li>
37983800
<li><dfn data-x-href="https://w3c.github.io/aria/#dfn-accessible-name" data-x="concept-accessible-name">accessible name</dfn></li>
3801+
<li>The <dfn data-x-href="https://w3c.github.io/aria/#ARIAMixin"><code>ARIAMixin</code></dfn> interface, with its associated
3802+
<dfn data-x-href="https://w3c.github.io/aria/#dfn-ariamixin-getter-steps"><code>ARIAMixin</code> getter steps</dfn> and
3803+
<dfn data-x-href="https://w3c.github.io/aria/#dfn-ariamixin-setter-steps"><code>ARIAMixin</code> setter steps</dfn> hooks</li>
37993804
</ul>
38003805
</dd>
38013806

@@ -12635,8 +12640,46 @@ interface <dfn>DOMStringMap</dfn> {
1263512640
<h4 id="wai-aria">Requirements related to ARIA and to platform accessibility APIs</h4>
1263612641

1263712642
<p>User agent requirements for implementing Accessibility API semantics on <span>HTML
12638-
elements</span> are defined in <cite>HTML Accessibility API Mappings</cite>. <ref
12639-
spec=HTMLAAM></p>
12643+
elements</span> are defined in <cite>HTML Accessibility API Mappings</cite>. In addition to the
12644+
rules there, for a <span>custom element</span> <var>element</var>, the default ARIA role
12645+
semantics are determined as follows: <ref spec=HTMLAAM></p>
12646+
12647+
<ol>
12648+
<li><p>Let <var>map</var> be <var>element</var>'s <span>native accessibility semantics
12649+
map</span>.</p></li>
12650+
12651+
<li><p>If <var>map</var>["<code data-x="">role</code>"] <span data-x="map exists">exists</span>,
12652+
then return it.</p></li>
12653+
12654+
<li><p>Return no role.</p></li>
12655+
</ol>
12656+
12657+
<p>Similarly, for a <span>custom element</span> <var>element</var>, the default ARIA state and
12658+
property semantics, for a state or property named <var>stateOrProperty</var>, are determined as
12659+
follows:</p>
12660+
12661+
<ol>
12662+
<li><p>Let <var>map</var> be <var>element</var>'s <span>native accessibility semantics
12663+
map</span>.</p></li>
12664+
12665+
<li><p>If <var>map</var>[<var>stateOrProperty</var>] <span data-x="map exists">exists</span>,
12666+
then return it.</p></li>
12667+
12668+
<li><p>Return the default value for <var>stateOrProperty</var>.</p></li>
12669+
</ol>
12670+
12671+
<p class="note">The "default semantics" referred to here are sometimes also called "native",
12672+
"implicit", or "host language" semantics in <cite>ARIA</cite>. <ref spec=ARIA></p>
12673+
12674+
<p class="note">One implication of these definitions is that the default semantics can change over
12675+
time. This allows custom elements the same expressivity as built-in elements; e.g., compare to how
12676+
the default ARIA role semantics of an <code>a</code> element change as the <code
12677+
data-x="attr-hyperlink-href">href</code> attribute is added or removed.</p>
12678+
12679+
<p>For an example of this in action, see <a href="#custom-elements-accessibility-example">the
12680+
custom elements section</a>.</p>
12681+
12682+
<hr>
1264012683

1264112684
<p>Conformance checker requirements for checking use of ARIA <code
1264212685
data-x="attr-aria-role">role</code> and <code data-x="attr-aria-*">aria-*</code> attributes on
@@ -65519,26 +65562,28 @@ document.body.appendChild(flagIcon)</code></pre>
6551965562

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

6552365567
constructor() {
6552465568
super();
6552565569
this._internals = this.attachInternals();
65526-
this._checked = false;
6552765570
this.addEventListener('click', this._onClick.bind(this));
6552865571
}
6552965572

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

65534-
get checked() { return this._checked; }
65535-
set checked(flag) {
65536-
this._checked = !!flag;
65537-
this._internals.setFormValue(this._checked ? 'on' : null);
65577+
get checked() { return this.getAttribute('checked'); }
65578+
set checked(flag) { this.toggleAttribute('checked', Boolean(flag)); }
65579+
65580+
attributeChangedCallback(name, oldValue, newValue) {
65581+
// name will always be "checked" due to observedAttributes
65582+
this._internals.setFormValue(this.checked ? 'on' : null);
6553865583
}
6553965584

6554065585
_onClick(event) {
65541-
this.checked = !this._checked;
65586+
this.checked = !this.checked;
6554265587
}
6554365588
}
6554465589
customElements.define('my-checkbox', MyCheckbox);</code></pre>
@@ -65555,6 +65600,62 @@ customElements.define('my-checkbox', MyCheckbox);</code></pre>
6555565600
&lt;/form>
6555665601
</code></pre>
6555765602

65603+
<h5 id="custom-elements-accessibility-example">Creating a custom element with default accessible roles, states, and properties</h5>
65604+
65605+
<!-- NON-NORMATIVE SECTION -->
65606+
65607+
<p>By using the appropriate properties of <code>ElementInternals</code>, your custom element can
65608+
have default accessibility semantics. The following code expands our form-associated checkbox from
65609+
the previous section to properly set its default role and checkedness, as viewed by accessibility
65610+
technology:</p>
65611+
65612+
<pre><code class="js" data-x="">class MyCheckbox extends HTMLElement {
65613+
static get formAssociated() { return true; }
65614+
static get observedAttributes() { return ['checked']; }
65615+
65616+
constructor() {
65617+
super();
65618+
this._internals = this.attachInternals();
65619+
this.addEventListener('click', this._onClick.bind(this));
65620+
65621+
<mark> this._internals.role = 'checkbox';
65622+
this._internals.ariaChecked = false;</mark>
65623+
}
65624+
65625+
get form() { return this._internals.form; }
65626+
get name() { return this.getAttribute('name'); }
65627+
get type() { return this.localName; }
65628+
65629+
get checked() { return this.getAttribute('checked'); }
65630+
set checked(flag) { this.toggleAttribute('checked', Boolean(flag)); }
65631+
65632+
attributeChangedCallback(name, oldValue, newValue) {
65633+
// name will always be "checked" due to observedAttributes
65634+
this._internals.setFormValue(this.checked ? 'on' : null);
65635+
<mark> this._internals.ariaChecked = this.checked;</mark>
65636+
}
65637+
65638+
_onClick(event) {
65639+
this.checked = !this.checked;
65640+
}
65641+
}
65642+
customElements.define('my-checkbox', MyCheckbox);</code></pre>
65643+
65644+
<p>Note that, like for built-in elements, these are only defaults, and can be overridden by the
65645+
page author using the <code data-x="attr-aria-role">role</code> and <code
65646+
data-x="attr-aria-*">aria-*</code> attributes:</p>
65647+
65648+
<pre class="bad"><code class="html" data-x="">&lt;!-- This markup is non-conforming -->
65649+
&lt;input type="checkbox" checked role="button" aria-checked="false"></code></pre>
65650+
65651+
<pre class="bad"><code class="html" data-x="">&lt;!-- This markup is probably not what the custom element author intended -->
65652+
&lt;my-checkbox role="button" checked aria-checked="false"></code></pre>
65653+
65654+
<p>Custom element authors are encouraged to state what aspects of their accessibility semantics
65655+
are strong native semantics, i.e., should not be overriden by users of the custom element. In our
65656+
example, the author of the <code data-x="">my-checkbox</code> element would state that its
65657+
<span>role</span> and <code data-x="attr-aria-checked">aria-checked</code> values are strong
65658+
native semantics, thus discouraging code such as the above.</p>
6555865659

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

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

65675-
<li><p>The addition of various ARIA attributes helps convey semantics to accessibility
65676-
technology. For example, setting the <code data-x="attr-aria-role">role</code> attribute to
65677-
"<code data-x="attr-aria-role-button">button</code>" will convey the semantics that this is a
65678-
button, enabling users to successfully interact with the control using usual button-like
65679-
interactions in their accessibility technology. Setting the <code
65680-
data-x="attr-aria-label">aria-label</code> attribute is necessary to give the button an
65681-
<span data-x="concept-accessible-name">accessible name</span>, instead of having accessibility
65682-
technology traverse its child text nodes and announce them. And setting <code
65683-
data-x="attr-aria-disabled">aria-disabled</code> to "<code data-x="">true</code>" when the button
65684-
is logically disabled conveys to accessibility technology the button's disabled state.</p></li>
65776+
<li><p>The addition of an ARIA role and various ARIA states and properties helps convey semantics
65777+
to accessibility technology. For example, setting the <span>role</span> to "<code
65778+
data-x="attr-aria-role-button">button</code>" will convey the semantics that this is a button,
65779+
enabling users to successfully interact with the control using usual button-like interactions in
65780+
their accessibility technology. Setting the <code data-x="attr-aria-label">aria-label</code>
65781+
property is necessary to give the button an <span data-x="concept-accessible-name">accessible
65782+
name</span>, instead of having accessibility technology traverse its child text nodes and
65783+
announce them. And setting the <code data-x="attr-aria-disabled">aria-disabled</code> state to
65784+
"<code data-x="">true</code>" when the button is logically disabled conveys to accessibility
65785+
technology the button's disabled state.</p></li>
6568565786

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

6570665807
constructor() {
6570765808
super();
65809+
this._internals = this.attachInternals();
65810+
this._internals.role = "button";
6570865811

6570965812
this.addEventListener("keydown", e => {
65710-
if (e.keyCode === 32 || e.keyCode === 13) {
65813+
if (e.code === "Enter" || e.code === "Space") {
6571165814
this.dispatchEvent(new MouseEvent("click", {
6571265815
bubbles: true,
6571365816
cancelable: true
@@ -65718,17 +65821,16 @@ console.log(plasticButton.outerHTML); // will output '&lt;button is="plastic-but
6571865821
this.addEventListener("click", e => {
6571965822
if (this.disabled) {
6572065823
e.preventDefault();
65721-
e.stopPropagation();
65824+
e.stopImmediatePropagation();
6572265825
}
6572365826
});
6572465827

6572565828
this._observer = new MutationObserver(() => {
65726-
this.setAttribute("aria-label", this.textContent);
65829+
this._internals.ariaLabel = this.textContent;
6572765830
});
6572865831
}
6572965832

6573065833
connectedCallback() {
65731-
this.setAttribute("role", "button");
6573265834
this.setAttribute("tabindex", "0");
6573365835

6573465836
this._observer.observe(this, {
@@ -65745,33 +65847,29 @@ console.log(plasticButton.outerHTML); // will output '&lt;button is="plastic-but
6574565847
get disabled() {
6574665848
return this.hasAttribute("disabled");
6574765849
}
65748-
65749-
set disabled(v) {
65750-
if (v) {
65751-
this.setAttribute("disabled", "");
65752-
} else {
65753-
this.removeAttribute("disabled");
65754-
}
65850+
set disabled(flag) {
65851+
this.toggleAttribute("disabled", Boolean(flag));
6575565852
}
6575665853

65757-
attributeChangedCallback() {
65758-
// only is called for the disabled attribute due to observedAttributes
65854+
attributeChangedCallback(name, oldValue, newValue) {
65855+
// name will always be "disabled" due to observedAttributes
6575965856
if (this.disabled) {
6576065857
this.removeAttribute("tabindex");
65761-
this.setAttribute("aria-disabled", "true");
65858+
this._internals.ariaDisabled = "true";
6576265859
} else {
6576365860
this.setAttribute("tabindex", "0");
65764-
this.setAttribute("aria-disabled", "false");
65861+
this._internals.ariaDisabled = "false";
6576565862
}
6576665863
}
6576765864
}</code></pre>
6576865865

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

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

67216+
ElementInternals includes <span>ARIAMixin</span>;
67217+
6711867218
dictionary <dfn>ValidityStateFlags</dfn> {
6711967219
boolean valueMissing = false;
6712067220
boolean typeMismatch = false;
@@ -67199,6 +67299,17 @@ dictionary <dfn>ValidityStateFlags</dfn> {
6719967299
<dd><p>Returns a <code>NodeList</code> of all the <code>label</code> elements that
6720067300
<var>internals</var>'s <span data-x="internals-target">target element</span> is associated
6720167301
with.</p></dd>
67302+
67303+
<dt><var>internals</var> . <code data-x=""><a href="#dom-ElementInternals-accessibility-idl-get">role</a></code> [ = <var>value</var> ]</dt>
67304+
<dd><p>Sets or retrieves the default ARIA role for <var>internals</var>'s <span
67305+
data-x="internals-target">target element</span>, which will be used unless the page author
67306+
overrides it using the <code data-x="attr-aria-role">role</code> attribute.</p></dd>
67307+
67308+
<dt><var>internals</var> . <code data-x=""><a href="#dom-ElementInternals-accessibility-idl-get">aria*</a></code> [ = <var>value</var> ]</dt>
67309+
<dd><p>Sets or retrieves various default ARIA states or property values for
67310+
<var>internals</var>'s <span data-x="internals-target">target element</span>, which will be used
67311+
unless the page author overrides them using the <code data-x="attr-aria-*">aria-*</code>
67312+
attributes.</p></dd>
6720267313
</dl>
6720367314

6720467315
<p>Each <code>ElementInternals</code> has a <dfn data-x="internals-target">target element</dfn>,
@@ -67408,6 +67519,55 @@ dictionary <dfn>ValidityStateFlags</dfn> {
6740867519

6740967520
</div>
6741067521

67522+
<hr>
67523+
67524+
<p w-nohtml>By using the <code data-x="">role</code> and <code data-x="">aria*</code> properties
67525+
of <code>ElementInternals</code>, custom element authors can set default accessibile roles,
67526+
states, and property values for their custom element, similar to how native elements behave. See
67527+
<a href="#custom-elements-accessibility-example">the example above</a> for more details.</p>
67528+
67529+
<div w-nodev>
67530+
67531+
<p>Each <span>custom element</span> has a <dfn>native accessibility semantics map</dfn>, which is
67532+
a <span>map</span>, initially empty. See the <a href="#wai-aria">Requirements related to ARIA and
67533+
to platform accessibility APIs</a> section for information on how this impacts platform
67534+
accessibility APIs.</p>
67535+
67536+
<p><code>ElementInternals</code> includes the <code>ARIAMixin</code> mixin. The accessors provided
67537+
by this mixin are used to manipulate the <span data-x="internals-target">target element</span>'s
67538+
<span>native accessibility semantics map</span>, as follows:</p>
67539+
67540+
<p id="dom-ElementInternals-accessibility-idl">The <span><code>ARIAMixin</code> getter
67541+
steps</span> for <code>ElementInternals</code>, given <var>internals</var>,
67542+
<var>idlAttribute</var>, and <var>contentAttribute</var>, are:</p>
67543+
67544+
<ol>
67545+
<li><p>Let <var>map</var> be <var>internals</var>'s <span data-x="internals-target">target
67546+
element</span>'s <span>native accessibility semantics map</span>.</p></li>
67547+
67548+
<li><p>If <var>map</var>[<var>contentAttribute</var>] <span data-x="map exists">exists</span>,
67549+
then return it.</p></li>
67550+
67551+
<li><p>Return null.</p></li>
67552+
</ol>
67553+
67554+
<p>The <span><code>ARIAMixin</code> setter steps</span> for <code>ElementInternals</code>, given
67555+
<var>internals</var>, <var>idlAttribute</var>, <var>contentAttribute</var>, and
67556+
<var>value</var>, are:</p>
67557+
67558+
<ol>
67559+
<li><p>Let <var>map</var> be <var>internals</var>'s <span data-x="internals-target">target
67560+
element</span>'s <span>native accessibility semantics map</span>.</p></li>
67561+
67562+
<li><p>If <var>value</var> is null, then <span data-x="map remove">remove</span>
67563+
<var>map</var>[<var>contentAttribute</var>].</p></li>
67564+
67565+
<li><p>Otherwise, <span data-x="map set">set</span> <var>map</var>[<var>contentAttribute</var>]
67566+
to <var>value</var>.</p></li>
67567+
</ol>
67568+
67569+
</div>
67570+
6741167571
<h3 split-filename="semantics-other" id="common-idioms">Common idioms without dedicated elements</h3>
6741267572

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

0 commit comments

Comments
 (0)