diff --git a/docs/src/pages/customization/theme-components/GlobalThemeOverrideCallback.js b/docs/src/pages/customization/theme-components/GlobalThemeOverrideCallback.js new file mode 100644 index 00000000000000..09799839316294 --- /dev/null +++ b/docs/src/pages/customization/theme-components/GlobalThemeOverrideCallback.js @@ -0,0 +1,54 @@ +import * as React from 'react'; +import { ThemeProvider, createTheme } from '@mui/material/styles'; +import Box from '@mui/material/Box'; +import Slider, { sliderClasses } from '@mui/material/Slider'; + +const finalTheme = createTheme({ + components: { + MuiSlider: { + styleOverrides: { + valueLabel: ({ ownerState, theme }) => ({ + ...(ownerState.orientation === 'vertical' && { + backgroundColor: 'transparent', + color: theme.palette.grey[500], + fontWeight: 700, + padding: 0, + left: '2rem', + }), + [`&.${sliderClasses.valueLabelOpen}`]: { + transform: 'none', + top: 'initial', + }, + }), + }, + }, + }, +}); + +function valuetext(value) { + return `${value}°C`; +} + +export default function GlobalThemeOverride() { + return ( + + + 'Temperature'} + orientation="vertical" + getAriaValueText={valuetext} + defaultValue={[25, 50]} + marks={[ + { value: 0 }, + { value: 25 }, + { value: 50 }, + { value: 75 }, + { value: 100 }, + ]} + valueLabelFormat={valuetext} + valueLabelDisplay="on" + /> + + + ); +} diff --git a/docs/src/pages/customization/theme-components/GlobalThemeOverrideCallback.tsx b/docs/src/pages/customization/theme-components/GlobalThemeOverrideCallback.tsx new file mode 100644 index 00000000000000..96fbf48247ae66 --- /dev/null +++ b/docs/src/pages/customization/theme-components/GlobalThemeOverrideCallback.tsx @@ -0,0 +1,54 @@ +import * as React from 'react'; +import { ThemeProvider, createTheme } from '@mui/material/styles'; +import Box from '@mui/material/Box'; +import Slider, { sliderClasses } from '@mui/material/Slider'; + +const finalTheme = createTheme({ + components: { + MuiSlider: { + styleOverrides: { + valueLabel: ({ ownerState, theme }) => ({ + ...(ownerState.orientation === 'vertical' && { + backgroundColor: 'transparent', + color: theme.palette.grey[500], + fontWeight: 700, + padding: 0, + left: '2rem', + }), + [`&.${sliderClasses.valueLabelOpen}`]: { + transform: 'none', + top: 'initial', + }, + }), + }, + }, + }, +}); + +function valuetext(value: number) { + return `${value}°C`; +} + +export default function GlobalThemeOverride() { + return ( + + + 'Temperature'} + orientation="vertical" + getAriaValueText={valuetext} + defaultValue={[25, 50]} + marks={[ + { value: 0 }, + { value: 25 }, + { value: 50 }, + { value: 75 }, + { value: 100 }, + ]} + valueLabelFormat={valuetext} + valueLabelDisplay="on" + /> + + + ); +} diff --git a/docs/src/pages/customization/theme-components/GlobalThemeOverrideSx.js b/docs/src/pages/customization/theme-components/GlobalThemeOverrideSx.js new file mode 100644 index 00000000000000..557f682bc89694 --- /dev/null +++ b/docs/src/pages/customization/theme-components/GlobalThemeOverrideSx.js @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { + ThemeProvider, + createTheme, + experimental_sx as sx, +} from '@mui/material/styles'; +import Chip from '@mui/material/Chip'; +import Check from '@mui/icons-material/Check'; + +const finalTheme = createTheme({ + components: { + MuiChip: { + styleOverrides: { + root: sx({ + // https://mui.com/system/the-sx-prop/#spacing + px: 1, + py: 0.25, + // https://mui.com/system/borders/#border-radius + borderRadius: 1, // 4px as default. + }), + label: { + padding: 'initial', + }, + icon: sx({ + mr: 0.5, + ml: '-2px', + }), + }, + }, + }, +}); + +export default function GlobalThemeOverride() { + return ( + + + Status: Completed + + } + icon={} + /> + + ); +} diff --git a/docs/src/pages/customization/theme-components/GlobalThemeOverrideSx.tsx b/docs/src/pages/customization/theme-components/GlobalThemeOverrideSx.tsx new file mode 100644 index 00000000000000..557f682bc89694 --- /dev/null +++ b/docs/src/pages/customization/theme-components/GlobalThemeOverrideSx.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { + ThemeProvider, + createTheme, + experimental_sx as sx, +} from '@mui/material/styles'; +import Chip from '@mui/material/Chip'; +import Check from '@mui/icons-material/Check'; + +const finalTheme = createTheme({ + components: { + MuiChip: { + styleOverrides: { + root: sx({ + // https://mui.com/system/the-sx-prop/#spacing + px: 1, + py: 0.25, + // https://mui.com/system/borders/#border-radius + borderRadius: 1, // 4px as default. + }), + label: { + padding: 'initial', + }, + icon: sx({ + mr: 0.5, + ml: '-2px', + }), + }, + }, + }, +}); + +export default function GlobalThemeOverride() { + return ( + + + Status: Completed + + } + icon={} + /> + + ); +} diff --git a/docs/src/pages/customization/theme-components/GlobalThemeOverrideSx.tsx.preview b/docs/src/pages/customization/theme-components/GlobalThemeOverrideSx.tsx.preview new file mode 100644 index 00000000000000..91e91af0434675 --- /dev/null +++ b/docs/src/pages/customization/theme-components/GlobalThemeOverrideSx.tsx.preview @@ -0,0 +1,11 @@ + + + Status: Completed + + } + icon={} + /> + \ No newline at end of file diff --git a/docs/src/pages/customization/theme-components/theme-components.md b/docs/src/pages/customization/theme-components/theme-components.md index 49000bd0c0e08b..2fc8e9d65891b1 100644 --- a/docs/src/pages/customization/theme-components/theme-components.md +++ b/docs/src/pages/customization/theme-components/theme-components.md @@ -2,6 +2,29 @@

The theme's components key allows you to customize a component without wrapping it in another component. You can change the styles, the default props, and more.

+## Default props + +You can change the default of every prop of a MUI component. +A `defaultProps` key is exposed in the theme's `components` key for this use case. + +```js +const theme = createTheme({ + components: { + // Name of the component + MuiButtonBase: { + defaultProps: { + // The props to change the default for. + disableRipple: true, // No more ripple! + }, + }, + }, +}); +``` + +{{"demo": "pages/customization/theme-components/DefaultProps.js"}} + +To override lab component styles with TypeScript, check [this page](/components/about-the-lab/#typescript). + ## Global style overrides You can use the theme's `styleOverrides` key to potentially change every single style injected by MUI into the DOM. @@ -29,31 +52,45 @@ The list of each component's classes is documented under the **CSS** section of To override a lab component's styles with TypeScript, check [this section of the documentation](/components/about-the-lab/#typescript). -## Default props +### Overrides based on props -You can change the default of every prop of a MUI component. -A `defaultProps` key is exposed in the theme's `components` key for this use case. +You can pass a callback as a value in each slot of the component's `styleOverrides` to apply styles based on props. + +The `ownerState` prop is a combination of public props that you pass to the component + internal state of the component. ```js -const theme = createTheme({ +const finalTheme = createTheme({ components: { - // Name of the component - MuiButtonBase: { - defaultProps: { - // The props to change the default for. - disableRipple: true, // No more ripple! + MuiSlider: { + styleOverrides: { + valueLabel: ({ ownerState, theme }) => ({ + ...(ownerState.orientation === 'vertical' && { + backgroundColor: 'transparent', + color: theme.palette.grey[500], + }), + }), }, }, }, }); ``` -{{"demo": "pages/customization/theme-components/DefaultProps.js"}} +{{"demo": "pages/customization/theme-components/GlobalThemeOverrideCallback.js"}} -To override lab component styles with TypeScript, check [this page](/components/about-the-lab/#typescript). +### Using `sx` (experimental) syntax + +If you are not familiar `sx`, first check out [the concept](/system/the-sx-prop) and [the difference with the `styled`](/system/styled/#difference-with-the-sx-prop). + +`sx` is also compatible with theme style overrides if you prefer the shorthand notation. + +{{"demo": "pages/customization/theme-components/GlobalThemeOverrideSx.js"}} ## Adding new component variants +> ⚠️ This API has been **deprecated** and will likely be removed in the next major release. If you want to apply styles based on props, take a look at [Overrides based on props](#overrides-based-on-props) instead. +> +> If you are interested to see the reasoning behind this change, check out [issue #30412](https://github.com/mui-org/material-ui/issues/30412) + You can use the `variants` key in the theme's `components` section to add new variants to MUI components. These new variants can specify what styles the component should have when specific props are applied. The definitions are specified in an array, under the component's name. For each of them a CSS class is added to the HTML ``. The order is important, so make sure that the styles that should win are specified last. diff --git a/packages/mui-joy/src/Button/Button.test.js b/packages/mui-joy/src/Button/Button.test.js index 29b110ff47766b..f90484573aab82 100644 --- a/packages/mui-joy/src/Button/Button.test.js +++ b/packages/mui-joy/src/Button/Button.test.js @@ -14,7 +14,6 @@ describe('Joy +; + +/** + * ============================================================ + */ diff --git a/packages/mui-material/src/styles/useThemeProps.d.ts b/packages/mui-material/src/styles/useThemeProps.d.ts index 811028ac2bffc5..1fb5722e78b23a 100644 --- a/packages/mui-material/src/styles/useThemeProps.d.ts +++ b/packages/mui-material/src/styles/useThemeProps.d.ts @@ -1,7 +1,8 @@ +import { Theme } from './createTheme'; import { Components } from './components'; export interface ThemeWithProps { - components?: Components; + components?: Components>; } export type ThemedProps = Theme extends { diff --git a/packages/mui-material/test/typescript/moduleAugmentation/styleOverridesCallback.spec.tsx b/packages/mui-material/test/typescript/moduleAugmentation/styleOverridesCallback.spec.tsx new file mode 100644 index 00000000000000..f06290de5c3648 --- /dev/null +++ b/packages/mui-material/test/typescript/moduleAugmentation/styleOverridesCallback.spec.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import Chip from '@mui/material/Chip'; +import { createTheme } from '@mui/material/styles'; + +// Update the Chip's extendable props options +declare module '@mui/material/Chip' { + interface ChipPropsVariantOverrides { + dashed: true; + outlined: false; + } + interface ChipPropsColorOverrides { + success: true; + } + interface ChipPropsSizeOverrides { + extraLarge: true; + } +} + +// theme typings should work as expected +const finalTheme = createTheme({ + components: { + MuiChip: { + styleOverrides: { + root: ({ ownerState, theme }) => ({ + ...(ownerState.variant && + { + dashed: { + border: '1px dashed', + }, + filled: { + backgroundColor: ownerState.color === 'success' ? 'lime' : theme.palette.grey[100], + }, + }[ownerState.variant]), + }), + label: ({ ownerState }) => [ + ownerState.color === 'success' && { + color: 'lime', + }, + ], + }, + }, + }, +}); + +; + +// @ts-expect-error The contained variant was disabled +; diff --git a/packages/mui-material/test/typescript/moduleAugmentation/styleOverridesCallback.tsconfig.json b/packages/mui-material/test/typescript/moduleAugmentation/styleOverridesCallback.tsconfig.json new file mode 100644 index 00000000000000..11e671f45f81a4 --- /dev/null +++ b/packages/mui-material/test/typescript/moduleAugmentation/styleOverridesCallback.tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../../../tsconfig", + "files": ["styleOverridesCallback.spec.tsx"] +} diff --git a/packages/mui-system/src/createStyled.js b/packages/mui-system/src/createStyled.js index 74c8629360111e..36b534475d1402 100644 --- a/packages/mui-system/src/createStyled.js +++ b/packages/mui-system/src/createStyled.js @@ -135,7 +135,12 @@ export default function createStyled(input = {}) { const styleOverrides = getStyleOverrides(componentName, theme); if (styleOverrides) { - return overridesResolver(props, styleOverrides); + const resolvedStyleOverrides = {}; + Object.entries(styleOverrides).forEach(([slotKey, slotStyle]) => { + resolvedStyleOverrides[slotKey] = + typeof slotStyle === 'function' ? slotStyle(props) : slotStyle; + }); + return overridesResolver(props, resolvedStyleOverrides); } return null; diff --git a/packages/mui-system/src/createStyled.test.js b/packages/mui-system/src/createStyled.test.js index 58369457a25694..39be9742a592f6 100644 --- a/packages/mui-system/src/createStyled.test.js +++ b/packages/mui-system/src/createStyled.test.js @@ -3,6 +3,7 @@ import { expect } from 'chai'; import { ThemeProvider, createTheme } from '@mui/material/styles'; import { createRenderer } from 'test/utils'; import createStyled from './createStyled'; +import sx from './sx'; describe('createStyled', () => { const { render } = createRenderer(); @@ -100,4 +101,184 @@ describe('createStyled', () => { }); }); }); + + describe('styleOverrides callback', () => { + const styled = createStyled({}); + const ButtonRoot = styled('button', { + name: 'MuiButton', + slot: 'Root', + overridesResolver: (props, styles) => [ + styles.root, + { [`& .MuiButton-avatar`]: styles.avatar }, + ], + })({}); + const ButtonIcon = styled('span', { + name: 'MuiButton', + slot: 'Icon', + overridesResolver: (props, styles) => styles.icon, + })({}); + const Button = ({ children, startIcon, endIcon, color = 'primary', ...props }) => { + const ownerState = { startIcon, endIcon, color, ...props }; + return ( + + {startIcon && {startIcon}} + {children} + {endIcon && {endIcon}} + + ); + }; + + it('spread ownerState as props to the slot styleOverrides', () => { + const finalTheme = createTheme({ + components: { + MuiButton: { + styleOverrides: { + avatar: () => { + return { + width: '100px', + }; + }, + }, + }, + }, + }); + const { container } = render( + + + , + ); + expect(container.firstChild.firstChild).toHaveComputedStyle({ + width: '100px', + }); + }); + + it('support slot as nested class', () => { + const finalTheme = createTheme({ + typography: { + button: { + fontSize: '20px', + }, + }, + components: { + MuiButton: { + styleOverrides: { + root: ({ ownerState, theme }) => { + const { color, variant } = ownerState; + const styles = []; + if (color === 'primary') { + styles.push({ + width: 120, + height: 48, + }); + } + if (variant === 'contained') { + styles.push(theme.typography.button); + } + return styles; + }, + icon: ({ ownerState }) => [ + ownerState.startIcon && { marginRight: 8 }, + ownerState.endIcon && { marginLeft: 8 }, + ], + }, + }, + }, + }); + const { container } = render( + + + , + ); + expect(container.firstChild).toHaveComputedStyle({ + width: '120px', + height: '48px', + fontSize: '20px', + }); + expect( + container.firstChild.firstChild, // startIcon + ).toHaveComputedStyle({ + marginRight: '8px', + }); + }); + + it('support object return from the callback', () => { + const finalTheme = createTheme({ + components: { + MuiButton: { + styleOverrides: { + root: () => ({ + width: '300px', + }), + }, + }, + }, + }); + const { container } = render( + + + , + ); + expect(container.firstChild).toHaveComputedStyle({ + width: '300px', + }); + }); + + it('support template string return from the callback', () => { + const finalTheme = createTheme({ + components: { + MuiButton: { + styleOverrides: { + root: () => ` + width: 300px; + `, + }, + }, + }, + }); + const { container } = render( + + + , + ); + expect(container.firstChild).toHaveComputedStyle({ + width: '300px', + }); + }); + + it('works with sx', () => { + const finalTheme = createTheme({ + components: { + MuiButton: { + styleOverrides: { + root: sx({ + pt: 10, + }), + icon: ({ ownerState }) => [ + ownerState.color === 'primary' && + sx({ + mr: 10, + }), + ], + }, + }, + }, + }); + const { container } = render( + + + , + ); + expect(container.firstChild).toHaveComputedStyle({ + paddingTop: '80px', + }); + expect(container.firstChild.firstChild).toHaveComputedStyle({ + marginRight: '80px', + }); + }); + }); });