Skip to content

Commit 535d6d3

Browse files
authored
Merge pull request #63 from sparksuite/rethink-behavior
Rethink behavior on button clicks
2 parents e0166ee + 7c8e43c commit 535d6d3

File tree

5 files changed

+76
-24
lines changed

5 files changed

+76
-24
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ Done!
4848
## Accessibility notes
4949
Our team carefully studied and adhered to [Web Content Accessibility Guidelines 2.1](https://www.w3.org/WAI/standards-guidelines/wcag/) and [WAI-ARIA Authoring Practices 1.1](https://www.w3.org/TR/wai-aria-practices/) when designing this Hook. Here are some facets of accessibility that are handled automatically:
5050

51-
- Strict adherence to the best practices for menus ([WAI-ARIA: 3.15](https://www.w3.org/TR/wai-aria-practices/#menu))
51+
- Careful following of the best practices for menus ([WAI-ARIA: 3.15](https://www.w3.org/TR/wai-aria-practices/#menu))
52+
- The only deviation is that the first menu item is only focused when the menu is revealed via the keyboard ([see why](https://github.com/sparksuite/react-accessible-dropdown-menu-hook/pull/63))
5253
- Strict adherence to the best practices for menu buttons ([WAI-ARIA: 3.16](https://www.w3.org/TR/wai-aria-practices/#menubutton))
5354
- Full keyboard accessibility ([WCAG: 2.1](https://www.w3.org/WAI/WCAG21/quickref/#keyboard-accessible))
5455
- Use of ARIA properties and roles to establish relationships amongst elements ([WCAG: 1.3.1](https://www.w3.org/WAI/WCAG21/quickref/#info-and-relationships))

package.json

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-accessible-dropdown-menu-hook",
3-
"version": "1.0.0",
3+
"version": "1.1.0",
44
"description": "A simple Hook for creating fully accessible dropdown menus in React",
55
"main": "dist/use-dropdown-menu.js",
66
"types": "dist/use-dropdown-menu.d.ts",
@@ -77,6 +77,7 @@
7777
"collectCoverageFrom": [
7878
"<rootDir>/src/**"
7979
],
80+
"verbose": true,
8081
"projects": [
8182
{
8283
"displayName": "Enzyme",
@@ -86,17 +87,15 @@
8687
"testMatch": [
8788
"<rootDir>/test/*.test.ts",
8889
"<rootDir>/test/*.test.tsx"
89-
],
90-
"verbose": true
90+
]
9191
},
9292
{
9393
"displayName": "Puppeteer",
9494
"preset": "jest-puppeteer",
9595
"testMatch": [
9696
"<rootDir>/test/puppeteer/*.test.ts",
9797
"<rootDir>/test/puppeteer/*.test.tsx"
98-
],
99-
"verbose": true
98+
]
10099
}
101100
]
102101
}

src/use-dropdown-menu.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export default function useDropdownMenu(itemCount: number) {
77
const [isOpen, setIsOpen] = useState<boolean>(false);
88
const currentFocusIndex = useRef<number | null>(null);
99
const firstRun = useRef(true);
10+
const clickedOpen = useRef(false);
1011

1112
// Create refs
1213
const buttonRef = useRef<HTMLButtonElement>(null);
@@ -31,8 +32,10 @@ export default function useDropdownMenu(itemCount: number) {
3132
}
3233

3334
// If the menu is currently open focus on the first item in the menu
34-
if (isOpen) {
35+
if (isOpen && !clickedOpen.current) {
3536
moveFocus(0);
37+
} else if (!isOpen) {
38+
clickedOpen.current = false;
3639
}
3740
}, [isOpen]);
3841

@@ -53,7 +56,7 @@ export default function useDropdownMenu(itemCount: number) {
5356
}
5457

5558
// Ignore if we're clicking inside the menu
56-
if (event.target.closest('[role="menu"]')) {
59+
if (event.target.closest('[role="menu"]') instanceof Element) {
5760
return;
5861
}
5962

@@ -75,16 +78,19 @@ export default function useDropdownMenu(itemCount: number) {
7578
if (isKeyboardEvent(e)) {
7679
const { key } = e;
7780

78-
if (key !== 'Tab' && key !== 'Shift') {
79-
e.preventDefault();
81+
if (!['Enter', ' ', 'Tab', 'ArrowDown'].includes(key)) {
82+
return;
8083
}
8184

82-
if (key === 'Enter' || key === ' ') {
85+
if ((key === 'Tab' || key === 'ArrowDown') && clickedOpen.current && isOpen) {
86+
e.preventDefault();
87+
moveFocus(0);
88+
} else if (key !== 'Tab') {
89+
e.preventDefault();
8390
setIsOpen(true);
84-
} else if (key === 'Tab') {
85-
setIsOpen(false);
8691
}
8792
} else {
93+
clickedOpen.current = !isOpen;
8894
setIsOpen(!isOpen);
8995
}
9096
};

test/puppeteer/demo.test.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,16 @@ it('has the correct page title', async () => {
2020
await expect(page.title()).resolves.toMatch('React Accessible Dropdown Menu Hook');
2121
});
2222

23-
it('focuses on the first menu item when the enter key is pressed', async () => {
24-
await page.focus('#menu-button');
25-
await keyboard.down('Enter');
23+
it('leaves focus on the button after clicking it', async () => {
24+
await page.click('#menu-button');
2625
await menuOpen();
2726

28-
expect(await currentFocusID()).toBe('menu-item-1');
27+
expect(await currentFocusID()).toBe('menu-button');
2928
});
3029

3130
it('focuses on the menu button after pressing escape', async () => {
32-
await page.click('#menu-button');
31+
await page.focus('#menu-button');
32+
await keyboard.down('Enter');
3333
await menuOpen();
3434

3535
await keyboard.down('Escape');
@@ -39,7 +39,8 @@ it('focuses on the menu button after pressing escape', async () => {
3939
});
4040

4141
it('focuses on the next item in the tab order after pressing tab', async () => {
42-
await page.click('#menu-button');
42+
await page.focus('#menu-button');
43+
await keyboard.down('Enter');
4344
await menuOpen();
4445

4546
await keyboard.down('Tab');
@@ -49,7 +50,8 @@ it('focuses on the next item in the tab order after pressing tab', async () => {
4950
});
5051

5152
it('focuses on the previous item in the tab order after pressing shift-tab', async () => {
52-
await page.click('#menu-button');
53+
await page.focus('#menu-button');
54+
await keyboard.down('Enter');
5355
await menuOpen();
5456

5557
await keyboard.down('Shift');
@@ -60,17 +62,19 @@ it('focuses on the previous item in the tab order after pressing shift-tab', asy
6062
});
6163

6264
it('closes the menu if you click outside of it', async () => {
63-
await page.click('#menu-button');
65+
await page.focus('#menu-button');
66+
await keyboard.down('Enter');
6467
await menuOpen();
6568

66-
await page.click('body');
69+
await page.click('h1');
6770
await menuClosed(); // times out if menu doesn't close
6871

6972
expect(true).toBe(true);
7073
});
7174

7275
it('leaves the menu open if you click inside of it', async () => {
73-
await page.click('#menu-button');
76+
await page.focus('#menu-button');
77+
await keyboard.down('Enter');
7478
await menuOpen();
7579

7680
await page.click('#menu-item-1');
@@ -87,7 +91,8 @@ it('leaves the menu open if you click inside of it', async () => {
8791
it('reroutes enter presses on menu items as clicks', async () => {
8892
let alertAppeared = false;
8993

90-
await page.click('#menu-button');
94+
await page.focus('#menu-button');
95+
await keyboard.down('Enter');
9196
await menuOpen();
9297

9398
// eslint-disable-next-line @typescript-eslint/no-misused-promises

test/use-dropdown-menu.test.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,36 @@ it('moves the focus to the first menu item after pressing enter while focused on
1818
expect(document.activeElement?.id).toBe('menu-item-1');
1919
});
2020

21+
it('moves the focus to the first menu item after pressing space while focused on the menu button', () => {
22+
const component = mount(<TestComponent />);
23+
const button = component.find('#menu-button');
24+
25+
button.getDOMNode<HTMLButtonElement>().focus();
26+
button.simulate('keydown', { key: ' ' });
27+
28+
expect(document.activeElement?.id).toBe('menu-item-1');
29+
});
30+
31+
it('moves the focus to the first menu item after clicking the menu to open it, then pressing tab while focused on the menu button', () => {
32+
const component = mount(<TestComponent />);
33+
const button = component.find('#menu-button');
34+
35+
button.simulate('click');
36+
button.simulate('keydown', { key: 'Tab' });
37+
38+
expect(document.activeElement?.id).toBe('menu-item-1');
39+
});
40+
41+
it('moves the focus to the first menu item after clicking the menu to open it, then pressing arrow down while focused on the menu button', () => {
42+
const component = mount(<TestComponent />);
43+
const button = component.find('#menu-button');
44+
45+
button.simulate('click');
46+
button.simulate('keydown', { key: 'ArrowDown' });
47+
48+
expect(document.activeElement?.id).toBe('menu-item-1');
49+
});
50+
2151
it('sets isOpen to true after pressing enter while focused on the menu button', () => {
2252
const component = mount(<TestComponent />);
2353
const button = component.find('#menu-button');
@@ -29,6 +59,17 @@ it('sets isOpen to true after pressing enter while focused on the menu button',
2959
expect(span.text()).toBe('true');
3060
});
3161

62+
it('sets isOpen to true after pressing space while focused on the menu button', () => {
63+
const component = mount(<TestComponent />);
64+
const button = component.find('#menu-button');
65+
const span = component.find('#is-open-indicator');
66+
67+
button.getDOMNode<HTMLButtonElement>().focus();
68+
button.simulate('keydown', { key: ' ' });
69+
70+
expect(span.text()).toBe('true');
71+
});
72+
3273
it('moves the focus to the next element in the menu after pressing the down arrow', () => {
3374
const component = mount(<TestComponent />);
3475
const button = component.find('#menu-button');

0 commit comments

Comments
 (0)