diff --git a/docs/pages/api-docs/typography.json b/docs/pages/api-docs/typography.json index 1245dd47c7e337..9e780146bd7b5a 100644 --- a/docs/pages/api-docs/typography.json +++ b/docs/pages/api-docs/typography.json @@ -81,5 +81,5 @@ "filename": "/packages/material-ui/src/Typography/Typography.js", "inheritance": null, "demos": "", - "styledComponent": false + "styledComponent": true } diff --git a/docs/scripts/buildApi.ts b/docs/scripts/buildApi.ts index 6e324d49d73a54..6a63e35c809b7f 100644 --- a/docs/scripts/buildApi.ts +++ b/docs/scripts/buildApi.ts @@ -688,6 +688,50 @@ async function updateStylesDefinition(context: { // Do nothing as not every components has an unstyled version } + // If there is no unstyledFile we need to extract this info from the component's definition file + if (typesFilename !== unstyledFileName) { + try { + const typesSource = readFileSync(typesFilename, { encoding: 'utf8' }); + const typesAST = await babel.parseAsync(typesSource, { + configFile: false, + filename: typesFilename, + presets: [require.resolve('@babel/preset-typescript')], + }); + if (typesAST === null) { + throw new Error('No AST returned from babel.'); + } + + traverse(typesAST, { + TSPropertySignature(babelPath) { + const { node } = babelPath; + const possiblyPropName = (node.key as babel.types.Identifier).name; + if (possiblyPropName === 'classes' && node.typeAnnotation !== null) { + const members = (node.typeAnnotation.typeAnnotation as babel.types.TSTypeLiteral) + .members; + + if (members) { + styles.descriptions = styles.descriptions || {}; + members.forEach((member) => { + const className = ((member as babel.types.TSPropertySignature) + .key as babel.types.Identifier).name; + styles.classes.push(className); + if (member.leadingComments) { + styles.descriptions[className] = trimComment(member.leadingComments[0].value); + } + }); + } + } + }, + }); + + if (styles.classes.length > 0) { + styles.name = generateMuiName(path.parse(component.filename).name); + } + } catch (e) { + // Do nothing as not every components has an unstyled version + } + } + styles.classes = Array.from(new Set(styles.classes)); } diff --git a/docs/src/modules/components/Link.js b/docs/src/modules/components/Link.js index 97fe732e68db3f..7cb5c643b7e116 100644 --- a/docs/src/modules/components/Link.js +++ b/docs/src/modules/components/Link.js @@ -8,18 +8,18 @@ import MuiLink from '@material-ui/core/Link'; import { useUserLanguage } from 'docs/src/modules/utils/i18n'; const NextComposed = React.forwardRef(function NextComposed(props, ref) { - const { as, href, ...other } = props; + const { linkAs, href, ...other } = props; return ( - + ); }); NextComposed.propTypes = { - as: PropTypes.string, href: PropTypes.string, + linkAs: PropTypes.string, }; // A styled version of the Next.js Link component: @@ -64,7 +64,7 @@ function Link(props) { if (naked) { return ( ({ })); ``` +### Core components + +As the core components use emotion as a styled engine, the props used by emotion are not intercepted. The prop `as` in the following codesnippet will not be propagated to the `SomeOtherComponent`. + +`` + ### AppBar - [AppBar] Remove z-index when position static and relative diff --git a/examples/nextjs-with-typescript/src/Link.tsx b/examples/nextjs-with-typescript/src/Link.tsx index 18d0af0b620997..59a30a098a2eef 100644 --- a/examples/nextjs-with-typescript/src/Link.tsx +++ b/examples/nextjs-with-typescript/src/Link.tsx @@ -6,16 +6,16 @@ import NextLink, { LinkProps as NextLinkProps } from 'next/link'; import MuiLink, { LinkProps as MuiLinkProps } from '@material-ui/core/Link'; type NextComposedProps = Omit, 'href'> & - NextLinkProps; + Omit & { linkAs?: NextLinkProps['as'] }; const NextComposed = React.forwardRef((props, ref) => { - const { as, href, replace, scroll, passHref, shallow, prefetch, ...other } = props; + const { href, linkAs, replace, scroll, passHref, shallow, prefetch, ...other } = props; return ( ', () => { const mount = createMount(); let classes; - let typographyClasses; const render = createClientRender(); before(() => { - typographyClasses = getClasses(); classes = getClasses(); }); diff --git a/packages/material-ui/src/InputAdornment/InputAdornment.test.js b/packages/material-ui/src/InputAdornment/InputAdornment.test.js index d83c9ce185f7e7..5d795ab470fe7a 100644 --- a/packages/material-ui/src/InputAdornment/InputAdornment.test.js +++ b/packages/material-ui/src/InputAdornment/InputAdornment.test.js @@ -1,7 +1,7 @@ import * as React from 'react'; import { expect } from 'chai'; import { getClasses, createMount, createClientRender, describeConformance } from 'test/utils'; -import Typography from '../Typography'; +import { typographyClasses } from '../Typography'; import InputAdornment from './InputAdornment'; import TextField from '../TextField'; import FormControl from '../FormControl'; @@ -26,7 +26,6 @@ describe('', () => { it('should wrap text children in a Typography', () => { const { container } = render(foo); - const typographyClasses = getClasses(); const typography = container.querySelector(`.${typographyClasses.root}`); expect(typography).not.to.equal(null); @@ -161,7 +160,6 @@ describe('', () => { foo , ); - const typographyClasses = getClasses(); expect(container.querySelector(`.${typographyClasses.root}`)).to.equal(null); }); diff --git a/packages/material-ui/src/Link/Link.test.js b/packages/material-ui/src/Link/Link.test.js index 10a757c07d8a6e..1e9b972a715900 100644 --- a/packages/material-ui/src/Link/Link.test.js +++ b/packages/material-ui/src/Link/Link.test.js @@ -10,7 +10,7 @@ import { fireEvent, } from 'test/utils'; import Link from './Link'; -import Typography from '../Typography'; +import Typography, { typographyClasses } from '../Typography'; function focusVisible(element) { act(() => { @@ -24,11 +24,9 @@ describe('', () => { const mount = createMount(); const render = createClientRender(); let classes; - let typographyClasses; before(() => { classes = getClasses(Home); - typographyClasses = getClasses(); }); describeConformance(Home, () => ({ diff --git a/packages/material-ui/src/ListItemText/ListItemText.test.js b/packages/material-ui/src/ListItemText/ListItemText.test.js index 7181636e79e247..687eeecf416ea3 100644 --- a/packages/material-ui/src/ListItemText/ListItemText.test.js +++ b/packages/material-ui/src/ListItemText/ListItemText.test.js @@ -1,18 +1,16 @@ import * as React from 'react'; import { expect } from 'chai'; import { getClasses, createMount, createClientRender, describeConformance } from 'test/utils'; -import Typography from '../Typography'; +import Typography, { typographyClasses } from '../Typography'; import ListItemText from './ListItemText'; describe('', () => { const mount = createMount(); const render = createClientRender(); let classes; - let typographyClasses; before(() => { classes = getClasses(); - typographyClasses = getClasses(); }); describeConformance(, () => ({ diff --git a/packages/material-ui/src/StepLabel/StepLabel.test.js b/packages/material-ui/src/StepLabel/StepLabel.test.js index 747985cda0d3fe..2851ea3c8c101e 100644 --- a/packages/material-ui/src/StepLabel/StepLabel.test.js +++ b/packages/material-ui/src/StepLabel/StepLabel.test.js @@ -1,7 +1,7 @@ import * as React from 'react'; import { expect } from 'chai'; import { getClasses, createClientRender, createMount, describeConformance } from 'test/utils'; -import Typography from '../Typography'; +import Typography, { typographyClasses } from '../Typography'; import Stepper from '../Stepper'; import Step from '../Step'; import StepIcon from '../StepIcon'; @@ -10,14 +10,12 @@ import StepLabel from './StepLabel'; describe('', () => { let classes; let iconClasses; - let typographyClasses; const mount = createMount({ strict: true }); const render = createClientRender(); before(() => { classes = getClasses(); iconClasses = getClasses(); - typographyClasses = getClasses(); }); describeConformance(, () => ({ diff --git a/packages/material-ui/src/Typography/Typography.js b/packages/material-ui/src/Typography/Typography.js index 49033ad16b7707..05401c3c660e98 100644 --- a/packages/material-ui/src/Typography/Typography.js +++ b/packages/material-ui/src/Typography/Typography.js @@ -1,106 +1,69 @@ 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 experimentalStyled from '../styles/experimentalStyled'; +import useThemeProps from '../styles/useThemeProps'; import capitalize from '../utils/capitalize'; +import typographyClasses, { getTypographyUtilityClass } from './typographyClasses'; -export const styles = (theme) => ({ - /* Styles applied to the root element. */ - root: { - margin: 0, - }, - /* Styles applied to the root element if `variant="body2"`. */ - body2: theme.typography.body2, - /* Styles applied to the root element if `variant="body1"`. */ - body1: theme.typography.body1, - /* Styles applied to the root element if `variant="caption"`. */ - caption: theme.typography.caption, - /* Styles applied to the root element if `variant="button"`. */ - button: theme.typography.button, - /* Styles applied to the root element if `variant="h1"`. */ - h1: theme.typography.h1, - /* Styles applied to the root element if `variant="h2"`. */ - h2: theme.typography.h2, - /* Styles applied to the root element if `variant="h3"`. */ - h3: theme.typography.h3, - /* Styles applied to the root element if `variant="h4"`. */ - h4: theme.typography.h4, - /* Styles applied to the root element if `variant="h5"`. */ - h5: theme.typography.h5, - /* Styles applied to the root element if `variant="h6"`. */ - h6: theme.typography.h6, - /* Styles applied to the root element if `variant="subtitle1"`. */ - subtitle1: theme.typography.subtitle1, - /* Styles applied to the root element if `variant="subtitle2"`. */ - subtitle2: theme.typography.subtitle2, - /* Styles applied to the root element if `variant="overline"`. */ - overline: theme.typography.overline, - /* Styles applied to the root element if `variant="inherit"`. */ - inherit: {}, - /* Styles applied to the root element if `align="left"`. */ - alignLeft: { - textAlign: 'left', - }, - /* Styles applied to the root element if `align="center"`. */ - alignCenter: { - textAlign: 'center', - }, - /* Styles applied to the root element if `align="right"`. */ - alignRight: { - textAlign: 'right', - }, - /* Styles applied to the root element if `align="justify"`. */ - alignJustify: { - textAlign: 'justify', - }, - /* Styles applied to the root element if `nowrap={true}`. */ - noWrap: { +const getTextColor = (color, palette) => { + if (color.indexOf('text') === 0) { + return palette.text[color.split('text').pop().toLowerCase()]; + } + + if (color === 'inherit' || color === 'initial') { + return color; + } + + return palette[color].main; +}; + +const overridesResolver = (props, styles) => { + const { styleProps = {} } = props; + + const styleOverrides = { + ...styles.root, + ...(styleProps.variant && styles[styleProps.variant]), + ...(styleProps.color && styles[`color${capitalize(styleProps.color)}`]), + ...(styleProps.align && styles[`align${capitalize(styleProps.align)}`]), + ...(styleProps.display && styles[`display${capitalize(styleProps.display)}`]), + ...(styleProps.noWrap && styles.noWrap), + ...(styleProps.gutterBottom && styles.gutterBottom), + ...(styleProps.paragraph && styles.paragraph), + }; + + return styleOverrides; +}; + +export const TypographyRoot = experimentalStyled( + 'span', + {}, + { name: 'Typography', slot: 'Root', overridesResolver }, +)((props) => ({ + margin: 0, + ...(props.styleProps.variant && props.theme.typography[props.styleProps.variant]), + ...(props.styleProps.align !== 'inherit' && { + textAlign: props.styleProps.align, + }), + ...(props.styleProps.noWrap && { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', - }, - /* Styles applied to the root element if `gutterBottom={true}`. */ - gutterBottom: { + }), + ...(props.styleProps.gutterBottom && { marginBottom: '0.35em', - }, - /* Styles applied to the root element if `paragraph={true}`. */ - paragraph: { + }), + ...(props.styleProps.paragraph && { marginBottom: 16, - }, - /* Styles applied to the root element if `color="inherit"`. */ - colorInherit: { - color: 'inherit', - }, - /* Styles applied to the root element if `color="primary"`. */ - colorPrimary: { - color: theme.palette.primary.main, - }, - /* Styles applied to the root element if `color="secondary"`. */ - colorSecondary: { - color: theme.palette.secondary.main, - }, - /* Styles applied to the root element if `color="textPrimary"`. */ - colorTextPrimary: { - color: theme.palette.text.primary, - }, - /* Styles applied to the root element if `color="textSecondary"`. */ - colorTextSecondary: { - color: theme.palette.text.secondary, - }, - /* Styles applied to the root element if `color="error"`. */ - colorError: { - color: theme.palette.error.main, - }, - /* Styles applied to the root element if `display="inline"`. */ - displayInline: { - display: 'inline', - }, - /* Styles applied to the root element if `display="block"`. */ - displayBlock: { - display: 'block', - }, -}); + }), + ...(props.styleProps.color && + props.styleProps.color !== 'initial' && { + color: getTextColor(props.styleProps.color, props.theme.palette), + }), + ...(props.styleProps.display !== 'initial' && { + display: props.styleProps.display, + }), +})); const defaultVariantMapping = { h1: 'h1', @@ -116,10 +79,40 @@ const defaultVariantMapping = { inherit: 'p', }; -const Typography = React.forwardRef(function Typography(props, ref) { +const useTypographyClasses = (props) => { + const { align, color, display, gutterBottom, noWrap, paragraph, variant, classes = {} } = props; + + const utilityClasses = { + root: clsx( + typographyClasses['root'], + classes['root'], + getTypographyUtilityClass(`color${capitalize(color)}`), + classes[`color${capitalize(color)}`], + typographyClasses[`align${capitalize(align)}`], + classes[`align${capitalize(align)}`], + typographyClasses[`display${capitalize(display)}`], + classes[`display${capitalize(display)}`], + getTypographyUtilityClass(variant), + classes[variant], + { + [typographyClasses['gutterBottom']]: gutterBottom, + [classes['gutterBottom']]: gutterBottom, + [typographyClasses['noWrap']]: noWrap, + [classes['noWrap']]: noWrap, + [typographyClasses['paragraph']]: paragraph, + [classes['paragraph']]: paragraph, + }, + ), + }; + + return utilityClasses; +}; + +const Typography = React.forwardRef(function Typography(inProps, ref) { + const props = useThemeProps({ props: inProps, name: 'MuiTypography' }); + const { align = 'inherit', - classes, className, color = 'initial', component, @@ -132,43 +125,33 @@ const Typography = React.forwardRef(function Typography(props, ref) { ...other } = props; - const themeVariantsClasses = useThemeVariants( - { - ...props, - align, - color, - display, - gutterBottom, - noWrap, - paragraph, - variant, - variantMapping, - }, - 'MuiTypography', - ); + const stateAndProps = { + ...props, + align, + className, + color, + component, + display, + gutterBottom, + noWrap, + paragraph, + variant, + variantMapping, + }; const Component = component || (paragraph ? 'p' : variantMapping[variant] || defaultVariantMapping[variant]) || 'span'; + const classes = useTypographyClasses(stateAndProps); + return ( - ); @@ -282,4 +265,4 @@ Typography.propTypes = { variantMapping: PropTypes /* @typescript-to-proptypes-ignore */.object, }; -export default withStyles(styles, { name: 'MuiTypography' })(Typography); +export default Typography; diff --git a/packages/material-ui/src/Typography/Typography.test.js b/packages/material-ui/src/Typography/Typography.test.js index 4c62786a1b77c5..9516f56d762223 100644 --- a/packages/material-ui/src/Typography/Typography.test.js +++ b/packages/material-ui/src/Typography/Typography.test.js @@ -1,32 +1,26 @@ // @ts-check import * as React from 'react'; import { expect } from 'chai'; -import { getClasses, createClientRender, createMount, describeConformance } from 'test/utils'; +import { createClientRender, createMount, describeConformanceV5 } from 'test/utils'; import Typography from './Typography'; +import classes from './typographyClasses'; describe('', () => { /** * @type {ReturnType} */ const mount = createMount(); - /** - * // we test at runtime that this is equal to - * Record - * @type {Record} - */ - let classes; const render = createClientRender(); - before(() => { - classes = getClasses(); - }); - - describeConformance(, () => ({ + describeConformanceV5(, () => ({ classes, inheritComponent: 'p', mount, refInstanceof: window.HTMLParagraphElement, + muiName: 'MuiTypography', + testVariantProps: { color: 'secondary', variant: 'dot' }, + skip: ['componentsProp'], })); it('should render the text', () => { @@ -50,17 +44,30 @@ describe('', () => { expect(container.firstChild).to.have.class(classes.alignCenter); }); - ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'subtitle1', 'body2', 'body1', 'caption', 'button'].forEach( - (variant) => { - it(`should render ${variant} text`, () => { - // @ts-ignore literal/tuple type widening - const { container } = render(Hello); - - expect(classes[variant] != null).to.equal(true); - expect(container.firstChild).to.have.class(classes[variant]); - }); - }, - ); + [ + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'subtitle1', + 'body2', + 'body1', + 'caption', + 'button', + 'overline', + ].forEach((variant) => { + it(`should render ${variant} text`, () => { + // @ts-ignore literal/tuple type widening + const { container } = render(Hello); + + expect(classes).to.have.property(variant); + + // @ts-ignore + expect(container.firstChild).to.have.class(classes[variant]); + }); + }); [ ['primary', 'colorPrimary'], @@ -73,7 +80,9 @@ describe('', () => { // @ts-ignore literal/tuple type widening const { container } = render(Hello); + // @ts-ignore expect(classes[className] != null).to.equal(true); + // @ts-ignore expect(container.firstChild).to.have.class(classes[className]); }); }); diff --git a/packages/material-ui/src/Typography/index.js b/packages/material-ui/src/Typography/index.js index eabc291fd82ca6..64ab72d2bf4100 100644 --- a/packages/material-ui/src/Typography/index.js +++ b/packages/material-ui/src/Typography/index.js @@ -1 +1,2 @@ export { default } from './Typography'; +export { default as typographyClasses, getTypographyUtilityClass } from './typographyClasses'; diff --git a/packages/material-ui/src/Typography/typographyClasses.js b/packages/material-ui/src/Typography/typographyClasses.js new file mode 100644 index 00000000000000..c187797c7dde81 --- /dev/null +++ b/packages/material-ui/src/Typography/typographyClasses.js @@ -0,0 +1,38 @@ +export function getTypographyUtilityClass(name) { + return `MuiTypography-${name}`; +} + +const typographyClasses = { + root: getTypographyUtilityClass('root'), + h1: getTypographyUtilityClass('h1'), + h2: getTypographyUtilityClass('h2'), + h3: getTypographyUtilityClass('h3'), + h4: getTypographyUtilityClass('h4'), + h5: getTypographyUtilityClass('h5'), + h6: getTypographyUtilityClass('h6'), + subtitle1: getTypographyUtilityClass('subtitle1'), + subtitle2: getTypographyUtilityClass('subtitle2'), + body1: getTypographyUtilityClass('body1'), + body2: getTypographyUtilityClass('body2'), + inherit: getTypographyUtilityClass('inherit'), + button: getTypographyUtilityClass('button'), + caption: getTypographyUtilityClass('caption'), + overline: getTypographyUtilityClass('overline'), + alignLeft: getTypographyUtilityClass('alignLeft'), + alignRight: getTypographyUtilityClass('alignRight'), + alignCenter: getTypographyUtilityClass('alignCenter'), + alignJustify: getTypographyUtilityClass('alignJustify'), + noWrap: getTypographyUtilityClass('noWrap'), + gutterBottom: getTypographyUtilityClass('gutterBottom'), + paragraph: getTypographyUtilityClass('paragraph'), + colorInherit: getTypographyUtilityClass('colorInherit'), + colorPrimary: getTypographyUtilityClass('colorPrimary'), + colorSecondary: getTypographyUtilityClass('colorSecondary'), + colorTextPrimary: getTypographyUtilityClass('colorTextPrimary'), + colorTextSecondary: getTypographyUtilityClass('colorTextSecondary'), + colorError: getTypographyUtilityClass('colorError'), + displayInline: getTypographyUtilityClass('displayInline'), + displayBlock: getTypographyUtilityClass('displayBlock'), +}; + +export default typographyClasses;