From 60106b31d0b4e5e304fdb4d612cc62c1c21a20f6 Mon Sep 17 00:00:00 2001 From: Siriwat K Date: Fri, 10 Jan 2025 14:32:42 +0700 Subject: [PATCH] [material-ui][Alert] complete `slots` and `slotProps` (#44971) --- docs/pages/material-ui/api/alert.json | 52 ++++++------- docs/translations/api-docs/alert/alert.json | 20 ++--- .../test-utils/src/describeConformance.tsx | 12 ++- packages/mui-material/src/Alert/Alert.d.ts | 69 +++++++++++++++++ packages/mui-material/src/Alert/Alert.js | 74 +++++++++++++------ .../mui-material/src/Alert/Alert.spec.tsx | 25 +++++++ packages/mui-material/src/Alert/Alert.test.js | 14 +++- packages/mui-material/src/utils/useSlot.ts | 20 ++++- 8 files changed, 217 insertions(+), 69 deletions(-) diff --git a/docs/pages/material-ui/api/alert.json b/docs/pages/material-ui/api/alert.json index f1c0e452bb0968..0a5490bb494611 100644 --- a/docs/pages/material-ui/api/alert.json +++ b/docs/pages/material-ui/api/alert.json @@ -50,14 +50,14 @@ "slotProps": { "type": { "name": "shape", - "description": "{ closeButton?: func
| object, closeIcon?: func
| object }" + "description": "{ action?: func
| object, closeButton?: func
| object, closeIcon?: func
| object, icon?: func
| object, message?: func
| object, root?: func
| object }" }, "default": "{}" }, "slots": { "type": { "name": "shape", - "description": "{ closeButton?: elementType, closeIcon?: elementType }" + "description": "{ action?: elementType, closeButton?: elementType, closeIcon?: elementType, icon?: elementType, message?: elementType, root?: elementType }" }, "default": "{}" }, @@ -79,6 +79,30 @@ "name": "Alert", "imports": ["import Alert from '@mui/material/Alert';", "import { Alert } from '@mui/material';"], "slots": [ + { + "name": "root", + "description": "The component that renders the root slot.", + "default": "Paper", + "class": "MuiAlert-root" + }, + { + "name": "icon", + "description": "The component that renders the icon slot.", + "default": "div", + "class": "MuiAlert-icon" + }, + { + "name": "message", + "description": "The component that renders the message slot.", + "default": "div", + "class": "MuiAlert-message" + }, + { + "name": "action", + "description": "The component that renders the action slot.", + "default": "div", + "class": "MuiAlert-action" + }, { "name": "closeButton", "description": "The component that renders the close button.", @@ -93,12 +117,6 @@ } ], "classes": [ - { - "key": "action", - "className": "MuiAlert-action", - "description": "Styles applied to the action wrapper element if `action` is provided.", - "isGlobal": false - }, { "key": "colorError", "className": "MuiAlert-colorError", @@ -157,18 +175,6 @@ "isGlobal": false, "isDeprecated": true }, - { - "key": "icon", - "className": "MuiAlert-icon", - "description": "Styles applied to the icon wrapper element.", - "isGlobal": false - }, - { - "key": "message", - "className": "MuiAlert-message", - "description": "Styles applied to the message wrapper element.", - "isGlobal": false - }, { "key": "outlined", "className": "MuiAlert-outlined", @@ -203,12 +209,6 @@ "isGlobal": false, "isDeprecated": true }, - { - "key": "root", - "className": "MuiAlert-root", - "description": "Styles applied to the root element.", - "isGlobal": false - }, { "key": "standard", "className": "MuiAlert-standard", diff --git a/docs/translations/api-docs/alert/alert.json b/docs/translations/api-docs/alert/alert.json index 6bf0c4bce51033..5577b02873c7ff 100644 --- a/docs/translations/api-docs/alert/alert.json +++ b/docs/translations/api-docs/alert/alert.json @@ -38,11 +38,6 @@ "variant": { "description": "The variant to use." } }, "classDescriptions": { - "action": { - "description": "Styles applied to {{nodeName}} if {{conditions}}.", - "nodeName": "the action wrapper element", - "conditions": "action is provided" - }, "colorError": { "description": "Styles applied to {{nodeName}} if {{conditions}}.", "nodeName": "the root element", @@ -90,14 +85,6 @@ "description": "Styles applied to the root element if variant="filled" and color="warning"", "deprecationInfo": "Combine the .MuiAlert-filled and .MuiAlert-colorWarning classes instead. See Migrating from deprecated APIs for more details." }, - "icon": { - "description": "Styles applied to {{nodeName}}.", - "nodeName": "the icon wrapper element" - }, - "message": { - "description": "Styles applied to {{nodeName}}.", - "nodeName": "the message wrapper element" - }, "outlined": { "description": "Styles applied to {{nodeName}} if {{conditions}}.", "nodeName": "the root element", @@ -127,7 +114,6 @@ "conditions": "variant=\"outlined\" and color=\"warning\"", "deprecationInfo": "Combine the .MuiAlert-outlined and .MuiAlert-colorWarning classes instead. See Migrating from deprecated APIs for more details." }, - "root": { "description": "Styles applied to the root element." }, "standard": { "description": "Styles applied to {{nodeName}} if {{conditions}}.", "nodeName": "the root element", @@ -159,7 +145,11 @@ } }, "slotDescriptions": { + "action": "The component that renders the action slot.", "closeButton": "The component that renders the close button.", - "closeIcon": "The component that renders the close icon." + "closeIcon": "The component that renders the close icon.", + "icon": "The component that renders the icon slot.", + "message": "The component that renders the message slot.", + "root": "The component that renders the root slot." } } diff --git a/packages-internal/test-utils/src/describeConformance.tsx b/packages-internal/test-utils/src/describeConformance.tsx index 2cd9d11010c56b..943c71e214fecd 100644 --- a/packages-internal/test-utils/src/describeConformance.tsx +++ b/packages-internal/test-utils/src/describeConformance.tsx @@ -64,7 +64,7 @@ export interface ConformanceOptions { testStateOverrides?: { prop?: string; value?: any; styleKey: string }; testCustomVariant?: boolean; testVariantProps?: object; - testLegacyComponentsProp?: boolean; + testLegacyComponentsProp?: boolean | string[]; slots?: Record; ThemeProvider?: React.ElementType; /** @@ -387,7 +387,10 @@ function testSlotsProp( } // For testing Material UI components v5, and v6. Likely to be removed in v7. - if (testLegacyComponentsProp) { + if ( + testLegacyComponentsProp === true || + (Array.isArray(testLegacyComponentsProp) && testLegacyComponentsProp.includes(slotName)) + ) { it(`allows overriding the ${slotName} slot with a component using the components.${capitalize( slotName, )} prop`, async () => { @@ -541,7 +544,10 @@ function testSlotPropsProp( }); } - if (testLegacyComponentsProp) { + if ( + testLegacyComponentsProp === true || + (Array.isArray(testLegacyComponentsProp) && testLegacyComponentsProp.includes(slotName)) + ) { it(`sets custom properties on the ${slotName} slot's element with the componentsProps.${slotName} prop`, async () => { const componentsProps = { [slotName]: { diff --git a/packages/mui-material/src/Alert/Alert.d.ts b/packages/mui-material/src/Alert/Alert.d.ts index 2bbaf416f28055..24b8f2e53655af 100644 --- a/packages/mui-material/src/Alert/Alert.d.ts +++ b/packages/mui-material/src/Alert/Alert.d.ts @@ -10,10 +10,39 @@ export type AlertColor = 'success' | 'info' | 'warning' | 'error'; export interface AlertPropsVariantOverrides {} export interface AlertPropsColorOverrides {} + +export interface AlertRootSlotPropsOverrides {} + +export interface AlertIconSlotPropsOverrides {} + +export interface AlertMessageSlotPropsOverrides {} + +export interface AlertActionSlotPropsOverrides {} + export interface AlertCloseButtonSlotPropsOverrides {} export interface AlertCloseIconSlotPropsOverrides {} export interface AlertSlots { + /** + * The component that renders the root slot. + * @default Paper + */ + root: React.ElementType; + /** + * The component that renders the icon slot. + * @default div + */ + icon: React.ElementType; + /** + * The component that renders the message slot. + * @default div + */ + message: React.ElementType; + /** + * The component that renders the action slot. + * @default div + */ + action: React.ElementType; /** * The component that renders the close button. * @default IconButton @@ -29,11 +58,51 @@ export interface AlertSlots { export type AlertSlotsAndSlotProps = CreateSlotsAndSlotProps< AlertSlots, { + /** + * Props forwarded to the root slot. + * By default, the avaible props are based on the [Paper](https://mui.com/material-ui/api/paper/#props) component. + */ + root: SlotProps, AlertRootSlotPropsOverrides, AlertOwnerState>; + /** + * Props forwarded to the icon slot. + * By default, the avaible props are based on a div element. + */ + icon: SlotProps< + React.ElementType>, + AlertIconSlotPropsOverrides, + AlertOwnerState + >; + /** + * Props forwarded to the message slot. + * By default, the avaible props are based on a div element. + */ + message: SlotProps< + React.ElementType>, + AlertMessageSlotPropsOverrides, + AlertOwnerState + >; + /** + * Props forwarded to the action slot. + * By default, the avaible props are based on a div element. + */ + action: SlotProps< + React.ElementType>, + AlertActionSlotPropsOverrides, + AlertOwnerState + >; + /** + * Props forwarded to the closeButton slot. + * By default, the avaible props are based on the [IconButton](https://mui.com/material-ui/api/icon-button/#props) component. + */ closeButton: SlotProps< React.ElementType, AlertCloseButtonSlotPropsOverrides, AlertOwnerState >; + /** + * Props forwarded to the closeIcon slot. + * By default, the avaible props are based on the [SvgIcon](https://mui.com/material-ui/api/svg-icon/#props) component. + */ closeIcon: SlotProps< React.ElementType, AlertCloseIconSlotPropsOverrides, diff --git a/packages/mui-material/src/Alert/Alert.js b/packages/mui-material/src/Alert/Alert.js index 33c3f68e7b8f4c..6ff5ba766a9a35 100644 --- a/packages/mui-material/src/Alert/Alert.js +++ b/packages/mui-material/src/Alert/Alert.js @@ -202,6 +202,43 @@ const Alert = React.forwardRef(function Alert(inProps, ref) { }, }; + const [RootSlot, rootSlotProps] = useSlot('root', { + ref, + shouldForwardComponentProp: true, + className: clsx(classes.root, className), + elementType: AlertRoot, + externalForwardedProps: { + ...externalForwardedProps, + ...other, + }, + ownerState, + additionalProps: { + role, + elevation: 0, + }, + }); + + const [IconSlot, iconSlotProps] = useSlot('icon', { + className: classes.icon, + elementType: AlertIcon, + externalForwardedProps, + ownerState, + }); + + const [MessageSlot, messageSlotProps] = useSlot('message', { + className: classes.message, + elementType: AlertMessage, + externalForwardedProps, + ownerState, + }); + + const [ActionSlot, actionSlotProps] = useSlot('action', { + className: classes.action, + elementType: AlertAction, + externalForwardedProps, + ownerState, + }); + const [CloseButtonSlot, closeButtonProps] = useSlot('closeButton', { elementType: IconButton, externalForwardedProps, @@ -215,29 +252,16 @@ const Alert = React.forwardRef(function Alert(inProps, ref) { }); return ( - + {icon !== false ? ( - + {icon || iconMapping[severity] || defaultIconMapping[severity]} - - ) : null} - - {children} - - {action != null ? ( - - {action} - + ) : null} + {children} + {action != null ? {action} : null} {action == null && onClose ? ( - + - + ) : null} - + ); }); @@ -356,16 +380,24 @@ Alert.propTypes /* remove-proptypes */ = { * @default {} */ slotProps: PropTypes.shape({ + action: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), closeButton: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), closeIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + icon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + message: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), }), /** * The components used for each slot inside. * @default {} */ slots: PropTypes.shape({ + action: PropTypes.elementType, closeButton: PropTypes.elementType, closeIcon: PropTypes.elementType, + icon: PropTypes.elementType, + message: PropTypes.elementType, + root: PropTypes.elementType, }), /** * The system prop that allows defining system overrides as well as additional CSS styles. diff --git a/packages/mui-material/src/Alert/Alert.spec.tsx b/packages/mui-material/src/Alert/Alert.spec.tsx index 349694b19f9ba4..e57e4f80d0e794 100644 --- a/packages/mui-material/src/Alert/Alert.spec.tsx +++ b/packages/mui-material/src/Alert/Alert.spec.tsx @@ -1,5 +1,7 @@ +import * as React from 'react'; import CloseRounded from '@mui/icons-material/CloseRounded'; import { createTheme } from '@mui/material'; +import Alert from '@mui/material/Alert'; createTheme({ components: { @@ -12,3 +14,26 @@ createTheme({ }, }, }); + +; diff --git a/packages/mui-material/src/Alert/Alert.test.js b/packages/mui-material/src/Alert/Alert.test.js index e5d4ff4b54f2e7..0a6b0b04dd9fc9 100644 --- a/packages/mui-material/src/Alert/Alert.test.js +++ b/packages/mui-material/src/Alert/Alert.test.js @@ -20,8 +20,20 @@ describe('', () => { muiName: 'MuiAlert', testVariantProps: { variant: 'standard', color: 'success' }, testDeepOverrides: { slotName: 'message', slotClassName: classes.message }, - testLegacyComponentsProp: true, + testLegacyComponentsProp: ['closeButton', 'closeIcon'], slots: { + root: { + expectedClassName: classes.root, + }, + icon: { + expectedClassName: classes.icon, + }, + message: { + expectedClassName: classes.message, + }, + action: { + expectedClassName: classes.action, + }, closeButton: { expectedClassName: classes.closeButton, }, diff --git a/packages/mui-material/src/utils/useSlot.ts b/packages/mui-material/src/utils/useSlot.ts index c3ac14e91956d5..90f9ffc4bdd180 100644 --- a/packages/mui-material/src/utils/useSlot.ts +++ b/packages/mui-material/src/utils/useSlot.ts @@ -82,6 +82,14 @@ export default function useSlot< * e.g. Autocomplete's listbox uses Popper + StyledComponent */ internalForwardedProps?: any; + /** + * Set to true if the `elementType` is a styled component of another Material UI component. + * + * For example, the AlertRoot is a styled component of the Paper component. + * This flag is used to forward the `component` and `slotProps.root.component` to the Paper component. + * Otherwise, the `component` prop will be converted to `as` prop which replaces the Paper component (the paper styles are gone). + */ + shouldForwardComponentProp?: boolean; }, ) { const { @@ -90,6 +98,7 @@ export default function useSlot< ownerState, externalForwardedProps, internalForwardedProps, + shouldForwardComponentProp = false, ...useSlotPropsParams } = parameters; const { @@ -127,9 +136,14 @@ export default function useSlot< ...(name === 'root' && !rootComponent && !slots[name] && internalForwardedProps), ...(name !== 'root' && !slots[name] && internalForwardedProps), ...mergedProps, - ...(LeafComponent && { - as: LeafComponent, - }), + ...(LeafComponent && + !shouldForwardComponentProp && { + as: LeafComponent, + }), + ...(LeafComponent && + shouldForwardComponentProp && { + component: LeafComponent, + }), ref, }, ownerState,