Skip to content

Conversation

eatyourgreens
Copy link
Contributor

@eatyourgreens eatyourgreens commented Apr 20, 2022

Accessibility improvements to TextInput when suggestions are present, adding ARIA roles and states for a combobox with a list of selectable options.

What does this PR do?

Add the combobox and listbox roles to TextInput, plus their required ARIA states, when suggestions are present. The changes should follow the WAI-ARIA 1.2 design pattern for a combobox with manual autocomplete.
https://w3c.github.io/aria-practices/examples/combobox/combobox-autocomplete-list.html

Where should the reviewer start?

The changes are all in src/components/js/TextInput/Textinput.js and the TextInput snapshots.

What testing has been done on this PR?

Storybook: I've tried out the TextInput with suggestions and with a default suggestion in Safari/VoiceOver and Firefox/VoiceOver. I don't have access to a Windows machine with a screenreader, so I haven't tested JAWS or NVDA.

How should this be manually tested?

Use the TextInput stories with and without a screenreader. Screenreaders should announce the controls as a combobox, with options, for the stories where the text input has suggestions. Otherwise, the behaviour of the text input should not have changed.

I've also made a codepen with the WAI-ARIA 1.2 example combobox, which I found useful to compare screenreader behaviour.
https://codepen.io/eatyourgreens/full/abEQrBg

Do Jest tests follow these best practices?

  • screen is used for querying.
  • The correct query is used. (Refer to this list of queries)
  • userEvent is used in place of fireEvent.
  • asFragment() is used for snapshot testing.

Any background context you want to provide?

What are the relevant issues?

Screenshots (if appropriate)

Do the grommet docs need to be updated?

No.

Should this PR be mentioned in the release notes?

Yes, maybe as accessibility improvements for text inputs.

Is this change backwards compatible or is it a breaking change?

It should be backwards compatible. One possible breaking change is that VoiceOver opens a combobox's options with ctrl-alt-Space. I noticed, while testing in VoiceOver, that pressing Space selects the current active option so there could be a conflict of keyboard handlers there.

Copy link
Collaborator

@jcfilben jcfilben left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenreader output looks good. One thing I noticed was that previously when using a screenreader you could select one of the suggestions with 'ctrl-option-space' (when using voice over on mac). In this pull request when I hit 'ctrl-option-space' nothing happens. I think we should preserve the behavior of 'ctrl-option-space' selecting an option

@eatyourgreens
Copy link
Contributor Author

eatyourgreens commented Apr 21, 2022

@jcfilben ctrl-opt-space is the command to list the listbox options in VoiceOver, so I think there’s a clash there.

You can compare it against the WAI-ARIA 1.2 example here: https://codepen.io/eatyourgreens/full/abEQrBg

EDIT: I'm seeing different behaviour for ctrl-option-space between the codepen example and the Grommet text input. In Grommet, ctrl-option-space does nothing but in the codepen example it confirms my current selection.

Screenshot of the WAI-ARIA 1.2 combobox with manual autocomplete example. ctrl-option-space prompts me to confirm Alaska as the selected US state from a list of states.

@eatyourgreens
Copy link
Contributor Author

eatyourgreens commented Apr 21, 2022

Although I'm unable to test in JAWS and NVDA, I found a comprehensive set of combobox tests on https://a11ysupport.io. It looks like VoiceOver/Safari has partial support for selecting an option, but support is good in JAWS and NVDA.

@jcfilben
Copy link
Collaborator

Looking through the tests on the a11ysupport site I couldn't find the one where they are testing selecting an option from the listbox, there are a lot of tests so I definitely could have missed it. Can you send the name of the test you are looking at that shows partial support for selecting an option with voice over?

Looking at these listbox examples on w3.org I am able to navigate with ctrl-option-(up or down arrow) and then select an option with ctrl-option-space. From the examples I have seen ctrl-option-space is used to select an option, I haven't seen it used to list the options in the listbox. (Testing with voiceover in chrome browser)

@eatyourgreens
Copy link
Contributor Author

My mistake. I was looking at the tests for aria-activedescendant, which should track the active option when you navigate the list.
https://a11ysupport.io/tests/apg__aria-1-2-combobox-with-list-autocomplete-example/aria__aria-activedescendant_attribute/convey_value/vo_macos/safari

When I use ctrl-opt-shift on the w3c example, VoiceOver announces “confirm Alaska, State, list box popup” but the Grommet TextInput does nothing. The only difference that jumps out to me is that the Grommet implementation has buttons inside the options. Maybe my using a clickable button inside a clickable option is causing problems? I could see the equivalent HTML causing problems, because of nested clickable elements.

<select>
  <option><button>Alaska</button></option></select>

@eatyourgreens
Copy link
Contributor Author

I saw that Grommet Select uses listbox, so I’ll take a look at that to see how the option roles are handled. I thought a listbox was required to have options as children but I am not 100% sure of that now.

Comment on lines 367 to 369
id={`${id}-listbox-option-${index}`}
role="option"
aria-selected={selected ? 'true' : undefined}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking we should move role="option" and aria-selected={selected ? 'true' : undefined} to be on the Button instead of the li. Also not sure if we need to have the id, I wasn't seeing any difference in screenreader output with vs without it

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving role="option" to the button allows the screenreader users to select an option with ctrl-option-space

Copy link
Contributor Author

@eatyourgreens eatyourgreens Apr 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ID links up aria-activedescendant on the containing combobox, so that changes can be announced even though keyboard focus is actually in the text input.

I’m not sure about having a list item role that then contains an option. I think the options might have to be direct children of the listbox, or if not then the default role of the li elements might need to be changed too.

Great that this works in VoiceOver, though. Hopefully it’s valid ARIA too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If VO had better support for activedescendant, I think some of the live region updates could be removed too. I’m finding they sometimes lag behind the actual active option, or at other times I hear the active option announced multiple times.

JAWS and NVDA should pick up the active option, via aria-activedescendant, and announce it when you navigate the list with the arrow keys.

Copy link
Contributor Author

@eatyourgreens eatyourgreens Apr 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At Zooniverse, we use the Storybook a11y addon to test for things like invalid role nesting, or invalid role and state combinations.

I can give this change a try to see if the validator complains.

@eatyourgreens
Copy link
Contributor Author

@jcfilben thanks for the additional testing in VoiceOver. I found this in the ARIA spec for listboxes.

Each option in the listbox has role option and is contained in or owned by either:

  • The element with role listbox.
  • An element with role group that is contained in or owned by the element with role listbox.

https://w3c.github.io/aria-practices/#listbox_roles_states_props

So the options must be direct children of the listbox, or maybe I should change the li roles to group instead of listitem. Otherwise, I think the list of options could be using invalid markup.

Another option would be to remove the buttons completely, and just have a styled list. That seems like a major change to me. I'm not sure what the implications would be.

@eatyourgreens
Copy link
Contributor Author

I added @storybook/addon-a11y to my local copy of the Grommet storybook. It finds one violation in the Suggestions story for TextInput: there's no label for the input element.

It doesn't find any ARIA problems on this branch, so I think this PR might be ready.

Screenshot of the accessibility panel for the TextInput Suggestions story, showing that all the ARIA tests pass.

active={activeSuggestionIndex === index}
id={`${id}-listbox-option-${index}`}
role="option"
aria-selected={selected ? 'true' : undefined}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we update aria-selected to just be true or false instead of true or undefined? Setting aria-selected to undefined means the option is not selectable (see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-selected#values)

Suggested change
aria-selected={selected ? 'true' : undefined}
aria-selected={selected}

Copy link
Contributor Author

@eatyourgreens eatyourgreens Apr 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. I copied this from the WAI-ARIA example but hadn't seen that note on MDN.

I think aria-selected must be a string, not a boolean attribute, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the options to add aria-selected='false' as the default state.

@jcfilben
Copy link
Collaborator

I added @storybook/addon-a11y to my local copy of the Grommet storybook. It finds one violation in the Suggestions story for TextInput: there's no label for the input element.

I haven't used the addon-a11y tool before but it looks interesting! I'm thinking it could be useful to add to our storybook to help catch more accessibility issues

@eatyourgreens
Copy link
Contributor Author

I haven't used the addon-a11y tool before but it looks interesting! I'm thinking it could be useful to add to our storybook to help catch more accessibility issues.

It's very useful. One other thing it does is it adds a colour-blindness simulator to the Storybook canvas toolbar.

Copy link
Collaborator

@jcfilben jcfilben left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!

Copy link
Contributor

@ericsoderberghp ericsoderberghp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like seeing this addition. Thanks for working on it. One small comment.

@@ -327,6 +327,8 @@ const TextInput = forwardRef(
{...dropProps}
>
<ContainerBox
id={`${id}-listbox`}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should include these id elements conditionally, only if the caller provides an id, otherwise we are likely to be rendering multiple elements in the DOM with the same id. This would follow the pattern we have at line 321.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, that’s a good point. If the option IDs are omitted, then aria-activedescendent would have to be removed, which then means the active option can’t be announced by a screen reader.

Similarly, a combobox must be linked to a listbox via aria-controls, so removing the listbox ID would also remove the combobox role from the text input.

Basically, I think all the combobox props are conditional on IDs being defined.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any way that we could generate a unique ID by default for a component, if one hasn’t been passed in? ARIA pretty much relies on element IDs to link up related DOM nodes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We haven't gone down that path so far. And instead pursuing things like eslint plugins to warn when an id is missing, notably in input + label situations.

https://github.com/grommet/eslint-plugin-grommet

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. I've added IDs to the storybook, and conditional checks so that the combobox role and states are only added to the text input if the component has both id and suggestions. I've added conditional checks to the listbox and option IDs too.

Add the `combobox` and `listbox` roles to TextInput, plus their required ARIA states, when suggestions are present. The changes should follow the WAI-ARIA 1.2 design pattern for a combobox with manual autocomplete.
https://w3c.github.io/aria-practices/examples/combobox/combobox-autocomplete-list.html
Check for a supplied ID. Only add the combobox roles and states, and listbox element IDs, if the component has an ID.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants