diff --git a/docs/pages/api-docs/theme-provider.js b/docs/pages/api-docs/theme-provider.js new file mode 100644 index 00000000000000..ab8127ad82ccab --- /dev/null +++ b/docs/pages/api-docs/theme-provider.js @@ -0,0 +1,15 @@ +import React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import { prepareMarkdown } from 'docs/src/modules/utils/parseMarkdown'; + +const pageFilename = 'api/theme-provider'; +const requireRaw = require.context('!raw-loader!./', false, /\/theme-provider\.md$/); + +export default function Page({ docs }) { + return ; +} + +Page.getInitialProps = () => { + const { demos, docs } = prepareMarkdown({ pageFilename, requireRaw }); + return { demos, docs }; +}; diff --git a/docs/pages/api-docs/theme-provider.md b/docs/pages/api-docs/theme-provider.md new file mode 100644 index 00000000000000..93fc56a82c7d06 --- /dev/null +++ b/docs/pages/api-docs/theme-provider.md @@ -0,0 +1,33 @@ +--- +filename: /packages/material-ui/src/styles/ThemeProvider.js +--- + + + +# ThemeProvider API + +

The API documentation of the ThemeProvider React component. Learn more about the props and the CSS customization points.

+ +## Import + +```js +import ThemeProvider from '@material-ui/core/styles/ThemeProvider.js/ThemeProvider'; +// or +import { ThemeProvider } from '@material-ui/core/styles/ThemeProvider.js'; +``` + +You can learn more about the difference by [reading this guide](/guides/minimizing-bundle-size/). + +This component makes the `theme` available down the React tree. +It should preferably be used at **the root of your component tree**. + + + +## Props + +| Name | Type | Default | Description | +|:-----|:-----|:--------|:------------| + +The component cannot hold a ref. + + diff --git a/docs/src/pages/components/slider-styled/ContinuousSlider.js b/docs/src/pages/components/slider-styled/ContinuousSlider.js index 6b14f7b682ebe4..bae03e4997e2f8 100644 --- a/docs/src/pages/components/slider-styled/ContinuousSlider.js +++ b/docs/src/pages/components/slider-styled/ContinuousSlider.js @@ -1,19 +1,16 @@ import * as React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; +import { experimentalStyled as styled } from '@material-ui/core/styles'; import Grid from '@material-ui/core/Grid'; import Typography from '@material-ui/core/Typography'; import Slider from '@material-ui/lab/SliderStyled'; import VolumeDown from '@material-ui/icons/VolumeDown'; import VolumeUp from '@material-ui/icons/VolumeUp'; -const useStyles = makeStyles({ - root: { - width: 200, - }, +const Root = styled('div')({ + width: 200, }); export default function ContinuousSlider() { - const classes = useStyles(); const [value, setValue] = React.useState(30); const handleChange = (event, newValue) => { @@ -21,7 +18,7 @@ export default function ContinuousSlider() { }; return ( -
+ Volume @@ -44,6 +41,6 @@ export default function ContinuousSlider() { Disabled slider -
+ ); } diff --git a/docs/src/pages/components/slider-styled/ContinuousSlider.tsx b/docs/src/pages/components/slider-styled/ContinuousSlider.tsx index cd52b4eab9c878..2fbd5b89d16ea8 100644 --- a/docs/src/pages/components/slider-styled/ContinuousSlider.tsx +++ b/docs/src/pages/components/slider-styled/ContinuousSlider.tsx @@ -1,19 +1,16 @@ import * as React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; +import { experimentalStyled as styled } from '@material-ui/core/styles'; import Grid from '@material-ui/core/Grid'; import Typography from '@material-ui/core/Typography'; import Slider from '@material-ui/lab/SliderStyled'; import VolumeDown from '@material-ui/icons/VolumeDown'; import VolumeUp from '@material-ui/icons/VolumeUp'; -const useStyles = makeStyles({ - root: { - width: 200, - }, +const Root = styled('div')({ + width: 200, }); export default function ContinuousSlider() { - const classes = useStyles(); const [value, setValue] = React.useState(30); const handleChange = ( @@ -24,7 +21,7 @@ export default function ContinuousSlider() { }; return ( -
+ Volume @@ -47,6 +44,6 @@ export default function ContinuousSlider() { Disabled slider -
+ ); } diff --git a/packages/material-ui-lab/src/SliderStyled/SliderStyled.js b/packages/material-ui-lab/src/SliderStyled/SliderStyled.js index 87732e35f6a06a..0944da1033d177 100644 --- a/packages/material-ui-lab/src/SliderStyled/SliderStyled.js +++ b/packages/material-ui-lab/src/SliderStyled/SliderStyled.js @@ -1,6 +1,6 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import { useThemeProps, muiStyled, fade, lighten, darken } from '@material-ui/core/styles'; +import { useThemeProps, experimentalStyled, fade, lighten, darken } from '@material-ui/core/styles'; import { capitalize } from '@material-ui/core/utils'; import SliderUnstyled from '../SliderUnstyled'; import ValueLabelStyled from './ValueLabelStyled'; @@ -48,7 +48,7 @@ const overridesResolver = (props, styles, name) => { return styleOverrides; }; -const SliderRoot = muiStyled( +const SliderRoot = experimentalStyled( 'span', {}, { muiName: 'MuiSlider', overridesResolver }, diff --git a/packages/material-ui-lab/src/SliderStyled/ValueLabelStyled.js b/packages/material-ui-lab/src/SliderStyled/ValueLabelStyled.js index 1ac066f95062a8..622049d96b48cb 100644 --- a/packages/material-ui-lab/src/SliderStyled/ValueLabelStyled.js +++ b/packages/material-ui-lab/src/SliderStyled/ValueLabelStyled.js @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useThemeProps, muiStyled } from '@material-ui/core/styles'; +import { useThemeProps, experimentalStyled } from '@material-ui/core/styles'; import ValueLabelUnstyled from '../SliderUnstyled/ValueLabelUnstyled'; const overridesResolver = (_, styles) => { @@ -16,7 +16,7 @@ const overridesResolver = (_, styles) => { return styleOverrides; }; -const ValueLabelRoot = muiStyled( +const ValueLabelRoot = experimentalStyled( 'span', {}, { muiName: 'PrivateValueLabel', overridesResolver }, diff --git a/packages/material-ui-styled-engine-sc/src/index.js b/packages/material-ui-styled-engine-sc/src/index.js index 126204d13924d9..8fb1b642d592be 100644 --- a/packages/material-ui-styled-engine-sc/src/index.js +++ b/packages/material-ui-styled-engine-sc/src/index.js @@ -10,3 +10,5 @@ export default function styled(tag, options) { return scStyled(tag); } + +export { ThemeContext } from 'styled-components'; diff --git a/packages/material-ui-styled-engine/src/index.d.ts b/packages/material-ui-styled-engine/src/index.d.ts index 53bc27a1a267b7..3d3d28286b3347 100644 --- a/packages/material-ui-styled-engine/src/index.d.ts +++ b/packages/material-ui-styled-engine/src/index.d.ts @@ -1,2 +1,3 @@ export * from '@emotion/styled'; export { default } from '@emotion/styled'; +export { ThemeContext } from '@emotion/core'; diff --git a/packages/material-ui-styled-engine/src/index.js b/packages/material-ui-styled-engine/src/index.js index 9b24cbd7fcb37d..a0befe0bc7e71d 100644 --- a/packages/material-ui-styled-engine/src/index.js +++ b/packages/material-ui-styled-engine/src/index.js @@ -1 +1,2 @@ export { default } from '@emotion/styled'; +export { ThemeContext } from '@emotion/core'; diff --git a/packages/material-ui/src/styles/ThemeProvider.d.ts b/packages/material-ui/src/styles/ThemeProvider.d.ts new file mode 100644 index 00000000000000..0fef50ac9aa015 --- /dev/null +++ b/packages/material-ui/src/styles/ThemeProvider.d.ts @@ -0,0 +1,17 @@ +import { DefaultTheme } from '@material-ui/styles'; + +export interface ThemeProviderProps { + children?: React.ReactNode; + theme: Partial | ((outerTheme: Theme) => Theme); +} + +/** + * This component makes the `theme` available down the React tree. + * It should preferably be used at **the root of your component tree**. + * API: + * + * - [ThemeProvider API](https://material-ui.com/api/theme-provider/) + */ +export default function ThemeProvider( + props: ThemeProviderProps +): React.ReactElement>; diff --git a/packages/material-ui/src/styles/ThemeProvider.js b/packages/material-ui/src/styles/ThemeProvider.js new file mode 100644 index 00000000000000..aec494b37905bb --- /dev/null +++ b/packages/material-ui/src/styles/ThemeProvider.js @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { ThemeProvider as MuiThemeProvider } from '@material-ui/styles'; +import { exactProp } from '@material-ui/utils'; +import { ThemeContext as StyledEngineThemeContext } from '@material-ui/styled-engine'; +import useTheme from './useTheme'; + +function InnerThemeProvider({ children }) { + const theme = useTheme(); + return ( + + {children} + + ); +} + +InnerThemeProvider.propTypes = { + /** + * Your component tree. + */ + children: PropTypes.node, +}; + +/** + * This component makes the `theme` available down the React tree. + * It should preferably be used at **the root of your component tree**. + */ +function ThemeProvider(props) { + const { children, theme: localTheme } = props; + + return ( + + {children} + + ); +} + +ThemeProvider.propTypes = { + /** + * Your component tree. + */ + children: PropTypes.node, + /** + * A theme object. You can provide a function to extend the outer theme. + */ + theme: PropTypes.oneOfType([PropTypes.object, PropTypes.func]).isRequired, +}; + +if (process.env.NODE_ENV !== 'production') { + ThemeProvider.propTypes = exactProp(ThemeProvider.propTypes); +} + +export default ThemeProvider; diff --git a/packages/material-ui/src/styles/ThemeProvider.test.js b/packages/material-ui/src/styles/ThemeProvider.test.js new file mode 100644 index 00000000000000..a5edad67ff5042 --- /dev/null +++ b/packages/material-ui/src/styles/ThemeProvider.test.js @@ -0,0 +1,44 @@ +import React from 'react'; +import { expect } from 'chai'; +import { createClientRender } from 'test/utils'; +import { useTheme } from '@material-ui/styles'; +import { ThemeContext } from '@material-ui/styled-engine'; +import ThemeProvider from './ThemeProvider'; + +describe('ThemeProvider', () => { + const render = createClientRender(); + + it('should provide the theme to the mui theme context', () => { + let theme; + + function Test() { + theme = useTheme(); + + return null; + } + + render( + + + , + ); + expect(theme).to.deep.equal({ foo: 'foo' }); + }); + + it('should provide the theme to the styled engine theme context', () => { + let theme; + + function Test() { + theme = React.useContext(ThemeContext); + + return null; + } + + render( + + + , + ); + expect(theme).to.deep.equal({ foo: 'foo' }); + }); +}); diff --git a/packages/material-ui/src/styles/muiStyled.d.ts b/packages/material-ui/src/styles/experimentalStyled.d.ts similarity index 93% rename from packages/material-ui/src/styles/muiStyled.d.ts rename to packages/material-ui/src/styles/experimentalStyled.d.ts index bd9aca8a85a51e..11e0b8256aae21 100644 --- a/packages/material-ui/src/styles/muiStyled.d.ts +++ b/packages/material-ui/src/styles/experimentalStyled.d.ts @@ -183,7 +183,7 @@ interface MuiStyledOptions { overridesResolver?: (props: any, styles: string | object, name: string) => string | object; } -export interface CreateStyled { +export interface CreateMUIStyled { , ExtraProps = {}>( tag: Tag, options?: StyledOptions, @@ -198,11 +198,13 @@ export interface CreateStyled { } /** - * Cutom styled functionality that support mui specific config. + * Custom styled utility that has a default MUI theme. * - * @param options Takes an incomplete theme object and adds the missing parts. - * @returns A complete, ready to use theme object. + * @param tag HTML tag or component that should serve as base. + * @param options Styled options for the created component. + * @param muiOptions Material-UI specific style options. + * @returns React component that has styles attached to it. */ -declare const muiStyled: CreateStyled; +declare const experimentalStyled: CreateMUIStyled; -export default muiStyled; +export default experimentalStyled; diff --git a/packages/material-ui/src/styles/muiStyled.js b/packages/material-ui/src/styles/experimentalStyled.js similarity index 64% rename from packages/material-ui/src/styles/muiStyled.js rename to packages/material-ui/src/styles/experimentalStyled.js index d86f27239a4752..4e7aff61c7d6ba 100644 --- a/packages/material-ui/src/styles/muiStyled.js +++ b/packages/material-ui/src/styles/experimentalStyled.js @@ -2,6 +2,10 @@ import styled from '@material-ui/styled-engine'; import { propsToClassKey } from '@material-ui/styles'; import defaultTheme from './defaultTheme'; +function isEmpty(obj) { + return Object.keys(obj).length === 0; +} + const getStyleOverrides = (name, theme) => { let styleOverrides = {}; @@ -41,7 +45,7 @@ const variantsResolver = (props, styles, theme, name) => { themeVariants.forEach((themeVariant) => { let isMatch = true; Object.keys(themeVariant.props).forEach((key) => { - if (styleProps[key] !== themeVariant.props[key]) { + if (styleProps[key] !== themeVariant.props[key] && props[key] !== themeVariant.props[key]) { isMatch = false; } }); @@ -56,25 +60,34 @@ const variantsResolver = (props, styles, theme, name) => { const shouldForwardProp = (prop) => prop !== 'styleProps' && prop !== 'theme'; -const muiStyled = (tag, options, muiOptions) => { +const experimentalStyled = (tag, options, muiOptions = {}) => { const name = muiOptions.muiName; const defaultStyledResolver = styled(tag, { shouldForwardProp, label: name, ...options }); const muiStyledResolver = (...styles) => { - if (muiOptions.overridesResolver) { - styles.push((props) => { - const theme = props.theme || defaultTheme; + const stylesWithDefaultTheme = styles.map((stylesArg) => { + return typeof stylesArg === 'function' + ? ({ theme: themeInput, ...rest }) => + stylesArg({ theme: isEmpty(themeInput) ? defaultTheme : themeInput, ...rest }) + : stylesArg; + }); + + if (name && muiOptions.overridesResolver) { + stylesWithDefaultTheme.push((props) => { + const theme = isEmpty(props.theme) ? defaultTheme : props.theme; return muiOptions.overridesResolver(props, getStyleOverrides(name, theme), name); }); } - styles.push((props) => { - const theme = props.theme || defaultTheme; - return variantsResolver(props, getVariantStyles(name, theme), theme, name); - }); + if (name) { + stylesWithDefaultTheme.push((props) => { + const theme = isEmpty(props.theme) ? defaultTheme : props.theme; + return variantsResolver(props, getVariantStyles(name, theme), theme, name); + }); + } - return defaultStyledResolver(...styles); + return defaultStyledResolver(...stylesWithDefaultTheme); }; return muiStyledResolver; }; -export default muiStyled; +export default experimentalStyled; diff --git a/packages/material-ui/src/styles/experimentalStyled.test.js b/packages/material-ui/src/styles/experimentalStyled.test.js new file mode 100644 index 00000000000000..7d6ad23a3d2c53 --- /dev/null +++ b/packages/material-ui/src/styles/experimentalStyled.test.js @@ -0,0 +1,156 @@ +import React from 'react'; +import { expect } from 'chai'; +import { createClientRender, screen } from 'test/utils'; +import createMuiTheme from './createMuiTheme'; +import styled from './experimentalStyled'; +import ThemeProvider from './ThemeProvider'; + +describe('experimentalStyled', () => { + const render = createClientRender(); + it('should work', () => { + const Div = styled('div')({ + width: '200px', + }); + + render(
Test
); + + const style = window.getComputedStyle(screen.getByTestId('component')); + expect(style.getPropertyValue('width')).to.equal('200px'); + }); + + it('should use defaultTheme if no theme is provided', () => { + const Div = styled('div')((props) => ({ + width: props.theme.spacing(1), + })); + + render(
Test
); + + const style = window.getComputedStyle(screen.getByTestId('component')); + expect(style.getPropertyValue('width')).to.equal('8px'); + }); + + it('should use theme from context if available', () => { + const Div = styled('div')((props) => ({ + width: props.theme.spacing(1), + })); + + const theme = createMuiTheme({ + spacing: 10, + }); + + render( + +
Test
+
, + ); + + const style = window.getComputedStyle(screen.getByTestId('component')); + expect(style.getPropertyValue('width')).to.equal('10px'); + }); + + describe('muiOptions', () => { + const theme = createMuiTheme({ + components: { + MuiTest: { + variants: [ + { + props: { variant: 'rect', size: 'large' }, + style: { + width: '400px', + height: '400px', + }, + }, + ], + styleOverrides: { + root: { + width: '250px', + }, + rect: { + height: '250px', + }, + }, + }, + }, + }); + + const testOverridesResolver = (props, styles) => ({ + ...styles.root, + ...(props.variant && styles[props.variant]), + }); + + const Test = styled( + 'div', + { shouldForwardProp: (prop) => prop !== 'variant' && prop !== 'size' }, + { muiName: 'MuiTest', overridesResolver: testOverridesResolver }, + )` + width: 200px; + height: 300px; + `; + + it('should work with specified muiOptions', () => { + render(Test); + + const style = window.getComputedStyle(screen.getByTestId('component')); + expect(style.getPropertyValue('width')).to.equal('200px'); + expect(style.getPropertyValue('height')).to.equal('300px'); + }); + + it('overrides should be respected', () => { + render( + + Test + , + ); + + const style = window.getComputedStyle(screen.getByTestId('component')); + expect(style.getPropertyValue('width')).to.equal('250px'); + expect(style.getPropertyValue('height')).to.equal('300px'); + }); + + it('overrides should be respected when prop is specified', () => { + render( + + + Test + + , + ); + + const style = window.getComputedStyle(screen.getByTestId('component')); + expect(style.getPropertyValue('width')).to.equal('250px'); + expect(style.getPropertyValue('height')).to.equal('250px'); + }); + + it('variants should win over overrides', () => { + render( + + + Test + + , + ); + + const style = window.getComputedStyle(screen.getByTestId('component')); + expect(style.getPropertyValue('width')).to.equal('400px'); + expect(style.getPropertyValue('height')).to.equal('400px'); + }); + + it('styled wrapper should win over variants', () => { + const CustomTest = styled(Test)` + width: 500px; + `; + + render( + + + Test + + , + ); + + const style = window.getComputedStyle(screen.getByTestId('component')); + expect(style.getPropertyValue('width')).to.equal('500px'); + expect(style.getPropertyValue('height')).to.equal('400px'); + }); + }); +}); diff --git a/packages/material-ui/src/styles/index.d.ts b/packages/material-ui/src/styles/index.d.ts index 8006e6928cbee4..16ba132723f684 100644 --- a/packages/material-ui/src/styles/index.d.ts +++ b/packages/material-ui/src/styles/index.d.ts @@ -24,16 +24,18 @@ export { StyledComponentProps, } from './withStyles'; export { default as withTheme, WithTheme } from './withTheme'; -export { default as muiStyled } from './muiStyled'; +export { default as experimentalStyled, CreateMUIStyled } from './experimentalStyled'; export { default as styled, ComponentCreator, StyledProps } from './styled'; +export { + default as MuiThemeProvider, + default as ThemeProvider, + ThemeProviderProps, +} from './ThemeProvider'; export { createGenerateClassName, jssPreset, ServerStyleSheets, StylesProvider, - ThemeProvider as MuiThemeProvider, - ThemeProvider, - ThemeProviderProps, } from '@material-ui/styles'; export { ComponentsProps } from './props'; export { ComponentsVariants } from './variants'; diff --git a/packages/material-ui/src/styles/index.js b/packages/material-ui/src/styles/index.js index 6607732a24f0e1..b2e5dd8f49ff96 100644 --- a/packages/material-ui/src/styles/index.js +++ b/packages/material-ui/src/styles/index.js @@ -12,13 +12,12 @@ export { default as useTheme } from './useTheme'; export { default as useThemeProps } from './useThemeProps'; export { default as withStyles } from './withStyles'; export { default as withTheme } from './withTheme'; -export { default as muiStyled } from './muiStyled'; +export { default as experimentalStyled } from './experimentalStyled'; +export { default as MuiThemeProvider, default as ThemeProvider } from './ThemeProvider'; export { createGenerateClassName, jssPreset, ServerStyleSheets, StylesProvider, - ThemeProvider as MuiThemeProvider, - ThemeProvider, useThemeVariants, } from '@material-ui/styles';