Overview
Purpose: Help list-like components expose their selection state to screen readers and other assistive technologies via ARIA accessibility attributes. This allows components to satisfy the Gold Standard criteria Declared Semantics (Does the component expose its semantics by wrapping/extending a native element, or using ARIA roles, states, and properties?).
This mixin primarily works in the middle of the Elix render pipeline:
events → methods → setState ➞ render DOM → post-render
Expects the component to provide:
[internal.state].selectedIndex
property indicating the index of the currently selected item. This is usually provided by SingleSelectAPIMixin.items
property representing the items that can be selected. This is usually provided by ContentItemsMixin.
Provides the component with:
- internal.render method that applies ARIA attributes to the component's host element and its contained items.
Usage
import AriaListMixin from "elix/src/base/AriaListMixin.js";
class MyElement extends AriaListMixin(HTMLElement) {}
Elix mixins and components support universal access for all users. The work required to properly expose the selection state of a component in ARIA is complex, but thankfully fairly generalizable. AriaListMixin
provides a reasonable baseline implementation of ARIA support for list components. (Another important aspect of supporting universal access is to provide full keyboard support. See KeyboardMixin and its related mixins.)
If you would like help defining ARIA support for a menu-like element, see the related AriaMenuMixin.
Example
AriaListMixin
complements the model of selection formalized in the companion SingleSelectAPIMixin. A very simple element can expose its single-selection state via ARIA:
class AccessibleList extends AriaListMixin(
SingleSelectAPIMixin(ReactiveElement)
) {
// Simplistic definition for the component's items.
get items() {
return this.children;
}
}
customElements.define("accessible-list", AccessibleList);
Suppose the developer initially populates the DOM as follows:
<accessible-list aria-label="Fruits" tabindex="0">
<div>Apple</div>
<div>Banana</div>
<div>Cherry</div>
</accessible-list>
After the element is added to the page, the DOM result will be:
<accessible-list aria-label="Fruits" tabindex="0" role="listbox">
<div role="option" id="_option0" aria-selected="false">Apple</div>
<div role="option" id="_option1" aria-selected="false">Banana</div>
<div role="option" id="_option2" aria-selected="false">Cherry</div>
</accessible-list>
The AriaListMixin
has selected appropriate default values for the attributes role
, id
, aria-selected
. When the first item is selected, the DOM will update to:
<accessible-list
aria-label="Fruits"
tabindex="0"
role="listbox"
aria-activedescendant="_option0"
>
<div role="option" id="_option0" aria-selected="true">Apple</div>
<div role="option" id="_option1" aria-selected="false">Banana</div>
<div role="option" id="_option2" aria-selected="false">Cherry</div>
</accessible-list>
AriaListMixin
has updated the aria-selected
attribute of the selected item, and reflected this at the list level with aria-activedescendant
.
As shown above, some additional attributes must be manually set for ARIA to be useful. You should supply a meaningful, context-dependent label for the element with an aria-label
or aria-labeledby
attribute. You should also supply a tabindex
, or use a mixin like KeyboardMixin that defines a tabindex
for you.
As a demonstration, the following ListBox should be navigable with a keyboard and a screen reader such as Apple VoiceOver (usually invoked by pressing ⌘F5).
The mixin's primary work is setting ARIA attributes as follows.
role
attribute on the component and its items
The outer list-like component needs to have a role
assigned to it. For reference, the ARIA documentation defines the following roles for single-selection elements:
combobox
grid
listbox
menu
menubar
radiogroup
tablist
tree
treegrid
The most general purpose of these roles is listbox
, so unless otherwise specified, AriaListMixin
applies that role by default.
A suitable ARIA role must also be applied at the item level. The default role applied to items is option
, defined in the documentation as a selectable item in a list element with role listbox
.
In situations where different roles are defined, a component can provide default values as state, e.g. in defaultState:
import * as internal from 'elix/src/internal.js';
class TabList extends AriaListMixin(HTMLElement) {
get [internal.defaultState]() {
return Object.assign({}, super[internal.defaultState], {
itemRole: `tab`, // Pick a role for the items
role: `tablist` // Pick a role for the component
});
}
...
}
An app can override the role
on a per-instance basis by defining a role
attribute before adding the element to the page:
// Letting the mixin pick the role.
const tabList = new TabList();
document.appendChild(tabList);
tabList.getAttribute("role"); // "tablist" (this component's default role)
// Handling role on a per-element basis.
const menu = new TabList();
tabList.setAttribute("role", "menu");
document.appendChild(tabList);
tabList.getAttribute("role"); // "menu" (mixin left the role alone).
id
attribute on the items
ARIA references requires that a potentially selectable item have an id
attribute that can be used with aria-activedescendant
(see below). To that end, when the component renders, this mixin will generate and apply an id
attribute to any item in the list that doesn't already have an id
.
To minimize accidental id
collisions on a page, the generated default id
value for an item includes:
- An underscore prefix.
- The
id
attribute of the outer component, if one has been specified. - The word "option".
- An integer representing the item's index in the list.
Examples: a list with an id
of test
will produce default item IDs like _testOption7
. A list with no id
of its own will produce default item IDs like _option7
.
aria-activedescendant
attribute on the component
To let ARIA know which item is selected, the component must set its own aria-activedescendant
attribute to the id
attribute of the selected item. AriaListMixin
automatically handles that whenever the component's selectedItem
property is set.
aria-selected
attribute on the items
ARIA defines an aria-selected
attribute that should be set to true
on the currently-selected item, and false
on all other items. Therefore:
AriaListMixin
setsaria-selected
tofalse
for all new items. This is required to adhere to the ARIA spec for roles like tab: "inactive tab elements [should] have theiraria-selected
attribute set tofalse
". That is, it is insufficient for an element to omit thearia-selected
attribute; it must exist and be set tofalse
. This incurs a performance penalty, as every item must be touched by the mixin, but real-world experience indicates that screen readers do exist which require this behavior.When an item's selection state changes,
AriaListMixin
reflects its new state in itsaria-selected
attribute.
API
Used by classes Carousel, CarouselSlideshow, CarouselWithThumbnails, FilterListBox, ListBox, MultiSelectListBox, PlainCarousel, PlainCarouselSlideshow, PlainCarouselWithThumbnails, PlainFilterListBox, PlainListBox, PlainMultiSelectListBox, PlainSlideshowWithPlayControls, PlainSlidingPages, PlainTabStrip, SlideshowWithPlayControls, SlidingPages, and TabStrip.