diff --git a/.eslintrc.js b/.eslintrc.js index 670e229c62378b..c716674a2f2128 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -120,6 +120,10 @@ module.exports = /** @type {Config} */ ({ variables: true, }, ], + '@typescript-eslint/no-unused-vars': [ + 'error', + { vars: 'all', args: 'after-used', ignoreRestSiblings: true, argsIgnorePattern: '^_' }, + ], 'no-use-before-define': 'off', // disabled type-aware linting due to performance considerations diff --git a/docs/data/material/getting-started/templates/shared-theme/AppTheme.js b/docs/data/material/getting-started/templates/shared-theme/AppTheme.js index b1d55ca4408a14..6fde1fc9fd9be3 100644 --- a/docs/data/material/getting-started/templates/shared-theme/AppTheme.js +++ b/docs/data/material/getting-started/templates/shared-theme/AppTheme.js @@ -9,7 +9,8 @@ import { navigationCustomizations } from './customizations/navigation'; import { surfacesCustomizations } from './customizations/surfaces'; import { colorSchemes, typography, shadows, shape } from './themePrimitives'; -function AppTheme({ children, disableCustomTheme, themeComponents }) { +function AppTheme(props) { + const { children, disableCustomTheme, themeComponents } = props; const theme = React.useMemo(() => { return disableCustomTheme ? {} diff --git a/docs/data/material/getting-started/templates/shared-theme/AppTheme.tsx b/docs/data/material/getting-started/templates/shared-theme/AppTheme.tsx index 1fe35d6faeef48..a4a512c30aef59 100644 --- a/docs/data/material/getting-started/templates/shared-theme/AppTheme.tsx +++ b/docs/data/material/getting-started/templates/shared-theme/AppTheme.tsx @@ -17,11 +17,8 @@ interface AppThemeProps { themeComponents?: ThemeOptions['components']; } -export default function AppTheme({ - children, - disableCustomTheme, - themeComponents, -}: AppThemeProps) { +export default function AppTheme(props: AppThemeProps) { + const { children, disableCustomTheme, themeComponents } = props; const theme = React.useMemo(() => { return disableCustomTheme ? {} diff --git a/docs/pages/joy-ui/api/stack.json b/docs/pages/joy-ui/api/stack.json index 008afff7737bbc..8826d1eebc8a85 100644 --- a/docs/pages/joy-ui/api/stack.json +++ b/docs/pages/joy-ui/api/stack.json @@ -6,14 +6,16 @@ "type": { "name": "union", "description": "'column-reverse'
| 'column'
| 'row-reverse'
| 'row'
| Array<'column-reverse'
| 'column'
| 'row-reverse'
| 'row'>
| object" - } + }, + "default": "'column'" }, "divider": { "type": { "name": "node" } }, "spacing": { "type": { "name": "union", "description": "Array<number
| string>
| number
| object
| string" - } + }, + "default": "0" }, "sx": { "type": { @@ -22,7 +24,7 @@ }, "additionalInfo": { "sx": true } }, - "useFlexGap": { "type": { "name": "bool" } } + "useFlexGap": { "type": { "name": "bool" }, "default": "false" } }, "name": "Stack", "imports": ["import Stack from '@mui/joy/Stack';", "import { Stack } from '@mui/joy';"], diff --git a/docs/pages/material-ui/api/container.json b/docs/pages/material-ui/api/container.json index 29b78f807d6157..0f60f58e920597 100644 --- a/docs/pages/material-ui/api/container.json +++ b/docs/pages/material-ui/api/container.json @@ -2,13 +2,14 @@ "props": { "classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } }, "component": { "type": { "name": "elementType" } }, - "disableGutters": { "type": { "name": "bool" } }, - "fixed": { "type": { "name": "bool" } }, + "disableGutters": { "type": { "name": "bool" }, "default": "false" }, + "fixed": { "type": { "name": "bool" }, "default": "false" }, "maxWidth": { "type": { "name": "union", "description": "'xs'
| 'sm'
| 'md'
| 'lg'
| 'xl'
| false
| string" - } + }, + "default": "'lg'" }, "sx": { "type": { diff --git a/docs/pages/material-ui/api/grid-2.json b/docs/pages/material-ui/api/grid-2.json index c4b4b854151cfd..7f74fa6b051eef 100644 --- a/docs/pages/material-ui/api/grid-2.json +++ b/docs/pages/material-ui/api/grid-2.json @@ -5,7 +5,8 @@ "type": { "name": "union", "description": "Array<number>
| number
| object" - } + }, + "default": "12" }, "columnSpacing": { "type": { @@ -13,12 +14,13 @@ "description": "Array<number
| string>
| number
| object
| string" } }, - "container": { "type": { "name": "bool" } }, + "container": { "type": { "name": "bool" }, "default": "false" }, "direction": { "type": { "name": "union", "description": "'column-reverse'
| 'column'
| 'row-reverse'
| 'row'
| Array<'column-reverse'
| 'column'
| 'row-reverse'
| 'row'>
| object" - } + }, + "default": "'row'" }, "offset": { "type": { @@ -42,13 +44,15 @@ "type": { "name": "union", "description": "Array<number
| string>
| number
| object
| string" - } + }, + "default": "0" }, "wrap": { "type": { "name": "enum", "description": "'nowrap'
| 'wrap-reverse'
| 'wrap'" - } + }, + "default": "'wrap'" } }, "name": "Grid2", diff --git a/docs/pages/material-ui/api/stack.json b/docs/pages/material-ui/api/stack.json index ae424d7b57eea6..65e1dc1c8a28d2 100644 --- a/docs/pages/material-ui/api/stack.json +++ b/docs/pages/material-ui/api/stack.json @@ -6,14 +6,16 @@ "type": { "name": "union", "description": "'column-reverse'
| 'column'
| 'row-reverse'
| 'row'
| Array<'column-reverse'
| 'column'
| 'row-reverse'
| 'row'>
| object" - } + }, + "default": "'column'" }, "divider": { "type": { "name": "node" } }, "spacing": { "type": { "name": "union", "description": "Array<number
| string>
| number
| object
| string" - } + }, + "default": "0" }, "sx": { "type": { @@ -22,7 +24,7 @@ }, "additionalInfo": { "sx": true } }, - "useFlexGap": { "type": { "name": "bool" } } + "useFlexGap": { "type": { "name": "bool" }, "default": "false" } }, "name": "Stack", "imports": ["import Stack from '@mui/material/Stack';", "import { Stack } from '@mui/material';"], diff --git a/docs/pages/system/api/container.json b/docs/pages/system/api/container.json index 3f094034d61076..ae5bd4660c2576 100644 --- a/docs/pages/system/api/container.json +++ b/docs/pages/system/api/container.json @@ -2,13 +2,14 @@ "props": { "classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } }, "component": { "type": { "name": "elementType" } }, - "disableGutters": { "type": { "name": "bool" } }, - "fixed": { "type": { "name": "bool" } }, + "disableGutters": { "type": { "name": "bool" }, "default": "false" }, + "fixed": { "type": { "name": "bool" }, "default": "false" }, "maxWidth": { "type": { "name": "union", "description": "'xs'
| 'sm'
| 'md'
| 'lg'
| 'xl'
| false
| string" - } + }, + "default": "'lg'" }, "sx": { "type": { diff --git a/docs/pages/system/api/grid.json b/docs/pages/system/api/grid.json index 3a3c9d23850287..4cd038309b7d7b 100644 --- a/docs/pages/system/api/grid.json +++ b/docs/pages/system/api/grid.json @@ -5,7 +5,8 @@ "type": { "name": "union", "description": "Array<number>
| number
| object" - } + }, + "default": "12" }, "columnSpacing": { "type": { @@ -13,12 +14,13 @@ "description": "Array<number
| string>
| number
| object
| string" } }, - "container": { "type": { "name": "bool" } }, + "container": { "type": { "name": "bool" }, "default": "false" }, "direction": { "type": { "name": "union", "description": "'column-reverse'
| 'column'
| 'row-reverse'
| 'row'
| Array<'column-reverse'
| 'column'
| 'row-reverse'
| 'row'>
| object" - } + }, + "default": "'row'" }, "offset": { "type": { @@ -42,13 +44,15 @@ "type": { "name": "union", "description": "Array<number
| string>
| number
| object
| string" - } + }, + "default": "0" }, "wrap": { "type": { "name": "enum", "description": "'nowrap'
| 'wrap-reverse'
| 'wrap'" - } + }, + "default": "'wrap'" } }, "name": "Grid", diff --git a/docs/pages/system/api/stack.json b/docs/pages/system/api/stack.json index a5d69b0c81f265..2c838e2175b73d 100644 --- a/docs/pages/system/api/stack.json +++ b/docs/pages/system/api/stack.json @@ -6,14 +6,16 @@ "type": { "name": "union", "description": "'column-reverse'
| 'column'
| 'row-reverse'
| 'row'
| Array<'column-reverse'
| 'column'
| 'row-reverse'
| 'row'>
| object" - } + }, + "default": "'column'" }, "divider": { "type": { "name": "node" } }, "spacing": { "type": { "name": "union", "description": "Array<number
| string>
| number
| object
| string" - } + }, + "default": "0" }, "sx": { "type": { @@ -22,7 +24,7 @@ }, "additionalInfo": { "sx": true } }, - "useFlexGap": { "type": { "name": "bool" } } + "useFlexGap": { "type": { "name": "bool" }, "default": "false" } }, "name": "Stack", "imports": ["import Stack from '@mui/system/Stack';", "import { Stack } from '@mui/system';"], diff --git a/docs/src/modules/components/JoyThemeBuilder.tsx b/docs/src/modules/components/JoyThemeBuilder.tsx index 6f0feae3ffa79e..67753753411c4b 100644 --- a/docs/src/modules/components/JoyThemeBuilder.tsx +++ b/docs/src/modules/components/JoyThemeBuilder.tsx @@ -1268,7 +1268,15 @@ function getAvailableTokens(colorSchemes: any, colorMode: 'light' | 'dark') { return tokens; } -function TemplatesDialog({ children, data }: { children: React.ReactElement; data: any }) { +function TemplatesDialog({ + children, + data, +}: { + children: React.ReactElement<{ + onClick?: React.MouseEventHandler; + }>; + data: any; +}) { const [open, setOpen] = React.useState(false); const { map: templateMap } = sourceJoyTemplates(); const renderItem = (name: string, item: TemplateData) => { @@ -1339,9 +1347,9 @@ function TemplatesDialog({ children, data }: { children: React.ReactElement return ( {React.cloneElement(children, { - onClick: () => { + onClick: (event: React.MouseEvent) => { setOpen(true); - children.props.onClick?.(); + children.props.onClick?.(event); }, })} setOpen(false)}> diff --git a/packages/api-docs-builder/ApiBuilders/ComponentApiBuilder.ts b/packages/api-docs-builder/ApiBuilders/ComponentApiBuilder.ts index 577dfae4a22407..02a81285bb3ddb 100644 --- a/packages/api-docs-builder/ApiBuilders/ComponentApiBuilder.ts +++ b/packages/api-docs-builder/ApiBuilders/ComponentApiBuilder.ts @@ -697,13 +697,26 @@ export default async function generateComponentApi( const filename = componentInfo.filename; let reactApi: ComponentReactApi; - if (componentInfo.isSystemComponent || componentInfo.name === 'Grid2') { - try { + try { + reactApi = docgenParse(src, null, defaultHandlers.concat(muiDefaultPropsHandler), { + filename, + }); + } catch (error) { + // fallback to default logic if there is no `create*` definition. + if ((error as Error).message === 'No suitable component definition found.') { reactApi = docgenParse( src, (ast) => { let node; + // TODO migrate to react-docgen v6, using Babel AST now astTypes.visit(ast, { + visitFunctionDeclaration: (functionPath) => { + // @ts-ignore + if (functionPath.node.params[0].name === 'props') { + node = functionPath; + } + return false; + }, visitVariableDeclaration: (variablePath) => { const definitions: any[] = []; if (variablePath.node.declarations) { @@ -711,7 +724,6 @@ export default async function generateComponentApi( .get('declarations') .each((declarator: any) => definitions.push(declarator.get('init'))); } - definitions.forEach((definition) => { // definition.value.expression is defined when the source is in TypeScript. const expression = definition.value?.expression @@ -719,36 +731,25 @@ export default async function generateComponentApi( : definition; if (expression.value?.callee) { const definitionName = expression.value.callee.name; - if (definitionName === `create${componentInfo.name}`) { node = expression; } } }); - return false; }, }); return node; }, - defaultHandlers, - { filename }, - ); - } catch (error) { - // fallback to default logic if there is no `create*` definition. - if ((error as Error).message === 'No suitable component definition found.') { - reactApi = docgenParse(src, null, defaultHandlers.concat(muiDefaultPropsHandler), { + defaultHandlers.concat(muiDefaultPropsHandler), + { filename, - }); - } else { - throw error; - } + }, + ); + } else { + throw error; } - } else { - reactApi = docgenParse(src, null, defaultHandlers.concat(muiDefaultPropsHandler), { - filename, - }); } if (!reactApi.props) { diff --git a/packages/api-docs-builder/utils/defaultPropsHandler.ts b/packages/api-docs-builder/utils/defaultPropsHandler.ts index 9b49c019f1af88..6836e85cb677ec 100644 --- a/packages/api-docs-builder/utils/defaultPropsHandler.ts +++ b/packages/api-docs-builder/utils/defaultPropsHandler.ts @@ -131,6 +131,11 @@ function getExplicitPropsDeclaration( ): NodePath | undefined { const functionNode = getRenderBody(componentDefinition, importer); + // No function body available to inspect. + if (!functionNode.value) { + return undefined; + } + let propsPath: NodePath | undefined; // visitVariableDeclarator, can't use visit body.node since it looses scope information functionNode diff --git a/packages/api-docs-builder/utils/getPropsFromComponentNode.ts b/packages/api-docs-builder/utils/getPropsFromComponentNode.ts index d6af8aa4274a41..f3e1c63e3d4eb6 100644 --- a/packages/api-docs-builder/utils/getPropsFromComponentNode.ts +++ b/packages/api-docs-builder/utils/getPropsFromComponentNode.ts @@ -51,6 +51,7 @@ function isStyledFunction(node: ts.VariableDeclaration): boolean { ); } +// TODO update to reflect https://github.com/DefinitelyTyped/DefinitelyTyped/pull/65135 function getJSXLikeReturnValueFromFunction(type: ts.Type, project: TypeScriptProject) { return type .getCallSignatures() diff --git a/packages/mui-base/src/ClickAwayListener/ClickAwayListener.tsx b/packages/mui-base/src/ClickAwayListener/ClickAwayListener.tsx index e84ca9b1382b42..5f8f4da604c046 100644 --- a/packages/mui-base/src/ClickAwayListener/ClickAwayListener.tsx +++ b/packages/mui-base/src/ClickAwayListener/ClickAwayListener.tsx @@ -209,7 +209,7 @@ function ClickAwayListener(props: ClickAwayListenerProps): React.JSX.Element { return undefined; }, [handleClickAway, mouseEvent]); - return {React.cloneElement(children, childrenProps)}; + return React.cloneElement(children, childrenProps); } ClickAwayListener.propTypes /* remove-proptypes */ = { diff --git a/packages/mui-base/src/NoSsr/NoSsr.tsx b/packages/mui-base/src/NoSsr/NoSsr.tsx index c637df94e70b61..8468d401c64a86 100644 --- a/packages/mui-base/src/NoSsr/NoSsr.tsx +++ b/packages/mui-base/src/NoSsr/NoSsr.tsx @@ -38,8 +38,8 @@ function NoSsr(props: NoSsrProps): React.JSX.Element { } }, [defer]); - // We need the Fragment here to force react-docgen to recognise NoSsr as a component. - return {mountedState ? children : fallback}; + // TODO casting won't be needed at one point https://github.com/DefinitelyTyped/DefinitelyTyped/pull/65135 + return (mountedState ? children : fallback) as React.JSX.Element; } NoSsr.propTypes /* remove-proptypes */ = { diff --git a/packages/mui-base/src/Portal/Portal.tsx b/packages/mui-base/src/Portal/Portal.tsx index 830b229d8eb175..af607b51532adc 100644 --- a/packages/mui-base/src/Portal/Portal.tsx +++ b/packages/mui-base/src/Portal/Portal.tsx @@ -64,14 +64,10 @@ const Portal = React.forwardRef(function Portal( }; return React.cloneElement(children, newProps); } - return {children}; + return children; } - return ( - - {mountNode ? ReactDOM.createPortal(children, mountNode) : mountNode} - - ); + return mountNode ? ReactDOM.createPortal(children, mountNode) : mountNode; }) as React.ForwardRefExoticComponent>; Portal.propTypes /* remove-proptypes */ = { diff --git a/packages/mui-material/src/Avatar/Avatar.js b/packages/mui-material/src/Avatar/Avatar.js index 50157ee01ef149..fbf36c82d5fa3a 100644 --- a/packages/mui-material/src/Avatar/Avatar.js +++ b/packages/mui-material/src/Avatar/Avatar.js @@ -163,17 +163,23 @@ const Avatar = React.forwardRef(function Avatar(inProps, ref) { let children = null; - // Use a hook instead of onError on the img element to support server-side rendering. - const loaded = useLoaded({ ...imgProps, src, srcSet }); - const hasImg = src || srcSet; - const hasImgNotFailing = hasImg && loaded !== 'error'; - const ownerState = { ...props, - colorDefault: !hasImgNotFailing, component, variant, }; + + // Use a hook instead of onError on the img element to support server-side rendering. + const loaded = useLoaded({ + ...imgProps, + ...(typeof slotProps.img === 'function' ? slotProps.img(ownerState) : slotProps.img), + src, + srcSet, + }); + const hasImg = src || srcSet; + const hasImgNotFailing = hasImg && loaded !== 'error'; + + ownerState.colorDefault = !hasImgNotFailing; // This issue explains why this is required: https://github.com/mui/material-ui/issues/42184 delete ownerState.ownerState; diff --git a/packages/mui-material/src/Avatar/Avatar.test.js b/packages/mui-material/src/Avatar/Avatar.test.js index 5f3fb5796106ee..7feddbbd36731a 100644 --- a/packages/mui-material/src/Avatar/Avatar.test.js +++ b/packages/mui-material/src/Avatar/Avatar.test.js @@ -62,6 +62,20 @@ describe('', () => { fireEvent.error(img); expect(onError.callCount).to.equal(1); }); + + it('should pass slots.img to `useLoaded` hook', () => { + const originalImage = global.Image; + const image = {}; + global.Image = function Image() { + return image; + }; + + render(); + + expect(image.crossOrigin).to.equal('anonymous'); + + global.Image = originalImage; + }); }); describe('image avatar with unrendered children', () => { diff --git a/packages/mui-material/src/ClickAwayListener/ClickAwayListener.tsx b/packages/mui-material/src/ClickAwayListener/ClickAwayListener.tsx index 6a695e84f78352..5987a8f04dc1c8 100644 --- a/packages/mui-material/src/ClickAwayListener/ClickAwayListener.tsx +++ b/packages/mui-material/src/ClickAwayListener/ClickAwayListener.tsx @@ -210,7 +210,7 @@ function ClickAwayListener(props: ClickAwayListenerProps): React.JSX.Element { return undefined; }, [handleClickAway, mouseEvent]); - return {React.cloneElement(children, childrenProps)}; + return React.cloneElement(children, childrenProps); } ClickAwayListener.propTypes /* remove-proptypes */ = { diff --git a/packages/mui-material/src/Grid2/Grid2.test.js b/packages/mui-material/src/Grid2/Grid2.test.js index 87c952bca83cd7..533a3f0502d9e4 100644 --- a/packages/mui-material/src/Grid2/Grid2.test.js +++ b/packages/mui-material/src/Grid2/Grid2.test.js @@ -1,6 +1,8 @@ import * as React from 'react'; -import { createRenderer } from '@mui/internal-test-utils'; +import { createRenderer, screen } from '@mui/internal-test-utils'; import Grid2, { grid2Classes as classes } from '@mui/material/Grid2'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; +import { expect } from 'chai'; import describeConformance from '../../test/describeConformance'; // The main tests are in mui-system Grid folder @@ -20,4 +22,31 @@ describe('', () => { testVariantProps: { container: true, spacing: 5 }, skip: ['componentsProp', 'classesRoot'], })); + + it('should render with the container class', () => { + render(); + expect(screen.getByTestId('grid')).to.have.class(classes.container); + }); + + it('should have container styles passed from theme', () => { + const theme = createTheme({ + components: { + MuiGrid2: { + styleOverrides: { + container: { + padding: '11px', + }, + }, + }, + }, + }); + render( + + + hello + + , + ); + expect(screen.getByTestId('grid')).to.have.style('padding', '11px'); + }); }); diff --git a/packages/mui-material/src/Grid2/Grid2.tsx b/packages/mui-material/src/Grid2/Grid2.tsx index 1d2841bafa74c1..99c4148d33f2ef 100644 --- a/packages/mui-material/src/Grid2/Grid2.tsx +++ b/packages/mui-material/src/Grid2/Grid2.tsx @@ -127,7 +127,10 @@ const Grid2 = createGrid2({ createStyledComponent: styled('div', { name: 'MuiGrid2', slot: 'Root', - overridesResolver: (props, styles) => styles.root, + overridesResolver: (props, styles) => { + const { ownerState } = props; + return [styles.root, ownerState.container && styles.container]; + }, }), componentName: 'MuiGrid2', useThemeProps: (inProps) => useDefaultProps({ props: inProps, name: 'MuiGrid2' }), diff --git a/packages/mui-material/src/Hidden/HiddenJs.js b/packages/mui-material/src/Hidden/HiddenJs.js index b591452cef6299..a9ab0d0a965ee2 100644 --- a/packages/mui-material/src/Hidden/HiddenJs.js +++ b/packages/mui-material/src/Hidden/HiddenJs.js @@ -1,5 +1,4 @@ 'use client'; -import * as React from 'react'; import PropTypes from 'prop-types'; import exactProp from '@mui/utils/exactProp'; import withWidth, { isWidthDown, isWidthUp } from './withWidth'; @@ -50,7 +49,7 @@ function HiddenJs(props) { return null; } - return {children}; + return children; } HiddenJs.propTypes = { diff --git a/packages/mui-material/src/Modal/useModal.ts b/packages/mui-material/src/Modal/useModal.ts index 4cb2b992beae60..6a14ec7b7eb9c7 100644 --- a/packages/mui-material/src/Modal/useModal.ts +++ b/packages/mui-material/src/Modal/useModal.ts @@ -24,6 +24,8 @@ function getHasTransition(children: UseModalParameters['children']) { return children ? children.props.hasOwnProperty('in') : false; } +const noop = () => {}; + // A modal manager used to track and manage the state of open Modals. // Modals don't open on the server so this won't conflict with concurrent requests. const manager = new ModalManager(); @@ -227,8 +229,8 @@ function useModal(parameters: UseModalParameters): UseModalReturnValue { }; return { - onEnter: createChainedFunction(handleEnter, children?.props.onEnter), - onExited: createChainedFunction(handleExited, children?.props.onExited), + onEnter: createChainedFunction(handleEnter, children?.props.onEnter ?? noop), + onExited: createChainedFunction(handleExited, children?.props.onExited ?? noop), }; }; diff --git a/packages/mui-material/src/Modal/useModal.types.ts b/packages/mui-material/src/Modal/useModal.types.ts index a67b240a9c5708..9b384f7688f70f 100644 --- a/packages/mui-material/src/Modal/useModal.types.ts +++ b/packages/mui-material/src/Modal/useModal.types.ts @@ -22,7 +22,14 @@ export type UseModalParameters = { /** * A single child content element. */ - children: React.ReactElement | undefined | null; + children: + | React.ReactElement<{ + in?: boolean; + onEnter?: (this: unknown) => void; + onExited?: (this: unknown) => void; + }> + | undefined + | null; /** * When set to true the Modal waits until a nested Transition is completed before closing. * @default false diff --git a/packages/mui-material/src/NoSsr/NoSsr.tsx b/packages/mui-material/src/NoSsr/NoSsr.tsx index c7e2dfbc191a6e..0b18a597a751ae 100644 --- a/packages/mui-material/src/NoSsr/NoSsr.tsx +++ b/packages/mui-material/src/NoSsr/NoSsr.tsx @@ -38,8 +38,8 @@ function NoSsr(props: NoSsrProps): React.JSX.Element { } }, [defer]); - // We need the Fragment here to force react-docgen to recognise NoSsr as a component. - return {mountedState ? children : fallback}; + // TODO casting won't be needed at one point https://github.com/DefinitelyTyped/DefinitelyTyped/pull/65135 + return (mountedState ? children : fallback) as React.JSX.Element; } NoSsr.propTypes /* remove-proptypes */ = { diff --git a/packages/mui-material/src/Portal/Portal.tsx b/packages/mui-material/src/Portal/Portal.tsx index 8bd141d8a5f858..a028eb856836e0 100644 --- a/packages/mui-material/src/Portal/Portal.tsx +++ b/packages/mui-material/src/Portal/Portal.tsx @@ -64,14 +64,10 @@ const Portal = React.forwardRef(function Portal( }; return React.cloneElement(children, newProps); } - return {children}; + return children; } - return ( - - {mountNode ? ReactDOM.createPortal(children, mountNode) : mountNode} - - ); + return mountNode ? ReactDOM.createPortal(children, mountNode) : mountNode; }) as React.ForwardRefExoticComponent>; Portal.propTypes /* remove-proptypes */ = { diff --git a/packages/mui-material/src/Slider/SliderValueLabel.types.ts b/packages/mui-material/src/Slider/SliderValueLabel.types.ts index 8fac93f872dcb8..378930f97e345e 100644 --- a/packages/mui-material/src/Slider/SliderValueLabel.types.ts +++ b/packages/mui-material/src/Slider/SliderValueLabel.types.ts @@ -1,5 +1,5 @@ export interface SliderValueLabelProps { - children?: React.ReactElement; + children?: React.ReactElement<{ className?: string; children?: React.ReactNode }>; className?: string; style?: React.CSSProperties; /** diff --git a/packages/mui-material/src/Tabs/ScrollbarSize.js b/packages/mui-material/src/Tabs/ScrollbarSize.js index da7ec73c0c741b..4f5676cc428d13 100644 --- a/packages/mui-material/src/Tabs/ScrollbarSize.js +++ b/packages/mui-material/src/Tabs/ScrollbarSize.js @@ -49,7 +49,7 @@ export default function ScrollbarSize(props) { onChange(scrollbarHeight.current); }, [onChange]); - return
; + return
; } ScrollbarSize.propTypes = { diff --git a/packages/mui-material/src/Unstable_TrapFocus/FocusTrap.tsx b/packages/mui-material/src/Unstable_TrapFocus/FocusTrap.tsx index 0f6085f5b9f1d9..398a7b37421019 100644 --- a/packages/mui-material/src/Unstable_TrapFocus/FocusTrap.tsx +++ b/packages/mui-material/src/Unstable_TrapFocus/FocusTrap.tsx @@ -324,7 +324,7 @@ function FocusTrap(props: FocusTrapProps): React.JSX.Element { }; }, [disableAutoFocus, disableEnforceFocus, disableRestoreFocus, isEnabled, open, getTabbable]); - const onFocus = (event: FocusEvent) => { + const onFocus = (event: React.FocusEvent) => { if (nodeToRestore.current === null) { nodeToRestore.current = event.relatedTarget; } diff --git a/packages/mui-material/src/Unstable_TrapFocus/FocusTrap.types.ts b/packages/mui-material/src/Unstable_TrapFocus/FocusTrap.types.ts index 462100ec696479..399ddd84ea210a 100644 --- a/packages/mui-material/src/Unstable_TrapFocus/FocusTrap.types.ts +++ b/packages/mui-material/src/Unstable_TrapFocus/FocusTrap.types.ts @@ -24,7 +24,10 @@ export interface FocusTrapProps { /** * A single child content element. */ - children: React.ReactElement; + children: React.ReactElement<{ + onFocus?: React.FocusEventHandler; + ref?: React.RefCallback | null; + }>; /** * If `true`, the focus trap will not automatically shift focus to itself when it opens, and * replace it to the last focused element when it closes. diff --git a/packages/mui-material/src/styles/ThemeProviderWithVars.test.js b/packages/mui-material/src/styles/ThemeProviderWithVars.test.js index 57cc558313c691..e06a6d304d63dd 100644 --- a/packages/mui-material/src/styles/ThemeProviderWithVars.test.js +++ b/packages/mui-material/src/styles/ThemeProviderWithVars.test.js @@ -408,4 +408,35 @@ describe('[Material UI] ThemeProviderWithVars', () => { fireEvent.click(screen.getByText('Dark')); }).not.toErrorDev(); }); + + it('theme should remain the same when ThemeProvider rerenders', () => { + const theme = createTheme({ cssVariables: true }); + + function Inner() { + const upperTheme = useTheme(); + const themeRef = React.useRef(upperTheme); + const [changed, setChanged] = React.useState(false); + React.useEffect(() => { + if (themeRef.current !== upperTheme) { + setChanged(true); + } + }, [upperTheme]); + return changed ?
: null; + } + function App() { + const [, setState] = React.useState({}); + const rerender = () => setState({}); + return ( + + + + + ); + } + render(); + + fireEvent.click(screen.getByRole('button')); + + expect(screen.queryByTestId('theme-changed')).to.equal(null); + }); }); diff --git a/packages/mui-material/src/styles/createTheme.test.js b/packages/mui-material/src/styles/createTheme.test.js index 26df7f1fdf6398..e4351045dffa8f 100644 --- a/packages/mui-material/src/styles/createTheme.test.js +++ b/packages/mui-material/src/styles/createTheme.test.js @@ -221,6 +221,22 @@ describe('createTheme', () => { expect(theme.colorSchemes.dark).to.not.equal(undefined); }); + it('should be able to customize tonal offset', () => { + const theme = createTheme({ + cssVariables: true, + palette: { + primary: { + main: green[500], + }, + tonalOffset: { + light: 0.1, + dark: 0.9, + }, + }, + }); + expect(theme.palette.primary.main).to.equal('#4caf50'); + }); + describe('spacing', () => { it('should provide the default spacing', () => { const theme = createTheme({ cssVariables: true }); diff --git a/packages/mui-material/src/styles/createThemeWithVars.js b/packages/mui-material/src/styles/createThemeWithVars.js index 9e1fd9f76707e9..fde30948676edf 100644 --- a/packages/mui-material/src/styles/createThemeWithVars.js +++ b/packages/mui-material/src/styles/createThemeWithVars.js @@ -40,7 +40,7 @@ function setColor(obj, key, defaultValue) { } function toRgb(color) { - if (!color || !color.startsWith('hsl')) { + if (typeof color !== 'string' || !color.startsWith('hsl')) { return color; } return hslToRgb(color); @@ -421,7 +421,7 @@ export default function createThemeWithVars(options = {}, ...args) { // The default palettes (primary, secondary, error, info, success, and warning) errors are handled by the above `createTheme(...)`. - if (colors && typeof colors === 'object') { + if (color !== 'tonalOffset' && colors && typeof colors === 'object') { // Silent the error for custom palettes. if (colors.main) { setColor(palette[color], 'mainChannel', safeColorChannel(toRgb(colors.main))); diff --git a/packages/mui-styles/test/styles.spec.tsx b/packages/mui-styles/test/styles.spec.tsx index 8ed6a1d215f263..34ab5cac0f3711 100644 --- a/packages/mui-styles/test/styles.spec.tsx +++ b/packages/mui-styles/test/styles.spec.tsx @@ -215,7 +215,7 @@ withStyles((theme) => }); interface ListItemContentProps extends WithStyles { - children?: React.ReactElement; + children?: React.ReactElement; inset?: boolean; row?: boolean; } diff --git a/packages/mui-system/src/cssVars/createCssVarsProvider.js b/packages/mui-system/src/cssVars/createCssVarsProvider.js index e7a2a21febb207..64ed9f9fc4037d 100644 --- a/packages/mui-system/src/cssVars/createCssVarsProvider.js +++ b/packages/mui-system/src/cssVars/createCssVarsProvider.js @@ -48,6 +48,9 @@ export default function createCssVarsProvider(options) { const useColorScheme = () => React.useContext(ColorSchemeContext) || defaultContext; + const defaultColorSchemes = {}; + const defaultComponents = {}; + function CssVarsProvider(props) { const { children, @@ -75,12 +78,12 @@ export default function createCssVarsProvider(options) { return typeof defaultTheme === 'function' ? defaultTheme() : defaultTheme; }, [themeProp]); const scopedTheme = initialTheme[themeId]; + const restThemeProp = scopedTheme || initialTheme; const { - colorSchemes = {}, - components = {}, + colorSchemes = defaultColorSchemes, + components = defaultComponents, cssVarPrefix, - ...restThemeProp - } = scopedTheme || initialTheme; + } = restThemeProp; const joinedColorSchemes = Object.keys(colorSchemes) .filter((k) => !!colorSchemes[k]) .join(','); @@ -126,42 +129,46 @@ export default function createCssVarsProvider(options) { colorScheme = ctx.colorScheme; } - // `colorScheme` is undefined on the server and hydration phase - const calculatedColorScheme = colorScheme || restThemeProp.defaultColorScheme; + const memoTheme = React.useMemo(() => { + // `colorScheme` is undefined on the server and hydration phase + const calculatedColorScheme = colorScheme || restThemeProp.defaultColorScheme; - // 2. get the `vars` object that refers to the CSS custom properties - const themeVars = restThemeProp.generateThemeVars?.() || restThemeProp.vars; + // 2. get the `vars` object that refers to the CSS custom properties + const themeVars = restThemeProp.generateThemeVars?.() || restThemeProp.vars; - // 3. Start composing the theme object - const theme = { - ...restThemeProp, - components, - colorSchemes, - cssVarPrefix, - vars: themeVars, - }; - if (typeof theme.generateSpacing === 'function') { - theme.spacing = theme.generateSpacing(); - } + // 3. Start composing the theme object + const theme = { + ...restThemeProp, + components, + colorSchemes, + cssVarPrefix, + vars: themeVars, + }; + if (typeof theme.generateSpacing === 'function') { + theme.spacing = theme.generateSpacing(); + } - // 4. Resolve the color scheme and merge it to the theme - if (calculatedColorScheme) { - const scheme = colorSchemes[calculatedColorScheme]; - if (scheme && typeof scheme === 'object') { - // 4.1 Merge the selected color scheme to the theme - Object.keys(scheme).forEach((schemeKey) => { - if (scheme[schemeKey] && typeof scheme[schemeKey] === 'object') { - // shallow merge the 1st level structure of the theme. - theme[schemeKey] = { - ...theme[schemeKey], - ...scheme[schemeKey], - }; - } else { - theme[schemeKey] = scheme[schemeKey]; - } - }); + // 4. Resolve the color scheme and merge it to the theme + if (calculatedColorScheme) { + const scheme = colorSchemes[calculatedColorScheme]; + if (scheme && typeof scheme === 'object') { + // 4.1 Merge the selected color scheme to the theme + Object.keys(scheme).forEach((schemeKey) => { + if (scheme[schemeKey] && typeof scheme[schemeKey] === 'object') { + // shallow merge the 1st level structure of the theme. + theme[schemeKey] = { + ...theme[schemeKey], + ...scheme[schemeKey], + }; + } else { + theme[schemeKey] = scheme[schemeKey]; + } + }); + } } - } + + return resolveTheme ? resolveTheme(theme) : theme; + }, [restThemeProp, colorScheme, components, colorSchemes, cssVarPrefix]); // 5. Declaring effects // 5.1 Updates the selector value to use the current color scheme which tells CSS to use the proper stylesheet. @@ -248,7 +255,7 @@ export default function createCssVarsProvider(options) { process.env.NODE_ENV === 'production' ? setMode : (newMode) => { - if (theme.colorSchemeSelector === 'media') { + if (memoTheme.colorSchemeSelector === 'media') { console.error( [ 'MUI: The `setMode` function has no effect if `colorSchemeSelector` is `media` (`media` is the default value).', @@ -270,7 +277,7 @@ export default function createCssVarsProvider(options) { setColorScheme, setMode, systemMode, - theme.colorSchemeSelector, + memoTheme.colorSchemeSelector, ], ); @@ -285,13 +292,12 @@ export default function createCssVarsProvider(options) { const element = ( - + {children} - {shouldGenerateStyleSheet && } + {shouldGenerateStyleSheet && ( + + )} );