A proof of concept to show that complex custom widgets can be implemented in both a visually appealing and accessible way using standard HTML form controls.
It is a collaboration between ETH Zürich and Nothing.
- Needs to feel and look like traditional solutions (similar to Select2), but instead of using ARIA excessively, we want to rely on basic HTML form controls and intelligent focus management.
- Needs to work for both single and multi selection.
- For single selection, we will use radio buttons.
- For multi selection, we will use checkboxes.
- In a first version, it needs to support desktop computers:
- Mouse usage (obviously)
- Keyboard-only usage (
Up
,Down
,Enter
,Space
,Esc
) - Screen readers: JAWS+Chrome, NVDA+Chrome, NVDA+FF
- In a later version, it also needs mobile support
- ✅ When entering a filter term into the text field, the checkboxes should be filtered (set HTML
hidden
attribute).- ✅ Update "X options available" to "X options available for XYZ" (where XYZ is the filter term).
- ✅ When pressing
Up
/Down
keys, the keyboard focus jumps between the filter text field and the checkboxes back and forth.- ✅ I'm unsure whether the focus should jump back to the text field when reaching the bottom, or just back to the first option. UPDATE: Thinking about it, we probably keep it like that, as it gives screen reader users an important hint (search wrapped).
- ✅ When a checkbox is checked, following elements are updated accordingly: the "Selected hobbies" fieldset's legend and contained buttons, the "X options selected" button, and the "Available hobbies" fieldset's legend.
- ✅ When
Esc
is pressed while a checkbox is focused, the focus is put to the "X options selected" button, and the dropdown is closed. - ✅ When the "X options selected" button is pressed, then all checkboxes are unchecked, and the focus is set to the filter text field (and obviously, all other dependent elements are updated accordingly).
- ✅ Please select all text (so the user can replace a filter term right away)
- ✅ When a button inside "Selected hobbies" is pressed, uncheck the respective checkbox, then:
- ✅ Focus the next button, if available.
- ✅ Or focus the previous button, if available.
- ✅ Or focus the text field.
- ✅ When
Page Up
/Page Down
is pressed (regardless whether inside the text field or when an option is focused), move focus to the very first/last option. - ✅ When
Enter
is pressed on a checkbox, toggle it (same functionality likeSpace
).- BUG: While the checkbox indeed is checked, the rest of the widget does not react (ie. the newly selected item is not added to the "Selected hobbies", etc.)
- ✅ When a checkbox is focused and a character key is pressed, then move focus back to the filter input and append the typed character.
- ✅ There are probably some "special keys" we need to implement, for example
Backspace
- any other that come to your mind?- ✅
Delete
will remove the filter text
- ✅
- ✅ There are probably some "special keys" we need to implement, for example
- ✅ The first time a filter is entered, add
role="alert"
to.widget--available-options-counter
(this will make screen readers announce it). - ✅ Set
hidden
tofieldset.selected
when there is no option selected. - ✅ I added
3 selected
to "X options available", please update accordingly. - ✅ When
Esc
is pressed while the "X options selected" button is focused, then move focus back to the filter input (and select all text). - ✅ When
Esc
is pressed while filter input is focused, close dropdown (if opened). - ✅ When clicking into the filter input, open dropdown (if closed).
- ✅ Simply add/remove its
hidden
attribute to toggle visibility.
- ✅ Simply add/remove its
- ✅ When clicking outside the filter input and the dropdown, close dropdown (if opened).
- ✅ When focusing the filter input by keyboard, keep dropdown as is.
- ✅ When pressing
Up
/Down
while the dropdown is closed, open it (and keep focus inside filter input). - ✅ When pressing
Up
/Down
while the dropdown is open, move focus to last/first checkbox.
- ✅ When pressing
- ✅ Keep
aria-expanded
in sync with the dropdown: set it totrue
when it is open, and tofalse
when it is closed. - ✅ When clicking
.widget--toggle-options-button
button, closefieldset.selected
, set focus to filter text field, and select all text (if there is any). - ✅ When typing a filter and the dropdown is closed, open it.
- ✅ Hide the "Unselect all" button when there is no option selected.
- ✅ When keyboard focus leaves the widget, close the dropdown.
- ✅ Make the "Open/Close" button toggle the dropdown.
- ✅ Throw a custom event
option-selected
/option-unselected
when an element is toggled- ✅ If possible with a reference to the checkbox object, so someone can catch the event and act upon the toggled element.
- ✅ Maybe just catch it somewhere below in the page and display something like "Option XYZ was selected/unselected"
- ✅ If possible with a reference to the checkbox object, so someone can catch the event and act upon the toggled element.
- Checkboxes visible (remove
data-visually-hidden
) - Bullet list numbers visible (remove
color: transparent
)
- I would find it cool to be able to press
Space
to toggle options andEnter
to close (confirm) an opened dropdown.- This might result in confusion for some people, if they think that
Enter
would toggle options, too. We might leverage this by displaying a small hint "Press Space to toggle options" when somebody hitsEnter
while no option is checked yet.
- This might result in confusion for some people, if they think that
- Another cool thing would be the ability to reset the whole element by pressing
Esc
inside the filter text field: if there is a filter text, it is removed, and when pressingEsc
another time, the whole element is reset (uncheck all checkboxes).- It might be good to show a confirmation "Do you really want to reset the element?" before doing that.
When there is no filter term, setX options selected
asplaceholder
- Would be kinda cool for a quick visual info, but goes against the advise that a placeholder should always show an example of some data to input.
- As
Enter
is intercepted by NVDA and JAWS, we cannot listen to this key on a checkbox (in contrast to radio buttons, checkboxes do not trigger focus mode), which does not allow us to useEnter
to confirm-and-close => butEsc
works, so we can use this instead (instead of cancel-and-close) (more info) - I'm unsure about
Enter
on<li>
=> is this intercepted, too? While JAWS seems to toggle the checkbox in this situation, NVDA does not seem to... - The same holds true for
Arrow
keys, but the good thing is thatUp
/Down
will move the screen reader cursor anyway between the list items (options). - Live regions are still our "week spot", as expected:
- Although
aria-live
would be a much better replacement forrole="alert"
, JAWS seems to not support it, at least when just updating its contents! Maybe removing the whole element and then adding it back would trigger JAWS as expected? - Putting
role="alert"
seems to have some quirky effects:- While Chrome announces an alert immediately when loading the page, FF does not.
- UPDATE: We fix this by adding the attribute not at document load, but on first its update. UPDATE2: This is only the case when the
role
element is visible! This is not relevant anymore, as the element is hidden on document load.
- UPDATE: We fix this by adding the attribute not at document load, but on first its update. UPDATE2: This is only the case when the
- While FF announces "alert" plus the actual content of the element, Chrome does only announce its content.
- While Chrome announces an alert immediately when loading the page, FF does not.
- Conclusion: We could try to adapt to different browsers to optimise the user experience for screen readers, e.g. we could use
aria-live
in FF androle="alert"
in Chrome? If everything else fails, we just userole="alert"
everywhere (it's not the most beautiful option, but it works).
- Although
- The interplay between
aria-describedby
, putting "everything" into a<label>
, and having evenrole="alert"
mingled into one or the other, seems to have quite a variety of behaviours, depending on the combos of browsers and screen readers.- I vote for putting all relevant info (ie. "X options available, X selected") into the
<label>
. Some combos seem to announce changes inside those elements, others do not. - I tinkered around with
aria-describedby
instead, but this did not lead to better results. I vote not to use it, as it requires us to throw IDs into the code. - Having
role="alert"
inside those elements results in redundant announcements in some combos, as first the content of the alert is announced, and then also the whole<label>
/aria-describedby
(incl. the alert).- I think in general this is alright, as it just adds more context to the widget's current state.
- I'm not completely sure whether
aria-live
has the same effect.
- Conclusion: In the end, we will have to play around a bit with different solutions and see which one works best across all targeted browsers and screen readers. There might be no perfect solution, but in the end: as long as it's working as expected (and in a robust, predictable way), all screen reader users will be more than happy, even if there's a bit of redundancy in the announcements (which they can simply skip by further interacting with the element).
- I vote for putting all relevant info (ie. "X options available, X selected") into the
- In Chrome, JAWS jumps to the 2nd option when pressing
Down
inside the filter text field. This is unfortunate. NeitherstopPropagation
norpreventDefault
seems to change this. In general I'd say: if a screen reader obviously causes some simple standard HTML functionality to produce a bug, then it is their problem, not ours (in contrast to using heavy ARIA stuff, where WE would need to make sure that the functionality works as expected). - In general, NVDA is much more verbose than JAWS:
- NVDA announces changes to
<label>
of the currently focused input (this would allow to just put announcements into<label>
, instead of usingrole="alert"
) - When moving focus to an element, NVDA announces a lot of info that is related to this element, like a surrounding
<fieldset>
with<legend>
, an<ol>
with its number of items, etc. - General question: is this expected behaviour? Why is JAWS so conservative in announcing this useful associated info? Maybe we can ask Quentin about this.
- NVDA announces changes to
- Having the options'
<label>
s withdisplay: inline
has the nice effect that NVDA reads the number of the<li>
and all of its contents (checkbox with label) in one go, but for some reason we cannot activate it anymore withSpace
/Enter
in NVDA. Only if we usedisplay: block
it works, but then it splits the announcement into 1) the number of the<li>
, and 2) its contents.- UPDATE: I set
list-style: none
to "defuse" this situation completely. It is enough that screen readers can announce the total number of options, and the "search wrapped" info is given by placing the focus in the text field again.
- UPDATE: I set
- Is "uncheck" or "unselect" the right word when talking about checkboxes??
- Our old beloved friend
aria-expanded
does not seem to work anymore on plain<input type="text">
elements (at least in Chrome it does not), see https://a11ysupport.io/tech/aria/aria-expanded_attribute and https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-expanded.- When adding
role="combobox"
, it seems to work again. While I try to avoid anyrole
(they often used to f*ck up JAWS in earlier days), it seems to be safe here.- Should we care about additional ARIA attributes here, like
aria-controls
oraria-autocomplete
? I think rather not, because in fact we do not offer a "real" ARIA control, but just "misuse" ARIA to announce it as such, while the rest of the interaction is plain HTML and JavaScript.
- Should we care about additional ARIA attributes here, like
- Another option could be to only display "X options available" when expanding the list of options (instead of announcing it when focusing the filter text input), together with
role="alert"
.
- When adding
- The use of "advanced" CSS still seems to be dangerous: toggling some content inside
::after
when toggling a checkbox breaks the announcement of checked / not checked in Chrome! We better work around this with toggling an additional<span>
or similar... - In JAWS + FF, focus mode seems to be on when focusing a checkbox (test by hitting a character => it will be appended to filter)! This is very surprising, as all other combos don't do this!
- Chrome has a strange bug (regardless of NVDA or JAWS): the live region is sometimes not announced when the filter is focused (by keyboard) and then "a" or "d" is typed. Strange enough, when "f" is pressed, it seems to be announced all the time (it might have to do with the number of option displayed, or no options at all).
aria-autocomplete="list"
makes some screen readers announce the element as "has auto complete", which is nice.aria-live
does not work the first time it is un-hidden! Unfortunately, we need asetTimeout
to make it work.- VoiceOver/iOS needs the live region to exist early! Placing it at the page load seems optimal, otherwise it does not seem to be recognised reliantly.
- VoiceOver/iOS announces the existing content of a live region when unhiding it after page reload. JAWS/NVDA seem to need an actual change to the content, otherwise they don't announce it.
- If possible, use same texts for both visual and screen reader users!
- Apply role=alert (or aria-live) only the first time such an alert is displayed, otherwise some screen readers announce it when loading the page.
- The live region element must be displayed BEFORE its content is changed, otherwise some screen readers don't get the change.
- On the top container, always add/remove classes that describe what's going on inside the widget (ie.
filter-focused
,available-options-open
,available-options-focused
,selected-options-focused
)
- A typical single and multi selection use case from the ETH can be found here: https://www.bi.id.ethz.ch/pcm-open-services/
- A former proof of concept for a single select autocomplete (it keeps the focus inside the text field when walking through options): https://www.accessibility-developer-guide.com/examples/widgets/autosuggest/_examples/autosuggest-with-radio-buttons/
- A similar proof of concept, namely a date picker, which actually moves the focus into the dropdown when walking through options: https://www.accessibility-developer-guide.com/examples/widgets/datepicker/_examples/datepicker-with-radio-buttons/