Some checks failed
No response / noResponse (push) Has been cancelled
CI / Continuous releases (push) Has been cancelled
CI / test-dev (macos-latest) (push) Has been cancelled
CI / test-dev (ubuntu-latest) (push) Has been cancelled
CI / test-dev (windows-latest) (push) Has been cancelled
Maintenance / main (push) Has been cancelled
Scorecards supply-chain security / Scorecards analysis (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
343 lines
10 KiB
JavaScript
343 lines
10 KiB
JavaScript
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 (
|
|
<div>
|
|
<Button
|
|
aria-haspopup="true"
|
|
aria-controls="lock-menu"
|
|
aria-label="open menu"
|
|
onClick={handleClickListItem}
|
|
>
|
|
{`selectedIndex: ${selectedIndex}, open: ${open}`}
|
|
</Button>
|
|
<Menu
|
|
id="lock-menu"
|
|
anchorEl={anchorEl}
|
|
keepMounted
|
|
open={open}
|
|
onClose={handleClose}
|
|
transitionDuration={0}
|
|
BackdropProps={{ 'data-testid': 'Backdrop' }}
|
|
{...other}
|
|
>
|
|
{options.map((option, index) => (
|
|
<MenuItem
|
|
key={option}
|
|
selected={index === selectedIndex}
|
|
onClick={(event) => handleMenuItemClick(event, index)}
|
|
>
|
|
{option}
|
|
</MenuItem>
|
|
))}
|
|
</Menu>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
ButtonMenu.propTypes = { selectedIndex: PropTypes.number };
|
|
|
|
describe('<Menu /> integration', () => {
|
|
const { clock, render } = createRenderer({ clock: 'fake' });
|
|
|
|
it('is part of the DOM by default but hidden', () => {
|
|
render(<ButtonMenu />);
|
|
|
|
expect(screen.getByRole('menu', { hidden: true })).toBeInaccessible();
|
|
});
|
|
|
|
it('does not gain any focus when mounted', () => {
|
|
render(<ButtonMenu />);
|
|
|
|
expect(screen.getByRole('menu', { hidden: true })).not.to.contain(document.activeElement);
|
|
});
|
|
|
|
it('should focus the first item on open', async () => {
|
|
render(<ButtonMenu />);
|
|
|
|
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(<ButtonMenu />);
|
|
|
|
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(<ButtonMenu selectedIndex={2} />);
|
|
|
|
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 <Menu anchorEl={document.body} open {...props} />;
|
|
}
|
|
|
|
it('[variant=menu] will focus the first item if nothing is selected', () => {
|
|
render(
|
|
<OpenMenu variant="menu">
|
|
<MenuItem />
|
|
<MenuItem />
|
|
<MenuItem />
|
|
</OpenMenu>,
|
|
);
|
|
|
|
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(
|
|
<OpenMenu variant="selectedMenu">
|
|
<MenuItem />
|
|
<MenuItem />
|
|
<MenuItem />
|
|
</OpenMenu>,
|
|
);
|
|
|
|
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(
|
|
<OpenMenu variant="menu">
|
|
<MenuItem />
|
|
<MenuItem />
|
|
<MenuItem autoFocus />
|
|
</OpenMenu>,
|
|
);
|
|
|
|
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(
|
|
<OpenMenu variant="menu">
|
|
<MenuItem />
|
|
<MenuItem selected />
|
|
<MenuItem />
|
|
</OpenMenu>,
|
|
);
|
|
|
|
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(
|
|
<OpenMenu variant="selectedMenu">
|
|
<MenuItem />
|
|
<MenuItem selected />
|
|
<MenuItem />
|
|
</OpenMenu>,
|
|
);
|
|
|
|
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(
|
|
<OpenMenu variant="selectedMenu">
|
|
<MenuItem />
|
|
<MenuItem selected tabIndex={2} />
|
|
<MenuItem />
|
|
</OpenMenu>,
|
|
);
|
|
|
|
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(
|
|
<OpenMenu variant="selectedMenu">
|
|
<MenuItem disabled />
|
|
<MenuItem />
|
|
<MenuItem disabled selected />
|
|
<MenuItem />
|
|
</OpenMenu>,
|
|
);
|
|
|
|
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(
|
|
<OpenMenu autoFocus={false} variant="selectedMenu" PaperProps={{ 'data-testid': 'Paper' }}>
|
|
<MenuItem />
|
|
<MenuItem selected />
|
|
<MenuItem />
|
|
</OpenMenu>,
|
|
);
|
|
|
|
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(<ButtonMenu selectedIndex={1} variant="selectedMenu" />);
|
|
|
|
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(<ButtonMenu selectedIndex={1} variant="selectedMenu" />);
|
|
|
|
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(<ButtonMenu />);
|
|
|
|
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(<ButtonMenu />);
|
|
const button = screen.getByRole('button');
|
|
await act(async () => {
|
|
button.focus();
|
|
button.click();
|
|
screen.getByTestId('Backdrop').click();
|
|
});
|
|
|
|
expect(screen.getByRole('menu', { hidden: true })).toBeInaccessible();
|
|
});
|
|
});
|