diff --git a/docs/pages/api-docs/button.md b/docs/pages/api-docs/button.md
index 41e6fb529dc14d..53ef1362ffbfe5 100644
--- a/docs/pages/api-docs/button.md
+++ b/docs/pages/api-docs/button.md
@@ -41,7 +41,7 @@ The `MuiButton` name can be used for providing [default props](/customization/gl
| href | string | | The URL to link to when the button is clicked. If defined, an `a` element will be used as the root node. |
| size | 'large'
| 'medium'
| 'small' | 'medium' | The size of the button. `small` is equivalent to the dense button styling. |
| startIcon | node | | Element placed before the children. |
-| variant | 'contained'
| 'outlined'
| 'text' | 'text' | The variant to use. |
+| variant | 'contained'
| 'outlined'
| 'text'
| string | 'text' | The variant to use. |
The `ref` is forwarded to the root element.
diff --git a/docs/src/pages/customization/components/GlobalThemeVariants.js b/docs/src/pages/customization/components/GlobalThemeVariants.js
new file mode 100644
index 00000000000000..9f1125f3eacc3e
--- /dev/null
+++ b/docs/src/pages/customization/components/GlobalThemeVariants.js
@@ -0,0 +1,72 @@
+import React from 'react';
+import {
+ createMuiTheme,
+ makeStyles,
+ ThemeProvider,
+} from '@material-ui/core/styles';
+import Button from '@material-ui/core/Button';
+
+const useStyles = makeStyles((theme) => ({
+ root: {
+ '& > *': {
+ margin: theme.spacing(1),
+ },
+ },
+}));
+
+const defaultTheme = createMuiTheme();
+
+const theme = createMuiTheme({
+ variants: {
+ MuiButton: [
+ {
+ props: { variant: 'dashed' },
+ styles: {
+ textTransform: 'none',
+ border: `2px dashed ${defaultTheme.palette.primary.main}`,
+ color: defaultTheme.palette.primary.main,
+ },
+ },
+ {
+ props: { variant: 'dashed', color: 'secondary' },
+ styles: {
+ border: `2px dashed ${defaultTheme.palette.secondary.main}`,
+ color: defaultTheme.palette.secondary.main,
+ },
+ },
+ {
+ props: { variant: 'dashed', size: 'large' },
+ styles: {
+ borderWidth: 4,
+ },
+ },
+ {
+ props: { variant: 'dashed', color: 'secondary', size: 'large' },
+ styles: {
+ fontSize: 18,
+ },
+ },
+ ],
+ },
+});
+
+export default function GlobalThemeVariants() {
+ const classes = useStyles();
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/docs/src/pages/customization/components/GlobalThemeVariants.tsx b/docs/src/pages/customization/components/GlobalThemeVariants.tsx
new file mode 100644
index 00000000000000..7b7870bb589c58
--- /dev/null
+++ b/docs/src/pages/customization/components/GlobalThemeVariants.tsx
@@ -0,0 +1,79 @@
+import React from 'react';
+import {
+ createMuiTheme,
+ makeStyles,
+ Theme,
+ ThemeProvider,
+} from '@material-ui/core/styles';
+import Button from '@material-ui/core/Button';
+
+declare module '@material-ui/core/Button/Button' {
+ interface ButtonPropsVariantOverrides {
+ dashed: true;
+ }
+}
+
+const useStyles = makeStyles((theme: Theme) => ({
+ root: {
+ '& > *': {
+ margin: theme.spacing(1),
+ },
+ },
+}));
+
+const defaultTheme = createMuiTheme();
+
+const theme = createMuiTheme({
+ variants: {
+ MuiButton: [
+ {
+ props: { variant: 'dashed' },
+ styles: {
+ textTransform: 'none',
+ border: `2px dashed ${defaultTheme.palette.primary.main}`,
+ color: defaultTheme.palette.primary.main,
+ },
+ },
+ {
+ props: { variant: 'dashed', color: 'secondary' },
+ styles: {
+ border: `2px dashed ${defaultTheme.palette.secondary.main}`,
+ color: defaultTheme.palette.secondary.main,
+ },
+ },
+ {
+ props: { variant: 'dashed', size: 'large' },
+ styles: {
+ borderWidth: 4,
+ },
+ },
+ {
+ props: { variant: 'dashed', color: 'secondary', size: 'large' },
+ styles: {
+ fontSize: 18,
+ },
+ },
+ ],
+ },
+});
+
+export default function GlobalThemeVariants() {
+ const classes = useStyles();
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/docs/src/pages/customization/components/components.md b/docs/src/pages/customization/components/components.md
index 3af44a3abc80b9..906d0010aaf041 100644
--- a/docs/src/pages/customization/components/components.md
+++ b/docs/src/pages/customization/components/components.md
@@ -319,3 +319,43 @@ const theme = createMuiTheme({
```
{{"demo": "pages/customization/components/GlobalThemeOverride.js"}}
+
+### Adding new component variants
+
+You can take advantage of the `variants` key of the `theme` to add new variants to Material-UI components. These new variants, can specify which styles the component should have, if specific properties are defined together.
+
+The definitions are specified in an array, under the component's name. For every one of them a class is added in the head. The order is **important**, so make sure that the styles that should win will be specified lastly.
+
+```jsx
+const theme = createMuiTheme({
+ variants: {
+ MuiButton: [
+ {
+ props: { variant: 'dashed' },
+ styles: {
+ textTransform: 'none',
+ border: `2px dashed grey${blue[500]}`,
+ },
+ },
+ {
+ props: { variant: 'dashed', color: 'secondary' },
+ styles: {
+ border: `4px dashed ${red[500]}`,
+ },
+ },
+ ],
+ },
+});
+```
+
+If you are using TypeScript, you will need to specify your new variants/colors, using module augmentation.
+
+```tsx
+declare module '@material-ui/core/Button/Button' {
+ interface ButtonPropsVariantOverrides {
+ dashed: true;
+ }
+}
+```
+
+{{"demo": "pages/customization/components/GlobalThemeVariants.js"}}
diff --git a/packages/material-ui-styles/src/getStylesCreator/getStylesCreator.js b/packages/material-ui-styles/src/getStylesCreator/getStylesCreator.js
index 053a85973a63b0..1caeb3b55cf4a6 100644
--- a/packages/material-ui-styles/src/getStylesCreator/getStylesCreator.js
+++ b/packages/material-ui-styles/src/getStylesCreator/getStylesCreator.js
@@ -1,4 +1,5 @@
import { deepmerge } from '@material-ui/utils';
+import propsToClassKey from '../propsToClassKey';
import noopTheme from './noopTheme';
export default function getStylesCreator(stylesOrCreator) {
@@ -36,11 +37,15 @@ export default function getStylesCreator(stylesOrCreator) {
throw err;
}
- if (!name || !theme.overrides || !theme.overrides[name]) {
+ if (
+ !name ||
+ ((!theme.overrides || !theme.overrides[name]) && (!theme.variants || !theme.variants[name]))
+ ) {
return styles;
}
- const overrides = theme.overrides[name];
+ const overrides = (theme.overrides && theme.overrides[name]) || {};
+ const variants = (theme.variants && theme.variants[name]) || [];
const stylesWithOverrides = { ...styles };
Object.keys(overrides).forEach((key) => {
@@ -50,12 +55,22 @@ export default function getStylesCreator(stylesOrCreator) {
[
'Material-UI: You are trying to override a style that does not exist.',
`Fix the \`${key}\` key of \`theme.overrides.${name}\`.`,
+ '',
+ 'If you intentionally wanted to add a new key, please use the theme.variants option.',
].join('\n'),
);
}
}
- stylesWithOverrides[key] = deepmerge(stylesWithOverrides[key], overrides[key]);
+ stylesWithOverrides[key] = deepmerge(stylesWithOverrides[key] || {}, overrides[key]);
+ });
+
+ variants.forEach((definition) => {
+ const classKey = propsToClassKey(definition.props);
+ stylesWithOverrides[classKey] = deepmerge(
+ stylesWithOverrides[classKey] || {},
+ definition.styles,
+ );
});
return stylesWithOverrides;
diff --git a/packages/material-ui-styles/src/index.d.ts b/packages/material-ui-styles/src/index.d.ts
index 04f0677bfba08a..65d11adec915fe 100644
--- a/packages/material-ui-styles/src/index.d.ts
+++ b/packages/material-ui-styles/src/index.d.ts
@@ -31,6 +31,9 @@ export * from './ThemeProvider';
export { default as useTheme } from './useTheme';
export * from './useTheme';
+export { default as useThemeVariants } from './useThemeVariants';
+export * from './useThemeVariants';
+
export { default as withStyles } from './withStyles';
export * from './withStyles';
diff --git a/packages/material-ui-styles/src/index.js b/packages/material-ui-styles/src/index.js
index 55490730c6e870..dd9aac3207d387 100644
--- a/packages/material-ui-styles/src/index.js
+++ b/packages/material-ui-styles/src/index.js
@@ -58,6 +58,9 @@ export * from './ThemeProvider';
export { default as useTheme } from './useTheme';
export * from './useTheme';
+export { default as useThemeVariants } from './useThemeVariants';
+export * from './useThemeVariants';
+
export { default as withStyles } from './withStyles';
export * from './withStyles';
diff --git a/packages/material-ui-styles/src/makeStyles/makeStyles.js b/packages/material-ui-styles/src/makeStyles/makeStyles.js
index 113b1effd80d1d..cfa35bb1810577 100644
--- a/packages/material-ui-styles/src/makeStyles/makeStyles.js
+++ b/packages/material-ui-styles/src/makeStyles/makeStyles.js
@@ -241,6 +241,23 @@ export default function makeStyles(stylesOrCreator, options = {}) {
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useDebugValue(classes);
}
+ if (process.env.NODE_ENV !== 'production') {
+ const whitelistedComponents = ['MuiButton'];
+
+ if (
+ name &&
+ whitelistedComponents.indexOf(name) >= 0 &&
+ props.variant &&
+ !classes[props.variant]
+ ) {
+ console.error(
+ [
+ `Material-UI: You are using a variant value \`${props.variant}\` for which you didn't define styles.`,
+ `Please create a new variant matcher in your theme for this variant. To learn more about matchers visit https://material-ui.com/customization/components/#adding-new-component-variants.`,
+ ].join('\n'),
+ );
+ }
+ }
return classes;
};
diff --git a/packages/material-ui-styles/src/propsToClassKey/index.d.ts b/packages/material-ui-styles/src/propsToClassKey/index.d.ts
new file mode 100644
index 00000000000000..da423dd5fb1473
--- /dev/null
+++ b/packages/material-ui-styles/src/propsToClassKey/index.d.ts
@@ -0,0 +1,2 @@
+export { default } from './propsToClassKey';
+export * from './propsToClassKey';
diff --git a/packages/material-ui-styles/src/propsToClassKey/index.js b/packages/material-ui-styles/src/propsToClassKey/index.js
new file mode 100644
index 00000000000000..89e04af61e6d6a
--- /dev/null
+++ b/packages/material-ui-styles/src/propsToClassKey/index.js
@@ -0,0 +1 @@
+export { default } from './propsToClassKey';
diff --git a/packages/material-ui-styles/src/propsToClassKey/propsToClassKey.d.ts b/packages/material-ui-styles/src/propsToClassKey/propsToClassKey.d.ts
new file mode 100644
index 00000000000000..11cb34fce5f12a
--- /dev/null
+++ b/packages/material-ui-styles/src/propsToClassKey/propsToClassKey.d.ts
@@ -0,0 +1 @@
+export default function propsToClassKey(props: object): string;
diff --git a/packages/material-ui-styles/src/propsToClassKey/propsToClassKey.js b/packages/material-ui-styles/src/propsToClassKey/propsToClassKey.js
new file mode 100644
index 00000000000000..716fe6cd21acbc
--- /dev/null
+++ b/packages/material-ui-styles/src/propsToClassKey/propsToClassKey.js
@@ -0,0 +1,38 @@
+import MuiError from '@material-ui/utils/macros/MuiError.macro';
+
+// TODO: remove this once the capitalize method is moved to the @material-ui/utils package
+export function capitalize(string) {
+ if (typeof string !== 'string') {
+ throw new MuiError('Material-UI: capitalize(string) expects a string argument.');
+ }
+
+ return string.charAt(0).toUpperCase() + string.slice(1);
+}
+
+function isEmpty(string) {
+ return string.length === 0;
+}
+
+/**
+ * Generates string classKey based on the properties provided. It starts with the
+ * variant if defined, and then it appends all other properties in alphabetical order.
+ *
+ * @param {object} props - the properties for which the classKey should be created
+ */
+export default function propsToClassKey(props) {
+ const { variant, ...rest } = props;
+
+ let classKey = variant || '';
+
+ Object.keys(rest)
+ .sort()
+ .forEach((key) => {
+ if (key === 'color') {
+ classKey += isEmpty(classKey) ? props[key] : capitalize(props[key]);
+ } else {
+ classKey += `${isEmpty(classKey) ? key : capitalize(key)}${capitalize(props[key])}`;
+ }
+ });
+
+ return classKey;
+}
diff --git a/packages/material-ui-styles/src/propsToClassKey/propsToClassKey.test.js b/packages/material-ui-styles/src/propsToClassKey/propsToClassKey.test.js
new file mode 100644
index 00000000000000..dc885453acf415
--- /dev/null
+++ b/packages/material-ui-styles/src/propsToClassKey/propsToClassKey.test.js
@@ -0,0 +1,34 @@
+import { expect } from 'chai';
+import propsToClassKey from './propsToClassKey';
+
+describe('propsToClassKey', () => {
+ it('should return the variant value as string', () => {
+ expect(propsToClassKey({ variant: 'custom' })).to.equal('custom');
+ });
+
+ it('should combine the variant with other props', () => {
+ expect(propsToClassKey({ variant: 'custom', size: 'large' })).to.equal('customSizeLarge');
+ });
+
+ it('should append the props after the variant in alphabetical order', () => {
+ expect(propsToClassKey({ variant: 'custom', size: 'large', mode: 'static' })).to.equal(
+ 'customModeStaticSizeLarge',
+ );
+ });
+
+ it('should not prefix the color prop', () => {
+ expect(propsToClassKey({ variant: 'custom', color: 'primary' })).to.equal('customPrimary');
+ });
+
+ it('should work without variant in props', () => {
+ expect(propsToClassKey({ color: 'primary', size: 'large', mode: 'static' })).to.equal(
+ 'primaryModeStaticSizeLarge',
+ );
+ });
+
+ it('should not capitalize the first prop ', () => {
+ expect(propsToClassKey({ size: 'large', zIndex: 'toolbar' })).to.equal(
+ 'sizeLargeZIndexToolbar',
+ );
+ });
+});
diff --git a/packages/material-ui-styles/src/useThemeVariants/index.d.ts b/packages/material-ui-styles/src/useThemeVariants/index.d.ts
new file mode 100644
index 00000000000000..d01081d333c1d2
--- /dev/null
+++ b/packages/material-ui-styles/src/useThemeVariants/index.d.ts
@@ -0,0 +1,2 @@
+export { default } from './useThemeVariants';
+export * from './useThemeVariants';
diff --git a/packages/material-ui-styles/src/useThemeVariants/index.js b/packages/material-ui-styles/src/useThemeVariants/index.js
new file mode 100644
index 00000000000000..2ee16edd774b5e
--- /dev/null
+++ b/packages/material-ui-styles/src/useThemeVariants/index.js
@@ -0,0 +1 @@
+export { default } from './useThemeVariants';
diff --git a/packages/material-ui-styles/src/useThemeVariants/useThemeVariants.d.ts b/packages/material-ui-styles/src/useThemeVariants/useThemeVariants.d.ts
new file mode 100644
index 00000000000000..467b8a12699442
--- /dev/null
+++ b/packages/material-ui-styles/src/useThemeVariants/useThemeVariants.d.ts
@@ -0,0 +1 @@
+export default function useThemeVariants(props: object, name: string): string;
diff --git a/packages/material-ui-styles/src/useThemeVariants/useThemeVariants.js b/packages/material-ui-styles/src/useThemeVariants/useThemeVariants.js
new file mode 100644
index 00000000000000..ac9d07028d2a1e
--- /dev/null
+++ b/packages/material-ui-styles/src/useThemeVariants/useThemeVariants.js
@@ -0,0 +1,28 @@
+import useTheme from '../useTheme';
+import propsToClassKey from '../propsToClassKey';
+
+const useThemeVariants = (props, name) => {
+ const { classes = {} } = props;
+ const theme = useTheme();
+
+ let variantsClasses = '';
+ if (theme && theme.variants && theme.variants[name]) {
+ const themeVariants = theme.variants[name];
+
+ themeVariants.forEach((themeVariant) => {
+ let isMatch = true;
+ Object.keys(themeVariant.props).forEach((key) => {
+ if (props[key] !== themeVariant.props[key]) {
+ isMatch = false;
+ }
+ });
+ if (isMatch) {
+ variantsClasses = `${variantsClasses}${classes[propsToClassKey(themeVariant.props)]} `;
+ }
+ });
+ }
+
+ return variantsClasses;
+};
+
+export default useThemeVariants;
diff --git a/packages/material-ui-styles/src/useThemeVariants/useThemeVariants.test.js b/packages/material-ui-styles/src/useThemeVariants/useThemeVariants.test.js
new file mode 100644
index 00000000000000..0a1fdf4f344441
--- /dev/null
+++ b/packages/material-ui-styles/src/useThemeVariants/useThemeVariants.test.js
@@ -0,0 +1,97 @@
+import { expect } from 'chai';
+import React from 'react';
+import { createClientRender, screen } from 'test/utils';
+import { createMuiTheme } from '@material-ui/core/styles';
+import ThemeProvider from '../ThemeProvider';
+import useThemeVariants from './useThemeVariants';
+import withStyles from '../withStyles';
+
+describe('useThemeVariants', () => {
+ const render = createClientRender();
+
+ const ComponentInternal = (props) => {
+ const { className, ...other } = props;
+ const themeVariantsClasses = useThemeVariants(props, 'Test');
+ return ;
+ };
+
+ const Component = withStyles({}, { name: 'Test' })(ComponentInternal);
+
+ it('returns variants classes if props do match', () => {
+ const theme = createMuiTheme({
+ variants: {
+ Test: [
+ {
+ props: { variant: 'test' },
+ styles: { backgroundColor: 'rgb(255, 0, 0)' },
+ },
+ ],
+ },
+ });
+
+ render(
+
+
+ Test
+
+ ,
+ );
+
+ const style = window.getComputedStyle(screen.getByTestId('component'));
+ expect(style.getPropertyValue('background-color')).to.equal('rgb(255, 0, 0)');
+ });
+
+ it('does not return variants classes if props do not match', () => {
+ const theme = createMuiTheme({
+ variants: {
+ Test: [
+ {
+ props: { variant: 'test' },
+ styles: { backgroundColor: 'rgb(255, 0, 0)' },
+ },
+ ],
+ },
+ });
+
+ render(
+
+ Test
+ ,
+ );
+
+ const style = window.getComputedStyle(screen.getByTestId('component'));
+ expect(style.getPropertyValue('background-color')).not.to.equal('rgb(255, 0, 0)');
+ });
+
+ it('matches correctly multiple props', () => {
+ const theme = createMuiTheme({
+ variants: {
+ Test: [
+ {
+ props: { variant: 'test' },
+ styles: { backgroundColor: 'rgb(255, 0, 0)' },
+ },
+ {
+ props: { variant: 'test', color: 'primary' },
+ styles: { backgroundColor: 'rgb(255, 255, 0)' },
+ },
+ {
+ props: { variant: 'test', color: 'secondary' },
+ styles: { backgroundColor: 'rgb(0, 0, 255)' },
+ },
+ ],
+ },
+ });
+
+ render(
+
+
+ Test
+
+ ,
+ );
+
+ const style = window.getComputedStyle(screen.getByTestId('component'));
+ expect(style.getPropertyValue('background-color')).to.equal('rgb(255, 255, 0)');
+ });
+});
diff --git a/packages/material-ui-styles/src/withStyles/withStyles.test.js b/packages/material-ui-styles/src/withStyles/withStyles.test.js
index 2d61a678c8c8f0..9b4c194ead6262 100644
--- a/packages/material-ui-styles/src/withStyles/withStyles.test.js
+++ b/packages/material-ui-styles/src/withStyles/withStyles.test.js
@@ -235,6 +235,47 @@ describe('withStyles', () => {
expect(sheetsRegistry.registry[0].rules.raw).to.deep.equal({ root: { padding: 9 } });
});
+ it('should support the variants key', () => {
+ const styles = {};
+ const StyledComponent = withStyles(styles, { name: 'MuiButton' })(() => );
+ const generateClassName = createGenerateClassName();
+ const sheetsRegistry = new SheetsRegistry();
+
+ render(
+
+
+
+
+ ,
+ );
+
+ expect(sheetsRegistry.registry.length).to.equal(1);
+ expect(sheetsRegistry.registry[0].rules.raw).to.deep.equal({
+ test: { padding: 9 },
+ testSizeLarge: { fontSize: 20 },
+ sizeLargest: { fontSize: 22 },
+ });
+ });
+
describe('options: disableGeneration', () => {
it('should not generate the styles', () => {
const styles = { root: { display: 'flex' } };
diff --git a/packages/material-ui/src/Button/Button.d.ts b/packages/material-ui/src/Button/Button.d.ts
index 7287d5bc4242a5..023222cf43b7b7 100644
--- a/packages/material-ui/src/Button/Button.d.ts
+++ b/packages/material-ui/src/Button/Button.d.ts
@@ -1,6 +1,10 @@
+import { OverridableStringUnion } from '@material-ui/types';
import { ExtendButtonBase, ExtendButtonBaseTypeMap } from '../ButtonBase';
import { OverrideProps, OverridableComponent, OverridableTypeMap } from '../OverridableComponent';
+export interface ButtonPropsVariantOverrides {}
+export type VariantDefaults = Record<'text' | 'outlined' | 'contained', true>;
+
export type ButtonTypeMap<
P = {},
D extends React.ElementType = 'button'
@@ -51,7 +55,7 @@ export type ButtonTypeMap<
/**
* The variant to use.
*/
- variant?: 'text' | 'outlined' | 'contained';
+ variant?: OverridableStringUnion;
};
defaultComponent: D;
classKey: ButtonClassKey;
diff --git a/packages/material-ui/src/Button/Button.js b/packages/material-ui/src/Button/Button.js
index 332172cc569319..427e1a21d0cce9 100644
--- a/packages/material-ui/src/Button/Button.js
+++ b/packages/material-ui/src/Button/Button.js
@@ -1,6 +1,7 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
+import { useThemeVariants } from '@material-ui/styles';
import withStyles from '../styles/withStyles';
import { fade } from '../styles/colorManipulator';
import ButtonBase from '../ButtonBase';
@@ -278,6 +279,22 @@ const Button = React.forwardRef(function Button(props, ref) {
...other
} = props;
+ const themeVariantsClasses = useThemeVariants(
+ {
+ ...props,
+ color,
+ component,
+ disabled,
+ disableElevation,
+ disableFocusRipple,
+ fullWidth,
+ size,
+ type,
+ variant,
+ },
+ 'MuiButton',
+ );
+
const startIcon = startIconProp && (
{startIconProp}
@@ -304,6 +321,7 @@ const Button = React.forwardRef(function Button(props, ref) {
[classes.fullWidth]: fullWidth,
[classes.colorInherit]: color === 'inherit',
},
+ themeVariantsClasses,
className,
)}
component={component}
@@ -408,7 +426,10 @@ Button.propTypes = {
/**
* The variant to use.
*/
- variant: PropTypes.oneOf(['contained', 'outlined', 'text']),
+ variant: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
+ PropTypes.oneOf(['contained', 'outlined', 'text']),
+ PropTypes.string,
+ ]),
};
export default withStyles(styles, { name: 'MuiButton' })(Button);
diff --git a/packages/material-ui/src/Button/Button.test.js b/packages/material-ui/src/Button/Button.test.js
index e97c63e07471c3..a57507fd84cc49 100644
--- a/packages/material-ui/src/Button/Button.test.js
+++ b/packages/material-ui/src/Button/Button.test.js
@@ -1,4 +1,5 @@
import * as React from 'react';
+import PropTypes from 'prop-types';
import { expect } from 'chai';
import {
getClasses,
@@ -6,9 +7,11 @@ import {
describeConformance,
act,
createClientRender,
+ screen,
fireEvent,
createServerRender,
} from 'test/utils';
+import { ThemeProvider, createMuiTheme } from '../styles';
import Button from './Button';
import ButtonBase from '../ButtonBase';
@@ -367,4 +370,150 @@ describe('', () => {
expect(markup.text()).to.equal('Hello World');
});
});
+
+ describe('custom theme variants', () => {
+ const WrappedComponent = (props) => {
+ const { theme, ...other } = props;
+ return (
+
+
+
+ );
+ };
+
+ WrappedComponent.propTypes = {
+ theme: PropTypes.object,
+ };
+
+ it('should map the variant classkey to the component', function test() {
+ if (/jsdom/.test(window.navigator.userAgent)) {
+ // see https://github.com/jsdom/jsdom/issues/2953
+ this.skip();
+ }
+
+ const theme = createMuiTheme({
+ variants: {
+ MuiButton: [
+ {
+ props: { variant: 'test' },
+ styles: { backgroundColor: 'rgb(255, 0, 0)' },
+ },
+ ],
+ },
+ });
+
+ render();
+
+ const style = window.getComputedStyle(screen.getByTestId('component'));
+ expect(style.getPropertyValue('background-color')).to.equal('rgb(255, 0, 0)');
+ });
+
+ it('should map the latest props combination classkey to the component', function test() {
+ if (/jsdom/.test(window.navigator.userAgent)) {
+ // see https://github.com/jsdom/jsdom/issues/2953
+ this.skip();
+ }
+
+ const theme = createMuiTheme({
+ variants: {
+ MuiButton: [
+ {
+ props: { variant: 'test' },
+ styles: { backgroundColor: 'rgb(255, 0, 0)' },
+ },
+ {
+ props: { variant: 'test', size: 'large', color: 'primary' },
+ styles: { backgroundColor: 'rgb(0, 255, 0)' },
+ },
+ ],
+ },
+ });
+
+ render();
+
+ const style = window.getComputedStyle(screen.getByTestId('component'));
+ expect(style.getPropertyValue('background-color')).to.equal('rgb(0, 255, 0)');
+ });
+
+ it('should not add classKey if not all props match', function test() {
+ if (/jsdom/.test(window.navigator.userAgent)) {
+ // see https://github.com/jsdom/jsdom/issues/2953
+ this.skip();
+ }
+
+ const theme = createMuiTheme({
+ variants: {
+ MuiButton: [
+ {
+ props: { variant: 'test' },
+ styles: { backgroundColor: 'rgb(255, 0, 0)' },
+ },
+ {
+ props: { variant: 'test', size: 'large' },
+ styles: { backgroundColor: 'rgb(0, 255, 0)' },
+ },
+ ],
+ },
+ });
+
+ render();
+
+ const style = window.getComputedStyle(screen.getByTestId('component'));
+ expect(style.getPropertyValue('background-color')).not.to.equal('rgb(0, 255, 0)');
+ });
+
+ it('should consider default props when matching the props', function test() {
+ if (/jsdom/.test(window.navigator.userAgent)) {
+ // see https://github.com/jsdom/jsdom/issues/2953
+ this.skip();
+ }
+
+ const theme = createMuiTheme({
+ variants: {
+ MuiButton: [
+ {
+ props: { variant: 'test' },
+ styles: { backgroundColor: 'rgb(0, 0, 0)' },
+ },
+ {
+ props: { variant: 'test', color: 'primary', size: 'large' },
+ styles: { backgroundColor: 'rgb(255, 0, 0)' },
+ },
+ ],
+ },
+ });
+
+ render();
+
+ const style = window.getComputedStyle(screen.getByTestId('component'));
+ expect(style.getPropertyValue('background-color')).to.equal('rgb(255, 0, 0)');
+ });
+
+ it('should warn if the used variant is not defined in the theme', function test() {
+ const theme = createMuiTheme({
+ variants: {
+ MuiButton: [
+ {
+ props: { variant: 'test1' },
+ styles: { backgroundColor: 'rgb(255, 0, 0)' },
+ },
+ ],
+ },
+ });
+
+ expect(() => mount()).toErrorDev([
+ // strict mode renders twice
+ [
+ `Material-UI: You are using a variant value \`test\` for which you didn't define styles.`,
+ `Please create a new variant matcher in your theme for this variant. To learn more about matchers visit https://material-ui.com/customization/components/#adding-new-component-variants.`,
+ ].join('\n'),
+ [
+ `Material-UI: You are using a variant value \`test\` for which you didn't define styles.`,
+ `Please create a new variant matcher in your theme for this variant. To learn more about matchers visit https://material-ui.com/customization/components/#adding-new-component-variants.`,
+ ].join('\n'),
+ ]);
+ });
+ });
});
diff --git a/packages/material-ui/src/styles/createMuiTheme.d.ts b/packages/material-ui/src/styles/createMuiTheme.d.ts
index 4bfe849c8d7118..f0191dd2d92be5 100644
--- a/packages/material-ui/src/styles/createMuiTheme.d.ts
+++ b/packages/material-ui/src/styles/createMuiTheme.d.ts
@@ -8,6 +8,7 @@ import { Spacing, SpacingOptions } from './createSpacing';
import { Transitions, TransitionsOptions } from './transitions';
import { ZIndex, ZIndexOptions } from './zIndex';
import { Overrides } from './overrides';
+import { Variants } from './variants';
import { ComponentsProps } from './props';
export type Direction = 'ltr' | 'rtl';
@@ -24,6 +25,7 @@ export interface ThemeOptions {
spacing?: SpacingOptions;
transitions?: TransitionsOptions;
typography?: TypographyOptions | ((palette: Palette) => TypographyOptions);
+ variants?: Variants;
zIndex?: ZIndexOptions;
unstable_strictMode?: boolean;
}
@@ -40,6 +42,7 @@ export interface Theme {
spacing: Spacing;
transitions: Transitions;
typography: Typography;
+ variants?: Variants;
zIndex: ZIndex;
unstable_strictMode?: boolean;
}
diff --git a/packages/material-ui/src/styles/createMuiTheme.js b/packages/material-ui/src/styles/createMuiTheme.js
index b679500db0bca4..da1c8afcfbea45 100644
--- a/packages/material-ui/src/styles/createMuiTheme.js
+++ b/packages/material-ui/src/styles/createMuiTheme.js
@@ -36,6 +36,7 @@ function createMuiTheme(options = {}, ...args) {
spacing,
shape,
transitions,
+ variants: {},
zIndex,
},
other,
diff --git a/packages/material-ui/src/styles/variants.d.ts b/packages/material-ui/src/styles/variants.d.ts
new file mode 100644
index 00000000000000..2e6873fce532c8
--- /dev/null
+++ b/packages/material-ui/src/styles/variants.d.ts
@@ -0,0 +1,17 @@
+import { CSSProperties, CreateCSSProperties, PropsFunc } from '@material-ui/styles/withStyles';
+import { ComponentsPropsList } from './props';
+
+export type Variants = {
+ [Name in keyof ComponentsPropsList]?: Array<{
+ props: Partial;
+ styles: // JSS property bag
+ | CSSProperties
+ // JSS property bag where values are based on props
+ | CreateCSSProperties>
+ // JSS property bag based on props
+ | PropsFunc<
+ Partial,
+ CreateCSSProperties>
+ >;
+ }>;
+};