Skip to content

Commit 204845e

Browse files
committed
Merge branch 'root-steplabel' of https://github.com/sai6855/material-ui into root-steplabel
2 parents 48f1523 + 24be696 commit 204845e

File tree

5 files changed

+242
-245
lines changed

5 files changed

+242
-245
lines changed

docs/data/material/guides/composition/composition.md

Lines changed: 50 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -27,47 +27,56 @@ WrappedIcon.muiName = Icon.muiName;
2727
Use the `mergeSlotProps` utility function to merge custom props with the slot props.
2828
If the arguments are functions then they'll be resolved before merging, and the result from the first argument will override the second.
2929

30-
```jsx
31-
import Tooltip, { TooltipProps } from '@mui/material/Tooltip';
32-
import { mergeSlotProps } from '@mui/material/utils';
33-
34-
export const CustomTooltip = (props: TooltipProps) => {
35-
const { children, title, sx: sxProps } = props;
36-
37-
return (
38-
<Tooltip
39-
{...props}
40-
title={<Box sx={{ p: 4 }}>{title}</Box>}
41-
slotProps={{
42-
...props.slotProps,
43-
popper: mergeSlotProps(props.slotProps?.popper, {
44-
className: 'custom-tooltip-popper',
45-
disablePortal: true,
46-
placement: 'top',
47-
}),
48-
}}
49-
>
50-
{children}
51-
</Tooltip>
52-
);
53-
};
54-
```
55-
56-
:::info
57-
`className` values are concatenated rather than overriding one another.
58-
In the snippet above, the `custom-tooltip-popper` class is applied to the Tooltip's popper slot.
59-
If you added another `className` via the `slotProps` prop on the Custom Tooltip—as shown below—then both would be present on the rendered popper slot:
60-
61-
```js
62-
<CustomTooltip slotProps={{ popper: { className: 'foo' } }} />
63-
```
64-
65-
The popper slot in the original example would now have both classes applied to it, in addition to any others that may be present: `"[…] custom-tooltip-popper foo"`.
66-
:::
67-
68-
:::info
69-
`style` object are shallow merged rather than replacing one another. The style keys from the first argument have higher priority.
70-
:::
30+
Special properties that merged between the two arguments are listed below:
31+
32+
- `className`: values are concatenated rather than overriding one another.
33+
34+
In the snippet below, the `custom-tooltip-popper` class is applied to the Tooltip's popper slot.
35+
36+
```jsx
37+
import Tooltip, { TooltipProps } from '@mui/material/Tooltip';
38+
import { mergeSlotProps } from '@mui/material/utils';
39+
40+
export const CustomTooltip = (props: TooltipProps) => {
41+
const { children, title, sx: sxProps } = props;
42+
43+
return (
44+
<Tooltip
45+
{...props}
46+
title={<Box sx={{ p: 4 }}>{title}</Box>}
47+
slotProps={{
48+
...props.slotProps,
49+
popper: mergeSlotProps(props.slotProps?.popper, {
50+
className: 'custom-tooltip-popper',
51+
disablePortal: true,
52+
placement: 'top',
53+
}),
54+
}}
55+
>
56+
{children}
57+
</Tooltip>
58+
);
59+
};
60+
```
61+
62+
If you added another `className` via the `slotProps` prop on the Custom Tooltip—as shown below—then both would be present on the rendered popper slot:
63+
64+
```js
65+
<CustomTooltip slotProps={{ popper: { className: 'foo' } }} />
66+
```
67+
68+
The popper slot in the original example would now have both classes applied to it, in addition to any others that may be present: `"[…] custom-tooltip-popper foo"`.
69+
70+
- `style`: object are shallow merged rather than replacing one another. The style keys from the first argument have higher priority.
71+
- `sx`: values are concatenated into an array.
72+
- `^on[A-Z]` event handlers: these functions are composed between the two arguments.
73+
74+
```js
75+
mergeSlotProps(props.slotProps?.popper, {
76+
onClick: (event) => {}, // composed with the `slotProps?.popper?.onClick`
77+
createPopper: (popperOptions) => {}, // overridden by the `slotProps?.popper?.createPopper`
78+
});
79+
```
7180
7281
## Component prop
7382

packages/mui-material/src/utils/mergeSlotProps.spec.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as React from 'react';
22
import { expectType } from '@mui/types';
33
import Box from '@mui/material/Box';
4+
import Dialog, { DialogProps } from '@mui/material/Dialog';
45
import Tooltip, { TooltipProps } from '@mui/material/Tooltip';
56
import { mergeSlotProps, SlotComponentProps } from '@mui/material/utils';
67

@@ -62,3 +63,27 @@ export const CustomTooltip2 = (props: TooltipProps) => {
6263
</Tooltip>
6364
);
6465
};
66+
67+
type SimpleDialogProps = Omit<DialogProps, 'children' | 'onClose'> & {
68+
onClose: () => void;
69+
};
70+
function UserDetailsDialog(props: SimpleDialogProps) {
71+
const { onClose, slotProps: dialogSlotProps, ...dialogProps } = props;
72+
73+
return (
74+
<Dialog
75+
onClose={() => onClose()}
76+
slotProps={{
77+
...dialogSlotProps,
78+
transition: mergeSlotProps(dialogSlotProps?.transition, {
79+
onExited: (node) => {
80+
expectType<HTMLElement, typeof node>(node);
81+
},
82+
}),
83+
}}
84+
{...dialogProps}
85+
>
86+
content
87+
</Dialog>
88+
);
89+
}

packages/mui-material/src/utils/mergeSlotProps.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as React from 'react';
22
import { expect } from 'chai';
3+
import { spy } from 'sinon';
34
import { SxProps } from '@mui/material/styles';
45

56
import mergeSlotProps from './mergeSlotProps';
@@ -202,5 +203,46 @@ describe('utils/index.js', () => {
202203
'aria-label': 'bar',
203204
});
204205
});
206+
207+
it('automatically merge function based on the default slot props', () => {
208+
const slotPropsOnClick = spy();
209+
const defaultPropsOnClick = spy();
210+
211+
const defaultPropsOnChange = spy();
212+
213+
const slotPropsFoo = spy();
214+
const defaultPropsFoo = spy();
215+
216+
const mergedSlotProps = mergeSlotProps<{
217+
onClick: (arg1: string, arg2: string) => string;
218+
onChange?: (arg1: string, arg2: string) => string;
219+
foo: (arg1: string, arg2: string) => string;
220+
}>(
221+
{
222+
onClick: slotPropsOnClick,
223+
foo: slotPropsFoo,
224+
},
225+
{
226+
onClick: defaultPropsOnClick,
227+
onChange: defaultPropsOnChange,
228+
foo: defaultPropsFoo,
229+
},
230+
);
231+
232+
mergedSlotProps.onClick('arg1', 'arg2');
233+
expect(defaultPropsOnClick.callCount).to.equal(1);
234+
expect(defaultPropsOnClick.args[0]).to.deep.equal(['arg1', 'arg2']);
235+
expect(slotPropsOnClick.callCount).to.equal(1);
236+
expect(slotPropsOnClick.args[0]).to.deep.equal(['arg1', 'arg2']);
237+
238+
mergedSlotProps.onChange?.('arg1', 'arg2');
239+
expect(defaultPropsOnChange.callCount).to.equal(1);
240+
expect(defaultPropsOnChange.args[0]).to.deep.equal(['arg1', 'arg2']);
241+
242+
mergedSlotProps.foo('arg1', 'arg2');
243+
expect(defaultPropsFoo.callCount).to.equal(0);
244+
expect(slotPropsFoo.callCount).to.equal(1);
245+
expect(slotPropsFoo.args[0]).to.deep.equal(['arg1', 'arg2']);
246+
});
205247
});
206248
});

packages/mui-material/src/utils/mergeSlotProps.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
import { SlotComponentProps } from '@mui/utils';
22
import clsx from 'clsx';
33

4+
// Brought from [Base UI](https://github.com/mui/base-ui/blob/master/packages/react/src/merge-props/mergeProps.ts#L119)
5+
// Use it directly from Base UI once it's a package dependency.
6+
function isEventHandler(key: string, value: unknown) {
7+
// This approach is more efficient than using a regex.
8+
const thirdCharCode = key.charCodeAt(2);
9+
return (
10+
key[0] === 'o' &&
11+
key[1] === 'n' &&
12+
thirdCharCode >= 65 /* A */ &&
13+
thirdCharCode <= 90 /* Z */ &&
14+
typeof value === 'function'
15+
);
16+
}
17+
418
export default function mergeSlotProps<
519
T extends SlotComponentProps<React.ElementType, {}, {}>,
620
K = T,
@@ -10,6 +24,26 @@ export default function mergeSlotProps<
1024
if (!externalSlotProps) {
1125
return defaultSlotProps as unknown as U;
1226
}
27+
function extractHandlers(
28+
externalSlotPropsValue: Record<string, any>,
29+
defaultSlotPropsValue: Record<string, any>,
30+
) {
31+
const handlers: Record<string, Function> = {};
32+
33+
Object.keys(defaultSlotPropsValue).forEach((key) => {
34+
if (
35+
isEventHandler(key, defaultSlotPropsValue[key]) &&
36+
typeof externalSlotPropsValue[key] === 'function'
37+
) {
38+
// only compose the handlers if both default and external slot props match the event handler
39+
handlers[key] = (...args: unknown[]) => {
40+
externalSlotPropsValue[key](...args);
41+
defaultSlotPropsValue[key](...args);
42+
};
43+
}
44+
});
45+
return handlers;
46+
}
1347
if (typeof externalSlotProps === 'function' || typeof defaultSlotProps === 'function') {
1448
return ((ownerState: Record<string, any>) => {
1549
const defaultSlotPropsValue =
@@ -24,9 +58,12 @@ export default function mergeSlotProps<
2458
defaultSlotPropsValue?.className,
2559
externalSlotPropsValue?.className,
2660
);
61+
const handlers = extractHandlers(externalSlotPropsValue, defaultSlotPropsValue);
62+
2763
return {
2864
...defaultSlotPropsValue,
2965
...externalSlotPropsValue,
66+
...handlers,
3067
...(!!className && { className }),
3168
...(defaultSlotPropsValue?.style &&
3269
externalSlotPropsValue?.style && {
@@ -47,10 +84,12 @@ export default function mergeSlotProps<
4784
}) as U;
4885
}
4986
const typedDefaultSlotProps = defaultSlotProps as Record<string, any>;
87+
const handlers = extractHandlers(externalSlotProps, typedDefaultSlotProps);
5088
const className = clsx(typedDefaultSlotProps?.className, externalSlotProps?.className);
5189
return {
5290
...defaultSlotProps,
5391
...externalSlotProps,
92+
...handlers,
5493
...(!!className && { className }),
5594
...(typedDefaultSlotProps?.style &&
5695
externalSlotProps?.style && {

0 commit comments

Comments
 (0)