;
}
-export type ListItemButtonTypeMap =
- ExtendButtonBaseTypeMap<{
- props: P & ListItemButtonBaseProps;
- defaultComponent: D;
- }>;
+export type ListItemButtonTypeMap<
+ P = {},
+ D extends React.ElementType = 'div',
+> = ExtendButtonBaseTypeMap<{
+ props: P & ListItemButtonBaseProps;
+ defaultComponent: D;
+}>;
/**
*
@@ -73,6 +75,7 @@ export type ListItemButtonTypeMap
=
* API:
*
* - [ListItemButton API](https://material-ui.com/api/list-item-button/)
+ * - inherits [ButtonBase API](https://material-ui.com/api/button-base/)
*/
declare const ListItemButton: ExtendButtonBase;
diff --git a/packages/material-ui/src/ListItemButton/ListItemButton.js b/packages/material-ui/src/ListItemButton/ListItemButton.js
index ddeaa134cbb611..1818b244e10425 100644
--- a/packages/material-ui/src/ListItemButton/ListItemButton.js
+++ b/packages/material-ui/src/ListItemButton/ListItemButton.js
@@ -11,7 +11,7 @@ import useForkRef from '../utils/useForkRef';
import ListContext from '../List/ListContext';
import listItemButtonClasses, { getListItemButtonUtilityClass } from './listItemButtonClasses';
-const overridesResolver = (props, styles) => {
+export const overridesResolver = (props, styles) => {
const { styleProps } = props;
return {
@@ -144,7 +144,7 @@ const ListItemButton = React.forwardRef(function ListItemButton(inProps, ref) {
listItemRef.current.focus();
} else if (process.env.NODE_ENV !== 'production') {
console.error(
- 'Material-UI: Unable to set focus to a ListItem whose component has not been rendered.',
+ 'Material-UI: Unable to set focus to a ListItemButton whose component has not been rendered.',
);
}
}
diff --git a/packages/material-ui/src/ListItemButton/ListItemButton.test.js b/packages/material-ui/src/ListItemButton/ListItemButton.test.js
index 6e7161de3db096..2d3d530bdba876 100644
--- a/packages/material-ui/src/ListItemButton/ListItemButton.test.js
+++ b/packages/material-ui/src/ListItemButton/ListItemButton.test.js
@@ -2,6 +2,7 @@ import * as React from 'react';
import { expect } from 'chai';
import { createMount, describeConformanceV5, act, createClientRender, fireEvent } from 'test/utils';
import ListItemButton, { listItemButtonClasses as classes } from '@material-ui/core/ListItemButton';
+import ButtonBase from '@material-ui/core/ButtonBase';
import ListContext from '../List/ListContext';
describe('', () => {
@@ -10,10 +11,11 @@ describe('', () => {
describeConformanceV5(, () => ({
classes,
- inheritComponent: 'div',
+ inheritComponent: ButtonBase,
render,
mount,
refInstanceof: window.HTMLDivElement,
+ testComponentPropWith: 'a',
muiName: 'MuiListItemButton',
testVariantProps: { dense: true },
skip: ['componentsProp'],
diff --git a/packages/material-ui/src/Menu/Menu.js b/packages/material-ui/src/Menu/Menu.js
index 77e3510f7c28a3..805eda4eb677b0 100644
--- a/packages/material-ui/src/Menu/Menu.js
+++ b/packages/material-ui/src/Menu/Menu.js
@@ -157,7 +157,10 @@ const Menu = React.forwardRef(function Menu(inProps, ref) {
{
- props: P &
- DistributiveOmit['props'], 'children' | 'classes'> & {
- /**
- * The content of the component.
- */
- children?: React.ReactNode;
- /**
- * Override or extend the styles applied to the component.
- */
- classes?: Partial;
- /**
- * `classes` prop applied to the [`ListItem`](/api/list-item/) element.
- */
- ListItemClasses?: ListItemProps['classes'];
- /**
- * The system prop that allows defining system overrides as well as additional CSS styles.
- */
- sx?: SxProps;
- };
+export type MenuItemTypeMap = ExtendButtonBaseTypeMap<{
+ props: P & {
+ /**
+ * If `true`, the list item is focused during the first mount.
+ * Focus will also be triggered if the value changes from false to true.
+ * @default false
+ */
+ autoFocus?: boolean;
+ /**
+ * Override or extend the styles applied to the component.
+ */
+ classes?: Partial;
+ /**
+ * If `true`, compact vertical padding designed for keyboard and mouse input is used.
+ * The prop defaults to the value inherited from the parent Menu component.
+ * @default false
+ */
+ dense?: boolean;
+ /**
+ * If `true`, the component is disabled.
+ * @default false
+ */
+ disabled?: boolean;
+ /**
+ * If `true`, the left and right padding is removed.
+ * @default false
+ */
+ disableGutters?: boolean;
+ /**
+ * If `true`, a 1px light border is added to the bottom of the menu item.
+ * @default false
+ */
+ divider?: boolean;
+ /**
+ * Use to apply selected styling.
+ * @default false
+ */
+ selected?: boolean;
+ /**
+ * The system prop that allows defining system overrides as well as additional CSS styles.
+ */
+ sx?: SxProps;
+ };
defaultComponent: D;
-}
+}>;
/**
*
@@ -38,12 +59,9 @@ export interface MenuItemTypeMap {
* API:
*
* - [MenuItem API](https://material-ui.com/api/menu-item/)
- * - inherits [ListItem API](https://material-ui.com/api/list-item/)
+ * - inherits [ButtonBase API](https://material-ui.com/api/button-base/)
*/
-declare const MenuItem: OverridableComponent<
- MenuItemTypeMap<{ button: false }, MenuItemTypeMap['defaultComponent']>
-> &
- ExtendButtonBase>;
+declare const MenuItem: ExtendButtonBase;
export type MenuItemProps<
D extends React.ElementType = MenuItemTypeMap['defaultComponent'],
diff --git a/packages/material-ui/src/MenuItem/MenuItem.js b/packages/material-ui/src/MenuItem/MenuItem.js
index 4bb1706f6baf2c..4c0ff33ef3ff85 100644
--- a/packages/material-ui/src/MenuItem/MenuItem.js
+++ b/packages/material-ui/src/MenuItem/MenuItem.js
@@ -2,87 +2,201 @@ import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import { unstable_composeClasses as composeClasses } from '@material-ui/unstyled';
+import { alpha } from '@material-ui/system';
import styled, { rootShouldForwardProp } from '../styles/styled';
import useThemeProps from '../styles/useThemeProps';
-import { getMenuItemUtilityClass } from './menuItemClasses';
-import ListItem from '../ListItem';
-import { overridesResolver as listItemOverridesResolver, ListItemRoot } from '../ListItem/ListItem';
+import ListContext from '../List/ListContext';
+import ButtonBase from '../ButtonBase';
+import useEnhancedEffect from '../utils/useEnhancedEffect';
+import useForkRef from '../utils/useForkRef';
+import { dividerClasses } from '../Divider';
+import { listItemIconClasses } from '../ListItemIcon';
+import { listItemTextClasses } from '../ListItemText';
+import menuItemClasses, { getMenuItemUtilityClass } from './menuItemClasses';
+
+export const overridesResolver = (props, styles) => {
+ const { styleProps } = props;
+
+ return {
+ ...styles.root,
+ ...(styleProps.dense && styles.dense),
+ ...(styleProps.divider && styles.divider),
+ ...(!styleProps.disableGutters && styles.gutters),
+ };
+};
const useUtilityClasses = (styleProps) => {
- const { selected, dense, classes } = styleProps;
+ const { disabled, dense, divider, disableGutters, selected, classes } = styleProps;
const slots = {
- root: ['root', selected && 'selected', dense && 'dense'],
+ root: [
+ 'root',
+ dense && 'dense',
+ disabled && 'disabled',
+ !disableGutters && 'gutters',
+ divider && 'divider',
+ selected && 'selected',
+ ],
};
- return composeClasses(slots, getMenuItemUtilityClass, classes);
+ const composedClasses = composeClasses(slots, getMenuItemUtilityClass, classes);
+
+ return {
+ ...classes,
+ ...composedClasses,
+ };
};
-const MenuItemRoot = styled(ListItemRoot, {
+const MenuItemRoot = styled(ButtonBase, {
shouldForwardProp: (prop) => rootShouldForwardProp(prop) || prop === 'classes',
name: 'MuiMenuItem',
slot: 'Root',
- overridesResolver: (props, styles) => {
- const { styleProps } = props;
- return {
- ...listItemOverridesResolver(props, styles),
- ...(styleProps.dense && styles.dense),
- };
- },
+ overridesResolver,
})(({ theme, styleProps }) => ({
...theme.typography.body1,
+ display: 'flex',
+ justifyContent: 'flex-start',
+ alignItems: 'center',
+ position: 'relative',
+ textDecoration: 'none',
minHeight: 48,
paddingTop: 6,
paddingBottom: 6,
boxSizing: 'border-box',
- width: 'auto',
whiteSpace: 'nowrap',
- [theme.breakpoints.up('sm')]: {
- minHeight: 'auto',
+ ...(!styleProps.disableGutters && {
+ paddingLeft: 16,
+ paddingRight: 16,
+ }),
+ ...(styleProps.divider && {
+ borderBottom: `1px solid ${theme.palette.divider}`,
+ backgroundClip: 'padding-box',
+ }),
+ '&:hover': {
+ textDecoration: 'none',
+ backgroundColor: theme.palette.action.hover,
+ // Reset on touch devices, it doesn't add specificity
+ '@media (hover: none)': {
+ backgroundColor: 'transparent',
+ },
+ },
+ [`&.${menuItemClasses.selected}`]: {
+ backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity),
+ [`&.${menuItemClasses.focusVisible}`]: {
+ backgroundColor: alpha(
+ theme.palette.primary.main,
+ theme.palette.action.selectedOpacity + theme.palette.action.focusOpacity,
+ ),
+ },
+ },
+ [`&.${menuItemClasses.selected}:hover`]: {
+ backgroundColor: alpha(
+ theme.palette.primary.main,
+ theme.palette.action.selectedOpacity + theme.palette.action.hoverOpacity,
+ ),
+ // Reset on touch devices, it doesn't add specificity
+ '@media (hover: none)': {
+ backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity),
+ },
+ },
+ [`&.${menuItemClasses.focusVisible}`]: {
+ backgroundColor: theme.palette.action.focus,
+ },
+ [`&.${menuItemClasses.disabled}`]: {
+ opacity: theme.palette.action.disabledOpacity,
+ },
+ [`& + .${dividerClasses.root}`]: {
+ marginTop: theme.spacing(1),
+ marginBottom: theme.spacing(1),
+ },
+ [`& + .${dividerClasses.inset}`]: {
+ marginLeft: 52,
+ },
+ [`& .${listItemTextClasses.root}`]: {
+ marginTop: 0,
+ marginBottom: 0,
+ },
+ [`& .${listItemTextClasses.inset}`]: {
+ paddingLeft: 36,
},
+ [`& .${listItemIconClasses.root}`]: {
+ minWidth: 36,
+ },
+ ...(!styleProps.dense && {
+ [theme.breakpoints.up('sm')]: {
+ minHeight: 'auto',
+ },
+ }),
...(styleProps.dense && {
+ minHeight: 36,
...theme.typography.body2,
- minHeight: 'auto',
+ [`& .${listItemIconClasses.root} svg`]: {
+ fontSize: '1.25rem',
+ },
}),
}));
const MenuItem = React.forwardRef(function MenuItem(inProps, ref) {
const props = useThemeProps({ props: inProps, name: 'MuiMenuItem' });
const {
- className,
+ autoFocus = false,
component = 'li',
dense = false,
+ divider = false,
disableGutters = false,
- ListItemClasses,
+ focusVisibleClassName,
role = 'menuitem',
- selected,
tabIndex: tabIndexProp,
...other
} = props;
- const styleProps = { dense };
+ const context = React.useContext(ListContext);
+ const childContext = {
+ dense: dense || context.dense || false,
+ disableGutters,
+ };
+
+ const menuItemRef = React.useRef(null);
+ useEnhancedEffect(() => {
+ if (autoFocus) {
+ if (menuItemRef.current) {
+ menuItemRef.current.focus();
+ } else if (process.env.NODE_ENV !== 'production') {
+ console.error(
+ 'Material-UI: Unable to set focus to a MenuItem whose component has not been rendered.',
+ );
+ }
+ }
+ }, [autoFocus]);
+
+ const styleProps = {
+ ...props,
+ dense: childContext.dense,
+ divider,
+ disableGutters,
+ };
const classes = useUtilityClasses(props);
+ const handleRef = useForkRef(menuItemRef, ref);
+
let tabIndex;
if (!props.disabled) {
tabIndex = tabIndexProp !== undefined ? tabIndexProp : -1;
}
return (
-
+
+
+
);
});
@@ -92,9 +206,11 @@ MenuItem.propTypes /* remove-proptypes */ = {
// | To update them edit the d.ts file and run "yarn proptypes" |
// ----------------------------------------------------------------------
/**
- * @ignore
+ * If `true`, the list item is focused during the first mount.
+ * Focus will also be triggered if the value changes from false to true.
+ * @default false
*/
- button: PropTypes.bool,
+ autoFocus: PropTypes.bool,
/**
* The content of the component.
*/
@@ -103,10 +219,6 @@ MenuItem.propTypes /* remove-proptypes */ = {
* Override or extend the styles applied to the component.
*/
classes: PropTypes.object,
- /**
- * @ignore
- */
- className: PropTypes.string,
/**
* The component used for the root node.
* Either a string to use a HTML element or a component.
@@ -114,7 +226,7 @@ MenuItem.propTypes /* remove-proptypes */ = {
component: PropTypes.elementType,
/**
* If `true`, compact vertical padding designed for keyboard and mouse input is used.
- * The prop defaults to the value inherited from the parent List component.
+ * The prop defaults to the value inherited from the parent Menu component.
* @default false
*/
dense: PropTypes.bool,
@@ -128,9 +240,19 @@ MenuItem.propTypes /* remove-proptypes */ = {
*/
disableGutters: PropTypes.bool,
/**
- * `classes` prop applied to the [`ListItem`](/api/list-item/) element.
+ * If `true`, a 1px light border is added to the bottom of the menu item.
+ * @default false
+ */
+ divider: PropTypes.bool,
+ /**
+ * This prop can help identify which element has keyboard focus.
+ * The class name will be applied when the element gains the focus through keyboard interaction.
+ * It's a polyfill for the [CSS :focus-visible selector](https://drafts.csswg.org/selectors-4/#the-focus-visible-pseudo).
+ * The rationale for using this feature [is explained here](https://github.com/WICG/focus-visible/blob/master/explainer.md).
+ * A [polyfill can be used](https://github.com/WICG/focus-visible) to apply a `focus-visible` class to other components
+ * if needed.
*/
- ListItemClasses: PropTypes.object,
+ focusVisibleClassName: PropTypes.string,
/**
* @ignore
*/
@@ -144,7 +266,7 @@ MenuItem.propTypes /* remove-proptypes */ = {
*/
sx: PropTypes.object,
/**
- * @ignore
+ * @default 0
*/
tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
};
diff --git a/packages/material-ui/src/MenuItem/MenuItem.test.js b/packages/material-ui/src/MenuItem/MenuItem.test.js
index 601dd1f64c98b5..5d281e29c82dd2 100644
--- a/packages/material-ui/src/MenuItem/MenuItem.test.js
+++ b/packages/material-ui/src/MenuItem/MenuItem.test.js
@@ -1,4 +1,3 @@
-// @ts-check
import * as React from 'react';
import { expect } from 'chai';
import { spy } from 'sinon';
@@ -10,8 +9,8 @@ import {
screen,
} from 'test/utils';
import MenuItem, { menuItemClasses as classes } from '@material-ui/core/MenuItem';
-import ListItem from '@material-ui/core/ListItem';
-import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
+import ButtonBase from '@material-ui/core/ButtonBase';
+import ListContext from '../List/ListContext';
describe('', () => {
const render = createClientRender();
@@ -19,13 +18,13 @@ describe('', () => {
describeConformanceV5(, () => ({
classes,
- inheritComponent: ListItem,
+ inheritComponent: ButtonBase,
render,
mount,
refInstanceof: window.HTMLLIElement,
testComponentPropWith: 'a',
muiName: 'MuiMenuItem',
- testVariantProps: { disableGutters: true },
+ testVariantProps: { dense: true },
skip: ['componentsProp'],
}));
@@ -127,31 +126,6 @@ describe('', () => {
});
});
- // Regression test for #10452.
- // Kept for backwards compatibility.
- // In the future we should have a better pattern for this UI.
- it('should not fail with a li > li error message', () => {
- const { rerender } = render(
- ,
- );
-
- expect(document.querySelectorAll('li')).to.have.length(1);
-
- rerender(
- ,
- );
-
- expect(document.querySelectorAll('li')).to.have.length(1);
- });
-
it('can be disabled', () => {
render();
const menuitem = screen.getByRole('menuitem');
@@ -159,22 +133,39 @@ describe('', () => {
expect(menuitem).to.have.attribute('aria-disabled', 'true');
});
- describe('prop: ListItemClasses', () => {
- it('should be able to change the style of ListItem', () => {
- render(
- ,
- );
- const menuitem = screen.getByRole('menuitem');
+ it('can be selected', () => {
+ render();
+ const menuitem = screen.getByRole('menuitem');
- expect(menuitem).not.to.have.class('foo');
- expect(menuitem).to.have.class('bar');
+ expect(menuitem).to.have.class(classes.selected);
+ });
+
+ it('prop: disableGutters', () => {
+ const { rerender } = render();
+ const menuitem = screen.getByRole('menuitem');
+
+ expect(menuitem).to.have.class(classes.gutters);
+
+ rerender();
+
+ expect(menuitem).not.to.have.class(classes.gutters);
+ });
+
+ describe('context: dense', () => {
+ it('should forward the context', () => {
+ let context = null;
+ const { setProps } = render(
+ ,
+ );
+ expect(context).to.have.property('dense', false);
+ setProps({ dense: true });
+ expect(context).to.have.property('dense', true);
});
});
});
diff --git a/packages/material-ui/src/MenuItem/menuItemClasses.ts b/packages/material-ui/src/MenuItem/menuItemClasses.ts
index ae4b7897b85e9b..02f5512f3dcb22 100644
--- a/packages/material-ui/src/MenuItem/menuItemClasses.ts
+++ b/packages/material-ui/src/MenuItem/menuItemClasses.ts
@@ -3,12 +3,18 @@ import { generateUtilityClass, generateUtilityClasses } from '@material-ui/unsty
export interface MenuItemClasses {
/** Styles applied to the root element. */
root: string;
- /** Styles applied to the root element unless `disableGutters={true}`. */
- gutters: string;
- /** Styles applied to the root element if `selected={true}`. */
- selected: string;
+ /** Pseudo-class applied to the root element if keyboard focused. */
+ focusVisible: string;
/** Styles applied to the root element if dense. */
dense: string;
+ /** Pseudo-class applied to the root element if `disabled={true}`. */
+ disabled: string;
+ /** Styles applied to the root element if `divider={true}`. */
+ divider: string;
+ /** Styles applied to the inner `component` element unless `disableGutters={true}`. */
+ gutters: string;
+ /** Pseudo-class applied to the root element if `selected={true}`. */
+ selected: string;
}
export type MenuItemClassKey = keyof MenuItemClasses;
@@ -19,9 +25,12 @@ export function getMenuItemUtilityClass(slot: string): string {
const menuItemClasses: MenuItemClasses = generateUtilityClasses('MuiMenuItem', [
'root',
+ 'focusVisible',
+ 'dense',
+ 'disabled',
+ 'divider',
'gutters',
'selected',
- 'dense',
]);
export default menuItemClasses;
diff --git a/packages/material-ui/test/typescript/components.spec.tsx b/packages/material-ui/test/typescript/components.spec.tsx
index 2eb1e0d397f881..e427e47edbcb7a 100644
--- a/packages/material-ui/test/typescript/components.spec.tsx
+++ b/packages/material-ui/test/typescript/components.spec.tsx
@@ -600,7 +600,6 @@ const MenuTest = () => {
Link Item