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

[a11y] Update dropdown component for keyboard usage #261

Merged
merged 15 commits into from
Jul 20, 2020

Conversation

ipc103
Copy link
Contributor

@ipc103 ipc103 commented Jul 10, 2020

Asana Task

Our Dropdown component renders two different components depending on the type of device. For devices with touchscreen enabled, it uses the MobileDropdown, which takes advantage of semantic select and option elements. However, for styling purposes, the DesktopDropdown is using a ul with behavior modifications to effectively function as a select box. Doing this causes a few different accessibility issues:

  • Keyboard controls do not work as expected
  • Screen reader output does not match what the component is actually doing

In order to preserve the existing visuals, I attempted to fix these issues by adding accessibility information and additional keyboard event handlers to the component. Definitely curious to get other opinions - it may be easier to rebuild this component using semantic HTML, where we'll get most of the correct behavior for free, and then re-style to match instead of what I've done here. Included in this PR:

  • Converts all files in the Dropdown directory to TypeScript
  • Adds a role of listbox to the ul representing the dropdown, and option to each li.
  • Each individual li is tab-able and select-able.
  • A keyboard event handler in the OffClickWrapper to fire the callback when the Escape key is pressed. This component is also used by our ImmersiveModal, which I see as a benefit (pressing the Escape key should allow for the same functionality as doing an off-click with the mouse)
  • Adds a useResetFocus hook to send focus back to the trigger button after an option is selected.
  • Updates the focus styles to match the hover styles for each option.

Future TODO/Other Potential Improvements:

  1. Once a listbox has been opened, you should be able to scroll the elements using the arrow keys instead of the tab key. Testing in Chrome and Safari with Voiceover turned off, this did not work as implemented - the options are only select-able using the tab key.
  2. Testing with Voiceover and Safari, the listbox does scroll correctly using the arrow keys. However, the screen reader just reads aloud the word "text" instead of the actual content of the list item.

@snags88 snags88 temporarily deployed to curology-radiance-pr-261 July 10, 2020 19:31 Inactive
};

removeOffClickListener = () => {
document.removeEventListener('click', this.handleOffClick, false);
document.removeEventListener('keydown', this.handleKeyPress, false);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is used in a couple of places, including in the ImmersiveModal to handle outside clicks. I can't think of a reason not to also respond to Esc for those, but I may need to also do a quick search in the PocketDerm repo to see if we are duplicating this logic there.

).toEqual(true);
});

describe('onSelectClick callback', () => {
it('should be invoked onClick', () => {
const spy = jest.fn();
const wrapper = shallow(
const wrapper = mount(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These tests were failing when using shallow - I believe the issue is that the hook was still firing, but it seemed like it was coming from the render of the class component, which violates the laws of Hooks. Using mount does a more full render of the tree and seemed to fix the issue.

@@ -5,6 +5,20 @@ import MobileDropdown from './mobileDropdown';
import DesktopDropdown from './desktopDropdown';
import allowNullPropType from '../../utils/allowNullPropType';

export type OptionType = {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This file is just TS conversion changes.

Copy link
Contributor

@michaeljaltamirano michaeljaltamirano Jul 20, 2020

Choose a reason for hiding this comment

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

@ipc103 I think we'll also want to convert this to a Function component. We're typing optionsContainerMaxHeight as optional in children components, but it's provided via defaultProps. If we convert this to a Function component we can rely on ES6 defaults to inform TypeScript that it is optional but, downstream, being required is not type inconsistent.

Fortunately the only non-render methods aren't lifecycle methods so it should be pretty straightforward to convert.

DropdownOptionsContainer,
DropdownOption,
} from './style';

Copy link
Contributor Author

Choose a reason for hiding this comment

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

File CHANGELOG:

  1. Convert to TS
  2. Add useResetFocus hook to move focus back to the button when an option is selected
  3. Update roles, tabIndex, and aria-labels for the DropdownOptionsContainer ul and DropdownOption li.
  4. Add a keydown handler to allow selecting an option using the enter key.

@ipc103
Copy link
Contributor Author

ipc103 commented Jul 10, 2020

I mentioned this to @michaeljaltamirano in Slack, but I'll be away from the office until 7/20. Feel free to pick up where this leaves off, reject this, or ignore it entirely and leave for me when I come back 😄

const { onChange } = this.props;

const { value, selectedOptions } = event.target;
const {target} = event;
Copy link
Contributor

Choose a reason for hiding this comment

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

Did this not auto-format to be { target }? I've noticed this before in this repo... wonder if something is up

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 did not - and apparently I've gotten very dependent on our linting to fix my bad whitespace habits 😅

@michaeljaltamirano
Copy link
Contributor

michaeljaltamirano commented Jul 17, 2020

Definitely curious to get other opinions - it may be easier to rebuild this component using semantic HTML, where we'll get most of the correct behavior for free, and then re-style to match instead of what I've done here.

We could also build off of reach-ui (Listbox component or Menu Button component) or Adobe's react-spectrum Menu component.

} = option;

return (
<DropdownOption
Copy link
Contributor

Choose a reason for hiding this comment

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

Did you play around at all with trying to achieve the (kind of) opposite of useResetFocus, but rather than resetting focus back to another element, we actually set the focus ahead to the first descendent? (Kind of like the menu button when using keyboard navigation here: https://reach.tech/menu-button/#reach-skip-nav).

I also have not dug into the internals of all this as much as you have, so I'm not use if that kind of functionality is consistent with "best practices" or we'd be potentially introducing hacky behavior that we ought to achieve with other code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a good idea - I'm going to write up a separate ticket for this and will investigate further. Looking at the reach-ui implementation, it looks like they generally send focus to the first list element, but it may make sense to focus on the selected element if there is one.

Copy link
Contributor

@michaeljaltamirano michaeljaltamirano left a comment

Choose a reason for hiding this comment

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

Most of my comments are type changes. This is good, and useResetFocus might be something we want to port over to PocketDerm as well. Let me know if I can clarify any comments!

@ipc103 ipc103 marked this pull request as ready for review July 20, 2020 19:49
@ipc103 ipc103 merged commit c305fd4 into master Jul 20, 2020
@ipc103 ipc103 deleted the a11y/update-dropdown-component-for-keyboard-use branch July 20, 2020 19:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants