Skip to content

Commit

Permalink
[Button] Custom variant (#21648)
Browse files Browse the repository at this point in the history
  • Loading branch information
mnajdova authored Jul 27, 2020
1 parent ea3e66f commit d18f86f
Show file tree
Hide file tree
Showing 25 changed files with 676 additions and 6 deletions.
2 changes: 1 addition & 1 deletion docs/pages/api-docs/button.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ The `MuiButton` name can be used for providing [default props](/customization/gl
| <span class="prop-name">href</span> | <span class="prop-type">string</span> | | The URL to link to when the button is clicked. If defined, an `a` element will be used as the root node. |
| <span class="prop-name">size</span> | <span class="prop-type">'large'<br>&#124;&nbsp;'medium'<br>&#124;&nbsp;'small'</span> | <span class="prop-default">'medium'</span> | The size of the button. `small` is equivalent to the dense button styling. |
| <span class="prop-name">startIcon</span> | <span class="prop-type">node</span> | | Element placed before the children. |
| <span class="prop-name">variant</span> | <span class="prop-type">'contained'<br>&#124;&nbsp;'outlined'<br>&#124;&nbsp;'text'</span> | <span class="prop-default">'text'</span> | The variant to use. |
| <span class="prop-name">variant</span> | <span class="prop-type">'contained'<br>&#124;&nbsp;'outlined'<br>&#124;&nbsp;'text'<br>&#124;&nbsp;string</span> | <span class="prop-default">'text'</span> | The variant to use. |

The `ref` is forwarded to the root element.

Expand Down
72 changes: 72 additions & 0 deletions docs/src/pages/customization/components/GlobalThemeVariants.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className={classes.root}>
<ThemeProvider theme={theme}>
<Button variant="dashed">Dashed</Button>
<Button variant="dashed" color="secondary">
Secondary
</Button>
<Button variant="dashed" size="large">
Large
</Button>
<Button variant="dashed" color="secondary" size="large">
Secondary large
</Button>
</ThemeProvider>
</div>
);
}
79 changes: 79 additions & 0 deletions docs/src/pages/customization/components/GlobalThemeVariants.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={classes.root}>
<ThemeProvider theme={theme}>
<Button variant="dashed">Dashed</Button>
<Button variant="dashed" color="secondary">
Secondary
</Button>
<Button variant="dashed" size="large">
Large
</Button>
<Button variant="dashed" color="secondary" size="large">
Secondary large
</Button>
</ThemeProvider>
</div>
);
}
40 changes: 40 additions & 0 deletions docs/src/pages/customization/components/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"}}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { deepmerge } from '@material-ui/utils';
import propsToClassKey from '../propsToClassKey';
import noopTheme from './noopTheme';

export default function getStylesCreator(stylesOrCreator) {
Expand Down Expand Up @@ -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) => {
Expand All @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions packages/material-ui-styles/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
3 changes: 3 additions & 0 deletions packages/material-ui-styles/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
17 changes: 17 additions & 0 deletions packages/material-ui-styles/src/makeStyles/makeStyles.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
2 changes: 2 additions & 0 deletions packages/material-ui-styles/src/propsToClassKey/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './propsToClassKey';
export * from './propsToClassKey';
1 change: 1 addition & 0 deletions packages/material-ui-styles/src/propsToClassKey/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './propsToClassKey';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default function propsToClassKey(props: object): string;
38 changes: 38 additions & 0 deletions packages/material-ui-styles/src/propsToClassKey/propsToClassKey.js
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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',
);
});
});
2 changes: 2 additions & 0 deletions packages/material-ui-styles/src/useThemeVariants/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './useThemeVariants';
export * from './useThemeVariants';
1 change: 1 addition & 0 deletions packages/material-ui-styles/src/useThemeVariants/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './useThemeVariants';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default function useThemeVariants(props: object, name: string): string;
Loading

0 comments on commit d18f86f

Please sign in to comment.