Skip to content

[core] Support merging of className and style from theme #45975

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

Open
wants to merge 33 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
efbe160
[core] Support merging of className and style from theme
sai6855 Apr 21, 2025
79dfa9a
fix logic
sai6855 Apr 21, 2025
0b9c7f0
fix logic
sai6855 Apr 21, 2025
797d83c
add teheme test
sai6855 Apr 21, 2025
0a58a00
fix test
sai6855 Apr 22, 2025
ffc6897
fix test
sai6855 Apr 22, 2025
55887c7
fix
sai6855 Apr 22, 2025
e214261
update logic
sai6855 Apr 22, 2025
e41ce88
add test
sai6855 Apr 22, 2025
550024e
fix tests
sai6855 Apr 22, 2025
bf09468
fix logic
sai6855 Apr 22, 2025
14d7410
fix tests
sai6855 Apr 22, 2025
904d2d1
fix tests
sai6855 Apr 22, 2025
c212733
prettier
sai6855 Apr 22, 2025
cbbf441
trigger CI
sai6855 Apr 22, 2025
970f099
fix tests
sai6855 Apr 22, 2025
9cdb15c
improve logic
sai6855 Apr 23, 2025
2b53472
Trigger Build
sai6855 Apr 23, 2025
15972e5
suggestions: code-review
sai6855 May 19, 2025
585bdc9
Merge branch 'master' into merge-classname-style
sai6855 May 19, 2025
f566486
trigger CI
sai6855 May 19, 2025
9ce9f84
Merge branch 'master' into merge-classname-style
sai6855 May 19, 2025
b3e86a9
comment cardheader tests
sai6855 May 22, 2025
210006e
add test back
sai6855 May 22, 2025
e3ee1f1
remove from d.ts
sai6855 May 22, 2025
e6bc427
add usetheme
sai6855 May 22, 2025
3bf9f64
add createthemewithvars
sai6855 May 22, 2025
4167360
add createthemewithvars
sai6855 May 22, 2025
750f406
fix types
sai6855 May 23, 2025
c49f00b
fix
sai6855 May 23, 2025
08be71f
Merge branch 'master' into merge-classname-style
sai6855 May 23, 2025
9bb39c8
add ts test
sai6855 May 23, 2025
4f57634
Merge branch 'merge-classname-style' of https://github.com/sai6855/ma…
sai6855 May 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions packages/mui-material/src/CardHeader/CardHeader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { typographyClasses } from '@mui/material/Typography';
import Avatar from '@mui/material/Avatar';
import IconButton from '@mui/material/IconButton';
import CardHeader, { cardHeaderClasses as classes } from '@mui/material/CardHeader';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import describeConformance from '../../test/describeConformance';

describe('<CardHeader />', () => {
Expand Down Expand Up @@ -106,4 +107,107 @@ describe('<CardHeader />', () => {
expect(subHeader).to.have.class(typographyClasses.body2);
});
});

it('should merge className and style from props and from the theme if mergeClassNameAndStyle is true', () => {
const { container } = render(
<ThemeProvider
theme={createTheme({
components: {
mergeClassNameAndStyle: true,
MuiCardHeader: {
defaultProps: {
className: 'theme-class',
style: { margin: '10px' },
slotProps: {
root: {
className: 'theme-slot-props-root-class',
style: {
fontSize: '10px',
},
},
title: {
className: 'theme-slot-props-title-class',
},
},
},
},
},
})}
>
<CardHeader
title="Title"
subheader="Subheader"
className="component-class"
style={{ padding: '10px' }}
slotProps={{
title: {
className: 'slot-props-title-class',
},
root: {
className: 'slot-props-root-class',
style: {
fontWeight: 'bold',
},
},
}}
/>
</ThemeProvider>,
);
const cardHeader = container.querySelector(`.${classes.root}`);
expect(cardHeader).to.have.class('theme-class');
expect(cardHeader).to.have.class('component-class');
expect(cardHeader).to.have.class('theme-slot-props-root-class');
expect(cardHeader).to.have.class('slot-props-root-class');
expect(cardHeader.style.margin).to.equal('10px'); // from theme
expect(cardHeader.style.padding).to.equal('10px'); // from props
expect(cardHeader.style.fontWeight).to.equal('bold'); // from props slotProps
expect(cardHeader.style.fontSize).to.equal('10px'); // from theme slotProps

const title = container.querySelector(`.${classes.title}`);
expect(title).to.have.class('theme-slot-props-title-class');
expect(title).to.have.class('slot-props-title-class');
});

it('should not merge className and style from props and from the theme if mergeClassNameAndStyle is false', () => {
render(
<ThemeProvider
theme={createTheme({
components: {
MuiCardHeader: {
defaultProps: {
className: 'test-class-1',
style: { margin: '10px' },
slotProps: {
title: {
className: 'title-class-1',
},
},
},
},
},
})}
>
<CardHeader
title="Title"
subheader="Subheader"
className="test-class-2"
style={{ padding: '10px' }}
slotProps={{
title: {
className: 'title-class-2',
},
}}
/>
</ThemeProvider>,
);
const cardHeader = document.querySelector(`.${classes.root}`);
expect(cardHeader).to.not.have.class('test-class-1');
expect(cardHeader).to.have.class('test-class-2');
expect(cardHeader).to.not.have.style('margin', '10px');
expect(cardHeader).to.have.style('padding', '10px');

const title = cardHeader.querySelector(`.${classes.title}`);
expect(title).to.not.have.class('title-class-1');
expect(title).to.have.class('title-class-2');
});
});
5 changes: 5 additions & 0 deletions packages/mui-material/src/styles/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { ComponentsOverrides } from './overrides';
import { ComponentsVariants } from './variants';

export interface Components<Theme = unknown> {
/**
* Whether to merge the className and style coming from the component props with the default props.
* @default false
*/
mergeClassNameAndStyle?: boolean;
MuiAlert?: {
defaultProps?: ComponentsProps['MuiAlert'];
styleOverrides?: ComponentsOverrides<Theme>['MuiAlert'];
Expand Down
8 changes: 8 additions & 0 deletions packages/mui-material/src/styles/createTheme.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,3 +306,11 @@ const theme = createTheme();
},
});
}

{
createTheme({
components: {
mergeClassNameAndStyle: true,
},
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ DefaultPropsProvider.propTypes /* remove-proptypes */ = {

function getThemeProps<
Theme extends {
components?: Record<string, { defaultProps?: any; styleOverrides?: any; variants?: any }>;
components?: Record<string, { defaultProps?: any; styleOverrides?: any; variants?: any }> & {
mergeClassNameAndStyle?: boolean;
};
},
Props,
Name extends string,
Expand All @@ -43,12 +45,12 @@ function getThemeProps<

if (config.defaultProps) {
// compatible with v5 signature
return resolveProps(config.defaultProps, props);
return resolveProps(config.defaultProps, props, theme.components.mergeClassNameAndStyle);
}

if (!config.styleOverrides && !config.variants) {
// v6 signature, no property 'defaultProps'
return resolveProps(config as any, props);
return resolveProps(config as any, props, theme.components.mergeClassNameAndStyle);
}
return props;
}
Expand Down
43 changes: 43 additions & 0 deletions packages/mui-utils/src/resolveProps/resolveProps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,47 @@ describe('resolveProps', () => {
notTheSlotProps: { className: 'input' },
});
});

it('merge className and style props', () => {
expect(
resolveProps(
{ className: 'input1', style: { color: 'red' } },
{ className: 'input2', style: { backgroundColor: 'blue' } },
true,
),
).to.deep.equal({
className: 'input1 input2',
style: { color: 'red', backgroundColor: 'blue' },
});
});

it('merge className props', () => {
expect(resolveProps({ className: 'input1' }, { className: 'input2' }, true)).to.deep.equal({
className: 'input1 input2',
});

expect(resolveProps({ className: 'input1' }, {}, true)).to.deep.equal({
className: 'input1',
});

expect(resolveProps({}, { className: 'input2' }, true)).to.deep.equal({
className: 'input2',
});
});

it('merge style props', () => {
expect(
resolveProps({ style: { color: 'red' } }, { style: { backgroundColor: 'blue' } }, true),
).to.deep.equal({
style: { color: 'red', backgroundColor: 'blue' },
});

expect(resolveProps({ style: { color: 'red' } }, {}, true)).to.deep.equal({
style: { color: 'red' },
});

expect(resolveProps({}, { style: { backgroundColor: 'blue' } }, true)).to.deep.equal({
style: { backgroundColor: 'blue' },
});
});
});
15 changes: 14 additions & 1 deletion packages/mui-utils/src/resolveProps/resolveProps.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import clsx from 'clsx';

/**
* Add keys, values of `defaultProps` that does not exist in `props`
* @param defaultProps
* @param props
* @param mergeClassNameAndStyle
* @returns resolved props
*/
export default function resolveProps<
Expand All @@ -10,8 +13,10 @@ export default function resolveProps<
componentsProps?: Record<string, unknown>;
slots?: Record<string, unknown>;
slotProps?: Record<string, unknown>;
className?: string;
style?: React.CSSProperties;
} & Record<string, unknown>,
>(defaultProps: T, props: T) {
>(defaultProps: T, props: T, mergeClassNameAndStyle: boolean = false) {
const output = { ...props };

for (const key in defaultProps) {
Expand Down Expand Up @@ -40,10 +45,18 @@ export default function resolveProps<
(output[propName] as Record<string, unknown>)[slotPropName] = resolveProps(
(defaultSlotProps as Record<string, any>)[slotPropName],
(slotProps as Record<string, any>)[slotPropName],
mergeClassNameAndStyle,
);
}
}
}
} else if (propName === 'className' && mergeClassNameAndStyle && props.className) {
output.className = clsx(defaultProps?.className, props?.className);
} else if (propName === 'style' && mergeClassNameAndStyle && props.style) {
output.style = {
...defaultProps?.style,
...props?.style,
};
} else if (output[propName] === undefined) {
output[propName] = defaultProps[propName];
}
Expand Down
Loading