diff --git a/.eslintrc.js b/.eslintrc.js index 7806144472207c..205c9b95d5021e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -124,6 +124,7 @@ module.exports = { 'material-ui/docgen-ignore-before-comment': 'error', 'material-ui/rules-of-use-theme-variants': 'error', 'material-ui/no-empty-box': 'error', + 'material-ui/no-styled-box': 'error', 'material-ui/straight-quotes': 'error', 'react-hooks/exhaustive-deps': ['error', { additionalHooks: 'useEnhancedEffect' }], diff --git a/docs/data/joy/main-features/color-inversion/ColorInversionAnyParentStyled.js b/docs/data/joy/main-features/color-inversion/ColorInversionAnyParentStyled.js index ce8f5e342f33e6..77f397766173b6 100644 --- a/docs/data/joy/main-features/color-inversion/ColorInversionAnyParentStyled.js +++ b/docs/data/joy/main-features/color-inversion/ColorInversionAnyParentStyled.js @@ -6,7 +6,7 @@ import Box from '@mui/joy/Box'; import Button from '@mui/joy/Button'; import Typography from '@mui/joy/Typography'; -const StyledBox = styled(Box)( +const StyledBox = styled('div')( ({ theme }) => ({ padding: 32, display: 'grid', diff --git a/docs/data/joy/main-features/color-inversion/ColorInversionAnyParentStyled.tsx b/docs/data/joy/main-features/color-inversion/ColorInversionAnyParentStyled.tsx index 81db36a1540b5f..a010716f89b829 100644 --- a/docs/data/joy/main-features/color-inversion/ColorInversionAnyParentStyled.tsx +++ b/docs/data/joy/main-features/color-inversion/ColorInversionAnyParentStyled.tsx @@ -5,7 +5,7 @@ import Box from '@mui/joy/Box'; import Button from '@mui/joy/Button'; import Typography from '@mui/joy/Typography'; -const StyledBox = styled(Box)( +const StyledBox = styled('div')( ({ theme }) => ({ padding: 32, display: 'grid', diff --git a/docs/data/material/components/drawers/SwipeableEdgeDrawer.js b/docs/data/material/components/drawers/SwipeableEdgeDrawer.js index 6084e75f007ecd..2850ef11b510a0 100644 --- a/docs/data/material/components/drawers/SwipeableEdgeDrawer.js +++ b/docs/data/material/components/drawers/SwipeableEdgeDrawer.js @@ -18,11 +18,11 @@ const Root = styled('div')(({ theme }) => ({ theme.palette.mode === 'light' ? grey[100] : theme.palette.background.default, })); -const StyledBox = styled(Box)(({ theme }) => ({ +const StyledBox = styled('div')(({ theme }) => ({ backgroundColor: theme.palette.mode === 'light' ? '#fff' : grey[800], })); -const Puller = styled(Box)(({ theme }) => ({ +const Puller = styled('div')(({ theme }) => ({ width: 30, height: 6, backgroundColor: theme.palette.mode === 'light' ? grey[300] : grey[900], diff --git a/docs/data/material/components/drawers/SwipeableEdgeDrawer.tsx b/docs/data/material/components/drawers/SwipeableEdgeDrawer.tsx index 62cbbdbc33a79e..76fa98d56759e7 100644 --- a/docs/data/material/components/drawers/SwipeableEdgeDrawer.tsx +++ b/docs/data/material/components/drawers/SwipeableEdgeDrawer.tsx @@ -25,11 +25,11 @@ const Root = styled('div')(({ theme }) => ({ theme.palette.mode === 'light' ? grey[100] : theme.palette.background.default, })); -const StyledBox = styled(Box)(({ theme }) => ({ +const StyledBox = styled('div')(({ theme }) => ({ backgroundColor: theme.palette.mode === 'light' ? '#fff' : grey[800], })); -const Puller = styled(Box)(({ theme }) => ({ +const Puller = styled('div')(({ theme }) => ({ width: 30, height: 6, backgroundColor: theme.palette.mode === 'light' ? grey[300] : grey[900], diff --git a/docs/data/material/components/material-icons/SearchIcons.js b/docs/data/material/components/material-icons/SearchIcons.js index 294873ed691edf..30863dcf8a4d69 100644 --- a/docs/data/material/components/material-icons/SearchIcons.js +++ b/docs/data/material/components/material-icons/SearchIcons.js @@ -1,7 +1,6 @@ import * as React from 'react'; import { styled } from '@mui/material/styles'; import MuiPaper from '@mui/material/Paper'; -import Box from '@mui/material/Box'; import copy from 'clipboard-copy'; import InputBase from '@mui/material/InputBase'; import Typography from '@mui/material/Typography'; @@ -209,7 +208,7 @@ const Title = styled(Typography)(({ theme }) => ({ }, })); -const CanvasComponent = styled(Box)(({ theme }) => ({ +const CanvasComponent = styled('div')(({ theme }) => ({ fontSize: 210, marginTop: theme.spacing(2), color: theme.palette.text.primary, @@ -226,7 +225,7 @@ const FontSizeComponent = styled('span')(({ theme }) => ({ margin: theme.spacing(2), })); -const ContextComponent = styled(Box, { +const ContextComponent = styled('div', { shouldForwardProp: (prop) => prop !== 'contextColor', })(({ theme, contextColor }) => ({ margin: theme.spacing(0.5), diff --git a/docs/src/pages/premium-themes/onepirate/modules/form/FormFeedback.js b/docs/src/pages/premium-themes/onepirate/modules/form/FormFeedback.js index b32b2b7113414b..88059b2389868f 100644 --- a/docs/src/pages/premium-themes/onepirate/modules/form/FormFeedback.js +++ b/docs/src/pages/premium-themes/onepirate/modules/form/FormFeedback.js @@ -1,10 +1,10 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { experimentalStyled as styled } from '@mui/material/styles'; -import Box from '@mui/material/Box'; + import Typography from '../components/Typography'; -const BoxStyled = styled(Box, { +const Root = styled('div', { shouldForwardProp: (prop) => prop !== 'error' && prop !== 'success', })(({ theme, error, success }) => ({ padding: theme.spacing(2), @@ -22,9 +22,9 @@ function FormFeedback(props) { const { className, children, error, success, ...others } = props; return ( - + {children} - + ); } diff --git a/docs/src/pages/premium-themes/onepirate/modules/form/FormFeedback.tsx b/docs/src/pages/premium-themes/onepirate/modules/form/FormFeedback.tsx index f876145ed91cc9..98946408e05ba7 100644 --- a/docs/src/pages/premium-themes/onepirate/modules/form/FormFeedback.tsx +++ b/docs/src/pages/premium-themes/onepirate/modules/form/FormFeedback.tsx @@ -1,14 +1,15 @@ import * as React from 'react'; -import { experimentalStyled as styled } from '@mui/material/styles'; -import Box, { BoxProps as MuiBoxProps } from '@mui/material/Box'; +import { experimentalStyled as styled, Theme } from '@mui/material/styles'; +import { SxProps } from '@mui/system'; import Typography from '../components/Typography'; -interface FormFeedbackProps extends MuiBoxProps { +interface FormFeedbackProps extends React.HTMLAttributes { error?: boolean; success?: boolean; + sx?: SxProps; } -const BoxStyled = styled(Box, { +const Root = styled('div', { shouldForwardProp: (prop) => prop !== 'error' && prop !== 'success', })(({ theme, error, success }) => ({ padding: theme.spacing(2), @@ -22,16 +23,12 @@ const BoxStyled = styled(Box, { }), })); -function FormFeedback( - props: React.HTMLAttributes & FormFeedbackProps, -) { +export default function FormFeedback(props: FormFeedbackProps) { const { className, children, error, success, ...others } = props; return ( - + {children} - + ); } - -export default FormFeedback; diff --git a/docs/src/pages/premium-themes/onepirate/modules/views/ProductHeroLayout.js b/docs/src/pages/premium-themes/onepirate/modules/views/ProductHeroLayout.js index ef69d6f33fe602..52972ba02c2fc7 100644 --- a/docs/src/pages/premium-themes/onepirate/modules/views/ProductHeroLayout.js +++ b/docs/src/pages/premium-themes/onepirate/modules/views/ProductHeroLayout.js @@ -17,7 +17,7 @@ const ProductHeroLayoutRoot = styled('section')(({ theme }) => ({ }, })); -const Background = styled(Box)({ +const Background = styled('div')({ position: 'absolute', left: 0, right: 0, diff --git a/docs/src/pages/premium-themes/onepirate/modules/views/ProductHeroLayout.tsx b/docs/src/pages/premium-themes/onepirate/modules/views/ProductHeroLayout.tsx index 4cc511f8e5c6e7..ec6b4eae21618c 100644 --- a/docs/src/pages/premium-themes/onepirate/modules/views/ProductHeroLayout.tsx +++ b/docs/src/pages/premium-themes/onepirate/modules/views/ProductHeroLayout.tsx @@ -16,7 +16,7 @@ const ProductHeroLayoutRoot = styled('section')(({ theme }) => ({ }, })); -const Background = styled(Box)({ +const Background = styled('div')({ position: 'absolute', left: 0, right: 0, diff --git a/packages/eslint-plugin-material-ui/src/index.js b/packages/eslint-plugin-material-ui/src/index.js index ac8619f6224f57..ae6e389b24cc00 100644 --- a/packages/eslint-plugin-material-ui/src/index.js +++ b/packages/eslint-plugin-material-ui/src/index.js @@ -6,5 +6,6 @@ module.exports.rules = { 'no-hardcoded-labels': require('./rules/no-hardcoded-labels'), 'rules-of-use-theme-variants': require('./rules/rules-of-use-theme-variants'), 'no-empty-box': require('./rules/no-empty-box'), + 'no-styled-box': require('./rules/no-styled-box'), 'straight-quotes': require('./rules/straight-quotes'), }; diff --git a/packages/eslint-plugin-material-ui/src/rules/no-styled-box.js b/packages/eslint-plugin-material-ui/src/rules/no-styled-box.js new file mode 100644 index 00000000000000..e17beba4ff4ac8 --- /dev/null +++ b/packages/eslint-plugin-material-ui/src/rules/no-styled-box.js @@ -0,0 +1,55 @@ +// Copy from https://github.com/eslint/eslint/blob/95075251fb3ce35aaf7eadbd1d0a737106c13ec6/lib/rules/utils/ast-utils.js#L299 +// Why is this not exported by ESLint? +/** + * Retrieve `ChainExpression#expression` value if the given node a `ChainExpression` node. Otherwise, pass through it. + * @param {ASTNode} node The node to address. + * @returns {ASTNode} The `ChainExpression#expression` value if the node is a `ChainExpression` node. Otherwise, the node. + */ +function skipChainExpression(node) { + return node && node.type === 'ChainExpression' ? node.expression : node; +} + +/** + * @type {import('eslint').Rule.RuleModule} + */ +const rule = { + meta: { + docs: { + description: 'Disallow use of styled(Box), we prefer the sx prop over system props.', + }, + messages: { + noBox: "The use of styled(Box) is not allowed, use styled('div') instead.", + }, + type: 'suggestion', + fixable: 'code', + schema: [], + }, + create(context) { + return { + CallExpression(node) { + const callee = skipChainExpression(node.callee); + if (callee.type !== 'Identifier') { + return; + } + if (callee.name !== 'styled') { + return; + } + if (!node.arguments[0]) { + return; + } + + if (node.arguments[0].type === 'Identifier' && node.arguments[0].name === 'Box') { + context.report({ + node, + messageId: 'noBox', + fix: (fixer) => { + return fixer.replaceText(node.arguments[0], "'div'"); + }, + }); + } + }, + }; + }, +}; + +module.exports = rule; diff --git a/packages/eslint-plugin-material-ui/src/rules/no-styled-box.test.js b/packages/eslint-plugin-material-ui/src/rules/no-styled-box.test.js new file mode 100644 index 00000000000000..35cfe116efa930 --- /dev/null +++ b/packages/eslint-plugin-material-ui/src/rules/no-styled-box.test.js @@ -0,0 +1,72 @@ +const eslint = require('eslint'); +const rule = require('./no-styled-box'); + +const ruleTester = new eslint.RuleTester({ + parser: require.resolve('@typescript-eslint/parser'), + parserOptions: { + ecmaFeatures: { jsx: true }, + }, +}); + +ruleTester.run('no-styled-box', rule, { + valid: [ + ` +import { styled } from '@mui/system'; +styled('div'); +`, + ` +import { styled } from '@mui/system'; +styled('div', {}); +`, + ], + invalid: [ + { + code: ` +import { styled } from '@mui/system'; +import Box from '@mui/material/Box'; + +const foo = styled(Box)({ + color: 'red', +}); +`, + errors: [ + { + messageId: 'noBox', + type: 'CallExpression', + }, + ], + output: ` +import { styled } from '@mui/system'; +import Box from '@mui/material/Box'; + +const foo = styled('div')({ + color: 'red', +}); +`, + }, + { + code: ` +import { styled } from '@mui/system'; +import Box from '@mui/material/Box'; + +const foo = styled(Box, {})({ + color: 'red', +}); +`, + errors: [ + { + messageId: 'noBox', + type: 'CallExpression', + }, + ], + output: ` +import { styled } from '@mui/system'; +import Box from '@mui/material/Box'; + +const foo = styled('div', {})({ + color: 'red', +}); +`, + }, + ], +}); diff --git a/packages/mui-joy/src/colorInversion/colorInversionUtils.spec.tsx b/packages/mui-joy/src/colorInversion/colorInversionUtils.spec.tsx index cad5e83fc348c8..b4172f201e3f20 100644 --- a/packages/mui-joy/src/colorInversion/colorInversionUtils.spec.tsx +++ b/packages/mui-joy/src/colorInversion/colorInversionUtils.spec.tsx @@ -21,7 +21,7 @@ import Box from '@mui/joy/Box'; /** * styled API type check */ -const StyledBox = styled(Box)( +const StyledBox = styled('div')( ({ theme }) => ({ padding: 32, display: 'grid', diff --git a/packages/mui-joy/src/colorInversion/colorInversionUtils.test.tsx b/packages/mui-joy/src/colorInversion/colorInversionUtils.test.tsx index 6bb5637509a8f7..b96b8d12b858a1 100644 --- a/packages/mui-joy/src/colorInversion/colorInversionUtils.test.tsx +++ b/packages/mui-joy/src/colorInversion/colorInversionUtils.test.tsx @@ -15,7 +15,7 @@ describe('colorInversionUtil', () => { it('should not throw error using styled API', () => { expect(() => { - styled(Box)(applySoftInversion('primary'), applySolidInversion('primary')); + styled('div')(applySoftInversion('primary'), applySolidInversion('primary')); }).not.to.throw(); }); }); diff --git a/packages/mui-system/src/Box/Box.spec.tsx b/packages/mui-system/src/Box/Box.spec.tsx index 022b6e89df9b7f..2e2dc63d4debbe 100644 --- a/packages/mui-system/src/Box/Box.spec.tsx +++ b/packages/mui-system/src/Box/Box.spec.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Box, Theme, styled } from '@mui/system'; +import { Box, styled } from '@mui/system'; interface TestProps { test?: string; @@ -105,6 +105,7 @@ function TestFillPropCallback() { />; } +// eslint-disable-next-line material-ui/no-styled-box const StyledBox = styled(Box)` color: white; `;