Listbox Select Widget Implementation Examples
This widget is based on the
Listbox with Grouped Options and the
Actions Menu Button examples
from the WAI ARIA Authoring Practices 1.2. It emulates most of the behaviors of an HTML select
element with
optgroup
containers.
Some of the “groups” in the first instance of this control on the page are collapsable. Other groups are static / non-collapsable. We attempt to ensure
a correct count of options via aria-posinset
and aria-setsize
and make group associations for options via
aria-describedby.
Typically, options should be static and selectable (unless disabled), but some of the options marking the group names in this implementation are toggles that expand the group. In a previous iteration, expandable groups were triggered by buttons (with a button role) using aria-expanded
to report state. Because JAWS will always toggle to Virtual PC Cursor/browse mode when a button is focused, even when the button is in a composite widget (in this case, in a listbox
), we were forced to use role="option"
on the buttons. In addition to triggering a 4.1.2 violation in axe, this will cause some screen readers to not report the expanded/collapsed state of the buttons, since aria-expanded
is not supported on elements with role="option"
. An ugly way around this would be to shim in some sort of aria-label or non-visible accessible text in order “fake” the state (🤮). In both instances on this page, we use aria-selected
to register the selected state of options.
Unlike a native select, selection only occurs via keyboard Spacebar or Enter or by clicking, not solely by roving
the focus via arrow keys. This allows a user to navigate the options list without having to make a selection. Escape collapses
the listbox and re-focuses the trigger aria-haspopup
button, which always displays the current selection. A click or focus
outside of the widget will also close it.
Listbox Select Implementation with Expandable Options (BAD)
A link that doesn't go anywhere, after the widget, as an anchor to test tabbing through or out of the widget.
Listbox Select Implementation with Staticly Open Options (GOOD)
Another link that doesn't go anywhere, after the widget, an anchor to test tabbing through or out of the widget.
Testing Results and Commentary
Environment: Testing conducted May, 2022. All technologies evaluated at most recently released versions as of testing date.
Summary: The BAD example is a kludge, and it shows in testing. Cardinality is confusing, as it is necessary to add the collapsible option elements into the total list count — thereby giving a confusing total number of options. In addition, not all screen readers are announcing the expanded/collapsed state of the expandable options, causing potential problems understanding one's position/orientation within the list. For iOS + VoiceOver, where semantics are minimal to begin with, the non-announcement of expanded/collapsed state of options would likely make the widget highly prone to causing incorrect entry or, minimally, user frustration.
The GOOD example is acceptable. Group affiliations and cardinality (when reported) are accurate and clear. The outlier is iOS + VoiceOver, which fails to report list boundaries or selected state. To make the widget boundaries clear, we could shim in visually hidden text announcing the beginning and end of the list. However, that may cause technical debt in the future, since the text for that announcement would need to be consistent across all uses of the component (difficult to guarantee) and would need to be internationalized.
Note: If this widget were used along with a dynamic search, an appropriate aria-live
announcement of number of results returned would be something like “X items, in Y groups.”
For the select with collapsible groups, better structures than a listbox with options are a tree or a menu. We have both an example of a tree-based select and an example of a menu-based select available, with test results and commentary.
-
NVDA + Firefox (BAD): Cardinality, button expansion, and groupings reported as expected. NVDA will report a grouping as you move in and out of it, in addition to reporting the association with the group via the
aria-describedby
association. Focus and browse modes are toggled between automatically, as expected. NVDA is not reporting the selected state of an option; however, it reports “not selected” for all non-selected options. - NVDA + Firefox (GOOD): Reports and navigates as expected — cardinality and groupings as described above. Selected behavior is also as above, with options reporting as not selected for all but the selected option. This appears to be a quirk of NVDA. Note that pressing Escape while in the menu is sometimes intercepted by the screen reader and can pop you out of focus mode, failing to close the control. It is unclear how to work around this, though it is not terribly disruptive to the screen reader experience and only occurs intermittently.
-
Narrator + Edge (BAD): Narrator does not automatically switch between “scan mode on” (like browse mode/Virtual PC Cursor mode) and scan mode off (pass-through/forms mode/PC cursor mode off). Narrator apparently does not dynamically recognize and mode toggle for widget roles, like NVDA and JAWS do. So for proper behavior, it is necessary to turn scan mode off (via Narrator + Spacebar). When scan mode is off (and the screen reader is passing control to the browser), the widget works as expected, announcing cardinality, selected state, button state, and registering groupings via the
aria-describedby
associations on the options. Note that Narrator is not announcing group affilation except via aria-describedby, unlike NVDA and JAWS, which announce groupings as you move into or out of them. -
Narrator + Edge (GOOD): Again, for the component to work properly, it is necessary to move out of scan mode. Once you do that, however, the experience is as described above, and there is full support for roles and states (with the exception of group affiliation announcements, outside of
aria-describedby
associations. -
JAWS + Chrome (BAD): Cardinality is reported as coded; however the collapsed options do not report the
aria-expanded
state, which makes it difficult to determine which options are expandable. (The user would need to intuit that the skipped option numbers meant that the preceding option was a collapsed option group.) -
JAWS + Chrome (GOOD): Cardinality and groupings are properly identified, both through announcement of the group when moved into and by the
aria-describedby
association. JAWS announces that you are in a list when focus moves into it and announces the number of options. Selected status is never announced but can be inferred upon selection, since the trigger button contents is updated (and announced). -
Android Talkback + Chrome (BAD): Cardinality is announced, though when in a group, the announcement is
posinset
of 0, that is, thesetsize
does not get announced when in a group. In addition, the collapsed/expanded state of the group buttons is not announced. This makes it hard to orient yourself when in the list. -
Android Talkback + Chrome (GOOD): The cardinality issues of the previous example are present in this one. However, group labels are skipped (not browsed) and so enumeration of the selectable options is accurate — no need to guess about how many are selectable, as in the previous example, where the expandable group button triggers were added to the overall size of the set of options. Group affiliation is announced via
aria-describedby
assocations. And when you move in an out of the list, the screen reader reports that you are in a list and the size of that list. So, eventhough like all mobile AT implementatons, you can walk off the bottom or top of the list (when using swipe navigation), you at least know when you enter and leave the list. -
iOS VoiceOver + Safari (BAD): No cardinality is reported. Neither is the collapsed/expanded states of groups. This would make it nearly impossible to determine whether an option was selectable or merely was used to expand a group. Group affiliation is announced because of the
aria-describedby
. The trigger button announces that a list is popped open. - iOS VoiceOver + Safari (GOOD): Group labels are browseable. This, along with the aria-describedby association, helps determine the items within a group. Like other mobile platforms, it is possible to walk off the top or bottom of the list.
- Mac VoiceOver + Safari (BAD): The groups announce, though the (only CSS) all-caps DOGS is spelled out in characters. The screen reader does not announce expanded or collabpsed options. So there is no simple way to determine if a group is expanded or collapsed. The screen reader announces that every element is a "text element." This appears to be a bug in VO, since a sample tested with the Chrome browser behaves similarly. The the non-grouped options are enumerated according to the posinset and setsize attributes but grouped have a stange count. It would appear it is trying to calculate all of the grouped options and summing them X of 9. The screen reader correctly announces that the trigger button references a listbox/that it is a listbox popup button.
- Mac VoiceOver + Safari (GOOD): As above with the exceptions that we don't have to worry about expanded/collapsed groups and the groups are not browsed to.