Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[system] Add support for variants in the styled() util #39073

Merged
merged 16 commits into from
Oct 2, 2023
30 changes: 30 additions & 0 deletions packages/mui-styles/src/styled/styled.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,33 @@ function acceptanceTest() {
</React.Fragment>
);
}

function variantsAPI() {
const ObjectSyntax = styled('div')<{}, { foo?: string; bar?: number }>({
variants: [
{
props: { foo: 'a' },
style: { color: 'blue' },
},
],
});

const FunctionSyntax = styled('div')<{}, { foo?: string; bar?: number }>(() => ({
variants: [
{
props: { foo: 'a' },
style: { color: 'blue' },
},
],
}));

const WrongUsage = styled('div')<{}, { foo?: string; bar?: number }>({
color: [
// @ts-expect-error the API is not valid for CSS properties
{
props: { foo: 'a' },
style: { color: 'blue' },
},
],
});
}
4 changes: 3 additions & 1 deletion packages/mui-styles/src/withStyles/withStyles.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ export type BaseCreateCSSProperties<Props extends object = {}> = {

export interface CreateCSSProperties<Props extends object = {}>
extends BaseCreateCSSProperties<Props> {
variants?: Array<{ props: Props; style: CSSProperties }>;
// Allow pseudo selectors and media queries
[k: string]:
| BaseCreateCSSProperties<Props>[keyof BaseCreateCSSProperties<Props>]
| CreateCSSProperties<Props>;
| CreateCSSProperties<Props>
| Array<{ props: Props; style: CSSProperties }>;
mnajdova marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down
159 changes: 121 additions & 38 deletions packages/mui-system/src/createStyled.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
/* eslint-disable no-underscore-dangle */
import styledEngineStyled, { internal_processStyles as processStyles } from '@mui/styled-engine';
import { getDisplayName, unstable_capitalize as capitalize } from '@mui/utils';
import {
getDisplayName,
unstable_capitalize as capitalize,
isPlainObject,
deepmerge,
} from '@mui/utils';
import createTheme from './createTheme';
import propsToClassKey from './propsToClassKey';
import styleFunctionSx from './styleFunctionSx';
Expand Down Expand Up @@ -28,43 +33,53 @@ const getStyleOverrides = (name, theme) => {
return null;
};

const transformVariants = (variants) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extracted so that it can be re-used in multiple places

const variantsStyles = {};

if (variants) {
variants.forEach((definition) => {
const key = propsToClassKey(definition.props);
variantsStyles[key] = definition.style;
});
}

return variantsStyles;
};
const getVariantStyles = (name, theme) => {
let variants = [];
if (theme && theme.components && theme.components[name] && theme.components[name].variants) {
variants = theme.components[name].variants;
}

const variantsStyles = {};

variants.forEach((definition) => {
const key = propsToClassKey(definition.props);
variantsStyles[key] = definition.style;
});

return variantsStyles;
return transformVariants(variants);
};

const variantsResolver = (props, styles, theme, name) => {
const variantsResolver = (props, styles, variants) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplified signature to be re-used in both theme & styled's variants

const { ownerState = {} } = props;
const variantsStyles = [];
const themeVariants = theme?.components?.[name]?.variants;
if (themeVariants) {
themeVariants.forEach((themeVariant) => {

if (variants) {
variants.forEach((variant) => {
let isMatch = true;
Object.keys(themeVariant.props).forEach((key) => {
if (ownerState[key] !== themeVariant.props[key] && props[key] !== themeVariant.props[key]) {
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(themeVariant.props)]);
variantsStyles.push(styles[propsToClassKey(variant.props)]);
}
});
}

return variantsStyles;
};

const themeVariantsResolver = (props, styles, theme, name) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the legacy variantsResolver

const themeVariants = theme?.components?.[name]?.variants;
return variantsResolver(props, styles, themeVariants);
};

// Update /system/styled/#api in case if this changes
export function shouldForwardProp(prop) {
return prop !== 'ownerState' && prop !== 'theme' && prop !== 'sx' && prop !== 'as';
Expand All @@ -90,6 +105,32 @@ function defaultOverridesResolver(slot) {
return (props, styles) => styles[slot];
}

const muiStyledFunctionResolver = ({ styledArg, props, defaultTheme, themeId }) => {
const resolvedStyles = styledArg({
...props,
theme: resolveTheme({ ...props, defaultTheme, themeId }),
});

let optionalVariants;
if (isPlainObject(resolvedStyles)) {
if (resolvedStyles && resolvedStyles.variants) {
optionalVariants = resolvedStyles.variants;
}
delete resolvedStyles.variants;
}
mnajdova marked this conversation as resolved.
Show resolved Hide resolved
if (optionalVariants) {
const variantsStyles = variantsResolver(
props,
transformVariants(optionalVariants),
optionalVariants,
);

return [resolvedStyles, ...variantsStyles];
}

return resolvedStyles;
};

export default function createStyled(input = {}) {
const {
themeId,
Expand Down Expand Up @@ -163,19 +204,75 @@ export default function createStyled(input = {}) {
// On the server Emotion doesn't use React.forwardRef for creating components, so the created
// component stays as a function. This condition makes sure that we do not interpolate functions
// which are basically components used as a selectors.
return typeof stylesArg === 'function' && stylesArg.__emotion_real !== stylesArg
? (props) => {
return stylesArg({
...props,
theme: resolveTheme({ ...props, defaultTheme, themeId }),
if (typeof stylesArg === 'function' && stylesArg.__emotion_real !== stylesArg) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the variants styled in case arrays are used for defining multiple styles.

return (props) =>
muiStyledFunctionResolver({ styledArg: stylesArg, props, defaultTheme, themeId });
}
if (isPlainObject(stylesArg)) {
let transformedStylesArg = stylesArg;
let styledArgVariants;
if (stylesArg && stylesArg.variants) {
styledArgVariants = stylesArg.variants;
}
delete transformedStylesArg.variants;

if (styledArgVariants) {
transformedStylesArg = (props) => {
let result = stylesArg;
const variantStyles = variantsResolver(
props,
transformVariants(styledArgVariants),
styledArgVariants,
);
variantStyles.forEach((variantStyle) => {
result = deepmerge(result, variantStyle);
});
}
: stylesArg;

return result;
};
}
return transformedStylesArg;
}
return stylesArg;
})
: [];

let transformedStyleArg = styleArg;

if (isPlainObject(styleArg)) {
mnajdova marked this conversation as resolved.
Show resolved Hide resolved
let styledArgVariants;
if (styleArg && styleArg.variants) {
styledArgVariants = styleArg.variants;
}
delete transformedStyleArg.variants;

if (styledArgVariants) {
transformedStyleArg = (props) => {
let result = styleArg;
const variantStyles = variantsResolver(
props,
transformVariants(styledArgVariants),
styledArgVariants,
);
variantStyles.forEach((variantStyle) => {
result = deepmerge(result, variantStyle);
});

return result;
};
}
} else if (
typeof styleArg === 'function' &&
// On the server Emotion doesn't use React.forwardRef for creating components, so the created
// component stays as a function. This condition makes sure that we do not interpolate functions
// which are basically components used as a selectors.
styleArg.__emotion_real !== styleArg
) {
// If the type is function, we need to define the default theme.
transformedStyleArg = (props) =>
muiStyledFunctionResolver({ styledArg: styleArg, props, defaultTheme, themeId });
}

if (componentName && overridesResolver) {
expressionsWithDefaultTheme.push((props) => {
const theme = resolveTheme({ ...props, defaultTheme, themeId });
Expand All @@ -197,7 +294,7 @@ export default function createStyled(input = {}) {
if (componentName && !skipVariantsResolver) {
expressionsWithDefaultTheme.push((props) => {
const theme = resolveTheme({ ...props, defaultTheme, themeId });
return variantsResolver(
return themeVariantsResolver(
props,
getVariantStyles(componentName, theme),
theme,
Expand All @@ -217,21 +314,7 @@ export default function createStyled(input = {}) {
// If the type is array, than we need to add placeholders in the template for the overrides, variants and the sx styles.
transformedStyleArg = [...styleArg, ...placeholders];
transformedStyleArg.raw = [...styleArg.raw, ...placeholders];
} else if (
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was moved above, so that we can do a transformation only once.

typeof styleArg === 'function' &&
// On the server Emotion doesn't use React.forwardRef for creating components, so the created
// component stays as a function. This condition makes sure that we do not interpolate functions
// which are basically components used as a selectors.
styleArg.__emotion_real !== styleArg
) {
// If the type is function, we need to define the default theme.
transformedStyleArg = (props) =>
styleArg({
...props,
theme: resolveTheme({ ...props, defaultTheme, themeId }),
});
}

const Component = defaultStyledResolver(transformedStyleArg, ...expressionsWithDefaultTheme);

if (process.env.NODE_ENV !== 'production') {
Expand Down
Loading