Skip to content

Commit cca088f

Browse files
authored
Merge pull request #51 from sparksuite/add-click-elsewhere-listener
The Hook will now listen for click events elsewhere
2 parents 65a3145 + 1176a5a commit cca088f

File tree

4 files changed

+92
-7
lines changed

4 files changed

+92
-7
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![Greenkeeper badge](https://badges.greenkeeper.io/sparksuite/react-accessible-dropdown-menu-hook.svg)](https://greenkeeper.io/)
44

5-
This Hook handles all the accessibility logic when building a dropdown menu, dropdown button, etc., and leaves the design completely up to you. [View the demo.](http://sparksuite.github.io/react-accessible-dropdown-menu-hook)
5+
This Hook handles all the accessibility logic when building a dropdown menu, dropdown button, etc., and leaves the design completely up to you. It also handles the logic for closing the menu when you click outside of it. [View the demo.](http://sparksuite.github.io/react-accessible-dropdown-menu-hook)
66

77
## Getting started
88

src/use-dropdown-menu.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,36 @@ export default function useDropdownMenu(itemCount: number) {
3939
if (isOpen) {
4040
moveFocus(0);
4141
}
42+
43+
// This function is designed to handle every click
44+
const handleEveryClick = (event: MouseEvent) => {
45+
// Ignore if the menu isn't open
46+
if (!isOpen) {
47+
return;
48+
}
49+
50+
// Make this happen asynchronously
51+
setTimeout(() => {
52+
// Type guard
53+
if (!(event.target instanceof Element)) {
54+
return;
55+
}
56+
57+
// Ignore if we're clicking inside the menu
58+
if (event.target.closest('[role="menu"]')) {
59+
return;
60+
}
61+
62+
// Hide dropdown
63+
setIsOpen(false);
64+
}, 10);
65+
};
66+
67+
// Add listener
68+
document.addEventListener('click', handleEveryClick);
69+
70+
// Return function to remove listener
71+
return () => document.removeEventListener('click', handleEveryClick);
4272
}, [isOpen]);
4373

4474
// Create a handler function for the button's clicks and keyboard events

test/puppeteer/demo.test.ts

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ const { keyboard } = page;
66

77
// Helper functions used in multiple tests
88
const currentFocusID = () => page.evaluate(() => document.activeElement.id);
9+
const menuOpen = () => page.waitForSelector('#menu', { visible: true });
10+
const menuClosed = () => page.waitForSelector('#menu', { hidden: true });
911

1012
// Tests
1113
beforeEach(async () => {
@@ -21,31 +23,63 @@ it('has the correct page title', async () => {
2123
it('focuses on the first menu item when the enter key is pressed', async () => {
2224
await page.focus('#menu-button');
2325
await keyboard.down('Enter');
26+
await menuOpen();
2427

2528
expect(await currentFocusID()).toBe('menu-item-1');
2629
});
2730

2831
it('focuses on the menu button after pressing escape', async () => {
29-
await page.focus('#menu-button');
30-
await keyboard.down('Enter');
32+
await page.click('#menu-button');
33+
await menuOpen();
34+
3135
await keyboard.down('Escape');
36+
await menuClosed();
3237

3338
expect(await currentFocusID()).toBe('menu-button');
3439
});
3540

3641
it('focuses on the next item in the tab order after pressing tab', async () => {
37-
await page.focus('#menu-button');
38-
await keyboard.down('Enter');
42+
await page.click('#menu-button');
43+
await menuOpen();
44+
3945
await keyboard.down('Tab');
46+
await menuClosed();
4047

4148
expect(await currentFocusID()).toBe('first-footer-link');
4249
});
4350

4451
it('focuses on the previous item in the tab order after pressing shift-tab', async () => {
45-
await page.focus('#menu-button');
46-
await keyboard.down('Enter');
52+
await page.click('#menu-button');
53+
await menuOpen();
54+
4755
await keyboard.down('Shift');
4856
await keyboard.down('Tab');
57+
await menuClosed();
4958

5059
expect(await currentFocusID()).toBe('menu-button');
5160
});
61+
62+
it('closes the menu if you click outside of it', async () => {
63+
await page.click('#menu-button');
64+
await menuOpen();
65+
66+
await page.click('body');
67+
await menuClosed(); // times out if menu doesn't close
68+
69+
expect(true).toBe(true);
70+
});
71+
72+
it('leaves the menu open if you click inside of it', async () => {
73+
await page.click('#menu-button');
74+
await menuOpen();
75+
76+
await page.click('#menu-item-1');
77+
await new Promise(resolve => setTimeout(resolve, 1000)); // visibility: hidden is delayed via CSS
78+
await menuOpen(); // times out if menu closes
79+
80+
await page.click('#menu');
81+
await new Promise(resolve => setTimeout(resolve, 1000)); // visibility: hidden is delayed via CSS
82+
await menuOpen(); // times out if menu closes
83+
84+
expect(true).toBe(true);
85+
});

test/use-dropdown-menu.test.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,24 @@ it('moves the focus to the menu button after pressing escape while focused on a
119119
firstMenuItem.simulate('keydown', { key: 'Escape' });
120120
expect(document.activeElement?.id).toBe('menu-button');
121121
});
122+
123+
it('opens the menu after clicking the button', () => {
124+
const component = mount(<TestComponent />);
125+
const button = component.find('#menu-button');
126+
const span = component.find('#is-open-indicator');
127+
128+
button.simulate('click');
129+
130+
expect(span.text()).toBe('true');
131+
});
132+
133+
it('closes the menu after clicking the button when the menu is open', () => {
134+
const component = mount(<TestComponent />);
135+
const button = component.find('#menu-button');
136+
const span = component.find('#is-open-indicator');
137+
138+
button.simulate('click');
139+
button.simulate('click');
140+
141+
expect(span.text()).toBe('false');
142+
});

0 commit comments

Comments
 (0)