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 585bc869801e3f..220c70975c1538 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 @@ -28,9 +28,15 @@ If you need to run a specific codemod, those are also linked below. ## Accordion +Use the [codemod](https://github.com/mui/material-ui/tree/HEAD/packages/mui-codemod#accordion-props) below to migrate the code as described in the following sections: + +```bash +npx @mui/codemod@latest deprecations/accordion-props +``` + ### TransitionComponent -The Accordion's `TransitionComponent` was deprecated in favor of `slots.transition` ([Codemod](https://github.com/mui/material-ui/tree/HEAD/packages/mui-codemod#accordion-props)): +The Accordion's `TransitionComponent` was deprecated in favor of `slots.transition`: ```diff ``` +## Avatar + +Use the [codemod](https://github.com/mui/material-ui/tree/HEAD/packages/mui-codemod#avatar-props) below to migrate the code as described in the following sections: + +```bash +npx @mui/codemod@latest deprecations/avatar-props +``` + +### imgProps + +The Avatar's `imgProps` was deprecated in favor of `slotProps.img`: + +```diff + {}, +- onLoad: () => {}, ++ slotProps={{ ++ img: { ++ onError: () => {}, ++ onLoad: () => {}, ++ } + }} + />; +``` + ## Divider +Use the [codemod](https://github.com/mui/material-ui/tree/HEAD/packages/mui-codemod#divider-props) below to migrate the code as described in the following sections: + +```bash +npx @mui/codemod@latest deprecations/divider-props +``` + ### light -The Divider's `light` prop was deprecated, Use `sx={{ opacity : "0.6" }}` (or any opacity). ([Codemod](https://github.com/mui/material-ui/tree/HEAD/packages/mui-codemod#divider-props)): +The Divider's `light` prop was deprecated, Use `sx={{ opacity : "0.6" }}` (or any opacity): ```diff slotProps.img instead. This prop will be removed in v7. How to migrate." + }, "sizes": { "type": { "name": "string" } }, + "slotProps": { + "type": { "name": "shape", "description": "{ img?: func
| object }" }, + "default": "{}" + }, + "slots": { + "type": { "name": "shape", "description": "{ img?: elementType }" }, + "default": "{}" + }, "src": { "type": { "name": "string" } }, "srcSet": { "type": { "name": "string" } }, "sx": { @@ -28,6 +40,14 @@ "import Avatar from '@mui/material/Avatar';", "import { Avatar } from '@mui/material';" ], + "slots": [ + { + "name": "img", + "description": "The component that renders the transition.\n[Follow this guide](/material-ui/transitions/#transitioncomponent-prop) to learn more about the requirements for this component.", + "default": "Collapse", + "class": "MuiAvatar-img" + } + ], "classes": [ { "key": "circular", @@ -47,12 +67,6 @@ "description": "Styles applied to the fallback icon", "isGlobal": false }, - { - "key": "img", - "className": "MuiAvatar-img", - "description": "Styles applied to the img element if either `src` or `srcSet` is defined.", - "isGlobal": false - }, { "key": "root", "className": "MuiAvatar-root", diff --git a/docs/translations/api-docs/avatar/avatar.json b/docs/translations/api-docs/avatar/avatar.json index 0896257246b055..ed9ae7a397a2ba 100644 --- a/docs/translations/api-docs/avatar/avatar.json +++ b/docs/translations/api-docs/avatar/avatar.json @@ -17,6 +17,8 @@ "sizes": { "description": "The sizes attribute for the img element." }, + "slotProps": { "description": "The props used for each slot inside." }, + "slots": { "description": "The components used for each slot inside." }, "src": { "description": "The src attribute for the img element." }, "srcSet": { "description": "The srcSet attribute for the img element. Use this attribute for responsive image display." @@ -38,11 +40,6 @@ "conditions": "not src or srcSet" }, "fallback": { "description": "Styles applied to the fallback icon" }, - "img": { - "description": "Styles applied to {{nodeName}} if {{conditions}}.", - "nodeName": "the img element", - "conditions": "either src or srcSet is defined" - }, "root": { "description": "Styles applied to the root element." }, "rounded": { "description": "Styles applied to {{nodeName}} if {{conditions}}.", @@ -54,5 +51,8 @@ "nodeName": "the root element", "conditions": "variant=\"square\"" } + }, + "slotDescriptions": { + "img": "The component that renders the transition. Follow this guide to learn more about the requirements for this component." } } diff --git a/packages/mui-codemod/README.md b/packages/mui-codemod/README.md index b3224d55e96eac..487ceafc1523a5 100644 --- a/packages/mui-codemod/README.md +++ b/packages/mui-codemod/README.md @@ -91,6 +91,22 @@ A combination of all deprecations. npx @mui/codemod@latest deprecations/accordion-props ``` +#### `avatar-props` + +```diff + {}, +- onLoad: () => {}, ++ slotProps={{ ++ img: { ++ onError: () => {}, ++ onLoad: () => {}, ++ } + }} + />; +``` + #### `divider-props` ```diff diff --git a/packages/mui-codemod/src/deprecations/all/deprecations-all.js b/packages/mui-codemod/src/deprecations/all/deprecations-all.js index 08e1d4510f02fd..4b1880abc356c1 100644 --- a/packages/mui-codemod/src/deprecations/all/deprecations-all.js +++ b/packages/mui-codemod/src/deprecations/all/deprecations-all.js @@ -1,4 +1,6 @@ -import transformAccordionProps from '../accordion-props/accordion-props'; +import transformAccordionProps from '../accordion-props'; +import transformAvatarProps from '../avatar-props'; +import transformDividerProps from '../divider-props'; /** * @param {import('jscodeshift').FileInfo} file @@ -6,6 +8,8 @@ import transformAccordionProps from '../accordion-props/accordion-props'; */ export default function deprecationsAll(file, api, options) { file.source = transformAccordionProps(file, api, options); + file.source = transformAvatarProps(file, api, options); + file.source = transformDividerProps(file, api, options); return file.source; } diff --git a/packages/mui-codemod/src/deprecations/avatar-props/avatar-props.js b/packages/mui-codemod/src/deprecations/avatar-props/avatar-props.js new file mode 100644 index 00000000000000..e79dcc0256725e --- /dev/null +++ b/packages/mui-codemod/src/deprecations/avatar-props/avatar-props.js @@ -0,0 +1,56 @@ +import findComponentJSX from '../../util/findComponentJSX'; +import assignObject from '../../util/assignObject'; +import appendAttribute from '../../util/appendAttribute'; + +/** + * @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; + + findComponentJSX(j, { root, componentName: 'Avatar' }, (elementPath) => { + const index = elementPath.node.openingElement.attributes.findIndex( + (attr) => attr.type === 'JSXAttribute' && attr.name.name === 'imgProps', + ); + if (index !== -1) { + const removed = elementPath.node.openingElement.attributes.splice(index, 1); + let hasNode = false; + elementPath.node.openingElement.attributes.forEach((attr) => { + if (attr.name?.name === 'slotProps') { + hasNode = true; + assignObject(j, { + target: attr, + key: 'img', + expression: removed[0].value.expression, + }); + } + }); + if (!hasNode) { + appendAttribute(j, { + target: elementPath.node, + attributeName: 'slotProps', + expression: j.objectExpression([ + j.objectProperty(j.identifier('img'), removed[0].value.expression), + ]), + }); + } + } + }); + + root.find(j.ObjectProperty, { key: { name: 'imgProps' } }).forEach((path) => { + if (path.parent?.parent?.parent?.parent?.node.key?.name === 'MuiAvatar') { + path.replace( + j.property( + 'init', + j.identifier('slotProps'), + j.objectExpression([j.objectProperty(j.identifier('img'), path.node.value)]), + ), + ); + } + }); + + return root.toSource(printOptions); +} diff --git a/packages/mui-codemod/src/deprecations/avatar-props/avatar-props.test.js b/packages/mui-codemod/src/deprecations/avatar-props/avatar-props.test.js new file mode 100644 index 00000000000000..86eecdea4eacbf --- /dev/null +++ b/packages/mui-codemod/src/deprecations/avatar-props/avatar-props.test.js @@ -0,0 +1,53 @@ +import path from 'path'; +import { expect } from 'chai'; +import { jscodeshift } from '../../../testUtils'; +import transform from './avatar-props'; +import readFile from '../../util/readFile'; + +function read(fileName) { + return readFile(path.join(__dirname, fileName)); +} + +describe('@mui/codemod', () => { + describe('deprecations', () => { + describe('avatar-props', () => { + it('transforms props as needed', () => { + const actual = transform({ source: read('./test-cases/actual.js') }, { jscodeshift }, {}); + + const expected = read('./test-cases/expected.js'); + expect(actual).to.equal(expected, 'The transformed version should be correct'); + }); + + it('should be idempotent', () => { + const actual = transform({ source: read('./test-cases/expected.js') }, { jscodeshift }, {}); + + const expected = read('./test-cases/expected.js'); + expect(actual).to.equal(expected, 'The transformed version should be correct'); + }); + }); + + describe('[theme] avatar-props', () => { + it('transforms props as needed', () => { + const actual = transform( + { source: read('./test-cases/theme.actual.js') }, + { jscodeshift }, + {}, + ); + + const expected = read('./test-cases/theme.expected.js'); + expect(actual).to.equal(expected, 'The transformed version should be correct'); + }); + + it('should be idempotent', () => { + const actual = transform( + { source: read('./test-cases/theme.expected.js') }, + { jscodeshift }, + {}, + ); + + const expected = read('./test-cases/theme.expected.js'); + expect(actual).to.equal(expected, 'The transformed version should be correct'); + }); + }); + }); +}); diff --git a/packages/mui-codemod/src/deprecations/avatar-props/index.js b/packages/mui-codemod/src/deprecations/avatar-props/index.js new file mode 100644 index 00000000000000..9858cb5cca24a0 --- /dev/null +++ b/packages/mui-codemod/src/deprecations/avatar-props/index.js @@ -0,0 +1 @@ +export { default } from './avatar-props'; diff --git a/packages/mui-codemod/src/deprecations/avatar-props/test-cases/actual.js b/packages/mui-codemod/src/deprecations/avatar-props/test-cases/actual.js new file mode 100644 index 00000000000000..e553aae667fd1a --- /dev/null +++ b/packages/mui-codemod/src/deprecations/avatar-props/test-cases/actual.js @@ -0,0 +1,23 @@ +import Avatar from '@mui/material/Avatar'; +import { Avatar as MyAvatar } from '@mui/material'; + + {}, + onLoad: () => {}, + }} +/>; + {}, + onLoad: () => {}, + }} +/>; + +// should skip non MUI components + {}, + onLoad: () => {}, + }} +/>; diff --git a/packages/mui-codemod/src/deprecations/avatar-props/test-cases/expected.js b/packages/mui-codemod/src/deprecations/avatar-props/test-cases/expected.js new file mode 100644 index 00000000000000..e651ca74116c05 --- /dev/null +++ b/packages/mui-codemod/src/deprecations/avatar-props/test-cases/expected.js @@ -0,0 +1,27 @@ +import Avatar from '@mui/material/Avatar'; +import { Avatar as MyAvatar } from '@mui/material'; + + {}, + onLoad: () => {}, + } + }} +/>; + {}, + onLoad: () => {}, + } + }} +/>; + +// should skip non MUI components + {}, + onLoad: () => {}, + }} +/>; diff --git a/packages/mui-codemod/src/deprecations/avatar-props/test-cases/theme.actual.js b/packages/mui-codemod/src/deprecations/avatar-props/test-cases/theme.actual.js new file mode 100644 index 00000000000000..e28debd498e4e9 --- /dev/null +++ b/packages/mui-codemod/src/deprecations/avatar-props/test-cases/theme.actual.js @@ -0,0 +1,10 @@ +fn({ + MuiAvatar: { + defaultProps: { + imgProps: { + onError: () => {}, + onLoad: () => {}, + }, + }, + }, +}); diff --git a/packages/mui-codemod/src/deprecations/avatar-props/test-cases/theme.expected.js b/packages/mui-codemod/src/deprecations/avatar-props/test-cases/theme.expected.js new file mode 100644 index 00000000000000..a4ac9d4cf5fccb --- /dev/null +++ b/packages/mui-codemod/src/deprecations/avatar-props/test-cases/theme.expected.js @@ -0,0 +1,12 @@ +fn({ + MuiAvatar: { + defaultProps: { + slotProps: { + img: { + onError: () => {}, + onLoad: () => {}, + } + } + }, + }, +}); diff --git a/packages/mui-material/src/Avatar/Avatar.d.ts b/packages/mui-material/src/Avatar/Avatar.d.ts index daa7d9afa74b57..24bed78bf4928b 100644 --- a/packages/mui-material/src/Avatar/Avatar.d.ts +++ b/packages/mui-material/src/Avatar/Avatar.d.ts @@ -4,9 +4,30 @@ import { OverridableStringUnion } from '@mui/types'; import { Theme } from '../styles'; import { OverridableComponent, OverrideProps } from '../OverridableComponent'; import { AvatarClasses } from './avatarClasses'; +import { CreateSlotsAndSlotProps, SlotProps } from '../utils/types'; + +export interface AvatarSlots { + /** + * The component that renders the transition. + * [Follow this guide](/material-ui/transitions/#transitioncomponent-prop) to learn more about the requirements for this component. + * @default Collapse + */ + img?: React.JSXElementConstructor>; +} export interface AvatarPropsVariantOverrides {} +export type AvatarSlotsAndSlotProps = CreateSlotsAndSlotProps< + AvatarSlots, + { + img: SlotProps< + React.ElementType>, + {}, + AvatarOwnProps + >; + } +>; + export interface AvatarOwnProps { /** * Used in combination with `src` or `srcSet` to @@ -25,6 +46,7 @@ export interface AvatarOwnProps { /** * [Attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attributes) applied to the `img` element if the component is used to display an image. * It can be used to listen for the loading error event. + * @deprecated Use `slotProps.img` instead. This prop will be removed in v7. [How to migrate](/material-ui/migration/migrating-from-deprecated-apis/). */ imgProps?: React.ImgHTMLAttributes & { sx?: SxProps; @@ -57,7 +79,7 @@ export interface AvatarTypeMap< AdditionalProps = {}, RootComponent extends React.ElementType = 'div', > { - props: AdditionalProps & AvatarOwnProps; + props: AdditionalProps & AvatarOwnProps & AvatarSlotsAndSlotProps; defaultComponent: RootComponent; } diff --git a/packages/mui-material/src/Avatar/Avatar.js b/packages/mui-material/src/Avatar/Avatar.js index 71681e90fe7afa..e1994e72475b00 100644 --- a/packages/mui-material/src/Avatar/Avatar.js +++ b/packages/mui-material/src/Avatar/Avatar.js @@ -7,6 +7,7 @@ import styled from '../styles/styled'; import useThemeProps from '../styles/useThemeProps'; import Person from '../internal/svg-icons/Person'; import { getAvatarUtilityClass } from './avatarClasses'; +import useSlot from '../utils/useSlot'; const useUtilityClasses = (ownerState) => { const { classes, variant, colorDefault } = ownerState; @@ -147,6 +148,8 @@ const Avatar = React.forwardRef(function Avatar(inProps, ref) { children: childrenProp, className, component = 'div', + slots = {}, + slotProps = {}, imgProps, sizes, src, @@ -171,19 +174,19 @@ const Avatar = React.forwardRef(function Avatar(inProps, ref) { const classes = useUtilityClasses(ownerState); - if (hasImgNotFailing) { - children = ( - - ); + const [ImgSlot, imgSlotProps] = useSlot('img', { + className: classes.img, + elementType: AvatarImg, + externalForwardedProps: { + slots, + slotProps: { img: { ...imgProps, ...slotProps.img } }, + }, + additionalProps: { alt, src, srcSet, sizes }, + ownerState, + }); + if (hasImgNotFailing) { + children = ; // We only render valid children, non valid children are rendered with a fallback // We consider that invalid children are all falsy values, except 0, which is valid. } else if (!!childrenProp || childrenProp === 0) { @@ -238,12 +241,27 @@ Avatar.propTypes /* remove-proptypes */ = { /** * [Attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attributes) applied to the `img` element if the component is used to display an image. * It can be used to listen for the loading error event. + * @deprecated Use `slotProps.img` instead. This prop will be removed in v7. [How to migrate](/material-ui/migration/migrating-from-deprecated-apis/). */ imgProps: PropTypes.object, /** * The `sizes` attribute for the `img` element. */ sizes: PropTypes.string, + /** + * The props used for each slot inside. + * @default {} + */ + slotProps: PropTypes.shape({ + img: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + }), + /** + * The components used for each slot inside. + * @default {} + */ + slots: PropTypes.shape({ + img: PropTypes.elementType, + }), /** * The `src` attribute for the `img` element. */ diff --git a/packages/mui-material/src/Avatar/Avatar.spec.tsx b/packages/mui-material/src/Avatar/Avatar.spec.tsx index 4702071cbd2179..b64374d1603bf7 100644 --- a/packages/mui-material/src/Avatar/Avatar.spec.tsx +++ b/packages/mui-material/src/Avatar/Avatar.spec.tsx @@ -4,3 +4,9 @@ import Avatar from '@mui/material/Avatar'; function ImgPropsShouldSupportSx() { ; } + +function CustomImg() { + return ; +} +; +; diff --git a/packages/mui-material/src/Avatar/Avatar.test.js b/packages/mui-material/src/Avatar/Avatar.test.js index 2c1098b1d8b1b1..75e07a8d447ddc 100644 --- a/packages/mui-material/src/Avatar/Avatar.test.js +++ b/packages/mui-material/src/Avatar/Avatar.test.js @@ -46,12 +46,21 @@ describe('', () => { }); it('should be able to add more props to the image', () => { + // TODO: remove this test in v7 const onError = spy(); const { container } = render(); const img = container.querySelector('img'); fireEvent.error(img); expect(onError.callCount).to.equal(1); }); + + it('should be able to add more props to the img slot', () => { + const onError = spy(); + const { container } = render(); + const img = container.querySelector('img'); + fireEvent.error(img); + expect(onError.callCount).to.equal(1); + }); }); describe('image avatar with unrendered children', () => { @@ -64,12 +73,21 @@ describe('', () => { }); it('should be able to add more props to the image', () => { + // TODO: remove this test in v7 const onError = spy(); const { container } = render(); const img = container.querySelector('img'); fireEvent.error(img); expect(onError.callCount).to.equal(1); }); + + it('should be able to add more props to the img slot', () => { + const onError = spy(); + const { container } = render(); + const img = container.querySelector('img'); + fireEvent.error(img); + expect(onError.callCount).to.equal(1); + }); }); describe('font icon avatar', () => {