diff --git a/docs/data/material/migration/migrating-from-deprecated-apis/migrating-from-deprecated-apis.md b/docs/data/material/migration/migrating-from-deprecated-apis/migrating-from-deprecated-apis.md index 26cae1c2e903a8..d61de0db76fd37 100644 --- a/docs/data/material/migration/migrating-from-deprecated-apis/migrating-from-deprecated-apis.md +++ b/docs/data/material/migration/migrating-from-deprecated-apis/migrating-from-deprecated-apis.md @@ -1587,6 +1587,36 @@ Here's how to migrate: } ``` +## Menu + +Use the [codemod](https://github.com/mui/material-ui/tree/HEAD/packages/mui-codemod#menu-props) below to migrate the code as described in the following sections: + +```bash +npx @mui/codemod@latest deprecations/menu-props +``` + +### MenuListProps + +The Menu's `MenuListProps` prop was deprecated in favor of `slotProps.list`: + +```diff + +``` + +### TransitionProps + +The Menu's `TransitionProps` prop was deprecated in favor of `slotProps.transition`: + +```diff + +``` + ## MobileStepper Use the [codemod](https://github.com/mui/material-ui/tree/HEAD/packages/mui-codemod#mobile-stepper-props) below to migrate the code as described in the following sections: diff --git a/docs/pages/material-ui/api/menu.json b/docs/pages/material-ui/api/menu.json index 6516df0e9e3081..543e9f02cbbc04 100644 --- a/docs/pages/material-ui/api/menu.json +++ b/docs/pages/material-ui/api/menu.json @@ -6,7 +6,12 @@ "children": { "type": { "name": "node" } }, "classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } }, "disableAutoFocusItem": { "type": { "name": "bool" }, "default": "false" }, - "MenuListProps": { "type": { "name": "object" }, "default": "{}" }, + "MenuListProps": { + "type": { "name": "object" }, + "default": "{}", + "deprecated": true, + "deprecationInfo": "use the slotProps.list prop instead. This prop will be removed in v7. See Migrating from deprecated APIs for more details." + }, "onClose": { "type": { "name": "func" }, "signature": { @@ -18,14 +23,14 @@ "slotProps": { "type": { "name": "shape", - "description": "{ backdrop?: func
| object, paper?: func
| object, root?: func
| object, transition?: func
| object }" + "description": "{ backdrop?: func
| object, list?: func
| object, paper?: func
| object, root?: func
| object, transition?: func
| object }" }, "default": "{}" }, "slots": { "type": { "name": "shape", - "description": "{ backdrop?: elementType, paper?: elementType, root?: elementType, transition?: elementType }" + "description": "{ backdrop?: elementType, list?: elementType, paper?: elementType, root?: elementType, transition?: elementType }" }, "default": "{}" }, @@ -43,7 +48,12 @@ }, "default": "'auto'" }, - "TransitionProps": { "type": { "name": "object" }, "default": "{}" }, + "TransitionProps": { + "type": { "name": "object" }, + "default": "{}", + "deprecated": true, + "deprecationInfo": "use the slotProps.transition prop instead. This prop will be removed in v7. See Migrating from deprecated APIs for more details." + }, "variant": { "type": { "name": "enum", "description": "'menu'
| 'selectedMenu'" }, "default": "'selectedMenu'" @@ -51,26 +61,39 @@ }, "name": "Menu", "imports": ["import Menu from '@mui/material/Menu';", "import { Menu } from '@mui/material';"], - "classes": [ + "slots": [ + { + "name": "root", + "description": "The component used for the popper.", + "default": "Modal", + "class": "MuiMenu-root" + }, + { + "name": "paper", + "description": "The component used for the paper.", + "default": "Paper", + "class": "MuiMenu-paper" + }, { - "key": "list", - "className": "MuiMenu-list", - "description": "Styles applied to the List component via `MenuList`.", - "isGlobal": false + "name": "list", + "description": "The component used for the list.", + "default": "MenuList", + "class": "MuiMenu-list" }, { - "key": "paper", - "className": "MuiMenu-paper", - "description": "Styles applied to the Paper component.", - "isGlobal": false + "name": "transition", + "description": "The component used for the transition slot.", + "default": "Grow", + "class": null }, { - "key": "root", - "className": "MuiMenu-root", - "description": "Styles applied to the root element.", - "isGlobal": false + "name": "backdrop", + "description": "The component used for the backdrop slot.", + "default": "Backdrop", + "class": null } ], + "classes": [], "spread": true, "themeDefaultProps": false, "muiName": "MuiMenu", diff --git a/docs/translations/api-docs/menu/menu.json b/docs/translations/api-docs/menu/menu.json index afd676d6d50a63..cf5c9609d5fe73 100644 --- a/docs/translations/api-docs/menu/menu.json +++ b/docs/translations/api-docs/menu/menu.json @@ -41,15 +41,12 @@ "description": "The variant to use. Use menu to prevent selected items from impacting the initial focus." } }, - "classDescriptions": { - "list": { - "description": "Styles applied to {{nodeName}}.", - "nodeName": "the List component via MenuList" - }, - "paper": { - "description": "Styles applied to {{nodeName}}.", - "nodeName": "the Paper component" - }, - "root": { "description": "Styles applied to the root element." } + "classDescriptions": {}, + "slotDescriptions": { + "backdrop": "The component used for the backdrop slot.", + "list": "The component used for the list.", + "paper": "The component used for the paper.", + "root": "The component used for the popper.", + "transition": "The component used for the transition slot." } } diff --git a/packages/mui-codemod/src/deprecations/all/deprecations-all.js b/packages/mui-codemod/src/deprecations/all/deprecations-all.js index dac8ef291bda8e..d6821261c2eac2 100644 --- a/packages/mui-codemod/src/deprecations/all/deprecations-all.js +++ b/packages/mui-codemod/src/deprecations/all/deprecations-all.js @@ -40,6 +40,7 @@ import transformSnackbarProps from '../snackbar-props'; import transformerTabsProps from '../tabs-props'; import transformerTabsClasses from '../tabs-classes'; import transformDrawerProps from '../drawer-props'; +import transformMenuProps from '../menu-props'; /** * @param {import('jscodeshift').FileInfo} file @@ -88,6 +89,7 @@ export default function deprecationsAll(file, api, options) { file.source = transformerTabsProps(file, api, options); file.source = transformerTabsClasses(file, api, options); file.source = transformDrawerProps(file, api, options); + file.source = transformMenuProps(file, api, options); return file.source; } diff --git a/packages/mui-codemod/src/deprecations/menu-props/index.js b/packages/mui-codemod/src/deprecations/menu-props/index.js new file mode 100644 index 00000000000000..c8c171018fcccd --- /dev/null +++ b/packages/mui-codemod/src/deprecations/menu-props/index.js @@ -0,0 +1 @@ +export { default } from './menu-props'; diff --git a/packages/mui-codemod/src/deprecations/menu-props/menu-props.js b/packages/mui-codemod/src/deprecations/menu-props/menu-props.js new file mode 100644 index 00000000000000..f63fa7bae12e07 --- /dev/null +++ b/packages/mui-codemod/src/deprecations/menu-props/menu-props.js @@ -0,0 +1,35 @@ +import movePropIntoSlots from '../utils/movePropIntoSlots'; +import movePropIntoSlotProps from '../utils/movePropIntoSlotProps'; + +/** + * @param {import('jscodeshift').FileInfo} file + * @param {import('jscodeshift').API} api + */ +export default function transformer(file, api, options) { + const j = api.jscodeshift; + const root = j(file.source); + const printOptions = options.printOptions; + + movePropIntoSlotProps(j, { + root, + componentName: 'Menu', + propName: 'MenuListProps', + slotName: 'list', + }); + + movePropIntoSlots(j, { + root, + componentName: 'Menu', + propName: 'TransitionComponent', + slotName: 'transition', + }); + + movePropIntoSlotProps(j, { + root, + componentName: 'Menu', + propName: 'TransitionProps', + slotName: 'transition', + }); + + return root.toSource(printOptions); +} diff --git a/packages/mui-codemod/src/deprecations/menu-props/menu-props.test.js b/packages/mui-codemod/src/deprecations/menu-props/menu-props.test.js new file mode 100644 index 00000000000000..c3e716c43bb381 --- /dev/null +++ b/packages/mui-codemod/src/deprecations/menu-props/menu-props.test.js @@ -0,0 +1,16 @@ +import { describeJscodeshiftTransform } from '../../../testUtils'; +import transform from './menu-props'; + +describe('@mui/codemod', () => { + describe('deprecations', () => { + describeJscodeshiftTransform({ + transform, + transformName: 'menu-props', + dirname: __dirname, + testCases: [ + { actual: '/test-cases/actual.js', expected: '/test-cases/expected.js' }, + { actual: '/test-cases/theme.actual.js', expected: '/test-cases/theme.expected.js' }, + ], + }); + }); +}); diff --git a/packages/mui-codemod/src/deprecations/menu-props/test-cases/actual.js b/packages/mui-codemod/src/deprecations/menu-props/test-cases/actual.js new file mode 100644 index 00000000000000..17a922ade4a592 --- /dev/null +++ b/packages/mui-codemod/src/deprecations/menu-props/test-cases/actual.js @@ -0,0 +1,48 @@ +import Menu from '@mui/material/Menu'; +import { Menu as MyMenu } from '@mui/material'; + +; + +; + +; + +; + +; diff --git a/packages/mui-codemod/src/deprecations/menu-props/test-cases/expected.js b/packages/mui-codemod/src/deprecations/menu-props/test-cases/expected.js new file mode 100644 index 00000000000000..dbeb1e029f5414 --- /dev/null +++ b/packages/mui-codemod/src/deprecations/menu-props/test-cases/expected.js @@ -0,0 +1,63 @@ +import Menu from '@mui/material/Menu'; +import { Menu as MyMenu } from '@mui/material'; + +; + +; + +; + +; + +; diff --git a/packages/mui-codemod/src/deprecations/menu-props/test-cases/theme.actual.js b/packages/mui-codemod/src/deprecations/menu-props/test-cases/theme.actual.js new file mode 100644 index 00000000000000..29889ee6e1522a --- /dev/null +++ b/packages/mui-codemod/src/deprecations/menu-props/test-cases/theme.actual.js @@ -0,0 +1,22 @@ +fn({ + MuiMenu: { + defaultProps: { + MenuListProps: { disablePadding: true }, + TransitionComponent: CustomTransition, + TransitionProps: { timeout: 200 }, + }, + }, +}); + +fn({ + MuiMenu: { + defaultProps: { + TransitionComponent: CustomTransition, + MenuListProps: { disablePadding: true }, + TransitionProps: { timeout: 200 }, + slotProps: { + root: { disablePortal: true }, + }, + }, + }, +}); diff --git a/packages/mui-codemod/src/deprecations/menu-props/test-cases/theme.expected.js b/packages/mui-codemod/src/deprecations/menu-props/test-cases/theme.expected.js new file mode 100644 index 00000000000000..fa88e0b9e4aca7 --- /dev/null +++ b/packages/mui-codemod/src/deprecations/menu-props/test-cases/theme.expected.js @@ -0,0 +1,30 @@ +fn({ + MuiMenu: { + defaultProps: { + slotProps: { + list: { disablePadding: true }, + transition: { timeout: 200 } + }, + + slots: { + transition: CustomTransition + } + }, + }, +}); + +fn({ + MuiMenu: { + defaultProps: { + slotProps: { + root: { disablePortal: true }, + list: { disablePadding: true }, + transition: { timeout: 200 } + }, + + slots: { + transition: CustomTransition + } + }, + }, +}); diff --git a/packages/mui-material/src/Menu/Menu.d.ts b/packages/mui-material/src/Menu/Menu.d.ts index 9170b8ba9aaa7a..b27805730541eb 100644 --- a/packages/mui-material/src/Menu/Menu.d.ts +++ b/packages/mui-material/src/Menu/Menu.d.ts @@ -4,11 +4,94 @@ import { InternalStandardProps as StandardProps } from '..'; import { PaperProps } from '../Paper'; import { PopoverProps } from '../Popover'; import { MenuListProps } from '../MenuList'; +import { ModalProps } from '../Modal'; +import { BackdropProps } from '../Backdrop'; import { Theme } from '../styles'; import { TransitionProps } from '../transitions/transition'; import { MenuClasses } from './menuClasses'; +import { CreateSlotsAndSlotProps, SlotComponentProps, SlotProps } from '../utils/types'; -export interface MenuProps extends StandardProps { +export interface MenuRootSlotPropsOverrides {} + +export interface MenuPaperSlotPropsOverrides {} + +export interface MenuTransitionSlotPropsOverrides {} + +export interface MenuListSlotPropsOverrides {} + +export interface MenuBackdropSlotPropsOverrides {} + +export interface MenuSlots { + /** + * The component used for the popper. + * @default Modal + */ + root: React.ElementType; + /** + * The component used for the paper. + * @default Paper + */ + paper: React.ElementType; + /** + * The component used for the list. + * @default MenuList + */ + list: React.ElementType; + /** + * The component used for the transition slot. + * @default Grow + */ + transition: React.ElementType; + /** + * The component used for the backdrop slot. + * @default Backdrop + */ + backdrop: React.ElementType; +} + +export type MenuSlotsAndSlotProps = CreateSlotsAndSlotProps< + MenuSlots, + { + /** + * Props forwarded to the root slot. + * By default, the avaible props are based on the [Popover](https://mui.com/material-ui/api/popover/#props) component. + */ + root: SlotProps, MenuRootSlotPropsOverrides, MenuOwnerState>; + /** + * Props forwarded to the paper slot. + * By default, the avaible props are based on the [Paper](https://mui.com/material-ui/api/paper/#props) component. + */ + paper: SlotProps, MenuPaperSlotPropsOverrides, MenuOwnerState>; + /** + * Props forwarded to the list slot. + * By default, the avaible props are based on the [MenuList](https://mui.com/material-ui/api/menu-list/#props) component. + */ + list: SlotProps, MenuListSlotPropsOverrides, MenuOwnerState>; + /** + * Props forwarded to the transition slot. + * By default, the avaible props are based on the [Grow](https://mui.com/material-ui/api/grow/#props) component. + */ + transition: SlotComponentProps< + // use SlotComponentProps because transition slot does not support `component` and `sx` prop + React.ElementType, + TransitionProps & MenuTransitionSlotPropsOverrides, + MenuOwnerState + >; + /** + * Props forwarded to the backdrop slot. + * By default, the avaible props are based on the [Backdrop](https://mui.com/material-ui/api/backdrop/#props) component. + */ + backdrop: SlotProps< + React.ElementType, + MenuBackdropSlotPropsOverrides, + MenuOwnerState + >; + } +>; + +export interface MenuProps + extends StandardProps>, + MenuSlotsAndSlotProps { /** * An HTML element, or a function that returns one. * It's used to set the position of the menu. @@ -40,6 +123,7 @@ export interface MenuProps extends StandardProps { disableAutoFocusItem?: boolean; /** * Props applied to the [`MenuList`](https://mui.com/material-ui/api/menu-list/) element. + * @deprecated use the `slotProps.list` prop instead. This prop will be removed in v7. See [Migrating from deprecated APIs](https://mui.com/material-ui/migration/migrating-from-deprecated-apis/) for more details. * @default {} */ MenuListProps?: Partial; @@ -70,6 +154,7 @@ export interface MenuProps extends StandardProps { /** * Props applied to the transition element. * By default, the element is based on this [`Transition`](https://reactcommunity.org/react-transition-group/transition/) component. + * @deprecated use the `slotProps.transition` prop instead. This prop will be removed in v7. See [Migrating from deprecated APIs](https://mui.com/material-ui/migration/migrating-from-deprecated-apis/) for more details. * @default {} */ TransitionProps?: TransitionProps; @@ -80,6 +165,8 @@ export interface MenuProps extends StandardProps { variant?: 'menu' | 'selectedMenu'; } +export interface MenuOwnerState extends Omit {} + export declare const MenuPaper: React.FC; /** diff --git a/packages/mui-material/src/Menu/Menu.js b/packages/mui-material/src/Menu/Menu.js index f3d4a52afd36c5..13986179dbce76 100644 --- a/packages/mui-material/src/Menu/Menu.js +++ b/packages/mui-material/src/Menu/Menu.js @@ -13,6 +13,7 @@ import rootShouldForwardProp from '../styles/rootShouldForwardProp'; import { styled } from '../zero-styled'; import { useDefaultProps } from '../DefaultPropsProvider'; import { getMenuUtilityClass } from './menuClasses'; +import useSlot from '../utils/useSlot'; const RTL_ORIGIN = { vertical: 'top', @@ -162,8 +163,15 @@ const Menu = React.forwardRef(function Menu(inProps, ref) { } }); - const PaperSlot = slots.paper ?? MenuPaper; - const paperExternalSlotProps = slotProps.paper ?? PaperProps; + const externalForwardedProps = { + slots, + slotProps: { + list: MenuListProps, + transition: TransitionProps, + paper: PaperProps, + ...slotProps, + }, + }; const rootSlotProps = useSlotProps({ elementType: slots.root, @@ -172,13 +180,34 @@ const Menu = React.forwardRef(function Menu(inProps, ref) { className: [classes.root, className], }); - const paperSlotProps = useSlotProps({ - elementType: PaperSlot, - externalSlotProps: paperExternalSlotProps, - ownerState, + const [PaperSlot, paperSlotProps] = useSlot('paper', { className: classes.paper, + elementType: MenuPaper, + externalForwardedProps, + shouldForwardComponentProp: true, + ownerState, + }); + + const [ListSlot, listSlotProps] = useSlot('list', { + className: clsx(classes.list, MenuListProps.className), + elementType: MenuMenuList, + shouldForwardComponentProp: true, + externalForwardedProps, + getSlotProps: (handlers) => ({ + ...handlers, + onKeyDown: (event) => { + handleListKeyDown(event); + handlers.onKeyDown?.(event); + }, + }), + ownerState, }); + const resolvedTransitionProps = + typeof externalForwardedProps.slotProps.transition === 'function' + ? externalForwardedProps.slotProps.transition(ownerState) + : externalForwardedProps.slotProps.transition; + return ( { + handleEntering(...args); + resolvedTransitionProps?.onEntering?.(...args); + }, + }, }} open={open} ref={ref} transitionDuration={transitionDuration} - TransitionProps={{ onEntering: handleEntering, ...TransitionProps }} ownerState={ownerState} {...other} classes={PopoverClasses} > - {children} - + ); }); @@ -261,6 +303,7 @@ Menu.propTypes /* remove-proptypes */ = { disableAutoFocusItem: PropTypes.bool, /** * Props applied to the [`MenuList`](https://mui.com/material-ui/api/menu-list/) element. + * @deprecated use the `slotProps.list` prop instead. This prop will be removed in v7. See [Migrating from deprecated APIs](https://mui.com/material-ui/migration/migrating-from-deprecated-apis/) for more details. * @default {} */ MenuListProps: PropTypes.object, @@ -289,6 +332,7 @@ Menu.propTypes /* remove-proptypes */ = { */ slotProps: PropTypes.shape({ backdrop: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + list: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), paper: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), transition: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), @@ -299,6 +343,7 @@ Menu.propTypes /* remove-proptypes */ = { */ slots: PropTypes.shape({ backdrop: PropTypes.elementType, + list: PropTypes.elementType, paper: PropTypes.elementType, root: PropTypes.elementType, transition: PropTypes.elementType, @@ -327,6 +372,7 @@ Menu.propTypes /* remove-proptypes */ = { /** * Props applied to the transition element. * By default, the element is based on this [`Transition`](https://reactcommunity.org/react-transition-group/transition/) component. + * @deprecated use the `slotProps.transition` prop instead. This prop will be removed in v7. See [Migrating from deprecated APIs](https://mui.com/material-ui/migration/migrating-from-deprecated-apis/) for more details. * @default {} */ TransitionProps: PropTypes.object, diff --git a/packages/mui-material/src/Menu/Menu.spec.tsx b/packages/mui-material/src/Menu/Menu.spec.tsx new file mode 100644 index 00000000000000..c0f73c7fef2de6 --- /dev/null +++ b/packages/mui-material/src/Menu/Menu.spec.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; +import Menu, { MenuProps } from '@mui/material/Menu'; + +; + +; + +function Custom(props: MenuProps) { + const { slotProps, ...dialogProps } = props; + return ( + { + const transitionProps = + typeof slotProps?.transition === 'function' + ? slotProps.transition(ownerState) + : slotProps?.transition; + return { + ...transitionProps, + onExited: (node) => { + transitionProps?.onExited?.(node); + }, + }; + }, + }} + {...dialogProps} + > + test + + ); +} diff --git a/packages/mui-material/src/Menu/Menu.test.js b/packages/mui-material/src/Menu/Menu.test.js index 1d9ca77a1861cd..b9e04e603aeaf2 100644 --- a/packages/mui-material/src/Menu/Menu.test.js +++ b/packages/mui-material/src/Menu/Menu.test.js @@ -10,10 +10,18 @@ import { } from '@mui/internal-test-utils'; import Menu, { menuClasses as classes } from '@mui/material/Menu'; import Popover from '@mui/material/Popover'; +import { modalClasses } from '@mui/material/Modal'; import { createTheme, ThemeProvider } from '@mui/material/styles'; import describeConformance from '../../test/describeConformance'; import { paperClasses } from '../Paper'; +const CustomTransition = React.forwardRef(function CustomTransition( + { in: inProp, appear, onEnter, onEntering, onExited, ownerState, ...props }, + ref, +) { + return
; +}); + describe('', () => { const { render } = createRenderer({ clock: 'fake' }); @@ -30,6 +38,21 @@ describe('', () => { paper: { expectedClassName: classes.paper, }, + list: { + expectedClassName: classes.list, + testWithElement: null, // already tested with `testWithComponent` + }, + backdrop: { + expectedClassName: modalClasses.backdrop, + testWithElement: React.forwardRef(({ invisible, ownerState, ...props }, ref) => ( + + )), + }, + transition: { + expectedClassName: null, + testWithComponent: CustomTransition, + testWithElement: CustomTransition, + }, }, testDeepOverrides: { slotName: 'list', slotClassName: classes.list }, testRootOverrides: { slotName: 'root', slotClassName: classes.root }, @@ -217,33 +240,63 @@ describe('', () => { expect(screen.getByRole('menu')).not.toHaveFocus(); }); - it('should call TransitionProps.onEntering', () => { + it('should call slotProps.transition.onEntering', () => { const onEnteringSpy = spy(); render( , ); expect(onEnteringSpy.callCount).to.equal(1); }); - it('should call TransitionProps.onEntering, disableAutoFocusItem', () => { + it('should call slotProps.transition.onEntering, disableAutoFocusItem', () => { const onEnteringSpy = spy(); render( , ); expect(onEnteringSpy.callCount).to.equal(1); }); + // TODO: remove in v7 + describe('legacy TransitionProps', () => { + it('should call TransitionProps.onEntering', () => { + const onEnteringSpy = spy(); + render( + , + ); + + expect(onEnteringSpy.callCount).to.equal(1); + }); + + it('should call TransitionProps.onEntering, disableAutoFocusItem', () => { + const onEnteringSpy = spy(); + render( + , + ); + + expect(onEnteringSpy.callCount).to.equal(1); + }); + }); + it('should call onClose on tab', () => { function MenuItem(props) { const { autoFocus, children } = props; diff --git a/packages/mui-material/src/Select/SelectInput.js b/packages/mui-material/src/Select/SelectInput.js index a1acee8485330f..58255517fe8eac 100644 --- a/packages/mui-material/src/Select/SelectInput.js +++ b/packages/mui-material/src/Select/SelectInput.js @@ -556,16 +556,16 @@ const SelectInput = React.forwardRef(function SelectInput(props, ref) { horizontal: 'center', }} {...MenuProps} - MenuListProps={{ - 'aria-labelledby': labelId, - role: 'listbox', - 'aria-multiselectable': multiple ? 'true' : undefined, - disableListWrap: true, - id: listboxId, - ...MenuProps.MenuListProps, - }} slotProps={{ ...MenuProps.slotProps, + list: { + 'aria-labelledby': labelId, + role: 'listbox', + 'aria-multiselectable': multiple ? 'true' : undefined, + disableListWrap: true, + id: listboxId, + ...MenuProps.MenuListProps, + }, paper: { ...paperProps, style: {