-
Notifications
You must be signed in to change notification settings - Fork 742
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
Keyboard navigation in mobile web (Issue #2159) #2166
Keyboard navigation in mobile web (Issue #2159) #2166
Conversation
…ng-library/user-event to allow better a11y testing
Obviously broke some things with the test upgrades. I'm working through the changes - there were a few API updates with @julianguyen - happy to update all existing tests to still function. The benefit here is that we get a more realistic testing environment from |
@LMulvey Thanks for the summary and for taking this on 🎉 ! Yeah, let's update the existing tests like you said. |
Okay! Tests are updated but there are a few caveats. I've had to skip three tests for now - I need to think about an approach a bit. I left a comment on all three tests describing why they were skipped. Still, in summary,
I've tried manually dispatching an event with JSDom (you can see my scraps in the Does anyone have any ideas? 🤔 cc: @julianguyen |
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.
Thanks so much for taking this on! Thanks for also clarifying your approach and debugging! 🎉 Hope you're well :)
await user.keyboard('[Space]'); | ||
expect(container.querySelector('#headerMobile')).toBeInTheDocument(); | ||
await user.keyboard('[Space]'); | ||
expect(container.querySelector('#headerMobile')).not.toBeInTheDocument(); |
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.
Could you explain what exactly we're testing when we hit the Space
key twice?
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.
Ensuring that it opens AND closes the navbar with the space key!
await user.keyboard('{Tab}'); | ||
expect(screen.getByRole('link', { name: /link 1/i })).toHaveFocus(); | ||
|
||
// Shift-tab back to the hamburger and close the mobile menu |
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.
Nice!
|
||
if (event.key === 'Tab') { | ||
if (event.shiftKey) { | ||
if (document.activeElement === firstFocusableElement) { |
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.
I think the if-locks on lines 42 and 43 can be combined with an &&
to reduce nesting.
@@ -9,6 +9,13 @@ const getComponent = (extraProps = {}) => createInput(inputTagProps, extraProps) | |||
const component = getComponent(); | |||
const { checkboxes } = inputTagProps; | |||
|
|||
function setup(jsx) { |
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.
Would it be useful to create some kind of utility function so that it doesn't have to be re-created in multiple tests files?
userEvent.click(button); | ||
expect(pell.exec).toHaveBeenCalledWith(...expectedArgs); | ||
}); | ||
await Promise.all( |
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.
Can we use waitFor
instead?
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.
What's the rationale for waitFor
vs Promise.all
? I'm not sure I follow! My larger gut feeling is to break out of doing loops here entirely and write out all the Button conditions explicitly – just to avoid any hack-y-ness 🤔
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.
Could you do something like this instead with waitFor?
buttons.forEach(({ title, expectedArgs }) => {
const button = screen.getByTitle(title);
userEvent.click(button);
await waitFor(() => expect(pell.exec).toHaveBeenCalledWith(...expectedArgs));
});
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.
Wahoo! Worked like a charm 🎉
userEvent.click(button); | ||
expect(pell.exec).toHaveBeenCalledWith(...expectedArgs); | ||
}); | ||
await Promise.all( |
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.
Similar question as above.
client/package.json
Outdated
@@ -144,5 +144,6 @@ | |||
}, | |||
"browserslist": [ | |||
"defaults" | |||
] | |||
], | |||
"packageManager": "[email protected]" |
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.
What's the rationale for this? Is it necessary?
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.
🤦🏻 Not at all. My setup uses Yarn v3 and to avoid changing the lockfile formatting, I ran yarn set-version classic
- which introduced a bunch of random changes throughout the repo - I thought I had caught them all. Turns out it's not super intuitive to run a separate version of Yarn in a specific workspace. I'll uncommit this and watch for it again!
const firstFocusableElement = focusableElements[0]; | ||
const lastFocusableElement = focusableElements[focusableElements.length - 1]; | ||
|
||
if (event.key === 'Tab') { |
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.
When I do the following, the focus trap does not work:
Tab
into the hamburger icon- Hit
Enter
to open the mobile menu Tab
through the menu items- When the last item of the menu is reached, and I hit
Tab
again, the focus trap does not work and the contents behind the menu are hit.
The expected behaviour is for the focus to go back to logo at the top of the menu.
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.
I think the problem is that document.activeElement
is not giving you the active element that is being focused on. It's giving the element before it in the DOM. I think this is happening because it needs to be in a useEffect()
.
I think we want to make this focus trap code we're using in the Modal component a custom hook that we can reuse here.
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.
Another issue I see is that lastFocusableElement
is the "Sign out" button when it should be the "Resources" link.
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.
@julianguyen Very interesting! I was testing it in an unauthenticated state on the homepage–I feel like this is a separate state. I'll take a look (and look at lifting the Modal
component useEffect
into a reusable hook! 💪🏻 )
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 fixed now! It was related to a display: none
element in the Links - it isn't technically focusable by the browser but still appeared in our focusableElements
array. I've added an extra step to filter out non-visible elements here – the Focus Trap now works as expected while authenticated or unauthenticated.
|
||
checkbox = screen.getByRole('checkbox', queryOptions); | ||
expect(checkbox).toBeInTheDocument(); | ||
}); | ||
|
||
it('selects a value with keydown without specifying text', () => { | ||
/** | ||
* Skipping this one for now. react-autosuggest still checks for event.keyCode, which |
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.
Have you tried using fireEvent
instead in these skipped tests? I know it's not ideal since we want to simulate a complete interaction via userEvent
.
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.
I did try that - turns out it also writes the keyCode
is 0
so we end up with the same issue 😭
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.
Thanks for trying all this! Hmm, I'm okay with skipping this test for now, but we should have a fast follow to fix this. Since react-autosuggest
is deprecated, I think we'll need to migrate to another library. Could you create an issue for this? Are you able to take this on?
I think we can just keep the original contents of this test since it's being skipped anyways.
Co-authored-by: Julia Nguyen <[email protected]>
…none from focusable elements
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.
Awesome work! 🎉 Thanks for the updates. Tested things locally, and the keyboard navigation works as expected.
Shared some feedback around moving forward with react-autosuggest
and also the tests
|
||
checkbox = screen.getByRole('checkbox', queryOptions); | ||
expect(checkbox).toBeInTheDocument(); | ||
}); | ||
|
||
it('selects a value with keydown without specifying text', () => { | ||
/** | ||
* Skipping this one for now. react-autosuggest still checks for event.keyCode, which |
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.
Thanks for trying all this! Hmm, I'm okay with skipping this test for now, but we should have a fast follow to fix this. Since react-autosuggest
is deprecated, I think we'll need to migrate to another library. Could you create an issue for this? Are you able to take this on?
I think we can just keep the original contents of this test since it's being skipped anyways.
@@ -35,7 +35,7 @@ describe('InputSwitch', () => { | |||
expect(screen.getByRole('checkbox')).not.toBeChecked(); | |||
|
|||
/** | |||
* TODO: Follow up on `userEvent.type(inputSwitch, '{enter}')` in v12.1.7. | |||
* TODO: Follow up on `await userEvent.type(inputSwitch, '{enter}')` in v12.1.7. |
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.
Let's also make a GitHub issue for this, so it doesn't get lost in comments.
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 an old comment that I was able to just resolve & remove as part of this upgrade 🎉 !
userEvent.click(button); | ||
expect(pell.exec).toHaveBeenCalledWith(...expectedArgs); | ||
}); | ||
await Promise.all( |
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.
Could you do something like this instead with waitFor?
buttons.forEach(({ title, expectedArgs }) => {
const button = screen.getByTitle(title);
userEvent.click(button);
await waitFor(() => expect(pell.exec).toHaveBeenCalledWith(...expectedArgs));
});
useEffect(() => { | ||
const focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; | ||
const modal = modalEl.current; | ||
useFocusTrap(modalEl, open); |
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.
Woot!
@@ -135,7 +147,19 @@ describe('QuickCreate', () => { | |||
}); | |||
|
|||
describe('when the form', () => { | |||
it('is submitted it adds a new checkbox from data', async () => { | |||
/** |
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.
Similar comment around keeping original test like stated above!
@julianguyen Thank you for all the feedback! I've pushed up another round of fixes and created #2170 to cover off the |
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.
Awesome work! 🎉 Thanks so much for taking this on and navigating all the gotchas in a good way.
Description
Few a11y fixes related to the Header component
toggle
was being called every timeonKeyDown
was triggered on the hamburger button@testing-library/jest-dom
,@testing-library/react
and@testing-library/user-event
- the dependency update was largely to use the updateduserEvent.keyboard
event support from@testing-library/user-event
but I wanted to ensure the dependencies were in sync.Corresponding Issue
#2159
Reviewing this pull request? Check out our Code Review Practices guide if you haven't already!