Skip to content

Commit

Permalink
[system] Support variants in theme.styleOverrides (#40690)
Browse files Browse the repository at this point in the history
  • Loading branch information
siriwatknp committed Feb 6, 2024
1 parent 90f655a commit e57e674
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 205 deletions.
20 changes: 20 additions & 0 deletions packages/mui-material/src/styles/createTheme.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,26 @@ const theme = createTheme();
},
},
],
styleOverrides: {
root: {
variants: [
{
props: { variant: 'contained' },
style: {
backdropFilter: 'none',
},
},
],
},
endIcon: ({ theme: t }) => ({
backgroundColor: t.vars.palette.primary.main,
variants: [
{
props: ({ ownerState }) => ownerState.color === 'primary',
},
],
}),
},
},
},
});
Expand Down
22 changes: 13 additions & 9 deletions packages/mui-material/src/styles/overrides.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CSSObject, CSSInterpolation } from '@mui/system';
import { CSSObject, CSSInterpolation, Interpolation } from '@mui/system';
import { PopperClassKey } from '@mui/base/Popper';
import { ComponentsPropsList } from './props';
import { AccordionActionsClassKey } from '../AccordionActions';
Expand Down Expand Up @@ -122,14 +122,18 @@ export type OverridesStyleRules<
Theme = unknown,
> = Record<
ClassKey,
| CSSInterpolation
| ((
// Record<string, unknown> is for other props that the slot receive internally
// Documenting all ownerStates could be a huge work, let's wait until we have a real needs from developers.
props: (ComponentName extends keyof ComponentsPropsList
? { ownerState: ComponentsPropsList[ComponentName] & Record<string, unknown> }
: {}) & { theme: Theme } & Record<string, unknown>,
) => CSSInterpolation)
Interpolation<
// Record<string, unknown> is for other props that the slot receive internally
// Documenting all ownerStates could be a huge work, let's wait until we have a real needs from developers.
(ComponentName extends keyof ComponentsPropsList
? ComponentsPropsList[ComponentName] &
Record<string, unknown> & {
ownerState: ComponentsPropsList[ComponentName] & Record<string, unknown>;
}
: {}) & {
theme: Theme;
} & Record<string, unknown>
>
>;

export type ComponentsOverrides<Theme = unknown> = {
Expand Down
5 changes: 4 additions & 1 deletion packages/mui-styled-engine-sc/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@ export interface CSSObject
Omit<CSSOthersObject, 'variants'> {}

interface CSSObjectWithVariants<Props> extends Omit<CSSObject, 'variants'> {
variants: Array<{ props: Props; style: CSSObject }>;
variants: Array<{
props: Props | ((props: Props) => boolean);
style: CSSObject;
}>;
}

export type FalseyValue = undefined | null | false;
Expand Down
5 changes: 4 additions & 1 deletion packages/mui-styled-engine/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ export interface CSSObject
Omit<CSSOthersObject, 'variants'> {}

interface CSSObjectWithVariants<Props> extends Omit<CSSObject, 'variants'> {
variants: Array<{ props: Props; style: CSSObject }>;
variants: Array<{
props: Props | ((props: Props) => boolean);
style: CSSObject;
}>;
}

export interface ComponentSelector {
Expand Down
261 changes: 73 additions & 188 deletions packages/mui-system/src/createStyled.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
/* eslint-disable no-underscore-dangle */
import styledEngineStyled, { internal_processStyles as processStyles } from '@mui/styled-engine';
import {
getDisplayName,
unstable_capitalize as capitalize,
isPlainObject,
deepmerge,
} from '@mui/utils';
import { getDisplayName, unstable_capitalize as capitalize, isPlainObject } from '@mui/utils';
import createTheme from './createTheme';
import propsToClassKey from './propsToClassKey';
import styleFunctionSx from './styleFunctionSx';

function isEmpty(obj) {
Expand All @@ -25,82 +19,6 @@ function isStringTag(tag) {
);
}

const getStyleOverrides = (name, theme) => {
if (theme.components && theme.components[name] && theme.components[name].styleOverrides) {
return theme.components[name].styleOverrides;
}

return null;
};

const transformVariants = (variants) => {
let numOfCallbacks = 0;
const variantsStyles = {};

if (variants) {
variants.forEach((definition) => {
let key = '';
if (typeof definition.props === 'function') {
key = `callback${numOfCallbacks}`;
numOfCallbacks += 1;
} else {
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;
}

return transformVariants(variants);
};

const variantsResolver = (props, styles, variants) => {
const { ownerState = {} } = props;
const variantsStyles = [];
let numOfCallbacks = 0;

if (variants) {
variants.forEach((variant) => {
let isMatch = true;
if (typeof variant.props === 'function') {
const propsToCheck = { ...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) {
if (typeof variant.props === 'function') {
variantsStyles.push(styles[`callback${numOfCallbacks}`]);
} else {
variantsStyles.push(styles[propsToClassKey(variant.props)]);
}
}

if (typeof variant.props === 'function') {
numOfCallbacks += 1;
}
});
}

return variantsStyles;
};

const themeVariantsResolver = (props, styles, theme, name) => {
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 @@ -126,29 +44,49 @@ function defaultOverridesResolver(slot) {
return (props, styles) => styles[slot];
}

const muiStyledFunctionResolver = ({ styledArg, props, defaultTheme, themeId }) => {
const resolvedStyles = styledArg({
...props,
theme: resolveTheme({ ...props, defaultTheme, themeId }),
});
function processStyleArg(callableStyle, { ownerState, ...props }) {
const resolvedStylesArg =
typeof callableStyle === 'function' ? callableStyle({ ownerState, ...props }) : callableStyle;

let optionalVariants;
if (resolvedStyles && resolvedStyles.variants) {
optionalVariants = resolvedStyles.variants;
delete resolvedStyles.variants;
}
if (optionalVariants) {
const variantsStyles = variantsResolver(
props,
transformVariants(optionalVariants),
optionalVariants,
if (Array.isArray(resolvedStylesArg)) {
return resolvedStylesArg.flatMap((resolvedStyle) =>
processStyleArg(resolvedStyle, { ownerState, ...props }),
);

return [resolvedStyles, ...variantsStyles];
}

return resolvedStyles;
};
if (
!!resolvedStylesArg &&
typeof resolvedStylesArg === 'object' &&
Array.isArray(resolvedStylesArg.variants)
) {
const { variants = [], ...otherStyles } = resolvedStylesArg;
let result = otherStyles;
variants.forEach((variant) => {
let isMatch = true;
if (typeof variant.props === 'function') {
isMatch = variant.props({ ownerState, ...props });
} else {
Object.keys(variant.props).forEach((key) => {
if (ownerState?.[key] !== variant.props[key] && props[key] !== variant.props[key]) {
isMatch = false;
}
});
}
if (isMatch) {
if (!Array.isArray(result)) {
result = [result];
}
result.push(
typeof variant.style === 'function'
? variant.style({ ownerState, ...props })
: variant.style,
);
}
});
return result;
}
return resolvedStylesArg;
}

export default function createStyled(input = {}) {
const {
Expand Down Expand Up @@ -217,105 +155,52 @@ export default function createStyled(input = {}) {
label,
...options,
});
const muiStyledResolver = (styleArg, ...expressions) => {
const expressionsWithDefaultTheme = expressions
? expressions.map((stylesArg) => {
// 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.
if (typeof stylesArg === 'function' && stylesArg.__emotion_real !== stylesArg) {
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;

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

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

let transformedStyleArg = styleArg;

if (isPlainObject(styleArg)) {
let styledArgVariants;
if (styleArg && styleArg.variants) {
styledArgVariants = styleArg.variants;
delete transformedStyleArg.variants;

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

const transformStyleArg = (stylesArg) => {
// 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.
if (
(typeof stylesArg === 'function' && stylesArg.__emotion_real !== stylesArg) ||
isPlainObject(stylesArg)
) {
// If the type is function, we need to define the default theme.
transformedStyleArg = (props) =>
muiStyledFunctionResolver({ styledArg: styleArg, props, defaultTheme, themeId });
return (props) =>
processStyleArg(stylesArg, {
...props,
theme: resolveTheme({ theme: props.theme, defaultTheme, themeId }),
});
}
return stylesArg;
};
const muiStyledResolver = (styleArg, ...expressions) => {
let transformedStyleArg = transformStyleArg(styleArg);
const expressionsWithDefaultTheme = expressions ? expressions.map(transformStyleArg) : [];

if (componentName && overridesResolver) {
expressionsWithDefaultTheme.push((props) => {
const theme = resolveTheme({ ...props, defaultTheme, themeId });
const styleOverrides = getStyleOverrides(componentName, theme);

if (styleOverrides) {
const resolvedStyleOverrides = {};
Object.entries(styleOverrides).forEach(([slotKey, slotStyle]) => {
resolvedStyleOverrides[slotKey] =
typeof slotStyle === 'function' ? slotStyle({ ...props, theme }) : slotStyle;
});
return overridesResolver(props, resolvedStyleOverrides);
if (
!theme.components ||
!theme.components[componentName] ||
!theme.components[componentName].styleOverrides
) {
return null;
}

return null;
const styleOverrides = theme.components[componentName].styleOverrides;
const resolvedStyleOverrides = {};
// TODO: v7 remove iteration and use `resolveStyleArg(styleOverrides[slot])` directly
Object.entries(styleOverrides).forEach(([slotKey, slotStyle]) => {
resolvedStyleOverrides[slotKey] = processStyleArg(slotStyle, { ...props, theme });
});
return overridesResolver(props, resolvedStyleOverrides);
});
}

if (componentName && !skipVariantsResolver) {
expressionsWithDefaultTheme.push((props) => {
const theme = resolveTheme({ ...props, defaultTheme, themeId });
return themeVariantsResolver(
props,
getVariantStyles(componentName, theme),
theme,
componentName,
);
const themeVariants = theme?.components?.[componentName]?.variants;
return processStyleArg({ variants: themeVariants }, { ...props, theme });
});
}

Expand Down
Loading

0 comments on commit e57e674

Please sign in to comment.