-
Notifications
You must be signed in to change notification settings - Fork 3
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
[a11y] Update dropdown component for keyboard usage #261
Conversation
}; | ||
|
||
removeOffClickListener = () => { | ||
document.removeEventListener('click', this.handleOffClick, false); | ||
document.removeEventListener('keydown', this.handleKeyPress, false); |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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 = { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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'; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
File CHANGELOG:
- Convert to TS
- Add
useResetFocus
hook to move focus back to the button when an option is selected - Update roles, tabIndex, and aria-labels for the
DropdownOptionsContainer
ul andDropdownOption
li. - Add a keydown handler to allow selecting an option using the
enter
key.
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 😅
We could also build off of |
} = option; | ||
|
||
return ( | ||
<DropdownOption |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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!
Asana Task
Our
Dropdown
component renders two different components depending on the type of device. For devices with touchscreen enabled, it uses theMobileDropdown
, which takes advantage of semanticselect
andoption
elements. However, for styling purposes, theDesktopDropdown
is using aul
with behavior modifications to effectively function as a select box. Doing this causes a few different accessibility issues: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:
Dropdown
directory to TypeScriptlistbox
to theul
representing the dropdown, andoption
to eachli
.li
is tab-able and select-able.OffClickWrapper
to fire the callback when theEscape
key is pressed. This component is also used by ourImmersiveModal
, which I see as a benefit (pressing the Escape key should allow for the same functionality as doing an off-click with the mouse)useResetFocus
hook to send focus back to the trigger button after an option is selected.Future TODO/Other Potential Improvements:
listbox
has been opened, you should be able to scroll the elements using the arrow keys instead of thetab
key. Testing in Chrome and Safari with Voiceover turned off, this did not work as implemented - the options are only select-able using thetab
key.