From 7eac70fc5afef1fcfb140456c65d540c621f6357 Mon Sep 17 00:00:00 2001 From: mnajdova Date: Mon, 4 Dec 2023 13:10:01 +0100 Subject: [PATCH 1/7] [system] Support props callback in the variant's definition --- packages/mui-system/src/createStyled.js | 35 ++++++-- packages/mui-system/src/createStyled.test.js | 86 +++++++++++++++++++- packages/mui-system/src/styled.test.js | 44 ++++++++++ 3 files changed, 154 insertions(+), 11 deletions(-) diff --git a/packages/mui-system/src/createStyled.js b/packages/mui-system/src/createStyled.js index 768acec584a320..1b4f641763d664 100644 --- a/packages/mui-system/src/createStyled.js +++ b/packages/mui-system/src/createStyled.js @@ -34,11 +34,18 @@ const getStyleOverrides = (name, theme) => { }; const transformVariants = (variants) => { + let numOfCallbacks = 0; const variantsStyles = {}; if (variants) { variants.forEach((definition) => { - const key = propsToClassKey(definition.props); + let key = ''; + if (typeof definition.props === 'function') { + key = `callback${numOfCallbacks}`; + numOfCallbacks += 1; + } else { + key = propsToClassKey(definition.props); + } variantsStyles[key] = definition.style; }); } @@ -57,17 +64,31 @@ const getVariantStyles = (name, theme) => { const variantsResolver = (props, styles, variants) => { const { ownerState = {} } = props; const variantsStyles = []; + let numOfCallbacks = 0; if (variants) { variants.forEach((variant) => { let isMatch = true; - Object.keys(variant.props).forEach((key) => { - if (ownerState[key] !== variant.props[key] && props[key] !== variant.props[key]) { - isMatch = false; - } - }); + if (typeof variant.props === 'function') { + const propsToCheck = deepmerge(props, ownerState); + isMatch = variant.props(propsToCheck); + } else { + Object.keys(variant.props).forEach((key) => { + if (ownerState[key] !== variant.props[key] && props[key] !== variant.props[key]) { + isMatch = false; + } + }); + } if (isMatch) { - variantsStyles.push(styles[propsToClassKey(variant.props)]); + if (typeof variant.props === 'function') { + variantsStyles.push(styles[`callback${numOfCallbacks}`]); + } else { + variantsStyles.push(styles[propsToClassKey(variant.props)]); + } + } + + if (typeof variant.props === 'function') { + numOfCallbacks++; } }); } diff --git a/packages/mui-system/src/createStyled.test.js b/packages/mui-system/src/createStyled.test.js index 35d36dc0e3f18e..337e6413215066 100644 --- a/packages/mui-system/src/createStyled.test.js +++ b/packages/mui-system/src/createStyled.test.js @@ -439,7 +439,7 @@ describe('createStyled', () => { Filled - Filled + Text , ); @@ -473,7 +473,7 @@ describe('createStyled', () => { Filled - Filled + Text , ); @@ -526,7 +526,7 @@ describe('createStyled', () => { Filled - Filled + Text Outlined @@ -580,12 +580,90 @@ describe('createStyled', () => { Filled - Filled + Text , ); expect(getByTestId('filled')).toHaveComputedStyle({ backgroundColor: 'rgb(0, 0, 255)' }); expect(getByTestId('text')).toHaveComputedStyle({ color: 'rgb(0, 0, 220)' }); }); + + it('should accept variants in function props arg', () => { + const styled = createStyled({ defaultTheme: { colors: { blue: 'rgb(0, 0, 255)' } } }); + + const Test = styled('div')(({ theme }) => ({ + variants: [ + { + props: (props) => props.color === 'blue' && props.variant === 'filled', + style: { + backgroundColor: theme.colors.blue, + }, + }, + { + props: (props) => props.color === 'blue' && props.variant === 'text', + style: { + color: theme.colors.blue, + }, + }, + ], + })); + + const { getByTestId } = render( + + + Filled + + + Text + + , + ); + expect(getByTestId('filled')).toHaveComputedStyle({ backgroundColor: 'rgb(0, 0, 255)' }); + expect(getByTestId('text')).toHaveComputedStyle({ color: 'rgb(0, 0, 255)' }); + }); + + it('should accept variants with both object and function props arg', () => { + const styled = createStyled({ defaultTheme: { colors: { blue: 'rgb(0, 0, 255)' } } }); + + const Test = styled('div')(({ theme }) => ({ + variants: [ + { + props: (props) => props.color === 'blue' && props.variant === 'filled', + style: { + backgroundColor: theme.colors.blue, + }, + }, + { + props: { color: 'blue', variant: 'outlined' }, + style: { + borderColor: theme.colors.blue, + }, + }, + { + props: (props) => props.color === 'blue' && props.variant === 'text', + style: { + color: theme.colors.blue, + }, + }, + ], + })); + + const { getByTestId } = render( + + + Filled + + + Outlined + + + Text + + , + ); + expect(getByTestId('filled')).toHaveComputedStyle({ backgroundColor: 'rgb(0, 0, 255)' }); + expect(getByTestId('outlined')).toHaveComputedStyle({ borderTopColor: 'rgb(0, 0, 255)' }); + expect(getByTestId('text')).toHaveComputedStyle({ color: 'rgb(0, 0, 255)' }); + }); }); }); diff --git a/packages/mui-system/src/styled.test.js b/packages/mui-system/src/styled.test.js index a2d5b82a2d8fc1..d57fc06ea9c126 100644 --- a/packages/mui-system/src/styled.test.js +++ b/packages/mui-system/src/styled.test.js @@ -432,6 +432,50 @@ describe('styled', () => { }); }); + it('should support variants with props callbacks', () => { + const theme = createTheme({ + components: { + MuiTest: { + variants: [ + { + props: ({ size }) => size === 'large', + style: { + width: '400px', + height: '400px', + }, + }, + { + props: ({ size }) => size === 'small', + style: { + width: '200px', + height: '200px', + }, + }, + ], + }, + }, + }); + const { getByTestId } = render( + + + Test + + + Test + + , + ); + + expect(getByTestId('large')).toHaveComputedStyle({ + width: '400px', + height: '400px', + }); + expect(getByTestId('small')).toHaveComputedStyle({ + width: '200px', + height: '200px', + }); + }); + it('should resolve the sx prop of object type', () => { const { container } = render( From 8e6e464be94863dca1ed2d6125baf347255b2329 Mon Sep 17 00:00:00 2001 From: mnajdova Date: Mon, 4 Dec 2023 13:30:49 +0100 Subject: [PATCH 2/7] add docs --- .../theme-components/theme-components.md | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/data/material/customization/theme-components/theme-components.md b/docs/data/material/customization/theme-components/theme-components.md index ddcc93b043672e..752c757d16f934 100644 --- a/docs/data/material/customization/theme-components/theme-components.md +++ b/docs/data/material/customization/theme-components/theme-components.md @@ -187,6 +187,31 @@ declare module '@mui/material/Button' { {{"demo": "GlobalThemeVariants.js"}} +The variant's props property can be defined in a form of a callback too. This is useful if you want to apply some styles if you need to use a negation in the condition, for e.g. apply some styles if some property does not have some value. + +:::info +This feature is available from version 5.15.0. +::: + +```js +const theme = createTheme({ + components: { + MuiButton: { + variants: [ + { + props: (props) => + props.variant === 'dashed' && props.color !== 'secondary', + style: { + textTransform: 'none', + border: `2px dashed ${blue[500]}`, + }, + }, + ], + }, + }, +}); +``` + ## Theme variables Another way to override the look of all component instances is to adjust the [theme configuration variables](/material-ui/customization/theming/#theme-configuration-variables). From 53a9ce29fc654dfc22c1612614c40e5d679f53a0 Mon Sep 17 00:00:00 2001 From: mnajdova Date: Mon, 4 Dec 2023 13:32:16 +0100 Subject: [PATCH 3/7] lint issues --- packages/mui-system/src/createStyled.js | 2 +- packages/mui-system/src/styled.test.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/mui-system/src/createStyled.js b/packages/mui-system/src/createStyled.js index 1b4f641763d664..2aca48f787f5f1 100644 --- a/packages/mui-system/src/createStyled.js +++ b/packages/mui-system/src/createStyled.js @@ -88,7 +88,7 @@ const variantsResolver = (props, styles, variants) => { } if (typeof variant.props === 'function') { - numOfCallbacks++; + numOfCallbacks += 1; } }); } diff --git a/packages/mui-system/src/styled.test.js b/packages/mui-system/src/styled.test.js index d57fc06ea9c126..9ec318332dda0f 100644 --- a/packages/mui-system/src/styled.test.js +++ b/packages/mui-system/src/styled.test.js @@ -433,7 +433,7 @@ describe('styled', () => { }); it('should support variants with props callbacks', () => { - const theme = createTheme({ + const customTheme = createTheme({ components: { MuiTest: { variants: [ @@ -456,7 +456,7 @@ describe('styled', () => { }, }); const { getByTestId } = render( - + Test From fa8c5a274e33453dc54d96f46cd167113b02f351 Mon Sep 17 00:00:00 2001 From: mnajdova Date: Fri, 15 Dec 2023 12:28:56 +0100 Subject: [PATCH 4/7] [Badge] Migrate to use the variants API --- packages/mui-material/src/Badge/Badge.js | 235 ++++++++++++++--------- 1 file changed, 139 insertions(+), 96 deletions(-) diff --git a/packages/mui-material/src/Badge/Badge.js b/packages/mui-material/src/Badge/Badge.js index eecbce2c31d50e..2955cfbd46dd70 100644 --- a/packages/mui-material/src/Badge/Badge.js +++ b/packages/mui-material/src/Badge/Badge.js @@ -87,110 +87,153 @@ const BadgeBadge = styled('span', { easing: theme.transitions.easing.easeInOut, duration: theme.transitions.duration.enteringScreen, }), - ...(ownerState.color !== 'default' && { - backgroundColor: (theme.vars || theme).palette[ownerState.color].main, - color: (theme.vars || theme).palette[ownerState.color].contrastText, - }), - ...(ownerState.variant === 'dot' && { - borderRadius: RADIUS_DOT, - height: RADIUS_DOT * 2, - minWidth: RADIUS_DOT * 2, - padding: 0, - }), - ...(ownerState.anchorOrigin.vertical === 'top' && - ownerState.anchorOrigin.horizontal === 'right' && - ownerState.overlap === 'rectangular' && { - top: 0, - right: 0, - transform: 'scale(1) translate(50%, -50%)', - transformOrigin: '100% 0%', - [`&.${badgeClasses.invisible}`]: { - transform: 'scale(0) translate(50%, -50%)', + variants: [ + { + props: ({ ownerState }) => ownerState.color !== 'default', + style: { + backgroundColor: (theme.vars || theme).palette[ownerState.color].main, + color: (theme.vars || theme).palette[ownerState.color].contrastText, }, - }), - ...(ownerState.anchorOrigin.vertical === 'bottom' && - ownerState.anchorOrigin.horizontal === 'right' && - ownerState.overlap === 'rectangular' && { - bottom: 0, - right: 0, - transform: 'scale(1) translate(50%, 50%)', - transformOrigin: '100% 100%', - [`&.${badgeClasses.invisible}`]: { - transform: 'scale(0) translate(50%, 50%)', + }, + { + props: ({ ownerState }) => ownerState.variant === 'dot', + style: { + borderRadius: RADIUS_DOT, + height: RADIUS_DOT * 2, + minWidth: RADIUS_DOT * 2, + padding: 0, }, - }), - ...(ownerState.anchorOrigin.vertical === 'top' && - ownerState.anchorOrigin.horizontal === 'left' && - ownerState.overlap === 'rectangular' && { - top: 0, - left: 0, - transform: 'scale(1) translate(-50%, -50%)', - transformOrigin: '0% 0%', - [`&.${badgeClasses.invisible}`]: { - transform: 'scale(0) translate(-50%, -50%)', + }, + { + props: ({ ownerState }) => + ownerState.anchorOrigin.vertical === 'top' && + ownerState.anchorOrigin.horizontal === 'right' && + ownerState.overlap === 'rectangular', + style: { + top: 0, + right: 0, + transform: 'scale(1) translate(50%, -50%)', + transformOrigin: '100% 0%', + [`&.${badgeClasses.invisible}`]: { + transform: 'scale(0) translate(50%, -50%)', + }, }, - }), - ...(ownerState.anchorOrigin.vertical === 'bottom' && - ownerState.anchorOrigin.horizontal === 'left' && - ownerState.overlap === 'rectangular' && { - bottom: 0, - left: 0, - transform: 'scale(1) translate(-50%, 50%)', - transformOrigin: '0% 100%', - [`&.${badgeClasses.invisible}`]: { - transform: 'scale(0) translate(-50%, 50%)', + }, + { + props: ({ ownerState }) => + ownerState.anchorOrigin.vertical === 'bottom' && + ownerState.anchorOrigin.horizontal === 'right' && + ownerState.overlap === 'rectangular', + style: { + bottom: 0, + right: 0, + transform: 'scale(1) translate(50%, 50%)', + transformOrigin: '100% 100%', + [`&.${badgeClasses.invisible}`]: { + transform: 'scale(0) translate(50%, 50%)', + }, }, - }), - ...(ownerState.anchorOrigin.vertical === 'top' && - ownerState.anchorOrigin.horizontal === 'right' && - ownerState.overlap === 'circular' && { - top: '14%', - right: '14%', - transform: 'scale(1) translate(50%, -50%)', - transformOrigin: '100% 0%', - [`&.${badgeClasses.invisible}`]: { - transform: 'scale(0) translate(50%, -50%)', + }, + { + props: ({ ownerState }) => + ownerState.anchorOrigin.vertical === 'top' && + ownerState.anchorOrigin.horizontal === 'left' && + ownerState.overlap === 'rectangular', + style: { + top: 0, + left: 0, + transform: 'scale(1) translate(-50%, -50%)', + transformOrigin: '0% 0%', + [`&.${badgeClasses.invisible}`]: { + transform: 'scale(0) translate(-50%, -50%)', + }, }, - }), - ...(ownerState.anchorOrigin.vertical === 'bottom' && - ownerState.anchorOrigin.horizontal === 'right' && - ownerState.overlap === 'circular' && { - bottom: '14%', - right: '14%', - transform: 'scale(1) translate(50%, 50%)', - transformOrigin: '100% 100%', - [`&.${badgeClasses.invisible}`]: { - transform: 'scale(0) translate(50%, 50%)', + }, + { + props: ({ ownerState }) => + ownerState.anchorOrigin.vertical === 'bottom' && + ownerState.anchorOrigin.horizontal === 'left' && + ownerState.overlap === 'rectangular', + style: { + bottom: 0, + left: 0, + transform: 'scale(1) translate(-50%, 50%)', + transformOrigin: '0% 100%', + [`&.${badgeClasses.invisible}`]: { + transform: 'scale(0) translate(-50%, 50%)', + }, }, - }), - ...(ownerState.anchorOrigin.vertical === 'top' && - ownerState.anchorOrigin.horizontal === 'left' && - ownerState.overlap === 'circular' && { - top: '14%', - left: '14%', - transform: 'scale(1) translate(-50%, -50%)', - transformOrigin: '0% 0%', - [`&.${badgeClasses.invisible}`]: { - transform: 'scale(0) translate(-50%, -50%)', + }, + { + props: ({ ownerState }) => + ownerState.anchorOrigin.vertical === 'top' && + ownerState.anchorOrigin.horizontal === 'right' && + ownerState.overlap === 'circular', + style: { + top: '14%', + right: '14%', + transform: 'scale(1) translate(50%, -50%)', + transformOrigin: '100% 0%', + [`&.${badgeClasses.invisible}`]: { + transform: 'scale(0) translate(50%, -50%)', + }, }, - }), - ...(ownerState.anchorOrigin.vertical === 'bottom' && - ownerState.anchorOrigin.horizontal === 'left' && - ownerState.overlap === 'circular' && { - bottom: '14%', - left: '14%', - transform: 'scale(1) translate(-50%, 50%)', - transformOrigin: '0% 100%', - [`&.${badgeClasses.invisible}`]: { - transform: 'scale(0) translate(-50%, 50%)', + }, + { + props: ({ ownerState }) => + ownerState.anchorOrigin.vertical === 'bottom' && + ownerState.anchorOrigin.horizontal === 'right' && + ownerState.overlap === 'circular', + style: { + bottom: '14%', + right: '14%', + transform: 'scale(1) translate(50%, 50%)', + transformOrigin: '100% 100%', + [`&.${badgeClasses.invisible}`]: { + transform: 'scale(0) translate(50%, 50%)', + }, }, - }), - ...(ownerState.invisible && { - transition: theme.transitions.create('transform', { - easing: theme.transitions.easing.easeInOut, - duration: theme.transitions.duration.leavingScreen, - }), - }), + }, + { + props: ({ ownerState }) => + ownerState.anchorOrigin.vertical === 'top' && + ownerState.anchorOrigin.horizontal === 'left' && + ownerState.overlap === 'circular', + style: { + top: '14%', + left: '14%', + transform: 'scale(1) translate(-50%, -50%)', + transformOrigin: '0% 0%', + [`&.${badgeClasses.invisible}`]: { + transform: 'scale(0) translate(-50%, -50%)', + }, + }, + }, + { + props: ({ ownerState }) => + ownerState.anchorOrigin.vertical === 'bottom' && + ownerState.anchorOrigin.horizontal === 'left' && + ownerState.overlap === 'circular', + style: { + bottom: '14%', + left: '14%', + transform: 'scale(1) translate(-50%, 50%)', + transformOrigin: '0% 100%', + [`&.${badgeClasses.invisible}`]: { + transform: 'scale(0) translate(-50%, 50%)', + }, + }, + }, + { + props: ({ ownerState }) => ownerState.invisible, + style: { + transition: theme.transitions.create('transform', { + easing: theme.transitions.easing.easeInOut, + duration: theme.transitions.duration.leavingScreen, + }), + }, + }, + ], })); const Badge = React.forwardRef(function Badge(inProps, ref) { From 146d8689c5672da628effcd24c6088506f8ebc6e Mon Sep 17 00:00:00 2001 From: mnajdova Date: Fri, 15 Dec 2023 14:12:48 +0100 Subject: [PATCH 5/7] don't use owner state --- packages/mui-material/src/Badge/Badge.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/mui-material/src/Badge/Badge.js b/packages/mui-material/src/Badge/Badge.js index 2955cfbd46dd70..288e50cd3235b5 100644 --- a/packages/mui-material/src/Badge/Badge.js +++ b/packages/mui-material/src/Badge/Badge.js @@ -65,7 +65,7 @@ const BadgeBadge = styled('span', { ownerState.invisible && styles.invisible, ]; }, -})(({ theme, ownerState }) => ({ +})(({ theme }) => ({ display: 'flex', flexDirection: 'row', flexWrap: 'wrap', @@ -88,15 +88,15 @@ const BadgeBadge = styled('span', { duration: theme.transitions.duration.enteringScreen, }), variants: [ - { - props: ({ ownerState }) => ownerState.color !== 'default', + ...['primary', 'secondary', 'error', 'info', 'success', 'warning'].map((color) => ({ + props: { color }, style: { - backgroundColor: (theme.vars || theme).palette[ownerState.color].main, - color: (theme.vars || theme).palette[ownerState.color].contrastText, + backgroundColor: (theme.vars || theme).palette[color].main, + color: (theme.vars || theme).palette[color].contrastText, }, - }, + })), { - props: ({ ownerState }) => ownerState.variant === 'dot', + props: { variant: 'dot' }, style: { borderRadius: RADIUS_DOT, height: RADIUS_DOT * 2, @@ -225,7 +225,7 @@ const BadgeBadge = styled('span', { }, }, { - props: ({ ownerState }) => ownerState.invisible, + props: { invisible: true }, style: { transition: theme.transitions.create('transform', { easing: theme.transitions.easing.easeInOut, From 11acea01f04939fa4a69f86535963b297f0600ad Mon Sep 17 00:00:00 2001 From: mnajdova Date: Tue, 19 Dec 2023 13:26:02 +0100 Subject: [PATCH 6/7] use theme palette keys instead of hard-coded --- packages/mui-material/src/Badge/Badge.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/mui-material/src/Badge/Badge.js b/packages/mui-material/src/Badge/Badge.js index 288e50cd3235b5..aa61555433a28d 100644 --- a/packages/mui-material/src/Badge/Badge.js +++ b/packages/mui-material/src/Badge/Badge.js @@ -88,13 +88,19 @@ const BadgeBadge = styled('span', { duration: theme.transitions.duration.enteringScreen, }), variants: [ - ...['primary', 'secondary', 'error', 'info', 'success', 'warning'].map((color) => ({ - props: { color }, - style: { - backgroundColor: (theme.vars || theme).palette[color].main, - color: (theme.vars || theme).palette[color].contrastText, - }, - })), + ...Object.keys((theme.vars ?? theme).palette) + .filter( + (key) => + (theme.vars ?? theme).palette[key].main && + (theme.vars ?? theme).palette[key].contrastText, + ) + .map((color) => ({ + props: { color }, + style: { + backgroundColor: (theme.vars || theme).palette[color].main, + color: (theme.vars || theme).palette[color].contrastText, + }, + })), { props: { variant: 'dot' }, style: { From 2853ed9d54c533de1500c65f62ca97e6cbacf5b9 Mon Sep 17 00:00:00 2001 From: mnajdova Date: Wed, 20 Dec 2023 11:22:59 +0100 Subject: [PATCH 7/7] fix on createStyled --- packages/mui-system/src/createStyled.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mui-system/src/createStyled.js b/packages/mui-system/src/createStyled.js index 2aca48f787f5f1..0f89652febe4cd 100644 --- a/packages/mui-system/src/createStyled.js +++ b/packages/mui-system/src/createStyled.js @@ -70,7 +70,7 @@ const variantsResolver = (props, styles, variants) => { variants.forEach((variant) => { let isMatch = true; if (typeof variant.props === 'function') { - const propsToCheck = deepmerge(props, ownerState); + const propsToCheck = { ...props, ...ownerState }; isMatch = variant.props(propsToCheck); } else { Object.keys(variant.props).forEach((key) => {