Skip to content

Commit f4e9cb3

Browse files
fix(ui): make mobile nav toggle keyboard accessible
1 parent c52de6f commit f4e9cb3

File tree

3 files changed

+68
-24
lines changed

3 files changed

+68
-24
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, it } from 'node:test';
2+
import assert from 'node:assert/strict';
3+
4+
import { render, screen } from '@testing-library/react';
5+
import userEvent from '@testing-library/user-event';
6+
7+
import NavBar from '..';
8+
9+
const Logo = () => <span>Node.js</span>;
10+
11+
describe('NavBar', () => {
12+
it('uses a keyboard-accessible button to toggle the mobile navigation', async () => {
13+
const user = userEvent.setup();
14+
15+
render(
16+
<NavBar
17+
as="a"
18+
Logo={Logo}
19+
pathname="/"
20+
sidebarItemTogglerAriaLabel="Toggle navigation menu"
21+
navItems={[
22+
{ text: 'Learn', link: '/learn' },
23+
{ text: 'About', link: '/about' },
24+
]}
25+
>
26+
<a href="/search">Search</a>
27+
</NavBar>
28+
);
29+
30+
const menuButton = screen.getByRole('button', {
31+
name: 'Toggle navigation menu',
32+
});
33+
const navigation = screen
34+
.getByRole('navigation')
35+
.querySelector('#navbar-navigation');
36+
37+
assert.ok(menuButton);
38+
assert.ok(navigation);
39+
assert.equal(menuButton.tagName, 'BUTTON');
40+
assert.equal(menuButton.getAttribute('tabindex'), null);
41+
assert.equal(menuButton.getAttribute('aria-expanded'), 'false');
42+
assert.match(navigation.className, /\bhidden\b/);
43+
44+
await user.click(menuButton);
45+
46+
assert.equal(menuButton.getAttribute('aria-expanded'), 'true');
47+
assert.match(navigation.className, /\bflex\b/);
48+
49+
await user.click(menuButton);
50+
51+
assert.equal(menuButton.getAttribute('aria-expanded'), 'false');
52+
assert.match(navigation.className, /\bhidden\b/);
53+
});
54+
});

packages/ui-components/src/Containers/NavBar/index.module.css

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@
4141
.sidebarItemTogglerLabel {
4242
@apply block
4343
cursor-pointer
44+
appearance-none
45+
border-0
46+
bg-transparent
47+
p-0
4448
xl:hidden;
4549

4650
.navInteractionIcon {
@@ -49,16 +53,6 @@
4953
}
5054
}
5155

52-
.sidebarItemToggler {
53-
@apply absolute
54-
right-4
55-
-z-10
56-
size-6
57-
-translate-y-[200%]
58-
appearance-none
59-
opacity-0;
60-
}
61-
6256
.main {
6357
@apply flex-1
6458
flex-col

packages/ui-components/src/Containers/NavBar/index.tsx

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import Hamburger from '@heroicons/react/24/solid/Bars3Icon';
22
import XMark from '@heroicons/react/24/solid/XMarkIcon';
3-
import * as Label from '@radix-ui/react-label';
43
import classNames from 'classnames';
54
import { useState } from 'react';
65

@@ -55,25 +54,22 @@ const NavBar: FC<PropsWithChildren<NavbarProps>> = ({
5554
<Logo />
5655
</Component>
5756

58-
<Label.Root
57+
<button
5958
className={styles.sidebarItemTogglerLabel}
60-
htmlFor="sidebarItemToggler"
61-
role="button"
59+
type="button"
6260
aria-label={sidebarItemTogglerAriaLabel}
61+
aria-controls="navbar-navigation"
62+
aria-expanded={isMenuOpen}
63+
onClick={() => setIsMenuOpen(previousState => !previousState)}
6364
>
6465
{navInteractionIcons[isMenuOpen ? 'close' : 'show']}
65-
</Label.Root>
66+
</button>
6667
</div>
6768

68-
<input
69-
className={classNames('peer', styles.sidebarItemToggler)}
70-
id="sidebarItemToggler"
71-
type="checkbox"
72-
onChange={e => setIsMenuOpen(() => e.target.checked)}
73-
aria-label={sidebarItemTogglerAriaLabel}
74-
tabIndex={-1}
75-
/>
76-
<div className={classNames(styles.main, `hidden peer-checked:flex`)}>
69+
<div
70+
id="navbar-navigation"
71+
className={classNames(styles.main, isMenuOpen ? 'flex' : 'hidden')}
72+
>
7773
{navItems && navItems.length > 0 && (
7874
<div className={styles.navItems}>
7975
{navItems.map(({ text, link, target }) => (

0 commit comments

Comments
 (0)