Skip to content

Commit d1e9e50

Browse files
authored
[Avatar] Fix img slot types and add missing slots (#45483)
1 parent 3cf5a5d commit d1e9e50

File tree

7 files changed

+181
-38
lines changed

7 files changed

+181
-38
lines changed

docs/data/material/customization/overriding-component-structure/overriding-component-structure.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,34 @@ If both `slotProps.root` and additional props have the same keys but different v
9191
This does not apply to classes or the `style` prop—they will be merged instead.
9292
:::
9393

94+
### Type safety
95+
96+
The `slotProps` prop is not dynamically typed based on the custom `slots` prop, so if the custom slot has a different type than the default slot, you have to cast the type to avoid TypeScript errors and use `satisfies` (available in TypeScript 4.9) to ensure type safety for the custom slot.
97+
98+
The example below shows how to customize the `img` slot of the [Avatar](/material-ui/react-avatar/) component using [Next.js Image](https://nextjs.org/docs/app/api-reference/components/image) component:
99+
100+
```tsx
101+
import Image, { ImageProps } from 'next/image';
102+
import Avatar, { AvatarProps } from '@mui/material/Avatar';
103+
104+
<Avatar
105+
slots={{
106+
img: Image,
107+
}}
108+
slotProps={
109+
{
110+
img: {
111+
src: 'https://example.com/image.jpg',
112+
alt: 'Image',
113+
width: 40,
114+
height: 40,
115+
blurDataURL: 'data:image/png;base64',
116+
} satisfies ImageProps,
117+
} as AvatarProps['slotProps']
118+
}
119+
/>;
120+
```
121+
94122
## Best practices
95123

96124
Use the `component` or `slotProps.{slot}.component` prop when you need to override the element while preserving the styles of the slot.

docs/pages/material-ui/api/avatar.json

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,17 @@
1111
},
1212
"sizes": { "type": { "name": "string" } },
1313
"slotProps": {
14-
"type": { "name": "shape", "description": "{ img?: func<br>&#124;&nbsp;object }" },
14+
"type": {
15+
"name": "shape",
16+
"description": "{ fallback?: func<br>&#124;&nbsp;object, img?: func<br>&#124;&nbsp;object, root?: func<br>&#124;&nbsp;object }"
17+
},
1518
"default": "{}"
1619
},
1720
"slots": {
18-
"type": { "name": "shape", "description": "{ img?: elementType }" },
21+
"type": {
22+
"name": "shape",
23+
"description": "{ fallback?: elementType, img?: elementType, root?: elementType }"
24+
},
1925
"default": "{}"
2026
},
2127
"src": { "type": { "name": "string" } },
@@ -41,11 +47,23 @@
4147
"import { Avatar } from '@mui/material';"
4248
],
4349
"slots": [
50+
{
51+
"name": "root",
52+
"description": "The component that renders the root slot.",
53+
"default": "'div'",
54+
"class": "MuiAvatar-root"
55+
},
4456
{
4557
"name": "img",
46-
"description": "The component that renders the transition.\n[Follow this guide](https://mui.com/material-ui/transitions/#transitioncomponent-prop) to learn more about the requirements for this component.",
47-
"default": "Collapse",
58+
"description": "The component that renders the img slot.",
59+
"default": "'img'",
4860
"class": "MuiAvatar-img"
61+
},
62+
{
63+
"name": "fallback",
64+
"description": "The component that renders the fallback slot.",
65+
"default": "Person icon",
66+
"class": "MuiAvatar-fallback"
4967
}
5068
],
5169
"classes": [
@@ -61,18 +79,6 @@
6179
"description": "Styles applied to the root element if not `src` or `srcSet`.",
6280
"isGlobal": false
6381
},
64-
{
65-
"key": "fallback",
66-
"className": "MuiAvatar-fallback",
67-
"description": "Styles applied to the fallback icon",
68-
"isGlobal": false
69-
},
70-
{
71-
"key": "root",
72-
"className": "MuiAvatar-root",
73-
"description": "Styles applied to the root element.",
74-
"isGlobal": false
75-
},
7682
{
7783
"key": "rounded",
7884
"className": "MuiAvatar-rounded",

docs/translations/api-docs/avatar/avatar.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@
3939
"nodeName": "the root element",
4040
"conditions": "not <code>src</code> or <code>srcSet</code>"
4141
},
42-
"fallback": { "description": "Styles applied to the fallback icon" },
43-
"root": { "description": "Styles applied to the root element." },
4442
"rounded": {
4543
"description": "Styles applied to {{nodeName}} if {{conditions}}.",
4644
"nodeName": "the root element",
@@ -53,6 +51,8 @@
5351
}
5452
},
5553
"slotDescriptions": {
56-
"img": "The component that renders the transition. <a href=\"https://mui.com/material-ui/transitions/#transitioncomponent-prop\">Follow this guide</a> to learn more about the requirements for this component."
54+
"fallback": "The component that renders the fallback slot.",
55+
"img": "The component that renders the img slot.",
56+
"root": "The component that renders the root slot."
5757
}
5858
}

packages/mui-material/src/Avatar/Avatar.d.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,52 @@ import { Theme } from '../styles';
55
import { OverridableComponent, OverrideProps } from '../OverridableComponent';
66
import { AvatarClasses } from './avatarClasses';
77
import { CreateSlotsAndSlotProps, SlotProps } from '../utils/types';
8+
import { SvgIconProps } from '../SvgIcon';
89

910
export interface AvatarSlots {
1011
/**
11-
* The component that renders the transition.
12-
* [Follow this guide](https://mui.com/material-ui/transitions/#transitioncomponent-prop) to learn more about the requirements for this component.
13-
* @default Collapse
12+
* The component that renders the root slot.
13+
* @default 'div'
1414
*/
15-
img: React.JSXElementConstructor<React.ImgHTMLAttributes<HTMLImageElement>>;
15+
root: React.ElementType;
16+
/**
17+
* The component that renders the img slot.
18+
* @default 'img'
19+
*/
20+
img: React.ElementType;
21+
/**
22+
* The component that renders the fallback slot.
23+
* @default Person icon
24+
*/
25+
fallback: React.ElementType;
1626
}
1727

1828
export interface AvatarPropsVariantOverrides {}
1929

30+
export interface AvatarRootSlotPropsOverrides {}
31+
export interface AvatarImgSlotPropsOverrides {}
32+
export interface AvatarFallbackSlotPropsOverrides {}
33+
2034
export type AvatarSlotsAndSlotProps = CreateSlotsAndSlotProps<
2135
AvatarSlots,
2236
{
23-
img: SlotProps<
24-
React.ElementType<React.ImgHTMLAttributes<HTMLImageElement>>,
25-
{},
37+
/**
38+
* Props forwarded to the root slot.
39+
* By default, the avaible props are based on the div element.
40+
*/
41+
root: SlotProps<'div', AvatarRootSlotPropsOverrides, AvatarOwnProps>;
42+
/**
43+
* Props forwarded to the img slot.
44+
* By default, the avaible props are based on the img element.
45+
*/
46+
img: SlotProps<'img', AvatarImgSlotPropsOverrides, AvatarOwnProps>;
47+
/**
48+
* Props forwarded to the fallback slot.
49+
* By default, the avaible props are based on the [SvgIcon](https://mui.com/material-ui/api/svg-icon/#props) component.
50+
*/
51+
fallback: SlotProps<
52+
React.ElementType<SvgIconProps>,
53+
AvatarFallbackSlotPropsOverrides,
2654
AvatarOwnProps
2755
>;
2856
}

packages/mui-material/src/Avatar/Avatar.js

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,19 @@ const Avatar = React.forwardRef(function Avatar(inProps, ref) {
185185

186186
const classes = useUtilityClasses(ownerState);
187187

188+
const [RootSlot, rootSlotProps] = useSlot('root', {
189+
ref,
190+
className: clsx(classes.root, className),
191+
elementType: AvatarRoot,
192+
externalForwardedProps: {
193+
slots,
194+
slotProps,
195+
component,
196+
...other,
197+
},
198+
ownerState,
199+
});
200+
188201
const [ImgSlot, imgSlotProps] = useSlot('img', {
189202
className: classes.img,
190203
elementType: AvatarImg,
@@ -196,6 +209,17 @@ const Avatar = React.forwardRef(function Avatar(inProps, ref) {
196209
ownerState,
197210
});
198211

212+
const [FallbackSlot, fallbackSlotProps] = useSlot('fallback', {
213+
className: classes.fallback,
214+
elementType: AvatarFallback,
215+
externalForwardedProps: {
216+
slots,
217+
slotProps,
218+
},
219+
shouldForwardComponentProp: true,
220+
ownerState,
221+
});
222+
199223
if (hasImgNotFailing) {
200224
children = <ImgSlot {...imgSlotProps} />;
201225
// We only render valid children, non valid children are rendered with a fallback
@@ -205,20 +229,10 @@ const Avatar = React.forwardRef(function Avatar(inProps, ref) {
205229
} else if (hasImg && alt) {
206230
children = alt[0];
207231
} else {
208-
children = <AvatarFallback ownerState={ownerState} className={classes.fallback} />;
232+
children = <FallbackSlot {...fallbackSlotProps} />;
209233
}
210234

211-
return (
212-
<AvatarRoot
213-
as={component}
214-
className={clsx(classes.root, className)}
215-
ref={ref}
216-
{...other}
217-
ownerState={ownerState}
218-
>
219-
{children}
220-
</AvatarRoot>
221-
);
235+
return <RootSlot {...rootSlotProps}>{children}</RootSlot>;
222236
});
223237

224238
Avatar.propTypes /* remove-proptypes */ = {
@@ -264,14 +278,18 @@ Avatar.propTypes /* remove-proptypes */ = {
264278
* @default {}
265279
*/
266280
slotProps: PropTypes.shape({
281+
fallback: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
267282
img: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
283+
root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
268284
}),
269285
/**
270286
* The components used for each slot inside.
271287
* @default {}
272288
*/
273289
slots: PropTypes.shape({
290+
fallback: PropTypes.elementType,
274291
img: PropTypes.elementType,
292+
root: PropTypes.elementType,
275293
}),
276294
/**
277295
* The `src` attribute for the `img` element.

packages/mui-material/src/Avatar/Avatar.spec.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,58 @@ function CustomImg() {
1010
}
1111
<Avatar slotProps={{ img: { alt: '' } }} />;
1212
<Avatar slots={{ img: CustomImg }} />;
13+
14+
// Next.js Image component
15+
interface StaticImageData {
16+
src: string;
17+
height: number;
18+
width: number;
19+
blurDataURL?: string;
20+
blurWidth?: number;
21+
blurHeight?: number;
22+
}
23+
interface StaticRequire {
24+
default: StaticImageData;
25+
}
26+
type StaticImport = StaticRequire | StaticImageData;
27+
28+
type ImageLoaderProps = {
29+
src: string;
30+
width: number;
31+
quality?: number;
32+
};
33+
34+
type ImageLoader = (p: ImageLoaderProps) => string;
35+
36+
type PlaceholderValue = 'blur' | 'empty' | `data:image/${string}`;
37+
38+
type OnLoadingComplete = (img: HTMLImageElement) => void;
39+
40+
declare const Image: React.ForwardRefExoticComponent<
41+
Omit<
42+
React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>,
43+
'height' | 'width' | 'loading' | 'ref' | 'alt' | 'src' | 'srcSet'
44+
> & {
45+
src: string | StaticImport;
46+
alt: string;
47+
width?: number | `${number}`;
48+
height?: number | `${number}`;
49+
fill?: boolean;
50+
loader?: ImageLoader;
51+
quality?: number | `${number}`;
52+
priority?: boolean;
53+
loading?: 'eager' | 'lazy' | undefined;
54+
placeholder?: PlaceholderValue;
55+
blurDataURL?: string;
56+
unoptimized?: boolean;
57+
overrideSrc?: string;
58+
onLoadingComplete?: OnLoadingComplete;
59+
layout?: string;
60+
objectFit?: string;
61+
objectPosition?: string;
62+
lazyBoundary?: string;
63+
lazyRoot?: string;
64+
} & React.RefAttributes<HTMLImageElement | null>
65+
>;
66+
67+
<Avatar slots={{ img: Image }} />;

packages/mui-material/src/Avatar/Avatar.test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ describe('<Avatar />', () => {
2020
testDeepOverrides: { slotName: 'fallback', slotClassName: classes.fallback },
2121
testVariantProps: { variant: 'foo' },
2222
testStateOverrides: { prop: 'variant', value: 'rounded', styleKey: 'rounded' },
23+
slots: {
24+
root: {
25+
expectedClassName: classes.root,
26+
},
27+
fallback: {
28+
expectedClassName: classes.fallback,
29+
},
30+
},
2331
skip: ['componentsProp'],
2432
}));
2533

0 commit comments

Comments
 (0)