Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

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)
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)

## Getting started

Expand Down
30 changes: 30 additions & 0 deletions src/use-dropdown-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,36 @@ export default function useDropdownMenu(itemCount: number) {
if (isOpen) {
moveFocus(0);
}

// This function is designed to handle every click
const handleEveryClick = (event: MouseEvent) => {
// Ignore if the menu isn't open
if (!isOpen) {
return;
}

// Make this happen asynchronously
setTimeout(() => {
// Type guard
if (!(event.target instanceof Element)) {
return;
}

// Ignore if we're clicking inside the menu
if (event.target.closest('[role="menu"]')) {
return;
}

// Hide dropdown
setIsOpen(false);
}, 10);
};

// Add listener
document.addEventListener('click', handleEveryClick);

// Return function to remove listener
return () => document.removeEventListener('click', handleEveryClick);
}, [isOpen]);

// Create a handler function for the button's clicks and keyboard events
Expand Down
46 changes: 40 additions & 6 deletions test/puppeteer/demo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const { keyboard } = page;

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

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

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

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

await keyboard.down('Escape');
await menuClosed();

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

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

await keyboard.down('Tab');
await menuClosed();

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

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

await keyboard.down('Shift');
await keyboard.down('Tab');
await menuClosed();

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

it('closes the menu if you click outside of it', async () => {
await page.click('#menu-button');
await menuOpen();

await page.click('body');
await menuClosed(); // times out if menu doesn't close

expect(true).toBe(true);
});

it('leaves the menu open if you click inside of it', async () => {
await page.click('#menu-button');
await menuOpen();

await page.click('#menu-item-1');
await new Promise(resolve => setTimeout(resolve, 1000)); // visibility: hidden is delayed via CSS
await menuOpen(); // times out if menu closes

await page.click('#menu');
await new Promise(resolve => setTimeout(resolve, 1000)); // visibility: hidden is delayed via CSS
await menuOpen(); // times out if menu closes

expect(true).toBe(true);
});
21 changes: 21 additions & 0 deletions test/use-dropdown-menu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,24 @@ it('moves the focus to the menu button after pressing escape while focused on a
firstMenuItem.simulate('keydown', { key: 'Escape' });
expect(document.activeElement?.id).toBe('menu-button');
});

it('opens the menu after clicking the button', () => {
const component = mount(<TestComponent />);
const button = component.find('#menu-button');
const span = component.find('#is-open-indicator');

button.simulate('click');

expect(span.text()).toBe('true');
});

it('closes the menu after clicking the button when the menu is open', () => {
const component = mount(<TestComponent />);
const button = component.find('#menu-button');
const span = component.find('#is-open-indicator');

button.simulate('click');
button.simulate('click');

expect(span.text()).toBe('false');
});