Menu
Menu
is a list of groupable actions inside of a container. It is toggled by the user and is most often used when space is limited.MenuItemLink
andMenuItem
have the same styling and keyboard events- Menu items can be grouped to separate categories
- Icons are optional but should be applied to all items within the Menu when used
- Icons are always aligned to the left of the label
- Use a
MenuGroup
to separate significant actions or destructive actions from less consequential actions. E.g. actions that alter user data or costs should be separated from actions that link to another page - A label is required when using a group
- Use a
MenuSeparator
to help visually differentiate between groups
- Menu is pinned to the bottom of the viewport and covers 100% of the screen width on screens narrower than
600px
- On short screens, the content within Menu becomes scrollable
- Menu uses Popper to position the component on the screen
- The component handles positioning automatically; custom positioning is not currently supported
- Menu can be closed by clicking any element outside of the Menu
aria-label
is required and provides an accessible label for MenuuseMenu()
provides necessary accessibility attributes for Menu that are required on the triggering element
Keys | Action |
---|---|
return, space, ↑ and ↓ arrow keys | |
return or escape keys | Close |
↑ and ↓ arrow keys, or home and end | Focus menu items |
return | Select item |
a–z | Type the first letter of an action to move focus to that action. I.e., typing "p" would move focus to the “Payroll” action. |
Name | Type | Default | Description |
---|---|---|---|
aria-label Required | string | Accessible label for the element. Not displayed visually. | |
children | ReactNode | The content of the Menu. | |
onClose | function | A callback for when the Menu is closed. | |
open | boolean | false | Determines the visual treatment of the button. |
reference Required | Element VirtualElement null | HTMLElement used to position the popover (typically provided by useMenu). |
Menus are made up of several sub-components, each with their own special purpose and set of props (see: Menu anatomy).
Subcomponent | Purpose |
---|---|
MenuItem | Used for items that only need to respond to an onClick event. |
MenuItemLink | Used for items that will navigate to a new URL, optionally with an onClick . |
MenuGroup | Groups elements using a required label ; used in combination with MenuSeparator . |
MenuSeparator | Creates a visual divider between groups of items. |
The following testing snippet(s) offer suggestions for testing the component using React Testing Library with occasional help from Jest.
const MoreActionsMenu = () => {const [menuProps, menuButtonProps] = useMenu();return (<><IconButton {...menuButtonProps} title="More actions"><More /></IconButton><Menu {...menuProps} aria-label="More actions for email"><MenuItem before={<MailClosed />} onClick={onClick}>Inbox</MenuItem><MenuItem before={<StarOutline />} onClick={onClick}>Starred</MenuItem><MenuItem before={<Clock />} onClick={onClick}>Snoozed</MenuItem><MenuItem before={<Upload />} onClick={onClick}>Sent</MenuItem><MenuItem before={<Document />} onClick={onClick}>Drafts</MenuItem><MenuSeparator /><MenuGroup label="Meetings"><MenuItem before={<Plus />} onClick={onClick}>New meeting</MenuItem><MenuItem before={<CalendarOutline />} onClick={onClick}>My meetings</MenuItem></MenuGroup><MenuSeparator /><MenuItemLinkbefore={<ExternalLink />}color="error"href="https://gusto.com"onClick={onClick}>Sign out</MenuItemLink></Menu></>);};render(<MoreActionsMenu />);const menuButton = screen.getByLabelText('More actions');// Alternatively using getByRole:// const menuButton = screen.getByRole('button', {// name: 'More actions',// });act(() => {userEvent.click(menuButton);});const menu = screen.getByLabelText('More actions for email');// Alternatively using getByRole:// const menu = screen.getByRole('menu', {// name: 'More actions for email',// });expect(menu).toBeVisible();// Standard MenuItemconst inboxMenuItem = within(menu).getByRole('menuitem', {name: 'Inbox',});act(() => {userEvent.click(inboxMenuItem);});expect(menu).not.toBeVisible();act(() => {userEvent.click(menuButton);});expect(menu).toBeVisible();// MenuItemLinkconst signOutMenuItem = within(menu).getByRole('menuitem', {name: 'Sign out',});act(() => {userEvent.click(signOutMenuItem);});expect(menu).not.toBeVisible();const separators = within(menu).queryAllByRole('separator');expect(separators).toHaveLength(2);const mettingsGroup = within(menu).getByRole('group', {name: 'Meetings',});const newMeetingItem = within(meetingsGroup).getByRole('menuitem', {name: 'New meeting',});act(() => {userEvent.click(newMeetingItem);});expect(menu).not.toBeVisible();