Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Components: Add a WAI-ARIA compliant Combobox. #19657

Merged
merged 3 commits into from
Jun 26, 2020

Conversation

epiqueras
Copy link
Contributor

Description

This PR implements a fully accessible and customizable Combobox component with individually style-able items in the @wordpress/components package.

It uses Downshift, a library that is already used by our custom select control and that will be very useful for any of our other complex a11y input needs.

It has some modifications hooked on top of it to make it fully compliant with the spec. These modifications might soon make it into a future Downshift release, and at that point, we should remove them from our implementations.

How has this been tested?

Test the new component in its Storybook story.

Types of Changes

New Feature: @wordpress/components now has a fully WAI-ARIA compliant Combobox.

Checklist:

  • My code is tested.
  • My code follows the WordPress code style.
  • My code follows the accessibility standards.
  • My code has proper inline documentation.
  • I've included developer documentation if appropriate.
  • I've updated all React Native files affected by any refactorings/renamings in this PR. .

@epiqueras epiqueras added the [Feature] UI Components Impacts or related to the UI component system label Jan 15, 2020
@epiqueras epiqueras added this to the Future milestone Jan 15, 2020
@epiqueras epiqueras self-assigned this Jan 15, 2020
@youknowriad youknowriad added Needs Accessibility Feedback Need input from accessibility Needs Design Feedback Needs general design feedback. labels Jan 15, 2020
@getdave
Copy link
Contributor

getdave commented Jan 15, 2020

Is this related to the work on LinkControl? Love the use of downshift.

@shaunandrews
Copy link
Contributor

Could you add before/after visuals? I often review from a mobile device and without visuals it’s much harder to do a design review.

@epiqueras
Copy link
Contributor Author

Is this related to the work on LinkControl? Love the use of downshift.

No, but it can and should be leveraged there.

Could you add before/after visuals? I often review from a mobile device and without visuals it’s much harder to do a design review.

This PR adds an entirely new component. You need to run the storybook to check all of its states and interactions.

@epiqueras
Copy link
Contributor Author

gif

@Aljullu
Copy link
Contributor

Aljullu commented Jan 15, 2020

I was testing this component because we might want to use it in WooCommerce Blocks and found a visual glitch when hovering the arrow button (notice the box shadow in the top-left corner):
Peek 2020-01-15 16-28

I could reproduce it with Firefox and Chrome both in Linux. Do you see it too @epiqueras?

@epiqueras
Copy link
Contributor Author

I could reproduce it with Firefox and Chrome both in Linux. Do you see it too @epiqueras?

Yeah, good catch! Fixed 😄

@shaunandrews
Copy link
Contributor

Thanks for the GIF, that helps a lot.

My only point of visual feedback is to somehow connect the results to the input. Right now, the options/results are just kind of "floating" below and feel disconnected. Here's what it could look like it we add a shape around the results/options:

image

I added the placeholder (when the input has focus) which shows the currently selected option before you start typing, along with a visual indicator for the selected option in the list (the checkmark). The highlight on the first option is meant to indicate keyboard focus — using the arrows would move it up/down, and hitting return would select it.

I also explored a "connected" variation along with the "disconnected" option shown above:

image

@epiqueras
Copy link
Contributor Author

Here's what it could look like it we add a shape around the results/options:

It wasn't clear in the GIF, because of the size of the shape, but it already has an outline from the focus styles:

Screen Shot 2020-01-15 at 12 44 32 PM

I added the placeholder (when the input has focus) which shows the currently selected option before you start typing

I like that a lot, but is it OK from an a11y standpoint? cc @MarcoZehe

along with a visual indicator for the selected option in the list (the checkmark).

That should/was already working. Something has regressed in Downshift where selectedItem is now always returned as undefined. cc @silviuavram

I also explored a "connected" variation along with the "disconnected" option shown above:

I don't have a strong opinion on this. I think I like the connected one more. cc @jasmussen

@silviuaavram
Copy link

@epiqueras can you describe / codesandbox? In the normal useSelect codesandbox selection still works on 4.0.6. https://codesandbox.io/s/useselect-usage-53qfj

@shaunandrews
Copy link
Contributor

already has an outline from the focus styles:

That seems like an OS X system-level focus style. I've only seen it with the current font-size select UI and this new component. It makes me question where the focus actually is in this component; Is the input (where you type) focused, or is the list of options (where you can navigate/select with the keyboard) focused?

I think the focus should be on the input. I'd expect the input to get a 2px blue border, and the list of options to have a standard 1px grey border.

@epiqueras epiqueras force-pushed the add/combobox-control-component branch from 3cab8ea to cc3d3eb Compare January 15, 2020 18:54
@epiqueras
Copy link
Contributor Author

@silviuavram It has to do with how you detect un/controlled modes:

https://codesandbox.io/s/useselect-usage-1glc2

It doesn't differentiate between falsy or null values and undefined.

@epiqueras
Copy link
Contributor Author

@shaunandrews Focus should move between the two depending on what the user is doing, according to the spec. I think it's working correctly right now, but it needs a proper a11y team review.

@silviuaavram
Copy link

Found it, 3.3.2

    - prevState[key] = props[key] === undefined ? state[key] : props[key]
    + prevState[key] = key in props ? props[key] : state[key]

We should fix this and test if it's still working with the changes done to fix our useReducer logic.

@epiqueras
Copy link
Contributor Author

Nice catch! Let us know when a new version is released :)

@talldan
Copy link
Contributor

talldan commented Jan 16, 2020

Hey @epiqueras. As @getdave mentions it'd be great to explore using this for the LinkControl at some point.

I don't necessarily want to increase the scope of this PR, but though it'd be worth discussing how they might fit together. One of the fairly unique things about LinkControl is each item in the list of options has quite a bit of detail (more than just text). Is that something that could fit into this component in the near future?
Screen Shot 2020-01-16 at 12 13 28 pm

@silviuaavram
Copy link

silviuaavram commented Jan 16, 2020

In downshift we already released useCombobox specifically for the combobox case.

@epiqueras I want to try to keep the focus on the button even when the list is open, in the case of . Focusing the list at open already revealed a problem if you use React portal for the list. Also my colleagues think screen readers will have issues with this scenario as well. I will update you about both the controlled fix and the focus thing. The select should still be accessible even if the focus is kept on the button at all times, it's what I'm aiming for.

@epiqueras
Copy link
Contributor Author

@talldan Yes, the name properties can be any renderable.

@silviuavram This uses useCombobox, but no portals. Awesome, looking forward to the next release 😄 !

menuProps[ 'aria-activedescendant' ].slice( 0, 'downshift-null'.length ) ===
'downshift-null'
) {
delete menuProps[ 'aria-activedescendant' ];

Choose a reason for hiding this comment

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

this looks like it should be a downshift bug.

Choose a reason for hiding this comment

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

@epiqueras can you point out to me the use case when this happens? I want to fix it in v5. Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When mounting in uncontrolled mode.

Choose a reason for hiding this comment

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

I am confused, sorry :)). Do we have an example somewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Choose a reason for hiding this comment

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

@epiqueras downshift-js/downshift#945 is this fixing your use case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It should, yeah.

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we remove this code if it's fixed in Downshift?

@epiqueras
Copy link
Contributor Author

Edit infallible-faraday-z78fx

@tellthemachines
Copy link
Contributor

@tellthemachines I see that the menu closes when you remove the input value, in the ARIA example. Not sure if this is considered as standard though, because there is no mention of it specifically.

@silviuaavram the expected behaviour is described in the ARIA spec. Quoting the relevant section:

The popup is hidden by default, and the conditions that trigger its display are specific to each implementation. Some possible popup display conditions include:

  • It is displayed only if a certain number of characters are typed in the textbox and those characters match some portion of one of the suggested values.
  • It is displayed as soon as the textbox is focused, even if the textbox is empty.
  • It is displayed when the Down Arrow key is pressed or the show button is activated, possibly with a dependency on the content of the textbox.
  • It is displayed if the value of the textbox is altered in a way that creates one or more partial matches to a suggested value.

The current implementation fits into none of these conditions. I realise that this list is not all-inclusive and there might be other valid approaches, but having the empty input state act in one way when the input is newly focused, and in a different way when it is emptied of content, seems buggy.

The other thing to consider, in the WP case specifically, is that existing auto-suggests don't have that behaviour. They either show all options by default (Categories in post editor), or they show nothing when the input is empty (Tags in post editor). It would be good to not introduce yet a third behaviour to confuse things further 😅

@silviuaavram
Copy link

@epiqueras downshift-js/downshift#1058 is for the a11y issue, it should fix it. Will probably be available soon in 5.4.3.

As for the menu open it makes sense to implement one of those behaviors by default. I will create an issue on our side, but feel free to use state reducer to apply your specific use case in the meantime.

Related to that codesandbox, a couple of comments: no need for initialSelectedItem, since you also fully control the selectedItem. It won't do anything, and is expected. Also the aria-activedescendant bug should be fixed, and you can remove that delete part, since it won't be null. Remove and check if it works ok.

Thank you for the feedback everyone!

@epiqueras
Copy link
Contributor Author

@epiqueras downshift-js/downshift#1058 is for the a11y issue, it should fix it. Will probably be available soon in 5.4.3.

As for the menu open it makes sense to implement one of those behaviors by default. I will create an issue on our side, but feel free to use state reducer to apply your specific use case in the meantime.

Thank you for the quick fix!

Related to that codesandbox, a couple of comments: no need for initialSelectedItem, since you also fully control the selectedItem.

It's been a while since I looked at the code, but I think the idea is also to support an uncontrolled mode.

@jeryj
Copy link
Contributor

jeryj commented Jun 2, 2020

Howdy, y'all! I'm diving in here for an a11y review. I'm by no means an a11y expert, but doing my best to sift through specs and recommendations from people who know more than I do :)

Here's my initial take to kick things off:

  1. The specs for the combobox are changing quickly, and the implementation used here is the 1.1 spec. Unfortunately, it was proved to be a buggy release, and they're ditching this implementation. AFAIK, only one screen reader (NVDA) even implemented this spec.
  2. I very much expected all options to be open when the text field is empty, including when first focused. Otherwise, there's no way to know what options are available to you to know what to search for.
  3. I'm a little confused as to the intent of this combobox. My take is that it's a filterable <select>. If so, then it should show all options when first focused, then filter based on the entered text. Are there other use cases I'm missing? For example, if I have these options available:
  • Small
  • Medium
  • Large
    and I type "Biggest" (not one of the options), is the value I'm submitting "Biggest," or am I submitting nothing since it's not mapped to a valid option? If it is submitting "Biggest," I saw no indicator one way or the other to indicate this.

Hopefully none of these are major wrenches in the system :)

I can drill down into a more specific spec code-review after we confirm which spec (or hybrid of specs) we're trying to conform to. To help in this decision, Sarah Higley wrote an excellent article that I think will be enormously helpful for us in deciding our direction:

@tellthemachines
Copy link
Contributor

Hi @jeryj ! Thanks for your feedback and willingness to dive in and help with a11y work 😺
I'll try to answer to the best of my abilities.

after we confirm which spec (or hybrid of specs) we're trying to conform to

Currently, 1.2 is in Working Draft status, so it is still subject to change. Though it seems promising, we shouldn't try to implement it until it has reached at least Candidate Recommendation status, which means browsers/AT will start to implement it, and from that point onward, it's unlikely to change much.
Our best bet at the moment is to follow 1.1, test on as many platforms as we can and find workarounds for any browser/AT specific bugs we come across.

I'm a little confused as to the intent of this combobox. My take is that it's a filterable <select>.

This combobox is not meant to behave like a select, because we already have CustomSelectControl for that. The storybook example isn't awfully clear in that respect, because it reproduces the example from CustomSelectControl, but this component is meant instead to be used in situations where:

  • There are lots of options, that would result in an unmanageable dropdown if all visible at once (e.g. the dropdown might not fit on the page);
  • The user is selecting a known value, that is expected to be present: a postcode, or a language.

For these reasons, it's better to not show all the options as soon as the input is focused, but to only show filtered options as the user starts typing.

and I type "Biggest" (not one of the options), is the value I'm submitting "Biggest," or am I submitting nothing since it's not mapped to a valid option?

If what you type isn't a valid option, nothing happens. This is expected behaviour; there is no requirement in the specs for this type of component to show an error state.

I hope this addresses all your concerns!

@jeryj
Copy link
Contributor

jeryj commented Jun 3, 2020

@tellthemachines Thank you for the detailed response!

Which Spec to go with?

Currently, 1.2 is in Working Draft status, so it is still subject to change. Though it seems promising, we shouldn't try to implement it until it has reached at least Candidate Recommendation status, which means browsers/AT will start to implement it, and from that point onward, it's unlikely to change much.

Even though it's in working draft, because it lines up so much to 1.0 and how OS-level comboboxes work, it appears that the 1.2 proposed spec is more supported out-of-the-box right now than 1.1. From Sarah Higley's article on their testing the editable combobox:

Of the two semantic variations tested, the ARIA 1.2 pattern was less buggy across the board compared to the ARIA 1.1 pattern. Assuming the version in the working draft becomes a W3C recommendation, it seems to be the clear choice for combobox markup going forward.

IMO, since hardly any screen readers have supported 1.1, it seems better to go with either 1.0 or the proposed 1.2 spec, since it's already supported better than 1.1 just due to it working more inline with what screen readers expect. My instinct is to go with 1.2 since it appears to be supported by screen readers already. I'm happy to test this with NVDA and VoiceOver and report back to see if this implementation suffers the same results. In the article linked above though, Higley says:

Of the two semantic variations tested, the ARIA 1.2 pattern was less buggy across the board compared to the ARIA 1.1 pattern.

Expected behavior of ComboboxControl

Thanks for the clarifications on this. This was super helpful!

  • There are lots of options, that would result in an unmanageable dropdown if all visible at once (e.g. the dropdown might not fit on the page);

Why not display the items but limit the height to only show some of the options, say 5 - 10 max? That feels better to me than an input that has specific options to select from, but you can't clearly see all your possible options if you want to.

  • The user is selecting a known value, that is expected to be present: a postcode, or a language.

Wouldn't the markup be identical in the case of a filterable combobox with unknown options, such as in the example? Could we add a prop (or maybe there's one that exists) that will show options on empty? That would hopefully allow this component to be much more reusable across different implementations.

Hopefully this would resolve the issue already mentioned where the initial focus does not show options, and then searching and deleting the text does show options. I would expect the behavior should be the same when the text field is empty vs text deleted.

General Usability

I've been poking around at this, and have some more thoughts about it:

  1. Autoselect matched options: I think this should autoselect the option if it matches. It's confusing when you type in a matched option, like "Large," then tab off of the field. This doesn't automatically select "Large" even though it's a direct match. There's no indicator that you haven't selected it, so I imagine this will be a common issue. See the Autoselect with Filtering option on this CodePen: https://codepen.io/smhigley/pen/BayzXbO
  2. No results found error message: It's not clear when you type something in and tab off. It feels like your value is accepted.
  3. Empty/No value found: Like above, when you enter a value that isn't a 1:1 match with an option, it seems like it accepts your value even though nothing is selected. Some kind of warning/error/state to indicate you don't have anything selected would be enormously helpful, IMO.
  4. Down arrow not focusable: I'm not really sure what to do with this one, but you can click the down arrow to bring up the options, but it isn't able to receive focus. One possibility is to always open the listbox options on click and remove the down arrow as a separate clickable element.
  5. Items are selectable on tab
  • Open listbox options
  • Press down arrow to move into options
  • Tab off the input
  • The option you highlighted is now the selected option
    I'm not sure in spec what is expected here. I keep going back and forth if this is the best interaction or not. If this is left, then I think it makes sense to make the input autoselect the closest option as well, as then it'd be a similar pattern. Either way, it needs to be clear what option is selected.

Here's a gif that consolidates most of my usability concerns:

  1. Open the full option menu
  2. Select Large
  3. Delete the input text
  4. Type "Small"
  5. Tab off the input
  6. Delete the input text
  7. See that Large is still selected

comboox-unclear-option-selected

If we autoselected the closest option, then I think it would resolve this issue

Summary of recommendations

Whew! That was a lot for one comment 😅 Here's my recommendations in a more digestible format.

  • Use 1.2 proposed ARIA spec for markup
  • Autoselect closest typed option on blur/tab off of the field
  • Options list is always visible when the input is focused, but longer lists of options are hidden in a scrollable overflow (all options should still be present and visible in the DOM)
  • Remove down arrow as clickable element
  • Add "No results found" error state in the dropdown if there are no matched options
  • If the input text does not actually have a selected option aligned with it, show some kind of error state/indicator so this is clear.

It's worth mentioning that @diegohaz is working on integrating Reakit components into Gutenberg. I know there's a combobox in the works, but he's said it's a ways off. Something to keep in mind for the future.

@tellthemachines
Copy link
Contributor

Thanks again for your feedback @jeryj ! This PR is quite close to a mergeable state; it's been extensively tested with VoiceOver and NVDA, and is working well on both. I'm not too fussed about the spec question, but we haven't tested an implementation based on 1.2, so to go back and rebuild this would require a bunch of extra work that's hard to justify, given that what we have here is working well.

As for whether to show all items on open or not, I'd be more inclined to not, for the reasons I already stated, and because it is already possible to show all options by either clicking the button on the right hand side, or pressing Down Arrow. But for that, as well as for your other suggestions, I think the best course would be to create a separate issue for further iteration/discussion.

@jeryj
Copy link
Contributor

jeryj commented Jun 5, 2020

To be clear, my intention here wasn't to throw a wrench in the PR. Sorry if it came across that way! I know y'all have done a lot of work and testing to get it here, and that is so appreciated 🙌

I've created all the spin-off issues for further discussion. 👍

@silviuaavram
Copy link

@epiqueras downshift-js/downshift#1058 has been closed. Should be available in 5.4.3.

@epiqueras
Copy link
Contributor Author

it's been extensively tested with VoiceOver and NVDA, and is working well on both.

@tellthemachines

Is it ok if we merge this now and work on polishing it up in follow-ups? This is much better than what's currently broken on master.

Copy link
Contributor

@tellthemachines tellthemachines left a comment

Choose a reason for hiding this comment

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

It's close enough, let's merge what we have and work on further improvements 🚀

From my side, the biggest remaining issues are announcing the number of filtered options and having consistent behaviour when input is empty (whether it's just been opened or the content has been deleted). I'm very reluctant to have all items show on open by default, but we could consider making it an option.

@epiqueras epiqueras force-pushed the add/combobox-control-component branch from 35ed682 to 20b2cbf Compare June 25, 2020 23:33
@epiqueras
Copy link
Contributor Author

the biggest remaining issues are announcing the number of filtered options and having consistent behaviour when input is empty (whether it's just been opened or the content has been deleted). I'm very reluctant to have all items show on open by default, but we could consider making it an option.

@silviuaavram Those things are fixed in the next version of Downshift, right?

@epiqueras epiqueras merged commit 76ef596 into master Jun 26, 2020
@epiqueras epiqueras deleted the add/combobox-control-component branch June 26, 2020 00:31
@talldan
Copy link
Contributor

talldan commented Aug 14, 2020

Seems this component hasn't been exported from the package (#24443)? Is it still WIP?

If so, it might be worth removing the docs from the handbook.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] UI Components Impacts or related to the UI component system Needs Accessibility Feedback Need input from accessibility Needs Design Feedback Needs general design feedback.
Projects
None yet
Development

Successfully merging this pull request may close these issues.