AriaListMixin

Exposes a list's currently-selected item to assistive technologies

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 → setStaterender 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).

Acai
Akee
Apple
Apricot
Avocado
Banana
Bilberry
Black sapote
Blackberry
Blackcurrant
Blood orange
Blueberry
Boysenberry
Cantaloupe
Cherimoya
Cherry
Chico fruit
Clementine
Cloudberry
Coconut
Crab apple
Cranberry
Cucumber
Currant
Damson
Date
Dragonfruit
Durian
Elderberry
Feijoa
Fig
Goji berry
Gooseberry
Grape
Grapefruit
Guava
Honeyberry
Honeydew
Horned melon
Huckleberry
Jabuticaba
Jackfruit
Jambul
Japanese plum
Jostaberry
Jujube
Juniper berry
Kiwifruit
Kumquat
Lemon
Lime
Longan
Loquat
Lychee
Mandarin
Mango
Mangosteen
Marionberry
Miracle fruit
Mulberry
Nance
Nectarine
Orange
Papaya
Passionfruit
Peach
Pear
Persimmon
Pineapple
Pineberry
Plantain
Plum
Pluot
Pomegranate
Pomelo
Purple mangosteen
Quince
Rambutan
Raspberry
Redcurrant
Salak
Salal berry
Salmonberry
Satsuma
Soursop
Star apple
Star fruit
Strawberry
Surinam cherry
Tamarillo
Tamarind
Tangerine
Ugli fruit
Watermelon
White currant
White sapote
Yuzu
Demo: A list box exposing selection state via AriaListMixin

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 sets aria-selected to false for all new items. This is required to adhere to the ARIA spec for roles like tab: "inactive tab elements [should] have their aria-selected attribute set to false". That is, it is insufficient for an element to omit the aria-selected attribute; it must exist and be set to false. 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 its aria-selected attribute.