import * as React from 'react'; import PropTypes from 'prop-types'; import { expect } from 'chai'; import { act, createRenderer, fireEvent, screen } from '@mui/internal-test-utils'; import Button from '@mui/material/Button'; import MenuItem from '@mui/material/MenuItem'; import Menu from '@mui/material/Menu'; const options = [ 'Show some love to MUI', 'Show all notification content', 'Hide sensitive notification content', ]; function ButtonMenu(props) { const { selectedIndex: selectedIndexProp, ...other } = props; const [anchorEl, setAnchorEl] = React.useState(null); const [selectedIndex, setSelectedIndex] = React.useState(selectedIndexProp || null); const handleClickListItem = (event) => { setAnchorEl(event.currentTarget); }; const handleMenuItemClick = (event, index) => { setSelectedIndex(index); setAnchorEl(null); }; const handleClose = () => { setAnchorEl(null); }; const open = Boolean(anchorEl); return (
{options.map((option, index) => ( handleMenuItemClick(event, index)} > {option} ))}
); } ButtonMenu.propTypes = { selectedIndex: PropTypes.number }; describe(' integration', () => { const { clock, render } = createRenderer({ clock: 'fake' }); it('is part of the DOM by default but hidden', () => { render(); expect(screen.getByRole('menu', { hidden: true })).toBeInaccessible(); }); it('does not gain any focus when mounted', () => { render(); expect(screen.getByRole('menu', { hidden: true })).not.to.contain(document.activeElement); }); it('should focus the first item on open', async () => { render(); const button = screen.getByRole('button', { name: 'open menu' }); await act(async () => { button.focus(); button.click(); }); expect(screen.getAllByRole('menuitem')[0]).toHaveFocus(); }); it('changes focus according to keyboard navigation', async () => { render(); const button = screen.getByRole('button', { name: 'open menu' }); await act(async () => { button.focus(); button.click(); }); const menuitems = screen.getAllByRole('menuitem'); fireEvent.keyDown(menuitems[0], { key: 'ArrowDown' }); expect(menuitems[1]).toHaveFocus(); fireEvent.keyDown(menuitems[1], { key: 'ArrowUp' }); expect(menuitems[0]).toHaveFocus(); fireEvent.keyDown(menuitems[0], { key: 'ArrowUp' }); expect(menuitems[2]).toHaveFocus(); fireEvent.keyDown(menuitems[2], { key: 'Home' }); expect(menuitems[0]).toHaveFocus(); fireEvent.keyDown(menuitems[0], { key: 'End' }); expect(menuitems[2]).toHaveFocus(); fireEvent.keyDown(menuitems[2], { key: 'ArrowRight' }); expect(menuitems[2], 'no change on unassociated keys').toHaveFocus(); }); it('focuses the selected item when opening', async () => { render(); const button = screen.getByRole('button', { name: 'open menu' }); await act(async () => { button.focus(); button.click(); }); expect(screen.getAllByRole('menuitem')[2]).toHaveFocus(); }); describe('Menu variant differences', () => { function OpenMenu(props) { return ; } it('[variant=menu] will focus the first item if nothing is selected', () => { render( , ); const menuitems = screen.getAllByRole('menuitem'); expect(menuitems[0]).toHaveFocus(); expect(menuitems[0]).to.have.property('tabIndex', -1); expect(menuitems[1]).to.have.property('tabIndex', -1); expect(menuitems[2]).to.have.property('tabIndex', -1); }); it('[variant=selectedMenu] will focus the first item if nothing is selected', () => { render( , ); const menuitems = screen.getAllByRole('menuitem'); expect(menuitems[0]).toHaveFocus(); expect(menuitems[0]).to.have.property('tabIndex', 0); expect(menuitems[1]).to.have.property('tabIndex', -1); expect(menuitems[2]).to.have.property('tabIndex', -1); }); // no case for variant=selectedMenu it('[variant=menu] prioritizes `autoFocus` on `MenuItem`', () => { render( , ); const menuitems = screen.getAllByRole('menuitem'); expect(menuitems[2]).toHaveFocus(); expect(menuitems[0]).to.have.property('tabIndex', -1); expect(menuitems[1]).to.have.property('tabIndex', -1); expect(menuitems[2]).to.have.property('tabIndex', -1); }); it('[variant=menu] ignores `selected` on `MenuItem`', () => { render( , ); const menuitems = screen.getAllByRole('menuitem'); expect(menuitems[0]).toHaveFocus(); expect(menuitems[0]).to.have.property('tabIndex', -1); expect(menuitems[1]).to.have.property('tabIndex', -1); expect(menuitems[2]).to.have.property('tabIndex', -1); }); it('[variant=selectedMenu] focuses the `selected` `MenuItem`', () => { render( , ); const menuitems = screen.getAllByRole('menuitem'); expect(menuitems[1]).toHaveFocus(); expect(menuitems[0]).to.have.property('tabIndex', -1); expect(menuitems[1]).to.have.property('tabIndex', 0); expect(menuitems[2]).to.have.property('tabIndex', -1); }); it('[variant=selectedMenu] allows overriding `tabIndex` on `MenuItem`', () => { render( , ); const menuitems = screen.getAllByRole('menuitem'); expect(menuitems[1]).toHaveFocus(); expect(menuitems[0]).to.have.property('tabIndex', -1); expect(menuitems[1]).to.have.property('tabIndex', 2); expect(menuitems[2]).to.have.property('tabIndex', -1); }); // falling back to the menu immediately so that we don't have to come up // with custom fallbacks (for example what happens if the first item is also selected) // it's debatable whether disabled items should still be focusable it('[variant=selectedMenu] focuses the first non-disabled item if the selected menuitem is disabled', () => { render( , ); const menuitems = screen.getAllByRole('menuitem'); expect(menuitems[1]).toHaveFocus(); expect(menuitems[0]).to.have.property('tabIndex', -1); expect(menuitems[1]).to.have.property('tabIndex', 0); expect(menuitems[2]).to.have.property('tabIndex', -1); expect(menuitems[3]).to.have.property('tabIndex', -1); }); // no case for menu // TODO: should this even change focus? I would guess that autoFocus={false} // means "developer: I take care of focus don't steal it from me" it('[variant=selectedMenu] focuses no part of the menu when `autoFocus={false}`', () => { render( , ); const menuitems = screen.getAllByRole('menuitem'); expect(screen.getByTestId('Paper')).toHaveFocus(); expect(menuitems[0]).to.have.property('tabIndex', -1); expect(menuitems[1]).to.have.property('tabIndex', 0); expect(menuitems[2]).to.have.property('tabIndex', -1); }); it('[variant=selectedMenu] focuses nothing when it is closed and mounted', () => { render(); expect(screen.getByRole('menu', { hidden: true })).not.to.contain(document.activeElement); }); it('[variant=selectedMenu] focuses the selected item when opening when it was already mounted', async () => { render(); await act(async () => { screen.getByRole('button').focus(); screen.getByRole('button').click(); }); const menuitems = screen.getAllByRole('menuitem'); expect(menuitems[1]).toHaveFocus(); expect(menuitems[0]).to.have.property('tabIndex', -1); expect(menuitems[1]).to.have.property('tabIndex', 0); expect(menuitems[2]).to.have.property('tabIndex', -1); }); }); it('closes the menu when Tabbing while the list is active', async () => { render(); const trigger = screen.getByRole('button'); await act(async () => { trigger.focus(); }); await act(async () => { trigger.click(); }); // eslint-disable-next-line testing-library/no-unnecessary-act -- react-transition-group uses one commit per state transition so we need to wait a bit await act(async () => { fireEvent.keyDown(screen.getAllByRole('menuitem')[0], { key: 'Tab' }); }); clock.tick(0); expect(screen.getByRole('menu', { hidden: true })).toBeInaccessible(); }); it('closes the menu when the backdrop is clicked', async () => { render(); const button = screen.getByRole('button'); await act(async () => { button.focus(); button.click(); screen.getByTestId('Backdrop').click(); }); expect(screen.getByRole('menu', { hidden: true })).toBeInaccessible(); }); });