init project
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

This commit is contained in:
how2ice
2025-12-12 14:26:25 +09:00
commit 005cf56baf
43188 changed files with 1079531 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
import {
describeConformance as baseDescribeConformance,
ConformanceOptions,
} from '@mui/internal-test-utils';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import DefaultPropsProvider from '@mui/material/DefaultPropsProvider';
export default function describeConformance(
minimalElement: React.ReactElement<unknown>,
getOptions: () => ConformanceOptions,
) {
function getOptionsWithDefaults() {
return {
ThemeProvider,
createTheme,
DefaultPropsProvider,
...getOptions(),
};
}
return baseDescribeConformance(minimalElement, getOptionsWithDefaults);
}

View File

@@ -0,0 +1,31 @@
import * as React from 'react';
import { expect } from 'chai';
import { createRenderer, screen } from '@mui/internal-test-utils';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
describe('<Dialog /> integration', () => {
const { render } = createRenderer();
it('is automatically labelled by its DialogTitle', () => {
render(
<Dialog open>
<DialogTitle>Set backup account</DialogTitle>
</Dialog>,
);
expect(screen.getByRole('dialog')).toHaveAccessibleName('Set backup account');
});
it('can be manually labelled', () => {
render(
<Dialog open aria-labelledby="dialog-title">
<DialogTitle id="dialog-title">Set backup account</DialogTitle>
</Dialog>,
);
const dialog = screen.getByRole('dialog');
expect(dialog).toHaveAccessibleName('Set backup account');
expect(dialog).to.have.attr('aria-labelledby', 'dialog-title');
});
});

View File

@@ -0,0 +1,342 @@
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();
});
});

View File

@@ -0,0 +1,663 @@
import * as React from 'react';
import { expect } from 'chai';
import { spy } from 'sinon';
import {
act,
createRenderer,
fireEvent,
screen,
programmaticFocusTriggersFocusVisible,
isJsdom,
} from '@mui/internal-test-utils';
import MenuList from '@mui/material/MenuList';
import MenuItem from '@mui/material/MenuItem';
import Divider from '@mui/material/Divider';
describe('<MenuList> integration', () => {
const { clock, render } = createRenderer();
it('the MenuItems have the `menuitem` role', () => {
render(
<MenuList>
<MenuItem>Menu Item 1</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
<MenuItem>Menu Item 3</MenuItem>
</MenuList>,
);
expect(screen.getAllByRole('menuitem')).to.have.length(3);
});
describe('keyboard controls and tabIndex manipulation', () => {
it('the specified item should be in tab order while the rest is focusable', () => {
render(
<MenuList>
<MenuItem tabIndex={0}>Menu Item 1</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
<MenuItem>Menu Item 3</MenuItem>
</MenuList>,
);
const menuitems = screen.getAllByRole('menuitem');
expect(menuitems[0]).to.have.property('tabIndex', 0);
expect(menuitems[1]).to.have.property('tabIndex', -1);
expect(menuitems[2]).to.have.property('tabIndex', -1);
});
it('focuses the specified item on mount', () => {
render(
<MenuList autoFocusItem>
<MenuItem>Menu Item 1</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
<MenuItem>Menu Item 3</MenuItem>
</MenuList>,
);
expect(screen.getAllByRole('menuitem')[0]).toHaveFocus();
});
it('should select the last item when pressing up if the first item is focused', () => {
render(
<MenuList autoFocusItem>
<MenuItem selected>Menu Item 1</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
<MenuItem>Menu Item 3</MenuItem>
</MenuList>,
);
const menuitems = screen.getAllByRole('menuitem');
fireEvent.keyDown(menuitems[0], { key: 'ArrowUp' });
expect(menuitems[2]).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);
});
it('should select the second item when pressing down if the first item is selected', () => {
render(
<MenuList autoFocusItem>
<MenuItem selected>Menu Item 1</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
<MenuItem>Menu Item 3</MenuItem>
</MenuList>,
);
const menuitems = screen.getAllByRole('menuitem');
fireEvent.keyDown(menuitems[0], { key: 'ArrowDown' });
expect(menuitems[1]).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);
});
it('should still be focused and focusable when going back and forth', () => {
render(
<MenuList autoFocusItem>
<MenuItem selected>Menu Item 1</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
<MenuItem>Menu Item 3</MenuItem>
</MenuList>,
);
const menuitems = screen.getAllByRole('menuitem');
fireEvent.keyDown(menuitems[0], { key: 'ArrowDown' });
fireEvent.keyDown(menuitems[1], { key: 'ArrowUp' });
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);
});
it('should leave tabIndex on the first item after blur', () => {
const handleBlur = spy();
render(
<MenuList autoFocusItem onBlur={handleBlur}>
<MenuItem selected>Menu Item 1</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
<MenuItem>Menu Item 3</MenuItem>
</MenuList>,
);
expect(document.activeElement).not.to.equal(null);
act(() => {
document.activeElement.blur();
});
const menuitems = screen.getAllByRole('menuitem');
expect(handleBlur.callCount).to.equal(1);
expect(menuitems[0]).to.have.property('tabIndex', 0);
expect(menuitems[1]).to.have.property('tabIndex', -1);
expect(menuitems[2]).to.have.property('tabIndex', -1);
expect(menuitems[0]).not.toHaveFocus();
expect(menuitems[1]).not.toHaveFocus();
expect(menuitems[2]).not.toHaveFocus();
});
it('can imperatively focus the first item', () => {
render(
<MenuList autoFocusItem>
<MenuItem selected>Menu Item 1</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
<MenuItem>Menu Item 3</MenuItem>
</MenuList>,
);
const menuitems = screen.getAllByRole('menuitem');
act(() => {
menuitems[0].focus();
});
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);
});
it('down arrow can go to all items while not changing tabIndex', () => {
render(
<MenuList autoFocusItem>
<MenuItem selected>Menu Item 1</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
<MenuItem>Menu Item 3</MenuItem>
</MenuList>,
);
const menuitems = screen.getAllByRole('menuitem');
fireEvent.keyDown(menuitems[0], { key: 'ArrowDown' });
expect(menuitems[1]).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);
fireEvent.keyDown(menuitems[1], { key: 'ArrowDown' });
expect(menuitems[2]).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);
});
describe('when a modifier key is pressed', () => {
it('should not move the focus', () => {
render(
<MenuList autoFocusItem>
<MenuItem selected>Menu Item 1</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
<MenuItem>Menu Item 3</MenuItem>
</MenuList>,
);
const menuitems = screen.getAllByRole('menuitem');
fireEvent.keyDown(menuitems[0], { key: 'ArrowDown', ctrlKey: true });
expect(menuitems[0]).toHaveFocus();
expect(menuitems[1]).not.toHaveFocus();
fireEvent.keyDown(menuitems[0], { key: 'ArrowDown', altKey: true });
expect(menuitems[0]).toHaveFocus();
expect(menuitems[1]).not.toHaveFocus();
fireEvent.keyDown(menuitems[0], { key: 'ArrowDown', metaKey: true });
expect(menuitems[0]).toHaveFocus();
expect(menuitems[1]).not.toHaveFocus();
});
it('should call the onKeyDown and not prevent default on the event', () => {
const onKeyDown = spy();
render(
<MenuList autoFocusItem onKeyDown={onKeyDown}>
<MenuItem selected>Menu Item 1</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
<MenuItem>Menu Item 3</MenuItem>
</MenuList>,
);
const menuitems = screen.getAllByRole('menuitem');
fireEvent.keyDown(menuitems[0], { key: 'ArrowDown', ctrlKey: true });
expect(onKeyDown.callCount).to.equal(1);
expect(onKeyDown.firstCall.args[0]).to.have.property('ctrlKey', true);
expect(onKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', false);
});
});
});
describe('keyboard controls and tabIndex manipulation - preselected item', () => {
it('should auto focus the second item', () => {
render(
<MenuList>
<MenuItem>Menu Item 1</MenuItem>
<MenuItem autoFocus selected tabIndex={0}>
Menu Item 2
</MenuItem>
<MenuItem>Menu Item 3</MenuItem>
</MenuList>,
);
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('should focus next item on ArrowDown', () => {
render(
<MenuList>
<MenuItem>Menu Item 1</MenuItem>
<MenuItem autoFocus selected tabIndex={0}>
Menu Item 2
</MenuItem>
<MenuItem>Menu Item 3</MenuItem>
</MenuList>,
);
const menuitems = screen.getAllByRole('menuitem');
fireEvent.keyDown(menuitems[1], { key: 'ArrowDown' });
expect(menuitems[2]).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);
});
});
describe('keyboard controls and tabIndex manipulation - preselected item, no item autoFocus', () => {
it('should focus the first item if no item is focused when pressing ArrowDown', () => {
render(
<MenuList autoFocus>
<MenuItem>Menu Item 1</MenuItem>
<MenuItem selected>Menu Item 2</MenuItem>
<MenuItem>Menu Item 3</MenuItem>
</MenuList>,
);
const menuitems = screen.getAllByRole('menuitem');
fireEvent.keyDown(screen.getByRole('menu'), { key: 'ArrowDown' });
expect(menuitems[0]).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('should focus the third item if no item is focused when pressing ArrowUp', () => {
render(
<MenuList autoFocus>
<MenuItem>Menu Item 1</MenuItem>
<MenuItem selected tabIndex={0}>
Menu Item 2
</MenuItem>
<MenuItem>Menu Item 3</MenuItem>
</MenuList>,
);
const menuitems = screen.getAllByRole('menuitem');
fireEvent.keyDown(screen.getByRole('menu'), { key: 'ArrowUp' });
expect(menuitems[2]).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('initial focus is controlled by setting the selected prop when `autoFocusItem` is enabled', () => {
render(
<MenuList autoFocusItem>
<MenuItem>Menu Item 1</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
<MenuItem selected>Menu Item 3</MenuItem>
<MenuItem>Menu Item 4</MenuItem>
</MenuList>,
);
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', 0);
expect(menuitems[3]).to.have.property('tabIndex', -1);
});
describe('MenuList with disableListWrap', () => {
it('should not wrap focus with ArrowUp from first', () => {
render(
<MenuList autoFocusItem disableListWrap>
<MenuItem selected>Menu Item 1</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
</MenuList>,
);
const menuitems = screen.getAllByRole('menuitem');
fireEvent.keyDown(menuitems[0], { key: 'ArrowUp' });
expect(menuitems[0]).toHaveFocus();
expect(menuitems[0]).to.have.property('tabIndex', 0);
expect(menuitems[1]).to.have.property('tabIndex', -1);
});
it('should not wrap focus with ArrowDown from last', () => {
render(
<MenuList autoFocusItem disableListWrap>
<MenuItem>Menu Item 1</MenuItem>
<MenuItem selected>Menu Item 2</MenuItem>
</MenuList>,
);
const menuitems = screen.getAllByRole('menuitem');
fireEvent.keyDown(menuitems[1], { key: 'ArrowDown' });
expect(menuitems[1]).toHaveFocus();
expect(menuitems[0]).to.have.property('tabIndex', -1);
expect(menuitems[1]).to.have.property('tabIndex', 0);
});
});
it('should skip divider and disabled menu item', () => {
render(
<MenuList autoFocus>
<MenuItem>Menu Item 1</MenuItem>
<Divider component="li" />
<MenuItem>Menu Item 2</MenuItem>
<MenuItem disabled>Menu Item 3</MenuItem>
<MenuItem>Menu Item 4</MenuItem>
</MenuList>,
);
const menuitems = screen.getAllByRole('menuitem');
fireEvent.keyDown(screen.getByRole('menu'), { key: 'ArrowDown' });
expect(menuitems[0]).toHaveFocus();
fireEvent.keyDown(menuitems[0], { key: 'ArrowDown' });
expect(menuitems[1]).toHaveFocus();
fireEvent.keyDown(menuitems[1], { key: 'ArrowDown' });
expect(menuitems[3]).toHaveFocus();
fireEvent.keyDown(menuitems[3], { key: 'ArrowDown' });
expect(menuitems[0]).toHaveFocus();
// and ArrowUp again
fireEvent.keyDown(menuitems[0], { key: 'ArrowUp' });
expect(menuitems[3]).toHaveFocus();
fireEvent.keyDown(menuitems[3], { key: 'ArrowUp' });
expect(menuitems[1]).toHaveFocus();
fireEvent.keyDown(menuitems[1], { key: 'ArrowUp' });
expect(menuitems[0]).toHaveFocus();
fireEvent.keyDown(menuitems[0], { key: 'ArrowUp' });
expect(menuitems[3]).toHaveFocus();
});
it('should stay on a single item if it is the only focusable one', () => {
render(
<MenuList autoFocus>
<MenuItem disabled>Menu Item 1</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
<MenuItem disabled>Menu Item 3</MenuItem>
<MenuItem disabled>Menu Item 4</MenuItem>
</MenuList>,
);
const menuitems = screen.getAllByRole('menuitem');
fireEvent.keyDown(screen.getByRole('menu'), { key: 'ArrowDown' });
expect(menuitems[1]).toHaveFocus();
fireEvent.keyDown(menuitems[1], { key: 'ArrowDown' });
expect(menuitems[1]).toHaveFocus();
fireEvent.keyDown(menuitems[1], { key: 'ArrowDown' });
expect(menuitems[1]).toHaveFocus();
fireEvent.keyDown(menuitems[1], { key: 'ArrowUp' });
expect(menuitems[1]).toHaveFocus();
fireEvent.keyDown(menuitems[1], { key: 'ArrowUp' });
expect(menuitems[1]).toHaveFocus();
});
it('should keep focus on the menu if all items are disabled', () => {
render(
<MenuList autoFocus>
<MenuItem disabled>Menu Item 1</MenuItem>
<MenuItem disabled>Menu Item 2</MenuItem>
<MenuItem disabled>Menu Item 3</MenuItem>
<MenuItem disabled>Menu Item 4</MenuItem>
</MenuList>,
);
const menu = screen.getByRole('menu');
fireEvent.keyDown(menu, { key: 'Home' });
expect(menu).toHaveFocus();
fireEvent.keyDown(menu, { key: 'ArrowDown' });
expect(menu).toHaveFocus();
fireEvent.keyDown(menu, { key: 'ArrowDown' });
expect(menu).toHaveFocus();
fireEvent.keyDown(menu, { key: 'End' });
expect(menu).toHaveFocus();
fireEvent.keyDown(menu, { key: 'ArrowUp' });
expect(menu).toHaveFocus();
});
it('should allow focus on disabled items when disabledItemsFocusable=true', () => {
render(
<MenuList autoFocus disabledItemsFocusable>
<MenuItem disabled>Menu Item 1</MenuItem>
<MenuItem disabled>Menu Item 2</MenuItem>
<MenuItem disabled>Menu Item 3</MenuItem>
<MenuItem disabled>Menu Item 4</MenuItem>
</MenuList>,
);
const menuitems = screen.getAllByRole('menuitem');
fireEvent.keyDown(screen.getByRole('menu'), { key: 'Home' });
expect(menuitems[0]).toHaveFocus();
fireEvent.keyDown(menuitems[0], { key: 'ArrowDown' });
expect(menuitems[1]).toHaveFocus();
fireEvent.keyDown(menuitems[1], { key: 'ArrowDown' });
expect(menuitems[2]).toHaveFocus();
fireEvent.keyDown(menuitems[2], { key: 'End' });
expect(menuitems[3]).toHaveFocus();
fireEvent.keyDown(menuitems[3], { key: 'ArrowUp' });
expect(menuitems[2]).toHaveFocus();
});
describe('MenuList text-based keyboard controls', () => {
let innerTextSupported;
beforeAll(() => {
const element = document.createElement('div');
element.appendChild(document.createTextNode('Hello, Dave!'));
innerTextSupported = element.innerText !== undefined;
});
it('selects the first item starting with the character', () => {
render(
<MenuList>
<MenuItem>Arizona</MenuItem>
<MenuItem>Berizona</MenuItem>
</MenuList>,
);
const menu = screen.getByRole('menu');
act(() => {
menu.focus();
});
fireEvent.keyDown(menu, { key: 'a' });
expect(screen.getByText('Arizona')).toHaveFocus();
});
it('should cycle through items when repeating initial character', () => {
render(
<MenuList>
<MenuItem>Arizona</MenuItem>
<MenuItem>aardvark</MenuItem>
<MenuItem>Colorado</MenuItem>
<MenuItem>Argentina</MenuItem>
</MenuList>,
);
const menuitems = screen.getAllByRole('menuitem');
act(() => {
menuitems[0].focus();
});
fireEvent.keyDown(screen.getByText('Arizona'), { key: 'a' });
expect(screen.getByText('aardvark')).toHaveFocus();
fireEvent.keyDown(screen.getByText('aardvark'), { key: 'a' });
expect(screen.getByText('Argentina')).toHaveFocus();
fireEvent.keyDown(screen.getByText('Argentina'), { key: 'r' });
expect(screen.getByText('aardvark')).toHaveFocus();
});
it('selects the next item starting with the typed character', () => {
render(
<MenuList>
<MenuItem>Arizona</MenuItem>
<MenuItem>Arcansas</MenuItem>
</MenuList>,
);
act(() => {
screen.getByText('Arizona').focus();
});
fireEvent.keyDown(screen.getByText('Arizona'), { key: 'a' });
expect(screen.getByText('Arcansas')).toHaveFocus();
});
// JSDOM doesn't support :focus-visible
it.skipIf(isJsdom())('should not get focusVisible class on click', async function test() {
const { user } = render(
<MenuList>
<MenuItem focusVisibleClassName="focus-visible">Arizona</MenuItem>
</MenuList>,
);
const menuitem = screen.getByText('Arizona');
await user.click(menuitem);
expect(menuitem).toHaveFocus();
if (programmaticFocusTriggersFocusVisible()) {
expect(menuitem).to.have.class('focus-visible');
} else {
expect(menuitem).not.to.have.class('focus-visible');
}
});
it('should not move focus when no match', () => {
render(
<MenuList>
<MenuItem autoFocus>Arizona</MenuItem>
<MenuItem>Berizona</MenuItem>
</MenuList>,
);
fireEvent.keyDown(screen.getByText('Arizona'), { key: 'c' });
expect(screen.getByText('Arizona')).toHaveFocus();
});
it('should not move focus when keys match current focus', () => {
render(
<MenuList>
<MenuItem autoFocus>Arizona</MenuItem>
<MenuItem>Berizona</MenuItem>
</MenuList>,
);
fireEvent.keyDown(screen.getByText('Arizona'), { key: 'A' });
expect(screen.getByText('Arizona')).toHaveFocus();
fireEvent.keyDown(screen.getByText('Arizona'), { key: 'r' });
expect(screen.getByText('Arizona')).toHaveFocus();
});
it("should not move focus if focus starts on descendant and the key doesn't match", () => {
render(
<MenuList>
<MenuItem>Arizona</MenuItem>
<MenuItem>
<button type="button">Focusable Descendant</button>
</MenuItem>
</MenuList>,
);
const button = screen.getByText('Focusable Descendant');
act(() => {
button.focus();
});
fireEvent.keyDown(button, { key: 'z' });
expect(button).toHaveFocus();
});
it('matches rapidly typed text', () => {
render(
<MenuList autoFocus>
<MenuItem>War</MenuItem>
<MenuItem>Worm</MenuItem>
<MenuItem>Ordinary</MenuItem>
</MenuList>,
);
fireEvent.keyDown(screen.getByRole('menu'), { key: 'W' });
fireEvent.keyDown(screen.getByText('War'), { key: 'o' });
expect(screen.getByText('Worm')).toHaveFocus();
});
describe('time', () => {
clock.withFakeTimers();
it('should reset the character buffer after 500ms', () => {
render(
<MenuList autoFocus>
<MenuItem>Worm</MenuItem>
<MenuItem>Ordinary</MenuItem>
</MenuList>,
);
fireEvent.keyDown(screen.getByRole('menu'), { key: 'W' });
clock.tick(501);
fireEvent.keyDown(screen.getByText('Worm'), { key: 'o' });
expect(screen.getByText('Ordinary')).toHaveFocus();
});
});
// Will only be executed in browser tests, since jsdom doesn't support innerText
it.skipIf(!innerTextSupported)('should match ignoring hidden text', function testHiddenText() {
render(
<MenuList autoFocus>
<MenuItem>
W<span style={{ display: 'none' }}>Should not block type focus</span>orm
</MenuItem>
<MenuItem>Ordinary</MenuItem>
</MenuList>,
);
fireEvent.keyDown(screen.getByRole('menu'), { key: 'W' });
fireEvent.keyDown(screen.getByText('Worm'), { key: 'o' });
expect(screen.getByText('Worm')).toHaveFocus();
});
});
});

View File

@@ -0,0 +1,91 @@
import * as React from 'react';
import { expect } from 'chai';
import { createRenderer, within, screen } from '@mui/internal-test-utils';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
const NoTransition = React.forwardRef(function NoTransition(props, ref) {
const { children, in: inProp } = props;
if (!inProp) {
return null;
}
return <div ref={ref}>{children}</div>;
});
function NestedMenu(props) {
const { firstMenuOpen, secondMenuOpen } = props;
const [anchorEl, setAnchorEl] = React.useState(null);
const canBeOpen = Boolean(anchorEl);
return (
<div>
<button type="button" ref={setAnchorEl}>
anchor
</button>
<Menu
anchorEl={anchorEl}
hideBackdrop
MenuListProps={{ id: 'second-menu' }}
open={Boolean(secondMenuOpen && canBeOpen)}
TransitionComponent={NoTransition}
>
<MenuItem>Second Menu</MenuItem>
</Menu>
<Menu
anchorEl={anchorEl}
hideBackdrop
MenuListProps={{ id: 'first-menu' }}
open={Boolean(firstMenuOpen && canBeOpen)}
TransitionComponent={NoTransition}
>
<MenuItem>Profile 1</MenuItem>
<MenuItem>My account</MenuItem>
<MenuItem>Logout</MenuItem>
</Menu>
</div>
);
}
describe('<NestedMenu> integration', () => {
const { render } = createRenderer();
it('should not be open', () => {
render(<NestedMenu />);
expect(screen.queryAllByRole('menu')).to.have.length(0);
});
it('should focus the first item of the first menu when nothing has been selected', () => {
render(<NestedMenu firstMenuOpen />);
expect(screen.getByRole('menu')).to.have.id('first-menu');
expect(within(screen.getByRole('menu')).getAllByRole('menuitem')[0]).toHaveFocus();
});
it('should focus the first item of the second menu when nothing has been selected', () => {
render(<NestedMenu secondMenuOpen />);
expect(screen.getByRole('menu')).to.have.id('second-menu');
expect(within(screen.getByRole('menu')).getAllByRole('menuitem')[0]).toHaveFocus();
});
it('should open the first menu after it was closed', () => {
const { setProps } = render(<NestedMenu firstMenuOpen />);
setProps({ firstMenuOpen: false });
setProps({ firstMenuOpen: true });
expect(screen.getByRole('menu')).to.have.id('first-menu');
expect(within(screen.getByRole('menu')).getAllByRole('menuitem')[0]).toHaveFocus();
});
it('should be able to open second menu again', () => {
const { setProps } = render(<NestedMenu secondMenuOpen />);
setProps({ secondMenuOpen: false });
setProps({ secondMenuOpen: true });
expect(screen.getByRole('menu')).to.have.id('second-menu');
expect(within(screen.getByRole('menu')).getAllByRole('menuitem')[0]).toHaveFocus();
});
});

View File

@@ -0,0 +1,223 @@
import * as React from 'react';
import { expect } from 'chai';
import { spy } from 'sinon';
import { createRenderer, isJsdom } from '@mui/internal-test-utils';
import Collapse from '@mui/material/Collapse';
import Fade from '@mui/material/Fade';
import Grow from '@mui/material/Grow';
import Slide from '@mui/material/Slide';
import Zoom from '@mui/material/Zoom';
import Popper from '@mui/material/Popper';
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
describe.skipIf(isJsdom())('<Popper />', () => {
const { render } = createRenderer();
let originalScrollX;
let originalScrollY;
beforeEach(() => {
originalScrollX = window.screenX;
originalScrollY = window.scrollY;
});
afterEach(() => {
window.scrollTo(originalScrollX, originalScrollY);
});
describe('children layout integration', () => {
function BottomAnchoredPopper(props) {
const [anchorEl, anchorElRef] = React.useState(null);
React.useEffect(() => {
if (anchorEl !== null) {
window.scrollTo(0, anchorEl.getBoundingClientRect().top);
}
}, [anchorEl]);
return (
<React.Fragment>
<div style={{ height: '200vh' }}>Spacer</div>
<button ref={anchorElRef}>Anchor</button>
<Popper anchorEl={anchorEl} {...props} />
</React.Fragment>
);
}
it('autoFocus does not scroll', () => {
const handleFocus = spy();
const { setProps } = render(
<BottomAnchoredPopper open={false}>
<div>
<button autoFocus onFocus={handleFocus}>
will be focused
</button>
</div>
</BottomAnchoredPopper>,
);
expect(handleFocus.callCount).to.equal(0);
const scrollYBeforeOpen = window.scrollY;
setProps({ open: true });
expect(handleFocus.callCount).to.equal(1);
expect(window.scrollY, 'focus caused scroll').to.equal(scrollYBeforeOpen);
});
it('focus during layout effect does not scroll', () => {
const handleFocus = spy();
function LayoutEffectFocusButton(props) {
const buttonRef = React.useRef(null);
React.useLayoutEffect(() => {
buttonRef.current.focus();
}, []);
return <button {...props} ref={buttonRef} />;
}
const { setProps } = render(
<BottomAnchoredPopper open={false}>
<div>
<LayoutEffectFocusButton onFocus={handleFocus}>will be focused</LayoutEffectFocusButton>
</div>
</BottomAnchoredPopper>,
);
expect(handleFocus.callCount).to.equal(0);
const scrollYBeforeOpen = window.scrollY;
setProps({ open: true });
expect(handleFocus.callCount).to.equal(1);
expect(window.scrollY, 'focus caused scroll').to.equal(scrollYBeforeOpen);
});
it('focus during passive effects do not scroll', () => {
const handleFocus = spy();
function EffectFocusButton(props) {
const buttonRef = React.useRef(null);
React.useEffect(() => {
buttonRef.current.focus();
}, []);
return <button {...props} ref={buttonRef} />;
}
const { setProps } = render(
<BottomAnchoredPopper open={false}>
<div>
<EffectFocusButton onFocus={handleFocus}>will be focused</EffectFocusButton>
</div>
</BottomAnchoredPopper>,
);
expect(handleFocus.callCount).to.equal(0);
const scrollYBeforeOpen = window.scrollY;
setProps({ open: true });
expect(handleFocus.callCount).to.equal(1);
if (isSafari) {
expect(window.scrollY, 'focus caused scroll').to.equal(scrollYBeforeOpen);
} else {
// FIXME: should equal
expect(window.scrollY, 'focus caused scroll').not.to.equal(scrollYBeforeOpen);
}
});
[
[Collapse, 'Collapse'],
[Fade, 'Fade'],
[Grow, 'Grow'],
[Slide, 'Slide'],
[Zoom, 'Zoom'],
].forEach(([TransitionComponent, name]) => {
describe(`in TransitionComponent ${name}`, () => {
it('autoFocus does not scroll', () => {
const handleFocus = spy();
const { setProps } = render(
<BottomAnchoredPopper open={false} transition>
{({ TransitionProps }) => {
return (
<TransitionComponent {...TransitionProps}>
<div>
<button autoFocus onFocus={handleFocus}>
will be focused
</button>
</div>
</TransitionComponent>
);
}}
</BottomAnchoredPopper>,
);
expect(handleFocus.callCount).to.equal(0);
const scrollYBeforeOpen = window.scrollY;
setProps({ open: true });
expect(handleFocus.callCount).to.equal(1);
expect(window.scrollY, 'focus caused scroll').to.equal(scrollYBeforeOpen);
});
it('focus during layout effect does not scroll', () => {
const handleFocus = spy();
function LayoutEffectFocusButton(props) {
const buttonRef = React.useRef(null);
React.useLayoutEffect(() => {
buttonRef.current.focus();
}, []);
return <button {...props} ref={buttonRef} />;
}
const { setProps } = render(
<BottomAnchoredPopper open={false} transition>
{({ TransitionProps }) => {
return (
<TransitionComponent {...TransitionProps}>
<div>
<LayoutEffectFocusButton onFocus={handleFocus}>
will be focused
</LayoutEffectFocusButton>
</div>
</TransitionComponent>
);
}}
</BottomAnchoredPopper>,
);
expect(handleFocus.callCount).to.equal(0);
const scrollYBeforeOpen = window.scrollY;
setProps({ open: true });
expect(handleFocus.callCount).to.equal(1);
expect(window.scrollY, 'focus caused scroll').to.equal(scrollYBeforeOpen);
});
it('focus during passive effects do not scroll', () => {
const handleFocus = spy();
function EffectFocusButton(props) {
const buttonRef = React.useRef(null);
React.useEffect(() => {
buttonRef.current.focus();
}, []);
return <button {...props} ref={buttonRef} />;
}
const { setProps } = render(
<BottomAnchoredPopper open={false} transition>
{({ TransitionProps }) => {
return (
<TransitionComponent timeout={0} {...TransitionProps}>
<div>
<EffectFocusButton onFocus={handleFocus}>will be focused</EffectFocusButton>
</div>
</TransitionComponent>
);
}}
</BottomAnchoredPopper>,
);
expect(handleFocus.callCount).to.equal(0);
const scrollYBeforeOpen = window.scrollY;
setProps({ open: true });
expect(handleFocus.callCount).to.equal(1);
expect(window.scrollY, 'focus caused scroll').to.equal(scrollYBeforeOpen);
});
});
});
});
});

View File

@@ -0,0 +1,167 @@
import * as React from 'react';
import { expect } from 'chai';
import { act, createRenderer, fireEvent, screen } from '@mui/internal-test-utils';
import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select';
import Dialog from '@mui/material/Dialog';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
describe('<Select> integration', () => {
const { clock, render } = createRenderer({ clock: 'fake' });
describe('with Dialog', () => {
function SelectAndDialog() {
const [value, setValue] = React.useState(10);
const handleChange = (event) => {
setValue(Number(event.target.value));
};
return (
<Dialog open>
<Select
MenuProps={{
transitionDuration: 0,
BackdropProps: { 'data-testid': 'select-backdrop' },
}}
value={value}
onChange={handleChange}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
</Dialog>
);
}
it('should focus the selected item', async () => {
render(<SelectAndDialog />);
const trigger = screen.getByRole('combobox');
// Let's open the select component
// in the browser user click also focuses
fireEvent.mouseDown(trigger);
const options = screen.getAllByRole('option');
expect(options[1]).toHaveFocus();
// Now, let's close the select component
await act(async () => {
screen.getByTestId('select-backdrop').click();
});
clock.tick(0);
expect(screen.queryByRole('listbox')).to.equal(null);
expect(trigger).toHaveFocus();
});
it('should be able to change the selected item', async () => {
render(<SelectAndDialog />);
const trigger = screen.getByRole('combobox');
expect(trigger).toHaveAccessibleName('');
// Let's open the select component
// in the browser user click also focuses
fireEvent.mouseDown(trigger);
const options = screen.getAllByRole('option');
expect(options[1]).toHaveFocus();
// Now, let's close the select component
await act(async () => {
options[2].click();
});
clock.tick(0);
expect(screen.queryByRole('listbox')).to.equal(null);
expect(trigger).toHaveFocus();
expect(trigger).to.have.text('Twenty');
});
});
describe('with label', () => {
it('requires `id` and `labelId` for a proper accessible name', () => {
render(
<FormControl>
<InputLabel id="label">Age</InputLabel>
<Select id="input" labelId="label" value="10">
<MenuItem value="">none</MenuItem>
<MenuItem value="10">Ten</MenuItem>
</Select>
</FormControl>,
);
expect(screen.getByRole('combobox')).toHaveAccessibleName('Age');
});
// we're somewhat abusing "focus" here. What we're actually interested in is
// displaying it as "active". WAI-ARIA authoring practices do not consider the
// the trigger part of the widget while a native <select /> will outline the trigger
// as well
it('is displayed as focused while open', async () => {
render(
<FormControl>
<InputLabel classes={{ focused: 'focused-label' }} data-testid="label">
Age
</InputLabel>
<Select
MenuProps={{
transitionDuration: 0,
}}
value=""
>
<MenuItem value="">none</MenuItem>
<MenuItem value={10}>Ten</MenuItem>
</Select>
</FormControl>,
);
const trigger = screen.getByRole('combobox');
await act(async () => {
trigger.focus();
});
fireEvent.keyDown(trigger, { key: 'Enter' });
clock.tick(0);
expect(screen.getByTestId('label')).to.have.class('focused-label');
});
it('does not stays in an active state if an open action did not actually open', async () => {
// test for https://github.com/mui/material-ui/issues/17294
// we used to set a flag to stop blur propagation when we wanted to open the
// select but never considered what happened if the select never opened
const { container } = render(
<FormControl>
<InputLabel classes={{ focused: 'focused-label' }} htmlFor="age-simple">
Age
</InputLabel>
<Select inputProps={{ id: 'age' }} open={false} value="">
<MenuItem value="">none</MenuItem>
<MenuItem value={10}>Ten</MenuItem>
</Select>
</FormControl>,
);
const trigger = screen.getByRole('combobox');
await act(async () => {
trigger.focus();
});
expect(container.querySelector('[for="age-simple"]')).to.have.class('focused-label');
fireEvent.keyDown(trigger, { key: 'Enter' });
expect(container.querySelector('[for="age-simple"]')).to.have.class('focused-label');
await act(async () => {
trigger.blur();
});
expect(container.querySelector('[for="age-simple"]')).not.to.have.class('focused-label');
});
});
});

View File

@@ -0,0 +1,100 @@
import * as React from 'react';
import { expect } from 'chai';
import { createRenderer, screen } from '@mui/internal-test-utils';
import TableCell, { tableCellClasses as classes } from '@mui/material/TableCell';
import Table from '@mui/material/Table';
import TableFooter from '@mui/material/TableFooter';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import TableBody from '@mui/material/TableBody';
describe('<TableRow> integration', () => {
const { render } = createRenderer();
function renderInTable(node, Variant) {
return render(
<Table>
<Variant>
<TableRow>{node}</TableRow>
</Variant>
</Table>,
);
}
it('should render a th with the head class when in the context of a table head', () => {
renderInTable(<TableCell data-testid="cell" />, TableHead);
expect(screen.getByTestId('cell')).to.have.tagName('th');
expect(screen.getByTestId('cell')).to.have.class(classes.root);
expect(screen.getByTestId('cell')).to.have.class(classes.head);
expect(screen.getByTestId('cell')).to.have.attribute('scope', 'col');
});
it('should render specified scope attribute even when in the context of a table head', () => {
renderInTable(<TableCell scope="row" data-testid="cell" />, TableHead);
expect(screen.getByTestId('cell')).to.have.attribute('scope', 'row');
});
it('should render a th with the footer class when in the context of a table footer', () => {
renderInTable(<TableCell data-testid="cell" />, TableFooter);
expect(screen.getByTestId('cell')).to.have.tagName('td');
expect(screen.getByTestId('cell')).to.have.class(classes.root);
expect(screen.getByTestId('cell')).to.have.class(classes.footer);
});
it('should render with the footer class when in the context of a table footer', () => {
renderInTable(<TableCell data-testid="cell" />, TableFooter);
expect(screen.getByTestId('cell')).to.have.class(classes.root);
expect(screen.getByTestId('cell')).to.have.class(classes.footer);
});
it('should render with the head class when variant is head, overriding context', () => {
renderInTable(<TableCell variant="head" data-testid="cell" />, TableFooter);
expect(screen.getByTestId('cell')).to.have.class(classes.head);
expect(screen.getByTestId('cell')).not.to.have.attribute('scope');
});
it('should render without head class when variant is body, overriding context', () => {
renderInTable(<TableCell variant="body" data-testid="cell" />, TableFooter);
expect(screen.getByTestId('cell')).not.to.have.class(classes.head);
});
it('should render without footer class when variant is body, overriding context', () => {
renderInTable(<TableCell variant="body" data-testid="cell" />, TableFooter);
expect(screen.getByTestId('cell')).not.to.have.class(classes.footer);
});
it('should render with the footer class when variant is footer, overriding context', () => {
renderInTable(<TableCell variant="footer" data-testid="cell" />, TableHead);
expect(screen.getByTestId('cell')).to.have.class(classes.footer);
});
it('does not set `role` when `component` prop is set and used in the context of table head', () => {
render(
<TableHead component="div">
<TableCell component="div" data-testid="cell" />,
</TableHead>,
);
expect(screen.getByTestId('cell')).not.to.have.attribute('role');
});
it('does not set `role` when `component` prop is set and used in the context of table body', () => {
render(
<TableBody component="div">
<TableCell component="div" data-testid="cell" />,
</TableBody>,
);
expect(screen.getByTestId('cell')).not.to.have.attribute('role');
});
it('does not set `role` when `component` prop is set and used in the context of table footer', () => {
render(
<TableFooter component="div">
<TableCell component="div" data-testid="cell" />,
</TableFooter>,
);
expect(screen.getByTestId('cell')).not.to.have.attribute('role');
});
});

View File

@@ -0,0 +1,36 @@
import * as React from 'react';
import { expect } from 'chai';
import { createRenderer, screen } from '@mui/internal-test-utils';
import TableFooter from '@mui/material/TableFooter';
import TableHead from '@mui/material/TableHead';
import TableRow, { tableRowClasses as classes } from '@mui/material/TableRow';
describe('<TableRow> integration', () => {
const { render } = createRenderer();
it('should render with the head class when in the context of a table head', () => {
render(
<table>
<TableHead>
<TableRow />
</TableHead>
</table>,
);
expect(screen.getByRole('row')).to.have.class(classes.root);
expect(screen.getByRole('row')).to.have.class(classes.head);
});
it('should render with the footer class when in the context of a table footer', () => {
render(
<table>
<TableFooter>
<TableRow />
</TableFooter>
</table>,
);
expect(screen.getByRole('row')).to.have.class(classes.root);
expect(screen.getByRole('row')).to.have.class(classes.footer);
});
});

View File

@@ -0,0 +1,38 @@
import { act, fireEvent } from '@mui/internal-test-utils';
function delay(ms: number) {
return new Promise((r) => {
setTimeout(r, ms);
});
}
export async function asyncFireEvent(node: Element, event: keyof typeof fireEvent, options?: any) {
await act(async () => {
fireEvent[event](node, options);
await delay(1);
});
}
export function startTouch(node: Element, options?: any) {
return asyncFireEvent(node, 'mouseDown', options);
}
export async function stopTouch(node: Element) {
return asyncFireEvent(node, 'mouseUp');
}
export async function startFocus(node: HTMLElement) {
await act(async () => {
node.blur();
fireEvent.keyDown(document.body, { key: 'Tab' });
node.focus();
await delay(1);
});
}
export async function stopFocus(node: HTMLElement) {
await act(async () => {
node.blur();
await delay(1);
});
}

View File

@@ -0,0 +1,171 @@
import * as React from 'react';
import { expectType } from '@mui/types';
import { OverridableComponent, OverrideProps } from '@mui/material/OverridableComponent';
interface MyOverrideProps {
className: string;
myString?: string;
myCallback?(n: number): void;
}
declare const MyOverrideComponent: React.ComponentType<MyOverrideProps>;
class MyOverrideClassComponent extends React.Component<MyOverrideProps> {
render() {
return null;
}
}
const MyOverrideRefForwardingComponent = React.forwardRef<HTMLLegendElement>((props, ref) => (
<div ref={ref} />
));
declare const MyIncompatibleComponent1: React.ComponentType<{ inconsistentProp?: number }>;
declare const Foo: OverridableComponent<{
props: {
numberProp: number;
callbackProp?(b: boolean): void;
inconsistentProp?: string;
};
defaultComponent: React.ComponentType<{
defaultProp?: boolean;
defaultCallbackProp?(s: string): void;
}>;
classKey: 'root' | 'foo' | 'bar';
}>;
// Can provide basic props; callback parameter types will be inferred.
<Foo
numberProp={3}
className="foo"
style={{ backgroundColor: 'red' }}
classes={{ root: 'x', foo: 'y' }}
callbackProp={(b) => console.log(b)}
/>;
// Can pass props unique to the default component type; callback parameter types
// will be inferred.
<Foo numberProp={3} defaultProp defaultCallbackProp={(s) => console.log(s)} />;
// Can override the component and pass props unique to it; props of the override
// component that are provided from the wrapping component ("inner props") do
// not need to be specified.
<Foo component={MyOverrideComponent} myString="hello" numberProp={3} />;
// Can pass a callback prop with an override component; callback parameter must
// be explicitly specified.
<Foo component={MyOverrideComponent} myCallback={(n: number) => console.log(n)} numberProp={3} />;
// Can pass overriding component type as a parameter and callback parameters
// will be inferred.
<Foo<typeof MyOverrideComponent>
component={MyOverrideComponent}
myCallback={(n) => console.log(n)}
numberProp={3}
/>;
// Can provide a primitive override and an event handler with explicit type.
<Foo
component="button"
numberProp={3}
onClick={(event: React.MouseEvent<HTMLButtonElement>) => event.currentTarget.checkValidity()}
/>;
// Can get inferred type for events by providing a component type parameter.
<Foo<'button'>
numberProp={3}
component="button"
ref={(elem) => {
expectType<HTMLButtonElement | null, typeof elem>(elem);
}}
onClick={(event) => {
expectType<React.MouseEvent<HTMLButtonElement, MouseEvent>, typeof event>(event);
event.currentTarget.checkValidity();
}}
/>;
// Can use refs if the override is a class component
<Foo<typeof MyOverrideClassComponent>
numberProp={3}
component={MyOverrideClassComponent}
ref={(elem) => {
expectType<MyOverrideClassComponent | null, typeof elem>(elem);
}}
/>;
// ... or with ref-forwarding components
<Foo<typeof MyOverrideRefForwardingComponent>
numberProp={42}
component={MyOverrideRefForwardingComponent}
ref={(elem) => {
expectType<HTMLLegendElement | null, typeof elem>(elem);
}}
/>;
// ... but for an arbitrary ComponentType
// @ts-expect-error
<Foo<typeof MyOverrideComponent> component={MyOverrideComponent} ref={() => {}} />;
// @ts-expect-error
<Foo
numberProp={3}
bad="hi" // invalid prop
/>;
// @ts-expect-error
<Foo
component={MyOverrideComponent}
myString={4} // should be a string
numberProp={3}
/>;
<Foo
component={MyOverrideComponent}
myCallback={(n) => {
expectType<number, typeof n>(n);
}}
numberProp={3}
/>;
<Foo<typeof MyOverrideComponent>
component={MyOverrideComponent}
// @ts-expect-error
myString={4} // should be a string
myCallback={(n) => {
expectType<number, typeof n>(n);
}}
numberProp={3}
/>;
// inconsistent typing of base vs override prop
// but the assumption is that `Foo` intercepts `inconsistentProp` and doesn't forward it
<Foo
component={MyIncompatibleComponent1} // inconsistent typing of base vs override prop
numberProp={3}
inconsistentProp="hi"
/>;
<Foo<'div'>
component="div"
numberProp={3}
// event type doesn't match component type
// @ts-expect-error
onClick={(event: React.MouseEvent<HTMLButtonElement>) => event.currentTarget.checkValidity()}
/>;
// Typical polymorphic component from @mui/material
interface BarTypeMap<P = {}, D extends React.ElementType = 'span'> {
props: P & {
numberProp: number;
callbackProp?(b: boolean): void;
};
defaultComponent: D;
}
declare const Bar: OverridableComponent<BarTypeMap>;
type BarProps<D extends React.ElementType = BarTypeMap['defaultComponent'], P = {}> = OverrideProps<
BarTypeMap<P, D>,
D
>;
const Header = React.forwardRef<HTMLElement, BarProps>((props, ref) => (
<Bar ref={ref} component="header" {...props} />
));

View File

@@ -0,0 +1,49 @@
import * as React from 'react';
import {
Badge,
Button,
Checkbox,
Chip,
CircularProgress,
FormControl,
FormLabel,
FilledInput,
OutlinedInput,
IconButton,
Input,
InputLabel,
LinearProgress,
Radio,
TextField,
SvgIcon,
Switch,
} from '@mui/material';
function TestBaseColorPaletteProp() {
const baseColorPalette = ['primary', 'secondary', 'error', 'info', 'success', 'warning'] as const;
return (
<div>
{baseColorPalette.map((color) => (
<div key={color}>
<Badge color={color} />
<Button color={color} />
<Checkbox color={color} />
<Chip color={color} />
<CircularProgress color={color} />
<FormControl color={color} />
<FilledInput color={color} />
<FormLabel color={color} />
<OutlinedInput color={color} />
<IconButton color={color} />
<Input color={color} />
<InputLabel color={color} />
<LinearProgress color={color} />
<TextField color={color} />
<Radio color={color} />
<SvgIcon color={color} />
<Switch color={color} />
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,50 @@
import { colors, Color } from '@mui/material';
const {
amber,
blue,
blueGrey,
brown,
cyan,
deepOrange,
deepPurple,
green,
grey,
indigo,
lightBlue,
lightGreen,
lime,
orange,
pink,
purple,
red,
teal,
yellow,
common,
} = colors;
const colorList: Color[] = [
amber,
blue,
blueGrey,
brown,
cyan,
deepOrange,
deepPurple,
green,
grey,
indigo,
lightBlue,
lightGreen,
lime,
orange,
pink,
purple,
red,
teal,
yellow,
];
const { black, white } = common;
[black, white].forEach((color: string) => color);

View File

@@ -0,0 +1,36 @@
import * as React from 'react';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
function TestStandardPropsCallbackRefUsage() {
const contentRef = React.useRef<HTMLDivElement>(null);
const setContentRef = React.useCallback((node: HTMLDivElement | null) => {
contentRef.current = node;
// ...
}, []);
return (
<Dialog open>
<DialogTitle>Dialog Demo</DialogTitle>
<DialogContent ref={setContentRef}>
<DialogContentText>Dialog content</DialogContentText>
</DialogContent>
</Dialog>
);
}
function TestStandardPropsObjectRefUsage() {
const contentRef = React.useRef<HTMLDivElement>(null);
return (
<Dialog open>
<DialogTitle>Dialog Demo</DialogTitle>
<DialogContent ref={contentRef}>
<DialogContentText>Dialog content</DialogContentText>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,13 @@
import * as React from 'react';
import InputLabel from '@mui/material/InputLabel';
declare module '@mui/material/InputLabel' {
interface InputLabelPropsSizeOverrides {
customSize: true;
}
}
<InputLabel size="customSize" />;
// @ts-expect-error unknown size
<InputLabel size="foo" />;

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["InputLabelCustomProps.spec.tsx"]
}

View File

@@ -0,0 +1,34 @@
import * as React from 'react';
import Button from '@mui/material/Button';
import Tooltip from '@mui/material/Tooltip';
import { PaletteColor } from '@mui/material/styles';
declare module '@mui/material/styles' {
interface Palette {
custom: PaletteColor;
}
}
// Tooltip slotProps should only allow valid theme palettes to be accessed
<Tooltip
title="tooltip"
slotProps={{
tooltip: {
sx: {
color: (theme) => theme.palette.custom.main,
// @ts-expect-error Property 'invalid' does not exist on 'Palette'
backgroundColor: (theme) => theme.palette.invalid.main,
},
},
arrow: {
sx: {
color: (theme) => theme.palette.custom.main,
// @ts-expect-error Property 'invalid' does not exist on 'Palette'
backgroundColor: (theme) => theme.palette.invalid.main,
},
},
}}
>
<Button>Hover Me!</Button>
</Tooltip>;

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["TooltipSlotSxProps.spec.tsx"]
}

View File

@@ -0,0 +1,44 @@
import * as React from 'react';
import Alert from '@mui/material/Alert';
import { IconButton, IconButtonProps, svgIconClasses } from '@mui/material';
declare module '@mui/material/Alert' {
interface AlertCloseButtonSlotPropsOverrides {
iconSize: 'small' | 'medium';
}
}
type MyIconButtonProps = IconButtonProps<
'button',
{
iconSize?: 'small' | 'medium';
}
>;
const MyIconButton = ({ iconSize, ...other }: MyIconButtonProps) => {
return (
<IconButton
{...other}
sx={{
// whatever customization based on iconSize
[`.${svgIconClasses.root}`]: {
fontSize: iconSize === 'small' ? '1rem' : '1.5rem',
},
}}
/>
);
};
<Alert
severity="success"
slots={{
closeButton: MyIconButton,
}}
slotProps={{
closeButton: {
iconSize: 'medium',
},
}}
>
Here is a gentle confirmation that your action was successful.
</Alert>;

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["alertCustomSlotProps.spec.tsx"]
}

View File

@@ -0,0 +1,13 @@
import * as React from 'react';
import { AppBar } from '@mui/material';
declare module '@mui/material/AppBar' {
interface AppBarPropsColorOverrides {
customAppBarColor: true;
}
}
<AppBar color="customAppBarColor" />;
// @ts-expect-error unknown color
<AppBar color="foo" />;

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["appBarProps.spec.tsx"]
}

View File

@@ -0,0 +1,85 @@
import * as React from 'react';
import Autocomplete from '@mui/material/Autocomplete';
import Button from '@mui/material/Button';
import Paper, { PaperProps } from '@mui/material/Paper';
import Popper, { PopperProps } from '@mui/material/Popper';
import TextField from '@mui/material/TextField';
declare module '@mui/material/Autocomplete' {
interface AutocompletePaperSlotPropsOverrides {
value: Option[];
}
interface AutocompletePopperSlotPropsOverrides {
value: Option[];
}
}
function CustomPaper({ children, value, ...paperProps }: PaperProps & { value: Option[] }) {
return (
<Paper {...paperProps} onMouseDown={(event) => event.preventDefault()}>
{children}
<Button disabled={value.length === 0}>Next</Button>
</Paper>
);
}
function CustomPopper({ children, value, ...popperProps }: PopperProps & { value: Option[] }) {
return (
<Popper {...popperProps}>
{children as React.ReactNode}
<Button disabled={value.length === 0}>Next</Button>
</Popper>
);
}
interface Option {
title: string;
year: number;
}
function App() {
const [value, setValue] = React.useState<Option[]>([]);
return (
<React.Fragment>
{/* Testing Paper slot */}
<Autocomplete
multiple
isOptionEqualToValue={(option, valueParam) => option.title === valueParam.title}
renderInput={(params) => <TextField {...params} placeholder="Select" />}
onChange={(event, newValue) => {
setValue(newValue);
}}
getOptionLabel={(option) => `(${option?.year}) ${option?.title}`}
options={[...topFilms]}
value={value}
slots={{ paper: CustomPaper }}
slotProps={{ paper: { value } }}
/>
{/* Testing Popper slot */}
<Autocomplete
multiple
isOptionEqualToValue={(option, valueParam) => option.title === valueParam.title}
renderInput={(params) => <TextField {...params} placeholder="Select" />}
onChange={(event, newValue) => {
setValue(newValue);
}}
getOptionLabel={(option) => `(${option?.year}) ${option?.title}`}
options={[...topFilms]}
value={value}
slots={{ popper: CustomPopper }}
slotProps={{ popper: { value } }}
/>
</React.Fragment>
);
}
const topFilms = [
{ title: 'The Shawshank Redemption', year: 1994 },
{ title: 'The Godfather', year: 1972 },
{ title: 'The Godfather: Part II', year: 1974 },
{ title: 'The Dark Knight', year: 2008 },
{ title: '12 Angry Men', year: 1957 },
{ title: "Schindler's List", year: 1993 },
{ title: 'Pulp Fiction', year: 1994 },
];

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["autocompleteCustomSlotProps.spec.tsx"]
}

View File

@@ -0,0 +1,43 @@
import * as React from 'react';
import Badge from '@mui/material/Badge';
import { createTheme } from '@mui/material/styles';
// Update the Button's extendable props options
declare module '@mui/material/Badge' {
interface BadgePropsVariantOverrides {
action: true;
}
interface BadgePropsColorOverrides {
success: true;
}
}
// theme typings should work as expected
const theme = createTheme({
components: {
MuiBadge: {
variants: [
{
props: { variant: 'action' },
style: {
border: `2px dashed grey`,
},
},
{
props: { color: 'success' },
style: {
backgroundColor: 'green',
},
},
],
},
},
});
<Badge variant="action" color="success" badgeContent={123} />;
// @ts-expect-error typo
<Badge variant="Action" />;
// @ts-expect-error typo
<Badge color="Success" />;

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["badgeCustomProps.spec.tsx"]
}

View File

@@ -0,0 +1,50 @@
import * as React from 'react';
import Container from '@mui/material/Container';
import Dialog from '@mui/material/Dialog';
import { createTheme, ThemeProvider } from '@mui/material/styles';
// testing docs/src/pages/customization/breakpoints/breakpoints.md
declare module '@mui/material/styles' {
interface BreakpointOverrides {
xs: false; // removes the `xs` breakpoint
sm: false;
md: false;
lg: false;
xl: false;
mobile: true; // adds the `mobile` breakpoint
tablet: true;
laptop: true;
desktop: true;
}
}
const theme = createTheme({
breakpoints: {
values: {
mobile: 0,
tablet: 640,
laptop: 1024,
desktop: 1280,
},
},
components: {
MuiContainer: {
defaultProps: {
maxWidth: 'laptop',
},
},
},
});
function MyContainer() {
return (
<ThemeProvider theme={theme}>
hello
<Container maxWidth="tablet">yooo</Container>
<Dialog open maxWidth="tablet">
<div />
</Dialog>
</ThemeProvider>
);
}

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["breakpointsOverrides.spec.tsx"]
}

View File

@@ -0,0 +1,48 @@
import * as React from 'react';
import Button from '@mui/material/Button';
import { createTheme } from '@mui/material/styles';
// Update the Button's extendable props options
declare module '@mui/material/Button' {
interface ButtonPropsVariantOverrides {
dashed: true;
contained: false;
}
interface ButtonPropsColorOverrides {
success: true;
}
interface ButtonPropsSizeOverrides {
extraLarge: true;
}
}
// theme typings should work as expected
const theme = createTheme({
components: {
MuiButton: {
variants: [
{
props: { variant: 'dashed' },
style: {
border: `2px dashed grey`,
},
},
{
props: { size: 'extraLarge' },
style: {
fontSize: 26,
},
},
],
},
},
});
<Button variant="dashed" color="success" size="extraLarge">
Custom
</Button>;
// @ts-expect-error The contained variant was disabled
<Button variant="contained" color="primary">
Invalid
</Button>;

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["buttonCustomProps.spec.tsx"]
}

View File

@@ -0,0 +1,14 @@
import { Interpolation } from '@mui/system';
import { createTheme, styled } from '@mui/material/styles';
declare module '@mui/material/styles' {
interface Mixins {
customMixin: Interpolation<{}>;
}
}
// ensure MixinsOptions work
const theme = createTheme({ mixins: { customMixin: { paddingLeft: 2 } } });
// ensure Mixins work
const Example = styled('div')(({ theme: t }) => t.mixins.customMixin);

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["createTheme.spec.ts"]
}

View File

@@ -0,0 +1,36 @@
import * as React from 'react';
import FormHelperText from '@mui/material/FormHelperText';
import FormControl from '@mui/material/FormControl';
import { createTheme } from '@mui/material/styles';
declare module '@mui/material/FormHelperText' {
interface FormHelperTextPropsVariantOverrides {
warning: true;
}
}
// theme typings should work as expected
const theme = createTheme({
components: {
MuiFormHelperText: {
variants: [
{
props: { variant: 'warning' },
style: {
backgroundColor: '#ffa726',
color: '#ffffff',
},
},
],
},
},
});
<FormControl>
<FormHelperText variant="warning">This is warning helper text</FormHelperText>
</FormControl>;
<FormControl>
{/* @ts-expect-error unknown variant */}
<FormHelperText variant="checked">This is example helper text</FormHelperText>
</FormControl>;

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["formHelperTextCustomProps.spec.tsx"]
}

View File

@@ -0,0 +1,51 @@
import * as React from 'react';
import Grid from '@mui/material/Grid';
import { createTheme, ThemeProvider } from '@mui/material/styles';
declare module '@mui/material/styles' {
interface BreakpointOverrides {
xs: false;
sm: false;
md: false;
lg: false;
xl: false;
mobile: true;
tablet: true;
laptop: true;
desktop: true;
}
}
const theme = createTheme({
breakpoints: {
values: {
mobile: 0,
tablet: 640,
laptop: 1024,
desktop: 1280,
},
},
});
<ThemeProvider theme={theme}>
<Grid
size={{
mobile: 1,
tablet: 2,
laptop: 3,
desktop: 4,
}}
/>
</ThemeProvider>;
<ThemeProvider theme={theme}>
<Grid
size={{
mobile: 1,
tablet: 2,
laptop: 3,
/* @ts-expect-error unknown desk */
desk: 4,
}}
/>
</ThemeProvider>;

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["gridCustomBreakpoints.spec.tsx"]
}

View File

@@ -0,0 +1,37 @@
import * as React from 'react';
import Grid from '@mui/material/GridLegacy';
import { createTheme, ThemeProvider } from '@mui/material/styles';
declare module '@mui/material/styles' {
interface BreakpointOverrides {
xs: false;
sm: false;
md: false;
lg: false;
xl: false;
mobile: true;
tablet: true;
laptop: true;
desktop: true;
}
}
const theme = createTheme({
breakpoints: {
values: {
mobile: 0,
tablet: 640,
laptop: 1024,
desktop: 1280,
},
},
});
<ThemeProvider theme={theme}>
<Grid item mobile={1} tablet={2} laptop={3} desktop={4} />
</ThemeProvider>;
<ThemeProvider theme={theme}>
{/* @ts-expect-error unknown desk */}
<Grid item mobile={1} tablet={2} laptop={3} desk={4} />
</ThemeProvider>;

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["gridLegacyCustomBreakpoints.spec.tsx"]
}

View File

@@ -0,0 +1,50 @@
// testing docs/src/pages/customization/palette/palette.md
import { createTheme } from '@mui/material/styles';
declare module '@mui/material/styles' {
interface Theme {
status: {
danger: React.CSSProperties['color'];
};
}
interface Palette {
neutral: Palette['primary'];
}
interface PaletteOptions {
neutral: PaletteOptions['primary'];
}
interface PaletteColor {
darker?: string;
}
interface SimplePaletteColorOptions {
darker?: string;
}
interface ThemeOptions {
status: {
danger: React.CSSProperties['color'];
};
}
}
declare module '@mui/material/Button' {
interface ButtonPropsColorOverrides {
neutral: true;
}
}
const theme = createTheme({
status: {
danger: '#e53e3e',
},
palette: {
primary: {
main: '#0971f1',
darker: '#053e85',
},
neutral: {
main: '#5c6ac4',
},
},
});

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["paletteColors.spec.ts"]
}

View File

@@ -0,0 +1,31 @@
import * as React from 'react';
import { CircularProgress, LinearProgress } from '@mui/material';
declare module '@mui/material/CircularProgress' {
interface CircularProgressPropsColorOverrides {
customCircularColor: true;
}
interface CircularProgressPropsVariantOverrides {
dashed: true;
}
}
declare module '@mui/material/LinearProgress' {
interface LinearProgressPropsColorOverrides {
customLinearColor: true;
}
interface LinearProgressPropsVariantOverrides {
dashed: true;
}
}
<CircularProgress color="customCircularColor" />;
<CircularProgress variant="dashed" />;
// @ts-expect-error unknown color
<CircularProgress color="foo" />;
<LinearProgress color="customLinearColor" />;
<LinearProgress variant="dashed" />;
// @ts-expect-error unknown color
<LinearProgress color="foo" />;

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["progressProps.spec.tsx"]
}

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["responsiveFontSizes.spec.ts"]
}

View File

@@ -0,0 +1,35 @@
import { createTheme, responsiveFontSizes } from '@mui/material/styles';
declare module '@mui/material/styles' {
interface TypographyVariants {
poster: React.CSSProperties;
}
// allow configuration using `createTheme()`
interface TypographyVariantsOptions {
poster?: React.CSSProperties;
}
}
// Update the Typography's variant prop options. Also needed for custom variants options in responsiveFontSizes.
declare module '@mui/material/Typography' {
interface TypographyPropsVariantOverrides {
poster: true;
}
}
let theme = createTheme({
typography: {
poster: {
fontSize: '2rem',
lineHeight: 1,
},
h3: {
fontSize: '2em',
},
},
});
theme = responsiveFontSizes(theme, {
// custom variants
variants: ['poster'],
});

View File

@@ -0,0 +1,29 @@
import { createTheme } from '@mui/material/styles';
declare module '@mui/material/styles' {
interface Shape {
borderRadiusSecondary: number;
}
interface ShapeOptions {
borderRadiusSecondary: number;
}
}
createTheme({
shape: {
borderRadiusSecondary: 12,
},
components: {
MuiButton: {
styleOverrides: {
root: ({ theme }) => ({
borderRadius: theme.shape.borderRadiusSecondary,
'&:hover': {
borderRadius: theme.vars.shape.borderRadiusSecondary,
},
}),
},
},
},
});

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["shape.spec.ts"]
}

View File

@@ -0,0 +1,48 @@
import * as React from 'react';
import Chip from '@mui/material/Chip';
import { createTheme } from '@mui/material/styles';
// Update the Chip's extendable props options
declare module '@mui/material/Chip' {
interface ChipPropsVariantOverrides {
dashed: true;
outlined: false;
}
interface ChipPropsColorOverrides {
success: true;
}
interface ChipPropsSizeOverrides {
extraLarge: true;
}
}
// theme typings should work as expected
const finalTheme = createTheme({
components: {
MuiChip: {
styleOverrides: {
root: ({ ownerState, theme }) => ({
...(ownerState.variant &&
{
dashed: {
border: '1px dashed',
},
filled: {
backgroundColor: ownerState.color === 'success' ? 'lime' : theme.palette.grey[100],
},
}[ownerState.variant]),
}),
label: ({ ownerState }) => [
ownerState.color === 'success' && {
color: 'lime',
},
],
},
},
},
});
<Chip variant="dashed" color="success" size="extraLarge" label="Content" />;
// @ts-expect-error The contained variant was disabled
<Chip variant="outlined" color="primary" label="Content" />;

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["styleOverridesCallback.spec.tsx"]
}

View File

@@ -0,0 +1,7 @@
import * as React from 'react';
import Box from '@mui/material/Box';
<Box sx={{ borderColor: (theme) => theme.palette.primary.main }} />;
// @ts-expect-error unknown color
<Box sx={{ borderColor: (theme) => theme.palette.invalid }} />;

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["systemTheme.spec.tsx"]
}

View File

@@ -0,0 +1,56 @@
import * as React from 'react';
import Table from '@mui/material/Table';
import TableCell from '@mui/material/TableCell';
import { createTheme } from '@mui/material/styles';
declare module '@mui/material/Table' {
interface TablePropsSizeOverrides {
large: true;
}
}
declare module '@mui/material/TableCell' {
interface TableCellPropsSizeOverrides {
large: true;
}
interface TableCellPropsVariantOverrides {
tableBody: true;
}
}
// theme typings should work as expected
const theme = createTheme({
components: {
MuiTableCell: {
styleOverrides: {
root: ({ ownerState }) => ({
...(ownerState.size === 'large' && {
paddingBlock: '1rem',
}),
}),
},
variants: [
{
props: { variant: 'tableBody' },
style: {
fontSize: '1.2em',
color: '#C1D3FF',
},
},
],
},
},
});
<Table size="large">
<TableCell size="large" />
</Table>;
<Table size="large">
<TableCell variant="tableBody">Foo</TableCell>;
</Table>;
<Table size="large">
{/* @ts-expect-error unknown variant */}
<TableCell variant="tableHeading">Bar</TableCell>;
</Table>;

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["tableCellCustomProps.spec.tsx"]
}

View File

@@ -0,0 +1,37 @@
import * as React from 'react';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import { createTheme } from '@mui/material/styles';
declare module '@mui/material/Tabs' {
interface TabsPropsIndicatorColorOverrides {
success: true;
}
}
// theme typings should work as expected
const theme = createTheme({
components: {
MuiTabs: {
variants: [
{
props: { indicatorColor: 'success' },
style: {
backgroundColor: '#e70000',
},
},
],
},
},
});
<Tabs indicatorColor="success">
<Tab label="Item One" />
<Tab label="Item Two" />
</Tabs>;
// @ts-expect-error unknown indicatorColor
<Tabs indicatorColor="error">
<Tab label="Item One" />
<Tab label="Item Two" />
</Tabs>;

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["tabsCustomProps.spec.tsx"]
}

View File

@@ -0,0 +1,101 @@
import * as React from 'react';
import TextField from '@mui/material/TextField';
import { createTheme } from '@mui/material/styles';
// Update the TextField's extendable props options
declare module '@mui/material/TextField' {
interface TextFieldPropsColorOverrides {
customPalette: true;
}
interface TextFieldPropsSizeOverrides {
extraLarge: true;
}
}
declare module '@mui/material/FormControl' {
interface FormControlPropsColorOverrides {
customPalette: true;
}
interface FormControlPropsSizeOverrides {
extraLarge: true;
}
}
declare module '@mui/material/InputBase' {
interface InputBasePropsSizeOverrides {
extraLarge: true;
}
}
declare module '@mui/material/styles' {
interface Palette {
customPalette: Palette['primary'];
}
interface PaletteOptions {
customPalette: PaletteOptions['primary'];
}
}
// theme typings should work as expected
const theme = createTheme({
components: {
MuiOutlinedInput: {
variants: [
{
props: { size: 'extraLarge' },
style: {
padding: '30px 15px',
fontSize: 40,
},
},
],
},
},
palette: {
customPalette: {
main: 'blue',
},
},
});
<TextField color="customPalette" size="extraLarge">
Custom Color TextField
</TextField>;
<TextField variant="filled" size="extraLarge">
Custom Size TextField
</TextField>;
declare module '@mui/material/TextField' {
interface TextFieldFormHelperTextSlotPropsOverrides {
'data-cy'?: string;
}
interface TextFieldRootSlotPropsOverrides {
customRootProp?: string;
}
interface TextFieldInputSlotPropsOverrides {
customInputProp?: string;
}
interface TextFieldInputLabelSlotPropsOverrides {
customInputLabelProp?: string;
}
interface TextFieldSelectSlotPropsOverrides {
customSelectProp?: string;
}
}
<TextField
slotProps={{
formHelperText: { 'data-cy': 'email-error' },
root: {
customRootProp: 'abc',
},
input: {
customInputProp: 'abc',
},
inputLabel: {
customInputLabelProp: 'abc',
},
select: {
customSelectProp: 'abc',
},
}}
>
Custom TextField
</TextField>;

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["textFieldCustomProps.spec.tsx"]
}

View File

@@ -0,0 +1,31 @@
// testing docs/src/pages/customization/theme-components/theme-components.md
import { blue, red } from '@mui/material/colors';
import { createTheme } from '@mui/material/styles';
declare module '@mui/material/Button' {
interface ButtonPropsVariantOverrides {
dashed: true;
}
}
const theme = createTheme({
components: {
MuiButton: {
variants: [
{
props: { variant: 'dashed' },
style: {
textTransform: 'none',
border: `2px dashed grey${blue[500]}`,
},
},
{
props: { variant: 'dashed', color: 'secondary' },
style: {
border: `4px dashed ${red[500]}`,
},
},
],
},
},
});

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["themeComponents.spec.ts"]
}

View File

@@ -0,0 +1,47 @@
// testing docs/src/pages/customization/theme-components/theme-components.md
import { styled, extendTheme } from '@mui/material/styles';
import type {} from '@mui/material/themeCssVarsAugmentation';
declare module '@mui/material/styles' {
interface PaletteOptions {
gradient: {
default: string;
};
}
interface Palette {
gradient: {
default: string;
};
}
}
const StyledComponent = styled('button')(({ theme }) => ({
background: theme.vars.palette.gradient.default,
}));
const StyledComponent2 = styled('button')(({ theme }) => ({
// @ts-expect-error `default2` is not defined
background: theme.vars.palette.gradient.default2,
}));
const theme = extendTheme({
colorSchemes: {
light: {
palette: {
gradient: {
default: '',
},
},
},
dark: {
palette: {
gradient: {
default: '',
},
},
},
},
});
theme.getCssVar('palette-gradient-default');

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["themeCssVariables.spec.tsx"]
}

View File

@@ -0,0 +1,37 @@
import * as React from 'react';
import { createTheme, styled } from '@mui/material/styles';
import Box from '@mui/material/Box';
interface CustomNode {
background: string;
color: string;
}
declare module '@mui/material/styles' {
interface ThemeOptions {
customNode: CustomNode;
}
interface Theme {
customNode: CustomNode;
}
}
const customTheme = createTheme({
customNode: {
background: '#000',
color: '#fff',
},
});
const StyledComponent = styled('div')(({ theme }) => ({
background: theme.customNode.background,
color: theme.customNode.color,
}));
<Box
sx={(theme) => ({
background: theme.customNode.background,
color: theme.customNode.color,
})}
/>;

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["themeCustomNode.spec.tsx"]
}

View File

@@ -0,0 +1,12 @@
import { extendTheme } from '@mui/material/styles';
import type {} from '@mui/material/themeCssVarsAugmentation';
declare module '@mui/material/styles' {
interface ThemeCssVarOverrides {
'custom-color': true;
}
}
const theme = extendTheme();
theme.getCssVar('custom-color');

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["themeGetCssVar.spec.tsx"]
}

View File

@@ -0,0 +1,39 @@
// testing docs/src/pages/customization/typography/typography.md
import * as React from 'react';
import { createTheme } from '@mui/material/styles';
import Typography from '@mui/material/Typography';
declare module '@mui/material/styles' {
interface TypographyVariants {
poster: React.CSSProperties;
}
// allow configuration using `createTheme()`
interface TypographyVariantsOptions {
poster?: React.CSSProperties;
}
}
// Update the Typography's variant prop options
declare module '@mui/material/Typography' {
interface TypographyPropsVariantOverrides {
poster: true;
h3: false;
}
}
const theme = createTheme({
typography: {
poster: {
color: 'red',
},
// Disable h3 variant
h3: undefined,
},
});
<Typography variant="poster">poster</Typography>;
/* This variant is no longer supported */
// @ts-expect-error
<Typography variant="h3">h3</Typography>;

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../../../tsconfig.json",
"files": ["typographyVariants.spec.tsx"]
}

View File

@@ -0,0 +1,116 @@
import * as React from 'react';
import { createTheme, Theme, ThemeProvider } from '@mui/material/styles';
import Button from '@mui/material/Button';
import { blue } from '@mui/material/colors';
{
// Overriding styles
const theme = createTheme({
palette: {
mode: 'dark',
primary: blue,
contrastThreshold: 3,
tonalOffset: 0.2,
common: {
white: '#ffffff',
},
},
typography: {
h1: {
fontSize: 24,
},
fontSize: 18,
},
mixins: {
toolbar: {
backgroundColor: 'red',
},
},
breakpoints: {
step: 3,
},
transitions: {
duration: {
short: 50,
},
},
spacing: 5,
zIndex: {
appBar: 42,
},
components: {
MuiButton: {
defaultProps: {
disabled: true,
},
styleOverrides: {
// Name of the styleSheet
root: {
// Name of the rule
background: 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)',
borderRadius: 3,
border: 0,
color: 'white',
height: 48,
padding: '0 30px',
boxShadow: '0 3px 5px 2px rgba(255, 105, 135, .3)',
},
},
},
MuiAppBar: {
defaultProps: {
position: 'fixed',
},
},
},
});
<ThemeProvider theme={theme}>
<Button>Overrides</Button>
</ThemeProvider>;
}
const theme2 = createTheme({
palette: {
primary: {
main: blue[500],
},
},
components: {
MuiButton: {
defaultProps: {
disabled: false,
TouchRippleProps: {
center: true,
},
},
},
MuiTable: {
defaultProps: {
cellPadding: 12,
},
},
MuiButtonBase: {
defaultProps: {
disableRipple: true,
},
},
},
});
const t1: string = createTheme().spacing(1);
const t2: string = createTheme().spacing(1, 2);
const t3: string = createTheme().spacing(1, 2, 3);
const t4: string = createTheme().spacing(1, 2, 3, 4);
// @ts-expect-error
const t5 = createTheme().spacing(1, 2, 3, 4, 5);
function themeProviderTest() {
<ThemeProvider theme={{ foo: 1 }}>{null}</ThemeProvider>;
// @ts-expect-error
<ThemeProvider<Theme> theme={{ foo: 1 }}>{null}</ThemeProvider>;
<ThemeProvider<Theme>
theme={{ components: { MuiAppBar: { defaultProps: { 'aria-atomic': 'true' } } } }}
>
{null}
</ThemeProvider>;
}