Skip to content

Commit

Permalink
Merge pull request #279 from sparksuite/disable-focus-first-option
Browse files Browse the repository at this point in the history
Option to disable focusing first menu item on click
  • Loading branch information
WesCossick authored Aug 13, 2021
2 parents 4385c5e + acd63d9 commit 9401baa
Show file tree
Hide file tree
Showing 8 changed files with 61 additions and 19 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-accessible-dropdown-menu-hook",
"version": "2.3.1",
"version": "3.0.0",
"description": "A simple Hook for creating fully accessible dropdown menus in React",
"main": "dist/use-dropdown-menu.js",
"types": "dist/use-dropdown-menu.d.ts",
Expand Down
24 changes: 18 additions & 6 deletions src/use-dropdown-menu.test.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
// Imports
import React, { useState } from 'react';
import useDropdownMenu from './use-dropdown-menu';
import useDropdownMenu, { DropdownMenuOptions } from './use-dropdown-menu';
import { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

// A mock component for testing the Hook
const TestComponent: React.FC = () => {
interface Props {
options?: DropdownMenuOptions;
}

const TestComponent: React.FC<Props> = ({ options }) => {
const [itemCount, setItemCount] = useState(4);
const { buttonProps, itemProps, isOpen, setIsOpen } = useDropdownMenu(itemCount);
const { buttonProps, itemProps, isOpen, setIsOpen } = useDropdownMenu(itemCount, options);

const clickHandlers: (() => void)[] = [(): void => console.log('Item one clicked'), (): void => setIsOpen(false)];

Expand Down Expand Up @@ -79,20 +83,28 @@ it('Moves the focus to the first menu item after pressing space while focused on
expect(screen.getByText('1 Item')).toHaveFocus();
});

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', () => {
it('Moves the focus to the first menu item after clicking the menu to open it', () => {
render(<TestComponent />);

userEvent.click(screen.getByText('Primary'));

expect(screen.getByText('1 Item')).toHaveFocus();
});

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, if `disableFocusFirstItemOnClick` is specified', () => {
render(<TestComponent options={{ disableFocusFirstItemOnClick: true }} />);

userEvent.click(screen.getByText('Primary'));

expect(screen.getByText('Primary')).toHaveFocus();

userEvent.tab();

expect(screen.getByText('1 Item')).toHaveFocus();
});

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', () => {
render(<TestComponent />);
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, if `disableFocusFirstItemOnClick` is specified', () => {
render(<TestComponent options={{ disableFocusFirstItemOnClick: true }} />);

userEvent.click(screen.getByText('Primary'));

Expand Down
13 changes: 10 additions & 3 deletions src/use-dropdown-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ interface ButtonProps
}

// A custom Hook that abstracts away the listeners/controls for dropdown menus
export interface DropdownMenuOptions {
disableFocusFirstItemOnClick?: boolean;
}

interface DropdownMenuResponse {
readonly buttonProps: ButtonProps;
readonly itemProps: {
Expand All @@ -23,7 +27,7 @@ interface DropdownMenuResponse {
readonly setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
}

export default function useDropdownMenu(itemCount: number): DropdownMenuResponse {
export default function useDropdownMenu(itemCount: number, options?: DropdownMenuOptions): DropdownMenuResponse {
// Use state
const [isOpen, setIsOpen] = useState<boolean>(false);
const currentFocusIndex = useRef<number | null>(null);
Expand Down Expand Up @@ -56,7 +60,7 @@ export default function useDropdownMenu(itemCount: number): DropdownMenuResponse
}

// If the menu is currently open focus on the first item in the menu
if (isOpen && !clickedOpen.current) {
if (isOpen && !options?.disableFocusFirstItemOnClick) {
moveFocus(0);
} else if (!isOpen) {
clickedOpen.current = false;
Expand Down Expand Up @@ -137,7 +141,10 @@ export default function useDropdownMenu(itemCount: number): DropdownMenuResponse
setIsOpen(false);
}
} else {
clickedOpen.current = !isOpen;
if (options?.disableFocusFirstItemOnClick) {
clickedOpen.current = !isOpen;
}

setIsOpen(!isOpen);
}
};
Expand Down
4 changes: 2 additions & 2 deletions test-projects/browser/src/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ it('Has the correct page title', async () => {
await expect(page.title()).resolves.toMatch('Browser');
});

it('Leaves focus on the button after clicking it', async () => {
it('Focuses the first menu item when menu button is clicked', async () => {
await page.click('#menu-button');
await menuOpen();

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

it('Focuses on the menu button after pressing escape', async () => {
Expand Down
19 changes: 19 additions & 0 deletions website/docs/design/options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: Options
---

You can customize the behavior with options, passed as the second argument.

Option | Default | Possible values
:--- | :--- | :---
`disableFocusFirstItemOnClick` | `false` | `boolean`

Option | Explanation
:--- | :---
`disableFocusFirstItemOnClick` | If specified as `true` the default behavior of focusing the first menu item on click will be disabled. The menu button will instead retain focus.

```js
const { buttonProps, itemProps, isOpen, setIsOpen } = useDropdownMenu(numberOfItems, {
disableFocusFirstItemOnClick: true,
});
```
2 changes: 1 addition & 1 deletion website/sidebars.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module.exports = {
default: {
'Getting started': ['getting-started/install', 'getting-started/import', 'getting-started/using'],
Design: ['design/return-object', 'design/accessibility'],
Design: ['design/return-object', 'design/accessibility', 'design/options'],
},
};
9 changes: 9 additions & 0 deletions website/src/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,19 @@
text-decoration: underline;
}

/* Fallback for browsers that do not support :focus-visible. */
.demo-menu a:focus {
border-color: #3a8eb8;
}

.demo-menu a:focus:not(:focus-visible) {
border-color: transparent;
}

.demo-menu a:focus-visible {
border-color: #3a8eb8;
}

.demo-menu a svg {
color: #777;
margin-right: 0.5rem;
Expand Down
7 changes: 1 addition & 6 deletions website/src/pages/demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,7 @@ const Demo: React.FC = () => {
The menu can be revealed by clicking the button, or by focusing the button and pressing enter /
space
</li>
<li>If the menu is revealed with the keyboard, the first menu item is automatically focused</li>
<li>
If the menu is revealed with the mouse, the first menu item can be focused by pressing tab / arrow
down
</li>
<li>If the menu is revealed with the mouse, the menu can be be closed by pressing escape</li>
<li>When the menu is revealed, the first menu item is automatically focused</li>
<li>
<em>Once focus is in the menu…</em>

Expand Down

0 comments on commit 9401baa

Please sign in to comment.