From 5cb5b4ca961dffcd9727f4a9499e85bfb79c3938 Mon Sep 17 00:00:00 2001 From: Marija Najdova Date: Fri, 12 Jul 2024 13:31:57 +0200 Subject: [PATCH] [material] Remove dependency to @mui/base (#42907) --- docs/data/material/pagesApi.js | 4 + .../material-ui/api/click-away-listener.js | 23 + .../material-ui/api/click-away-listener.json | 34 + docs/pages/material-ui/api/no-ssr.js | 19 + docs/pages/material-ui/api/no-ssr.json | 17 + docs/pages/material-ui/api/popper.json | 9 +- docs/pages/material-ui/api/portal.js | 19 + docs/pages/material-ui/api/portal.json | 20 + .../material-ui/api/textarea-autosize.js | 23 + .../material-ui/api/textarea-autosize.json | 22 + .../click-away-listener.json | 19 + docs/translations/api-docs/no-ssr/no-ssr.json | 11 + docs/translations/api-docs/popper/popper.json | 2 +- docs/translations/api-docs/portal/portal.json | 13 + .../textarea-autosize/textarea-autosize.json | 8 + .../baseUi/projectSettings.ts | 3 + packages/api-docs-builder/ProjectSettings.ts | 4 + packages/api-docs-builder/buildApi.ts | 3 + .../mui-base/src/utils/appendOwnerState.ts | 52 +- .../src/utils/extractEventHandlers.ts | 31 +- packages/mui-base/src/utils/types.ts | 33 +- packages/mui-base/src/utils/useSlotProps.ts | 113 +- packages/mui-material/package.json | 3 +- .../src/Autocomplete/Autocomplete.d.ts | 13 +- .../src/Autocomplete/Autocomplete.js | 2 +- packages/mui-material/src/Badge/Badge.d.ts | 2 +- packages/mui-material/src/Badge/Badge.js | 4 +- packages/mui-material/src/Badge/useBadge.ts | 48 + .../mui-material/src/Badge/useBadge.types.ts | 40 + .../src/Breadcrumbs/Breadcrumbs.d.ts | 2 +- .../src/Breadcrumbs/Breadcrumbs.js | 4 +- .../ClickAwayListener.test.js | 441 ++++++ .../ClickAwayListener/ClickAwayListener.tsx | 262 ++++ .../src/ClickAwayListener/index.ts | 4 +- .../mui-material/src/InputBase/InputBase.js | 4 +- .../mui-material/src/ListItem/ListItem.js | 4 +- packages/mui-material/src/Menu/Menu.js | 2 +- packages/mui-material/src/Modal/Modal.d.ts | 2 +- packages/mui-material/src/Modal/Modal.js | 3 +- .../src/Modal/ModalManager.test.ts | 435 ++++++ .../mui-material/src/Modal/ModalManager.ts | 314 +++++ packages/mui-material/src/Modal/index.d.ts | 2 +- packages/mui-material/src/Modal/index.js | 2 +- packages/mui-material/src/Modal/useModal.ts | 243 ++++ .../mui-material/src/Modal/useModal.types.ts | 123 ++ .../mui-material/src/NoSsr/NoSsr.test.tsx | 56 + packages/mui-material/src/NoSsr/NoSsr.tsx | 72 + .../mui-material/src/NoSsr/NoSsr.types.ts | 19 + packages/mui-material/src/NoSsr/index.d.ts | 5 +- packages/mui-material/src/NoSsr/index.js | 2 +- packages/mui-material/src/Popover/Popover.js | 4 +- .../mui-material/src/Popper/BasePopper.tsx | 539 ++++++++ .../src/Popper/BasePopper.types.ts | 155 +++ packages/mui-material/src/Popper/Popper.tsx | 10 +- packages/mui-material/src/Popper/index.d.ts | 3 +- packages/mui-material/src/Popper/index.js | 1 + .../mui-material/src/Popper/popperClasses.ts | 17 + .../mui-material/src/Portal/Portal.test.tsx | 220 +++ packages/mui-material/src/Portal/Portal.tsx | 108 ++ .../mui-material/src/Portal/Portal.types.ts | 24 + packages/mui-material/src/Portal/index.d.ts | 5 +- packages/mui-material/src/Portal/index.js | 2 +- packages/mui-material/src/Slider/Slider.d.ts | 4 +- packages/mui-material/src/Slider/Slider.js | 5 +- .../mui-material/src/Slider/Slider.test.js | 5 +- .../mui-material/src/Slider/useSlider.test.js | 91 ++ packages/mui-material/src/Slider/useSlider.ts | 749 +++++++++++ .../src/Slider/useSlider.types.ts | 259 ++++ .../mui-material/src/Snackbar/Snackbar.d.ts | 2 +- .../mui-material/src/Snackbar/Snackbar.js | 9 +- .../src/Snackbar/useSnackbar.test.tsx | 54 + .../mui-material/src/Snackbar/useSnackbar.ts | 166 +++ .../src/Snackbar/useSnackbar.types.ts | 66 + .../src/SwipeableDrawer/SwipeableDrawer.js | 3 +- .../src/TabScrollButton/TabScrollButton.d.ts | 2 +- .../src/TabScrollButton/TabScrollButton.js | 2 +- .../src/TablePagination/TablePagination.js | 4 +- packages/mui-material/src/Tabs/Tabs.d.ts | 2 +- packages/mui-material/src/Tabs/Tabs.js | 4 +- .../TextareaAutosize.test.tsx | 448 +++++++ .../src/TextareaAutosize/TextareaAutosize.tsx | 264 ++++ .../TextareaAutosize.types.ts | 15 + .../src/TextareaAutosize/index.d.ts | 5 +- .../src/TextareaAutosize/index.js | 2 +- packages/mui-material/src/Tooltip/Tooltip.js | 6 +- .../src/Unstable_TrapFocus/FocusTrap.test.tsx | 410 ++++++ .../src/Unstable_TrapFocus/FocusTrap.tsx | 426 ++++++ .../src/Unstable_TrapFocus/FocusTrap.types.ts | 52 + .../src/Unstable_TrapFocus/index.d.ts | 4 +- .../src/Unstable_TrapFocus/index.js | 2 +- packages/mui-material/src/index.d.ts | 2 +- packages/mui-material/src/index.js | 2 +- .../mui-material/src/styles/overrides.d.ts | 2 +- .../src/useAutocomplete/useAutocomplete.d.ts | 470 ++++++- .../src/useAutocomplete/useAutocomplete.js | 1184 ++++++++++++++++- .../useAutocomplete/useAutocomplete.spec.ts | 184 +++ .../useAutocomplete/useAutocomplete.test.js | 380 ++++++ .../src/utils/PolymorphicComponent.ts | 25 + .../mui-material/src/utils/areArraysEqual.ts | 14 + packages/mui-material/src/utils/index.d.ts | 4 +- packages/mui-material/src/utils/index.js | 2 +- .../mui-material/src/utils/isHostComponent.ts | 10 + .../src/utils/omitEventHandlers.ts | 24 + .../src/utils/shouldSpreadAdditionalProps.js | 2 +- packages/mui-material/src/utils/types.ts | 9 +- .../mui-material/src/utils/useSlot.test.tsx | 4 +- packages/mui-material/src/utils/useSlot.ts | 4 +- packages/mui-utils/package.json | 1 + .../appendOwnerState.spec.tsx | 28 + .../appendOwnerState/appendOwnerState.test.ts | 67 + .../src/appendOwnerState/appendOwnerState.ts | 53 + .../mui-utils/src/appendOwnerState/index.ts | 2 + .../extractEventHandlers.test.ts | 43 + .../extractEventHandlers.ts | 32 + .../src/extractEventHandlers/index.ts | 1 + packages/mui-utils/src/index.ts | 5 + .../mui-utils/src/isHostComponent/index.ts | 1 + .../src/isHostComponent/isHostComponent.ts | 10 + .../mui-utils/src/mergeSlotProps/index.ts | 7 + .../src/mergeSlotProps/mergeSlotProps.test.ts | 192 +++ .../src/mergeSlotProps/mergeSlotProps.ts | 184 +++ .../mui-utils/src/omitEventHandlers/index.ts | 1 + .../omitEventHandlers.test.ts | 29 + .../omitEventHandlers/omitEventHandlers.ts | 24 + .../src/resolveComponentProps/index.ts | 1 + .../resolveComponentProps.ts | 23 + packages/mui-utils/src/types.ts | 27 + packages/mui-utils/src/useSlotProps/index.ts | 2 + .../src/useSlotProps/useSlotProps.test.tsx | 301 +++++ .../src/useSlotProps/useSlotProps.ts | 114 ++ pnpm-lock.yaml | 8 +- 131 files changed, 9911 insertions(+), 311 deletions(-) create mode 100644 docs/pages/material-ui/api/click-away-listener.js create mode 100644 docs/pages/material-ui/api/click-away-listener.json create mode 100644 docs/pages/material-ui/api/no-ssr.js create mode 100644 docs/pages/material-ui/api/no-ssr.json create mode 100644 docs/pages/material-ui/api/portal.js create mode 100644 docs/pages/material-ui/api/portal.json create mode 100644 docs/pages/material-ui/api/textarea-autosize.js create mode 100644 docs/pages/material-ui/api/textarea-autosize.json create mode 100644 docs/translations/api-docs/click-away-listener/click-away-listener.json create mode 100644 docs/translations/api-docs/no-ssr/no-ssr.json create mode 100644 docs/translations/api-docs/portal/portal.json create mode 100644 docs/translations/api-docs/textarea-autosize/textarea-autosize.json create mode 100644 packages/mui-material/src/Badge/useBadge.ts create mode 100644 packages/mui-material/src/Badge/useBadge.types.ts create mode 100644 packages/mui-material/src/ClickAwayListener/ClickAwayListener.test.js create mode 100644 packages/mui-material/src/ClickAwayListener/ClickAwayListener.tsx create mode 100644 packages/mui-material/src/Modal/ModalManager.test.ts create mode 100644 packages/mui-material/src/Modal/ModalManager.ts create mode 100644 packages/mui-material/src/Modal/useModal.ts create mode 100644 packages/mui-material/src/Modal/useModal.types.ts create mode 100644 packages/mui-material/src/NoSsr/NoSsr.test.tsx create mode 100644 packages/mui-material/src/NoSsr/NoSsr.tsx create mode 100644 packages/mui-material/src/NoSsr/NoSsr.types.ts create mode 100644 packages/mui-material/src/Popper/BasePopper.tsx create mode 100644 packages/mui-material/src/Popper/BasePopper.types.ts create mode 100644 packages/mui-material/src/Popper/popperClasses.ts create mode 100644 packages/mui-material/src/Portal/Portal.test.tsx create mode 100644 packages/mui-material/src/Portal/Portal.tsx create mode 100644 packages/mui-material/src/Portal/Portal.types.ts create mode 100644 packages/mui-material/src/Slider/useSlider.test.js create mode 100644 packages/mui-material/src/Slider/useSlider.ts create mode 100644 packages/mui-material/src/Slider/useSlider.types.ts create mode 100644 packages/mui-material/src/Snackbar/useSnackbar.test.tsx create mode 100644 packages/mui-material/src/Snackbar/useSnackbar.ts create mode 100644 packages/mui-material/src/Snackbar/useSnackbar.types.ts create mode 100644 packages/mui-material/src/TextareaAutosize/TextareaAutosize.test.tsx create mode 100644 packages/mui-material/src/TextareaAutosize/TextareaAutosize.tsx create mode 100644 packages/mui-material/src/TextareaAutosize/TextareaAutosize.types.ts create mode 100644 packages/mui-material/src/Unstable_TrapFocus/FocusTrap.test.tsx create mode 100644 packages/mui-material/src/Unstable_TrapFocus/FocusTrap.tsx create mode 100644 packages/mui-material/src/Unstable_TrapFocus/FocusTrap.types.ts create mode 100644 packages/mui-material/src/useAutocomplete/useAutocomplete.spec.ts create mode 100644 packages/mui-material/src/useAutocomplete/useAutocomplete.test.js create mode 100644 packages/mui-material/src/utils/PolymorphicComponent.ts create mode 100644 packages/mui-material/src/utils/areArraysEqual.ts create mode 100644 packages/mui-material/src/utils/isHostComponent.ts create mode 100644 packages/mui-material/src/utils/omitEventHandlers.ts create mode 100644 packages/mui-utils/src/appendOwnerState/appendOwnerState.spec.tsx create mode 100644 packages/mui-utils/src/appendOwnerState/appendOwnerState.test.ts create mode 100644 packages/mui-utils/src/appendOwnerState/appendOwnerState.ts create mode 100644 packages/mui-utils/src/appendOwnerState/index.ts create mode 100644 packages/mui-utils/src/extractEventHandlers/extractEventHandlers.test.ts create mode 100644 packages/mui-utils/src/extractEventHandlers/extractEventHandlers.ts create mode 100644 packages/mui-utils/src/extractEventHandlers/index.ts create mode 100644 packages/mui-utils/src/isHostComponent/index.ts create mode 100644 packages/mui-utils/src/isHostComponent/isHostComponent.ts create mode 100644 packages/mui-utils/src/mergeSlotProps/index.ts create mode 100644 packages/mui-utils/src/mergeSlotProps/mergeSlotProps.test.ts create mode 100644 packages/mui-utils/src/mergeSlotProps/mergeSlotProps.ts create mode 100644 packages/mui-utils/src/omitEventHandlers/index.ts create mode 100644 packages/mui-utils/src/omitEventHandlers/omitEventHandlers.test.ts create mode 100644 packages/mui-utils/src/omitEventHandlers/omitEventHandlers.ts create mode 100644 packages/mui-utils/src/resolveComponentProps/index.ts create mode 100644 packages/mui-utils/src/resolveComponentProps/resolveComponentProps.ts create mode 100644 packages/mui-utils/src/types.ts create mode 100644 packages/mui-utils/src/useSlotProps/index.ts create mode 100644 packages/mui-utils/src/useSlotProps/useSlotProps.test.tsx create mode 100644 packages/mui-utils/src/useSlotProps/useSlotProps.ts diff --git a/docs/data/material/pagesApi.js b/docs/data/material/pagesApi.js index 7bb6fe6df78aa8..22ff392d72c282 100644 --- a/docs/data/material/pagesApi.js +++ b/docs/data/material/pagesApi.js @@ -27,6 +27,7 @@ module.exports = [ { pathname: '/material-ui/api/checkbox' }, { pathname: '/material-ui/api/chip' }, { pathname: '/material-ui/api/circular-progress' }, + { pathname: '/material-ui/api/click-away-listener' }, { pathname: '/material-ui/api/collapse' }, { pathname: '/material-ui/api/container' }, { pathname: '/material-ui/api/css-baseline' }, @@ -76,12 +77,14 @@ module.exports = [ { pathname: '/material-ui/api/mobile-stepper' }, { pathname: '/material-ui/api/modal' }, { pathname: '/material-ui/api/native-select' }, + { pathname: '/material-ui/api/no-ssr' }, { pathname: '/material-ui/api/outlined-input' }, { pathname: '/material-ui/api/pagination' }, { pathname: '/material-ui/api/pagination-item' }, { pathname: '/material-ui/api/paper' }, { pathname: '/material-ui/api/popover' }, { pathname: '/material-ui/api/popper' }, + { pathname: '/material-ui/api/portal' }, { pathname: '/material-ui/api/radio' }, { pathname: '/material-ui/api/radio-group' }, { pathname: '/material-ui/api/rating' }, @@ -121,6 +124,7 @@ module.exports = [ { pathname: '/material-ui/api/tab-panel' }, { pathname: '/material-ui/api/tabs' }, { pathname: '/material-ui/api/tab-scroll-button' }, + { pathname: '/material-ui/api/textarea-autosize' }, { pathname: '/material-ui/api/text-field' }, { pathname: '/material-ui/api/timeline' }, { pathname: '/material-ui/api/timeline-connector' }, diff --git a/docs/pages/material-ui/api/click-away-listener.js b/docs/pages/material-ui/api/click-away-listener.js new file mode 100644 index 00000000000000..3776a973527b16 --- /dev/null +++ b/docs/pages/material-ui/api/click-away-listener.js @@ -0,0 +1,23 @@ +import * as React from 'react'; +import ApiPage from 'docs/src/modules/components/ApiPage'; +import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import jsonPageContent from './click-away-listener.json'; + +export default function Page(props) { + const { descriptions, pageContent } = props; + return ; +} + +Page.getInitialProps = () => { + const req = require.context( + 'docs/translations/api-docs/click-away-listener', + false, + /\.\/click-away-listener.*.json$/, + ); + const descriptions = mapApiPageTranslations(req); + + return { + descriptions, + pageContent: jsonPageContent, + }; +}; diff --git a/docs/pages/material-ui/api/click-away-listener.json b/docs/pages/material-ui/api/click-away-listener.json new file mode 100644 index 00000000000000..a33e412e20cdac --- /dev/null +++ b/docs/pages/material-ui/api/click-away-listener.json @@ -0,0 +1,34 @@ +{ + "props": { + "children": { "type": { "name": "custom", "description": "element" }, "required": true }, + "onClickAway": { "type": { "name": "func" }, "required": true }, + "disableReactTree": { "type": { "name": "bool" }, "default": "false" }, + "mouseEvent": { + "type": { + "name": "enum", + "description": "'onClick'
| 'onMouseDown'
| 'onMouseUp'
| 'onPointerDown'
| 'onPointerUp'
| false" + }, + "default": "'onClick'" + }, + "touchEvent": { + "type": { + "name": "enum", + "description": "'onTouchEnd'
| 'onTouchStart'
| false" + }, + "default": "'onTouchEnd'" + } + }, + "name": "ClickAwayListener", + "imports": [ + "import ClickAwayListener from '@mui/material/ClickAwayListener';", + "import { ClickAwayListener } from '@mui/material';" + ], + "classes": [], + "spread": false, + "themeDefaultProps": null, + "muiName": "MuiClickAwayListener", + "filename": "/packages/mui-material/src/ClickAwayListener/ClickAwayListener.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/material-ui/api/no-ssr.js b/docs/pages/material-ui/api/no-ssr.js new file mode 100644 index 00000000000000..03c084bbcf6a12 --- /dev/null +++ b/docs/pages/material-ui/api/no-ssr.js @@ -0,0 +1,19 @@ +import * as React from 'react'; +import ApiPage from 'docs/src/modules/components/ApiPage'; +import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import jsonPageContent from './no-ssr.json'; + +export default function Page(props) { + const { descriptions, pageContent } = props; + return ; +} + +Page.getInitialProps = () => { + const req = require.context('docs/translations/api-docs/no-ssr', false, /\.\/no-ssr.*.json$/); + const descriptions = mapApiPageTranslations(req); + + return { + descriptions, + pageContent: jsonPageContent, + }; +}; diff --git a/docs/pages/material-ui/api/no-ssr.json b/docs/pages/material-ui/api/no-ssr.json new file mode 100644 index 00000000000000..464dc7136b18ca --- /dev/null +++ b/docs/pages/material-ui/api/no-ssr.json @@ -0,0 +1,17 @@ +{ + "props": { + "children": { "type": { "name": "node" } }, + "defer": { "type": { "name": "bool" }, "default": "false" }, + "fallback": { "type": { "name": "node" }, "default": "null" } + }, + "name": "NoSsr", + "imports": ["import NoSsr from '@mui/material/NoSsr';", "import { NoSsr } from '@mui/material';"], + "classes": [], + "spread": false, + "themeDefaultProps": null, + "muiName": "MuiNoSsr", + "filename": "/packages/mui-material/src/NoSsr/NoSsr.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/material-ui/api/popper.json b/docs/pages/material-ui/api/popper.json index 06f2880cdc5b9e..14fedb4f023556 100644 --- a/docs/pages/material-ui/api/popper.json +++ b/docs/pages/material-ui/api/popper.json @@ -63,7 +63,14 @@ "import Popper from '@mui/material/Popper';", "import { Popper } from '@mui/material';" ], - "classes": [], + "classes": [ + { + "key": "root", + "className": "MuiPopper-root", + "description": "Class name applied to the root element.", + "isGlobal": false + } + ], "spread": true, "themeDefaultProps": false, "muiName": "MuiPopper", diff --git a/docs/pages/material-ui/api/portal.js b/docs/pages/material-ui/api/portal.js new file mode 100644 index 00000000000000..e53de2d6983e86 --- /dev/null +++ b/docs/pages/material-ui/api/portal.js @@ -0,0 +1,19 @@ +import * as React from 'react'; +import ApiPage from 'docs/src/modules/components/ApiPage'; +import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import jsonPageContent from './portal.json'; + +export default function Page(props) { + const { descriptions, pageContent } = props; + return ; +} + +Page.getInitialProps = () => { + const req = require.context('docs/translations/api-docs/portal', false, /\.\/portal.*.json$/); + const descriptions = mapApiPageTranslations(req); + + return { + descriptions, + pageContent: jsonPageContent, + }; +}; diff --git a/docs/pages/material-ui/api/portal.json b/docs/pages/material-ui/api/portal.json new file mode 100644 index 00000000000000..1079011f1f82f4 --- /dev/null +++ b/docs/pages/material-ui/api/portal.json @@ -0,0 +1,20 @@ +{ + "props": { + "children": { "type": { "name": "node" } }, + "container": { "type": { "name": "union", "description": "HTML element
| func" } }, + "disablePortal": { "type": { "name": "bool" }, "default": "false" } + }, + "name": "Portal", + "imports": [ + "import Portal from '@mui/material/Portal';", + "import { Portal } from '@mui/material';" + ], + "classes": [], + "spread": false, + "themeDefaultProps": null, + "muiName": "MuiPortal", + "filename": "/packages/mui-material/src/Portal/Portal.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/material-ui/api/textarea-autosize.js b/docs/pages/material-ui/api/textarea-autosize.js new file mode 100644 index 00000000000000..4a8184ae71999b --- /dev/null +++ b/docs/pages/material-ui/api/textarea-autosize.js @@ -0,0 +1,23 @@ +import * as React from 'react'; +import ApiPage from 'docs/src/modules/components/ApiPage'; +import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import jsonPageContent from './textarea-autosize.json'; + +export default function Page(props) { + const { descriptions, pageContent } = props; + return ; +} + +Page.getInitialProps = () => { + const req = require.context( + 'docs/translations/api-docs/textarea-autosize', + false, + /\.\/textarea-autosize.*.json$/, + ); + const descriptions = mapApiPageTranslations(req); + + return { + descriptions, + pageContent: jsonPageContent, + }; +}; diff --git a/docs/pages/material-ui/api/textarea-autosize.json b/docs/pages/material-ui/api/textarea-autosize.json new file mode 100644 index 00000000000000..06a5fbb2bf09b3 --- /dev/null +++ b/docs/pages/material-ui/api/textarea-autosize.json @@ -0,0 +1,22 @@ +{ + "props": { + "maxRows": { "type": { "name": "union", "description": "number
| string" } }, + "minRows": { + "type": { "name": "union", "description": "number
| string" }, + "default": "1" + } + }, + "name": "TextareaAutosize", + "imports": [ + "import TextareaAutosize from '@mui/material/TextareaAutosize';", + "import { TextareaAutosize } from '@mui/material';" + ], + "classes": [], + "spread": true, + "themeDefaultProps": null, + "muiName": "MuiTextareaAutosize", + "filename": "/packages/mui-material/src/TextareaAutosize/TextareaAutosize.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/translations/api-docs/click-away-listener/click-away-listener.json b/docs/translations/api-docs/click-away-listener/click-away-listener.json new file mode 100644 index 00000000000000..28136c93c705af --- /dev/null +++ b/docs/translations/api-docs/click-away-listener/click-away-listener.json @@ -0,0 +1,19 @@ +{ + "componentDescription": "Listen for click events that occur somewhere in the document, outside of the element itself.\nFor instance, if you need to hide a menu when people click anywhere else on your page.", + "propDescriptions": { + "children": { "description": "The wrapped element.", "requiresRef": true }, + "disableReactTree": { + "description": "If true, the React tree is ignored and only the DOM tree is considered. This prop changes how portaled elements are handled." + }, + "mouseEvent": { + "description": "The mouse event to listen to. You can disable the listener by providing false." + }, + "onClickAway": { + "description": "Callback fired when a "click away" event is detected." + }, + "touchEvent": { + "description": "The touch event to listen to. You can disable the listener by providing false." + } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/no-ssr/no-ssr.json b/docs/translations/api-docs/no-ssr/no-ssr.json new file mode 100644 index 00000000000000..ff903d12a20b33 --- /dev/null +++ b/docs/translations/api-docs/no-ssr/no-ssr.json @@ -0,0 +1,11 @@ +{ + "componentDescription": "NoSsr purposely removes components from the subject of Server Side Rendering (SSR).\n\nThis component can be useful in a variety of situations:\n\n* Escape hatch for broken dependencies not supporting SSR.\n* Improve the time-to-first paint on the client by only rendering above the fold.\n* Reduce the rendering time on the server.\n* Under too heavy server load, you can turn on service degradation.", + "propDescriptions": { + "children": { "description": "You can wrap a node." }, + "defer": { + "description": "If true, the component will not only prevent server-side rendering. It will also defer the rendering of the children into a different screen frame." + }, + "fallback": { "description": "The fallback content to display." } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/popper/popper.json b/docs/translations/api-docs/popper/popper.json index 178879e0bd5352..23b39e61d6fa7a 100644 --- a/docs/translations/api-docs/popper/popper.json +++ b/docs/translations/api-docs/popper/popper.json @@ -41,5 +41,5 @@ "description": "Help supporting a react-transition-group/Transition component." } }, - "classDescriptions": {} + "classDescriptions": { "root": { "description": "Class name applied to the root element." } } } diff --git a/docs/translations/api-docs/portal/portal.json b/docs/translations/api-docs/portal/portal.json new file mode 100644 index 00000000000000..d7fb72bca569c9 --- /dev/null +++ b/docs/translations/api-docs/portal/portal.json @@ -0,0 +1,13 @@ +{ + "componentDescription": "Portals provide a first-class way to render children into a DOM node\nthat exists outside the DOM hierarchy of the parent component.", + "propDescriptions": { + "children": { "description": "The children to render into the container." }, + "container": { + "description": "An HTML element or function that returns one. The container will have the portal children appended to it.
You can also provide a callback, which is called in a React layout effect. This lets you set the container from a ref, and also makes server-side rendering possible.
By default, it uses the body of the top-level document object, so it's simply document.body most of the time." + }, + "disablePortal": { + "description": "The children will be under the DOM hierarchy of the parent component." + } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/textarea-autosize/textarea-autosize.json b/docs/translations/api-docs/textarea-autosize/textarea-autosize.json new file mode 100644 index 00000000000000..ee91d0dd6d70c6 --- /dev/null +++ b/docs/translations/api-docs/textarea-autosize/textarea-autosize.json @@ -0,0 +1,8 @@ +{ + "componentDescription": "", + "propDescriptions": { + "maxRows": { "description": "Maximum number of rows to display." }, + "minRows": { "description": "Minimum number of rows to display." } + }, + "classDescriptions": {} +} diff --git a/packages/api-docs-builder-core/baseUi/projectSettings.ts b/packages/api-docs-builder-core/baseUi/projectSettings.ts index b9b64d42adc3c4..6e7df498b54278 100644 --- a/packages/api-docs-builder-core/baseUi/projectSettings.ts +++ b/packages/api-docs-builder-core/baseUi/projectSettings.ts @@ -27,6 +27,9 @@ export const projectSettings: ProjectSettings = { getHookInfo: getBaseUiHookInfo, translationLanguages: LANGUAGES, skipComponent: () => false, + skipHook: (filename) => { + return filename.match(/(useSlotProps)/) !== null; + }, onCompleted: async () => { await generateBaseUIApiPages(); }, diff --git a/packages/api-docs-builder/ProjectSettings.ts b/packages/api-docs-builder/ProjectSettings.ts index 2ba937f0a4e9b2..8baa6193c75912 100644 --- a/packages/api-docs-builder/ProjectSettings.ts +++ b/packages/api-docs-builder/ProjectSettings.ts @@ -56,6 +56,10 @@ export interface ProjectSettings { * Fuction called to detemine whether to skip the generation of a particular component's API docs */ skipComponent: (filename: string) => boolean; + /** + * Fuction called to detemine whether to skip the generation of a particular hook's API docs + */ + skipHook?: (filename: string) => boolean; /** * Determine is the component definition should be updated. */ diff --git a/packages/api-docs-builder/buildApi.ts b/packages/api-docs-builder/buildApi.ts index 886b04f2e02f07..29bec459529bc5 100644 --- a/packages/api-docs-builder/buildApi.ts +++ b/packages/api-docs-builder/buildApi.ts @@ -135,6 +135,9 @@ async function buildSingleProject( ); const projectHooks = findHooks(path.join(project.rootPath, 'src')).filter((hook) => { + if (projectSettings.skipHook?.(hook.filename)) { + return false; + } if (grep === null) { return true; } diff --git a/packages/mui-base/src/utils/appendOwnerState.ts b/packages/mui-base/src/utils/appendOwnerState.ts index 0ef5e0c47a4f60..9c4b778bef1add 100644 --- a/packages/mui-base/src/utils/appendOwnerState.ts +++ b/packages/mui-base/src/utils/appendOwnerState.ts @@ -1,51 +1,3 @@ -import * as React from 'react'; -import { Simplify } from '@mui/types'; -import { isHostComponent } from './isHostComponent'; +export { default as appendOwnerState } from '@mui/utils/appendOwnerState'; -/** - * Type of the ownerState based on the type of an element it applies to. - * This resolves to the provided OwnerState for React components and `undefined` for host components. - * Falls back to `OwnerState | undefined` when the exact type can't be determined in development time. - */ -type OwnerStateWhenApplicable = - ElementType extends React.ComponentType - ? OwnerState - : ElementType extends keyof JSX.IntrinsicElements - ? undefined - : OwnerState | undefined; - -export type AppendOwnerStateReturnType< - ElementType extends React.ElementType, - OtherProps, - OwnerState, -> = Simplify< - OtherProps & { - ownerState: OwnerStateWhenApplicable; - } ->; - -/** - * Appends the ownerState object to the props, merging with the existing one if necessary. - * - * @param elementType Type of the element that owns the `existingProps`. If the element is a DOM node or undefined, `ownerState` is not applied. - * @param otherProps Props of the element. - * @param ownerState - */ -export function appendOwnerState< - ElementType extends React.ElementType, - OtherProps extends Record, - OwnerState, ->( - elementType: ElementType | undefined, - otherProps: OtherProps, - ownerState: OwnerState, -): AppendOwnerStateReturnType { - if (elementType === undefined || isHostComponent(elementType)) { - return otherProps as AppendOwnerStateReturnType; - } - - return { - ...otherProps, - ownerState: { ...otherProps.ownerState, ...ownerState }, - } as AppendOwnerStateReturnType; -} +export type { AppendOwnerStateReturnType } from '@mui/utils/appendOwnerState'; diff --git a/packages/mui-base/src/utils/extractEventHandlers.ts b/packages/mui-base/src/utils/extractEventHandlers.ts index e3998c358e0240..9cead2050dc828 100644 --- a/packages/mui-base/src/utils/extractEventHandlers.ts +++ b/packages/mui-base/src/utils/extractEventHandlers.ts @@ -1,30 +1 @@ -import { EventHandlers } from './types'; - -/** - * Extracts event handlers from a given object. - * A prop is considered an event handler if it is a function and its name starts with `on`. - * - * @param object An object to extract event handlers from. - * @param excludeKeys An array of keys to exclude from the returned object. - */ -export function extractEventHandlers( - object: Record | undefined, - excludeKeys: string[] = [], -): EventHandlers { - if (object === undefined) { - return {}; - } - - const result: EventHandlers = {}; - - Object.keys(object) - .filter( - (prop) => - prop.match(/^on[A-Z]/) && typeof object[prop] === 'function' && !excludeKeys.includes(prop), - ) - .forEach((prop) => { - result[prop] = object[prop]; - }); - - return result; -} +export { default as extractEventHandlers } from '@mui/utils/extractEventHandlers'; diff --git a/packages/mui-base/src/utils/types.ts b/packages/mui-base/src/utils/types.ts index 4d2a96ce8f99e1..3a1883cec220b7 100644 --- a/packages/mui-base/src/utils/types.ts +++ b/packages/mui-base/src/utils/types.ts @@ -1,27 +1,6 @@ -import * as React from 'react'; - -export type EventHandlers = Record>; - -export type WithOptionalOwnerState = Omit< - Props, - 'ownerState' -> & - Partial>; - -export type SlotComponentProps = - | (Partial> & TOverrides) - | (( - ownerState: TOwnerState, - ) => Partial> & TOverrides); - -export type SlotComponentPropsWithSlotState< - TSlotComponent extends React.ElementType, - TOverrides, - TOwnerState, - TSlotState, -> = - | (Partial> & TOverrides) - | (( - ownerState: TOwnerState, - slotState: TSlotState, - ) => Partial> & TOverrides); +export type { + EventHandlers, + WithOptionalOwnerState, + SlotComponentProps, + SlotComponentPropsWithSlotState, +} from '@mui/utils'; diff --git a/packages/mui-base/src/utils/useSlotProps.ts b/packages/mui-base/src/utils/useSlotProps.ts index 10e40630768283..3d11566e23eb35 100644 --- a/packages/mui-base/src/utils/useSlotProps.ts +++ b/packages/mui-base/src/utils/useSlotProps.ts @@ -1,113 +1,4 @@ 'use client'; -import * as React from 'react'; -import { unstable_useForkRef as useForkRef } from '@mui/utils'; -import { appendOwnerState, AppendOwnerStateReturnType } from './appendOwnerState'; -import { - mergeSlotProps, - MergeSlotPropsParameters, - MergeSlotPropsResult, - WithCommonProps, -} from './mergeSlotProps'; -import { resolveComponentProps } from './resolveComponentProps'; +export { default as useSlotProps } from '@mui/utils/useSlotProps'; -export type UseSlotPropsParameters< - ElementType extends React.ElementType, - SlotProps, - ExternalForwardedProps, - ExternalSlotProps, - AdditionalProps, - OwnerState, -> = Omit< - MergeSlotPropsParameters, - 'externalSlotProps' -> & { - /** - * The type of the component used in the slot. - */ - elementType: ElementType | undefined; - /** - * The `slotProps.*` of the Base UI component. - */ - externalSlotProps: - | ExternalSlotProps - | ((ownerState: OwnerState) => ExternalSlotProps) - | undefined; - /** - * The ownerState of the Base UI component. - */ - ownerState: OwnerState; - /** - * Set to true if the slotProps callback should receive more props. - */ - skipResolvingSlotProps?: boolean; -}; - -export type UseSlotPropsResult< - ElementType extends React.ElementType, - SlotProps, - AdditionalProps, - OwnerState, -> = AppendOwnerStateReturnType< - ElementType, - MergeSlotPropsResult['props'] & { - ref: ((instance: any | null) => void) | null; - }, - OwnerState ->; - -/** - * @ignore - do not document. - * Builds the props to be passed into the slot of an unstyled component. - * It merges the internal props of the component with the ones supplied by the user, allowing to customize the behavior. - * If the slot component is not a host component, it also merges in the `ownerState`. - * - * @param parameters.getSlotProps - A function that returns the props to be passed to the slot component. - */ -export function useSlotProps< - ElementType extends React.ElementType, - SlotProps, - AdditionalProps, - OwnerState, ->( - parameters: UseSlotPropsParameters< - ElementType, - SlotProps, - object, - WithCommonProps>, - AdditionalProps, - OwnerState - >, -) { - const { - elementType, - externalSlotProps, - ownerState, - skipResolvingSlotProps = false, - ...rest - } = parameters; - const resolvedComponentsProps = skipResolvingSlotProps - ? {} - : resolveComponentProps(externalSlotProps, ownerState); - const { props: mergedProps, internalRef } = mergeSlotProps({ - ...rest, - externalSlotProps: resolvedComponentsProps, - }); - - const ref = useForkRef( - internalRef, - resolvedComponentsProps?.ref, - parameters.additionalProps?.ref, - ) as ((instance: any | null) => void) | null; - - const props: UseSlotPropsResult = - appendOwnerState( - elementType, - { - ...mergedProps, - ref, - }, - ownerState, - ); - - return props; -} +export type { UseSlotPropsParameters, UseSlotPropsResult } from '@mui/utils/useSlotProps'; diff --git a/packages/mui-material/package.json b/packages/mui-material/package.json index baf55b9e9e5175..bb8886efa10bcb 100644 --- a/packages/mui-material/package.json +++ b/packages/mui-material/package.json @@ -42,8 +42,7 @@ "typescript:module-augmentation": "node scripts/testModuleAugmentation.js" }, "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/base": "workspace:*", + "@babel/runtime": "^7.24.7", "@mui/core-downloads-tracker": "workspace:^", "@mui/system": "workspace:^", "@mui/types": "workspace:^", diff --git a/packages/mui-material/src/Autocomplete/Autocomplete.d.ts b/packages/mui-material/src/Autocomplete/Autocomplete.d.ts index d196d142531e42..8d76e70b68abc5 100644 --- a/packages/mui-material/src/Autocomplete/Autocomplete.d.ts +++ b/packages/mui-material/src/Autocomplete/Autocomplete.d.ts @@ -1,8 +1,11 @@ import * as React from 'react'; import { SxProps } from '@mui/system'; import { OverridableStringUnion } from '@mui/types'; -import { - useAutocomplete, +import { IconButtonProps, InternalStandardProps as StandardProps, Theme } from '@mui/material'; +import { ChipProps, ChipTypeMap } from '@mui/material/Chip'; +import { PaperProps } from '@mui/material/Paper'; +import { PopperProps } from '@mui/material/Popper'; +import useAutocomplete, { AutocompleteChangeDetails, AutocompleteChangeReason, AutocompleteCloseReason, @@ -11,11 +14,7 @@ import { createFilterOptions, UseAutocompleteProps, AutocompleteFreeSoloValueMapping, -} from '@mui/base'; -import { IconButtonProps, InternalStandardProps as StandardProps, Theme } from '@mui/material'; -import { ChipProps, ChipTypeMap } from '@mui/material/Chip'; -import { PaperProps } from '@mui/material/Paper'; -import { PopperProps } from '@mui/material/Popper'; +} from '../useAutocomplete'; import { AutocompleteClasses } from './autocompleteClasses'; export { diff --git a/packages/mui-material/src/Autocomplete/Autocomplete.js b/packages/mui-material/src/Autocomplete/Autocomplete.js index de6e7ba5844f6c..097ded86f4f109 100644 --- a/packages/mui-material/src/Autocomplete/Autocomplete.js +++ b/packages/mui-material/src/Autocomplete/Autocomplete.js @@ -4,9 +4,9 @@ import PropTypes from 'prop-types'; import clsx from 'clsx'; import integerPropType from '@mui/utils/integerPropType'; import chainPropTypes from '@mui/utils/chainPropTypes'; -import { useAutocomplete, createFilterOptions } from '@mui/base'; import composeClasses from '@mui/utils/composeClasses'; import { alpha } from '@mui/system/colorManipulator'; +import useAutocomplete, { createFilterOptions } from '../useAutocomplete'; import Popper from '../Popper'; import ListSubheader from '../ListSubheader'; import Paper from '../Paper'; diff --git a/packages/mui-material/src/Badge/Badge.d.ts b/packages/mui-material/src/Badge/Badge.d.ts index 5ca85710f17459..a50276479dc92d 100644 --- a/packages/mui-material/src/Badge/Badge.d.ts +++ b/packages/mui-material/src/Badge/Badge.d.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import { SxProps } from '@mui/system'; import { OverridableStringUnion, Simplify } from '@mui/types'; -import { SlotComponentProps } from '@mui/base/utils'; +import { SlotComponentProps } from '../utils/types'; import { Theme } from '../styles'; import { OverridableComponent, OverrideProps } from '../OverridableComponent'; import { BadgeClasses } from './badgeClasses'; diff --git a/packages/mui-material/src/Badge/Badge.js b/packages/mui-material/src/Badge/Badge.js index 4eff3401d1ff78..a01078205b6b1d 100644 --- a/packages/mui-material/src/Badge/Badge.js +++ b/packages/mui-material/src/Badge/Badge.js @@ -4,8 +4,8 @@ import PropTypes from 'prop-types'; import clsx from 'clsx'; import usePreviousProps from '@mui/utils/usePreviousProps'; import composeClasses from '@mui/utils/composeClasses'; -import { useBadge } from '@mui/base/useBadge'; -import { useSlotProps } from '@mui/base/utils'; +import useSlotProps from '@mui/utils/useSlotProps'; +import useBadge from './useBadge'; import { styled } from '../zero-styled'; import { useDefaultProps } from '../DefaultPropsProvider'; import capitalize from '../utils/capitalize'; diff --git a/packages/mui-material/src/Badge/useBadge.ts b/packages/mui-material/src/Badge/useBadge.ts new file mode 100644 index 00000000000000..214f281d084c35 --- /dev/null +++ b/packages/mui-material/src/Badge/useBadge.ts @@ -0,0 +1,48 @@ +'use client'; +import * as React from 'react'; +import { usePreviousProps } from '@mui/utils'; +import { UseBadgeParameters, UseBadgeReturnValue } from './useBadge.types'; + +/** + * + * Demos: + * + * - [Badge](https://next.mui.com/base-ui/react-badge/#hook) + * + * API: + * + * - [useBadge API](https://next.mui.com/base-ui/react-badge/hooks-api/#use-badge) + */ +function useBadge(parameters: UseBadgeParameters): UseBadgeReturnValue { + const { + badgeContent: badgeContentProp, + invisible: invisibleProp = false, + max: maxProp = 99, + showZero = false, + } = parameters; + + const prevProps = usePreviousProps({ + badgeContent: badgeContentProp, + max: maxProp, + }); + + let invisible = invisibleProp; + + if (invisibleProp === false && badgeContentProp === 0 && !showZero) { + invisible = true; + } + + const { badgeContent, max = maxProp } = invisible ? prevProps : parameters; + + const displayValue: React.ReactNode = + badgeContent && Number(badgeContent) > max ? `${max}+` : badgeContent; + + return { + badgeContent, + invisible, + max, + displayValue, + }; +} + +export default useBadge; diff --git a/packages/mui-material/src/Badge/useBadge.types.ts b/packages/mui-material/src/Badge/useBadge.types.ts new file mode 100644 index 00000000000000..be6ef34830c5f8 --- /dev/null +++ b/packages/mui-material/src/Badge/useBadge.types.ts @@ -0,0 +1,40 @@ +export interface UseBadgeParameters { + /** + * The content rendered within the badge. + */ + badgeContent?: React.ReactNode; + /** + * If `true`, the badge is invisible. + * @default false + */ + invisible?: boolean; + /** + * Max count to show. + * @default 99 + */ + max?: number; + /** + * Controls whether the badge is hidden when `badgeContent` is zero. + * @default false + */ + showZero?: boolean; +} + +export interface UseBadgeReturnValue { + /** + * Defines the content that's displayed inside the badge. + */ + badgeContent: React.ReactNode; + /** + * If `true`, the component will not be visible. + */ + invisible: boolean; + /** + * Maximum number to be displayed in the badge. + */ + max: number; + /** + * Value to be displayed in the badge. If `badgeContent` is greater than `max`, it will return `max+`. + */ + displayValue: React.ReactNode; +} diff --git a/packages/mui-material/src/Breadcrumbs/Breadcrumbs.d.ts b/packages/mui-material/src/Breadcrumbs/Breadcrumbs.d.ts index af7fb0280752cc..a7cce24589befd 100644 --- a/packages/mui-material/src/Breadcrumbs/Breadcrumbs.d.ts +++ b/packages/mui-material/src/Breadcrumbs/Breadcrumbs.d.ts @@ -1,6 +1,6 @@ import * as React from 'react'; import { SxProps } from '@mui/system'; -import { SlotComponentProps } from '@mui/base'; +import { SlotComponentProps } from '../utils/types'; import { Theme } from '../styles'; import { OverridableComponent, OverrideProps } from '../OverridableComponent'; import { BreadcrumbsClasses } from './breadcrumbsClasses'; diff --git a/packages/mui-material/src/Breadcrumbs/Breadcrumbs.js b/packages/mui-material/src/Breadcrumbs/Breadcrumbs.js index d18306a79339a0..a398e73d9fd778 100644 --- a/packages/mui-material/src/Breadcrumbs/Breadcrumbs.js +++ b/packages/mui-material/src/Breadcrumbs/Breadcrumbs.js @@ -4,9 +4,9 @@ import { isFragment } from 'react-is'; import PropTypes from 'prop-types'; import clsx from 'clsx'; import integerPropType from '@mui/utils/integerPropType'; -import { useSlotProps } from '@mui/base/utils'; import composeClasses from '@mui/utils/composeClasses'; -import styled from '../styles/styled'; +import useSlotProps from '@mui/utils/useSlotProps'; +import { styled } from '../zero-styled'; import { useDefaultProps } from '../DefaultPropsProvider'; import Typography from '../Typography'; import BreadcrumbCollapsed from './BreadcrumbCollapsed'; diff --git a/packages/mui-material/src/ClickAwayListener/ClickAwayListener.test.js b/packages/mui-material/src/ClickAwayListener/ClickAwayListener.test.js new file mode 100644 index 00000000000000..e0c4c3730a4a82 --- /dev/null +++ b/packages/mui-material/src/ClickAwayListener/ClickAwayListener.test.js @@ -0,0 +1,441 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { expect } from 'chai'; +import { spy } from 'sinon'; +import { + act, + createRenderer, + fireEvent, + fireDiscreteEvent, + screen, +} from '@mui/internal-test-utils'; +import Portal from '@mui/material/Portal'; +import ClickAwayListener from '@mui/material/ClickAwayListener'; + +describe('', () => { + const { render: clientRender, clock } = createRenderer({ clock: 'fake' }); + /** + * @type {typeof plainRender extends (...args: infer T) => any ? T : never} args + * + * @remarks + * This is for all intents and purposes the same as our client render method. + * `plainRender` is already wrapped in act(). + * However, React has a bug that flushes effects in a portal synchronously. + * We have to defer the effect manually like `useEffect` would so we have to flush the effect manually instead of relying on `act()`. + * React bug: https://github.com/facebook/react/issues/20074 + */ + function render(...args) { + const result = clientRender(...args); + clock.tick(0); + return result; + } + + it('should render the children', () => { + const children = ; + const { container } = render( + {}}>{children}, + ); + expect(container.querySelectorAll('span').length).to.equal(1); + }); + + describe('prop: onClickAway', () => { + it('should be called when clicking away', () => { + const handleClickAway = spy(); + render( + + + , + ); + + fireEvent.click(document.body); + expect(handleClickAway.callCount).to.equal(1); + expect(handleClickAway.args[0].length).to.equal(1); + }); + + it('should not be called when clicking inside', () => { + const handleClickAway = spy(); + const { container } = render( + + + , + ); + + fireEvent.click(container.querySelector('span')); + expect(handleClickAway.callCount).to.equal(0); + }); + + it('should be called when preventDefault is `true`', () => { + const handleClickAway = spy(); + render( + + + , + ); + const preventDefault = (event) => event.preventDefault(); + document.body.addEventListener('click', preventDefault); + + fireEvent.click(document.body); + expect(handleClickAway.callCount).to.equal(1); + + document.body.removeEventListener('click', preventDefault); + }); + + it('should not be called when clicking inside a portaled element', () => { + const handleClickAway = spy(); + const { getByText } = render( + +
+ + Inside a portal + +
+
, + ); + + fireEvent.click(getByText('Inside a portal')); + expect(handleClickAway.callCount).to.equal(0); + }); + + it('should be called when clicking inside a portaled element and `disableReactTree` is `true`', () => { + const handleClickAway = spy(); + const { getByText } = render( + +
+ + Inside a portal + +
+
, + ); + + fireEvent.click(getByText('Inside a portal')); + expect(handleClickAway.callCount).to.equal(1); + }); + + it('should not be called even if the event propagation is stopped', () => { + const handleClickAway = spy(); + const { getByText } = render( + +
+
{ + event.stopPropagation(); + }} + > + Outside a portal +
+ + { + event.stopPropagation(); + }} + > + Stop inside a portal + + + + { + event.stopPropagation(); + event.nativeEvent.stopImmediatePropagation(); + }} + > + Stop all inside a portal + + +
+
, + ); + + fireEvent.click(getByText('Outside a portal')); + expect(handleClickAway.callCount).to.equal(0); + + fireEvent.click(getByText('Stop all inside a portal')); + expect(handleClickAway.callCount).to.equal(0); + + fireEvent.click(getByText('Stop inside a portal')); + // undesired behavior in React 16 + expect(handleClickAway.callCount).to.equal(React.version.startsWith('16') ? 1 : 0); + }); + + ['onClick', 'onClickCapture'].forEach((eventListenerName) => { + it(`should not be called when ${eventListenerName} mounted the listener`, () => { + function Test() { + const [open, setOpen] = React.useState(false); + + return ( + + + {toggle ? : } + + ); + } + + render(); + const button = screen.getByRole('button'); + fireEvent.click(button); + await waitFor(() => { + expect(screen.queryByText('LazyRoute')).not.to.equal(null); + }); + }); + + // For https://github.com/mui/material-ui/pull/33253 + it('should update height without an infinite rendering loop', async () => { + function App() { + const [value, setValue] = React.useState('Controlled'); + + const handleChange = (event: React.ChangeEvent) => { + setValue(event.target.value); + }; + + return ; + } + const { container } = render(); + const input = container.querySelector('textarea')!; + act(() => { + input.focus(); + }); + const activeElement = document.activeElement!; + // set the value of the input to be 1 larger than its content width + fireEvent.change(activeElement, { + target: { value: 'Controlled\n' }, + }); + await sleep(0); + fireEvent.change(activeElement, { + target: { value: 'Controlled\n\n' }, + }); + }); + + // For https://github.com/mui/material-ui/pull/37135 + it('should update height without delay', async function test() { + if (/jsdom/.test(window.navigator.userAgent)) { + // It depends on ResizeObserver + this.skip(); + } + + function App() { + const ref = React.useRef(null); + return ( +
+ +
+ +
+
+ ); + } + const { container } = render(); + const input = container.querySelector('textarea')!; + const button = screen.getByRole('button'); + expect(parseInt(input.style.height, 10)).to.be.within(30, 32); + fireEvent.click(button); + await raf(); + await raf(); + expect(parseInt(input.style.height, 10)).to.be.within(15, 17); + }); + + describe('layout', () => { + const getComputedStyleStub = new Map>(); + function setLayout( + input: HTMLTextAreaElement, + shadow: Element, + { + getComputedStyle, + scrollHeight: scrollHeightArg, + lineHeight: lineHeightArg, + }: { + getComputedStyle: Partial; + scrollHeight?: number | (() => number); + lineHeight?: number | (() => number); + }, + ) { + const lineHeight = typeof lineHeightArg === 'function' ? lineHeightArg : () => lineHeightArg; + const scrollHeight = + typeof scrollHeightArg === 'function' ? scrollHeightArg : () => scrollHeightArg; + + getComputedStyleStub.set(input, getComputedStyle); + + let index = 0; + stub(shadow, 'scrollHeight').get(() => { + index += 1; + return index % 2 === 1 ? scrollHeight() : lineHeight(); + }); + } + + before(function beforeHook() { + // Only run the test on node. + if (!/jsdom/.test(window.navigator.userAgent)) { + this.skip(); + } + + stub(window, 'getComputedStyle').value( + (node: Element) => getComputedStyleStub.get(node) || {}, + ); + }); + + after(() => { + sinon.restore(); + }); + + describe('resize', () => { + clock.withFakeTimers(); + + it('should handle the resize event', () => { + const { container } = render(); + const input = container.querySelector('textarea[aria-hidden=null]')!; + const shadow = container.querySelector('textarea[aria-hidden=true]')!; + + expect(input.style).to.have.property('height', '0px'); + expect(input.style).to.have.property('overflow', 'hidden'); + + setLayout(input, shadow, { + getComputedStyle: { + boxSizing: 'content-box', + }, + scrollHeight: 30, + lineHeight: 15, + }); + window.dispatchEvent(new window.Event('resize', {})); + + clock.tick(166); + + expect(input.style).to.have.property('height', '30px'); + expect(input.style).to.have.property('overflow', 'hidden'); + }); + }); + + it('should update when uncontrolled', () => { + const handleChange = spy(); + const { container } = render(); + const input = container.querySelector('textarea[aria-hidden=null]')!; + const shadow = container.querySelector('textarea[aria-hidden=true]')!; + expect(input.style).to.have.property('height', '0px'); + expect(input.style).to.have.property('overflow', 'hidden'); + setLayout(input, shadow, { + getComputedStyle: { + boxSizing: 'content-box', + }, + scrollHeight: 30, + lineHeight: 15, + }); + act(() => { + input.focus(); + }); + const activeElement = document.activeElement!; + fireEvent.change(activeElement, { target: { value: 'a' } }); + expect(input.style).to.have.property('height', '30px'); + expect(input.style).to.have.property('overflow', 'hidden'); + expect(handleChange.callCount).to.equal(1); + }); + + it('should take the border into account with border-box', () => { + const border = 5; + const { container, forceUpdate } = render(); + const input = container.querySelector('textarea[aria-hidden=null]')!; + const shadow = container.querySelector('textarea[aria-hidden=true]')!; + expect(input.style).to.have.property('height', '0px'); + expect(input.style).to.have.property('overflow', 'hidden'); + setLayout(input, shadow, { + getComputedStyle: { + boxSizing: 'border-box', + borderBottomWidth: `${border}px`, + }, + scrollHeight: 30, + lineHeight: 15, + }); + forceUpdate(); + expect(input.style).to.have.property('height', `${30 + border}px`); + expect(input.style).to.have.property('overflow', 'hidden'); + }); + + it('should take the padding into account with content-box', () => { + const padding = 5; + const { container, forceUpdate } = render(); + const input = container.querySelector('textarea[aria-hidden=null]')!; + const shadow = container.querySelector('textarea[aria-hidden=true]')!; + setLayout(input, shadow, { + getComputedStyle: { + boxSizing: 'border-box', + paddingTop: `${padding}px`, + }, + scrollHeight: 30, + lineHeight: 15, + }); + forceUpdate(); + expect(input.style).to.have.property('height', `${30 + padding}px`); + expect(input.style).to.have.property('overflow', 'hidden'); + }); + + it('should have at least height of "minRows"', () => { + const minRows = 3; + const lineHeight = 15; + const { container, forceUpdate } = render(); + const input = container.querySelector('textarea[aria-hidden=null]')!; + const shadow = container.querySelector('textarea[aria-hidden=true]')!; + setLayout(input, shadow, { + getComputedStyle: { + boxSizing: 'content-box', + }, + scrollHeight: 30, + lineHeight, + }); + forceUpdate(); + expect(input.style).to.have.property('height', `${lineHeight * minRows}px`); + expect(input.style).to.have.property('overflow', ''); + }); + + it('should have at max "maxRows" rows', () => { + const maxRows = 3; + const lineHeight = 15; + const { container, forceUpdate } = render(); + const input = container.querySelector('textarea[aria-hidden=null]')!; + const shadow = container.querySelector('textarea[aria-hidden=true]')!; + setLayout(input, shadow, { + getComputedStyle: { + boxSizing: 'content-box', + }, + scrollHeight: 100, + lineHeight, + }); + forceUpdate(); + expect(input.style).to.have.property('height', `${lineHeight * maxRows}px`); + expect(input.style).to.have.property('overflow', ''); + }); + + it('should show scrollbar when having more rows than "maxRows"', () => { + const maxRows = 3; + const lineHeight = 15; + const { container, forceUpdate } = render(); + const input = container.querySelector('textarea[aria-hidden=null]')!; + const shadow = container.querySelector('textarea[aria-hidden=true]')!; + setLayout(input, shadow, { + getComputedStyle: { + boxSizing: 'border-box', + }, + scrollHeight: lineHeight * 2, + lineHeight, + }); + forceUpdate(); + expect(input.style).to.have.property('height', `${lineHeight * 2}px`); + expect(input.style).to.have.property('overflow', 'hidden'); + setLayout(input, shadow, { + getComputedStyle: { + boxSizing: 'border-box', + }, + scrollHeight: lineHeight * 3, + lineHeight, + }); + forceUpdate(); + expect(input.style).to.have.property('height', `${lineHeight * 3}px`); + expect(input.style).to.have.property('overflow', 'hidden'); + setLayout(input, shadow, { + getComputedStyle: { + boxSizing: 'border-box', + }, + scrollHeight: lineHeight * 4, + lineHeight, + }); + forceUpdate(); + expect(input.style).to.have.property('height', `${lineHeight * 3}px`); + expect(input.style).to.have.property('overflow', ''); + }); + + it('should update its height when the "maxRows" prop changes', () => { + const lineHeight = 15; + const { container, forceUpdate, setProps } = render(); + const input = container.querySelector('textarea[aria-hidden=null]')!; + const shadow = container.querySelector('textarea[aria-hidden=true]')!; + setLayout(input, shadow, { + getComputedStyle: { + boxSizing: 'content-box', + }, + scrollHeight: 100, + lineHeight, + }); + forceUpdate(); + expect(input.style).to.have.property('height', `${lineHeight * 3}px`); + expect(input.style).to.have.property('overflow', ''); + setProps({ maxRows: 2 }); + expect(input.style).to.have.property('height', `${lineHeight * 2}px`); + expect(input.style).to.have.property('overflow', ''); + }); + + it('should not sync height if container width is 0px', () => { + const lineHeight = 15; + const { container, forceUpdate } = render(); + const input = container.querySelector('textarea[aria-hidden=null]')!; + const shadow = container.querySelector('textarea[aria-hidden=true]')!; + + setLayout(input, shadow, { + getComputedStyle: { + boxSizing: 'content-box', + }, + scrollHeight: lineHeight * 2, + lineHeight, + }); + forceUpdate(); + + expect(input.style).to.have.property('height', `${lineHeight * 2}px`); + expect(input.style).to.have.property('overflow', 'hidden'); + + setLayout(input, shadow, { + getComputedStyle: { + boxSizing: 'content-box', + width: '0px', + }, + scrollHeight: lineHeight * 3, + lineHeight, + }); + + forceUpdate(); + expect(input.style).to.have.property('height', `${lineHeight * 2}px`); + expect(input.style).to.have.property('overflow', 'hidden'); + }); + + it('should compute the correct height if padding-right is greater than 0px', () => { + const paddingRight = 50; + const { container, forceUpdate } = render(); + const input = container.querySelector('textarea[aria-hidden=null]')!; + const shadow = container.querySelector('textarea[aria-hidden=true]')! as HTMLTextAreaElement; + const contentWidth = 100; + const lineHeight = 15; + const width = contentWidth + paddingRight; + setLayout(input, shadow, { + getComputedStyle: { + boxSizing: 'border-box', + width: `${width}px`, + }, + scrollHeight: () => { + // assuming that the width of the word is 1px, and substract the width of the paddingRight + const lineNum = Math.ceil( + input.value.length / (width - getStyleValue(shadow.style.paddingRight)), + ); + return lineNum * lineHeight; + }, + lineHeight, + }); + + act(() => { + input.focus(); + }); + const activeElement = document.activeElement!; + // set the value of the input to be 1 larger than its content width + fireEvent.change(activeElement, { + target: { value: new Array(contentWidth + 1).fill('a').join('') }, + }); + forceUpdate(); + + // the input should be 2 lines + expect(input.style).to.have.property('height', `${lineHeight * 2}px`); + }); + }); + + it('should apply the inline styles using the "style" prop', function test() { + if (/jsdom/.test(window.navigator.userAgent)) { + this.skip(); + } + + const { container } = render(); + const input = container.querySelector('textarea')!; + + expect(input).toHaveComputedStyle({ + backgroundColor: 'rgb(255, 255, 0)', + }); + }); +}); diff --git a/packages/mui-material/src/TextareaAutosize/TextareaAutosize.tsx b/packages/mui-material/src/TextareaAutosize/TextareaAutosize.tsx new file mode 100644 index 00000000000000..c284416fd79f87 --- /dev/null +++ b/packages/mui-material/src/TextareaAutosize/TextareaAutosize.tsx @@ -0,0 +1,264 @@ +'use client'; +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { + unstable_debounce as debounce, + unstable_useForkRef as useForkRef, + unstable_useEnhancedEffect as useEnhancedEffect, + unstable_ownerWindow as ownerWindow, +} from '@mui/utils'; +import { TextareaAutosizeProps } from './TextareaAutosize.types'; + +function getStyleValue(value: string) { + return parseInt(value, 10) || 0; +} + +const styles: { + shadow: React.CSSProperties; +} = { + shadow: { + // Visibility needed to hide the extra text area on iPads + visibility: 'hidden', + // Remove from the content flow + position: 'absolute', + // Ignore the scrollbar width + overflow: 'hidden', + height: 0, + top: 0, + left: 0, + // Create a new layer, increase the isolation of the computed values + transform: 'translateZ(0)', + }, +}; + +type TextareaStyles = { + outerHeightStyle: number; + overflowing: boolean; +}; + +function isEmpty(obj: TextareaStyles) { + return ( + obj === undefined || + obj === null || + Object.keys(obj).length === 0 || + (obj.outerHeightStyle === 0 && !obj.overflowing) + ); +} + +/** + * + * Demos: + * + * - [Textarea Autosize](https://next.mui.com/material-ui/react-textarea-autosize/) + * + * API: + * + * - [TextareaAutosize API](https://next.mui.com/material-ui/api/textarea-autosize/) + */ +const TextareaAutosize = React.forwardRef(function TextareaAutosize( + props: TextareaAutosizeProps, + forwardedRef: React.ForwardedRef, +) { + const { onChange, maxRows, minRows = 1, style, value, ...other } = props; + + const { current: isControlled } = React.useRef(value != null); + const inputRef = React.useRef(null); + const handleRef = useForkRef(forwardedRef, inputRef); + const heightRef = React.useRef(null); + const shadowRef = React.useRef(null); + + const calculateTextareaStyles = React.useCallback(() => { + const input = inputRef.current!; + + const containerWindow = ownerWindow(input); + const computedStyle = containerWindow.getComputedStyle(input); + + // If input's width is shrunk and it's not visible, don't sync height. + if (computedStyle.width === '0px') { + return { + outerHeightStyle: 0, + overflowing: false, + }; + } + + const inputShallow = shadowRef.current!; + + inputShallow.style.width = computedStyle.width; + inputShallow.value = input.value || props.placeholder || 'x'; + if (inputShallow.value.slice(-1) === '\n') { + // Certain fonts which overflow the line height will cause the textarea + // to report a different scrollHeight depending on whether the last line + // is empty. Make it non-empty to avoid this issue. + inputShallow.value += ' '; + } + + const boxSizing = computedStyle.boxSizing; + const padding = + getStyleValue(computedStyle.paddingBottom) + getStyleValue(computedStyle.paddingTop); + const border = + getStyleValue(computedStyle.borderBottomWidth) + getStyleValue(computedStyle.borderTopWidth); + + // The height of the inner content + const innerHeight = inputShallow.scrollHeight; + + // Measure height of a textarea with a single row + inputShallow.value = 'x'; + const singleRowHeight = inputShallow.scrollHeight; + + // The height of the outer content + let outerHeight = innerHeight; + + if (minRows) { + outerHeight = Math.max(Number(minRows) * singleRowHeight, outerHeight); + } + if (maxRows) { + outerHeight = Math.min(Number(maxRows) * singleRowHeight, outerHeight); + } + outerHeight = Math.max(outerHeight, singleRowHeight); + + // Take the box sizing into account for applying this value as a style. + const outerHeightStyle = outerHeight + (boxSizing === 'border-box' ? padding + border : 0); + const overflowing = Math.abs(outerHeight - innerHeight) <= 1; + + return { outerHeightStyle, overflowing }; + }, [maxRows, minRows, props.placeholder]); + + const syncHeight = React.useCallback(() => { + const textareaStyles = calculateTextareaStyles(); + + if (isEmpty(textareaStyles)) { + return; + } + + const outerHeightStyle = textareaStyles.outerHeightStyle; + const input = inputRef.current!; + if (heightRef.current !== outerHeightStyle) { + heightRef.current = outerHeightStyle; + input.style.height = `${outerHeightStyle}px`; + } + input.style.overflow = textareaStyles.overflowing ? 'hidden' : ''; + }, [calculateTextareaStyles]); + + useEnhancedEffect(() => { + const handleResize = () => { + syncHeight(); + }; + // Workaround a "ResizeObserver loop completed with undelivered notifications" error + // in test. + // Note that we might need to use this logic in production per https://github.com/WICG/resize-observer/issues/38 + // Also see https://github.com/mui/mui-x/issues/8733 + let rAF: any; + const rAFHandleResize = () => { + cancelAnimationFrame(rAF); + rAF = requestAnimationFrame(() => { + handleResize(); + }); + }; + const debounceHandleResize = debounce(handleResize); + const input = inputRef.current!; + const containerWindow = ownerWindow(input); + + containerWindow.addEventListener('resize', debounceHandleResize); + + let resizeObserver: ResizeObserver; + + if (typeof ResizeObserver !== 'undefined') { + resizeObserver = new ResizeObserver( + process.env.NODE_ENV === 'test' ? rAFHandleResize : handleResize, + ); + resizeObserver.observe(input); + } + + return () => { + debounceHandleResize.clear(); + cancelAnimationFrame(rAF); + containerWindow.removeEventListener('resize', debounceHandleResize); + if (resizeObserver) { + resizeObserver.disconnect(); + } + }; + }, [calculateTextareaStyles, syncHeight]); + + useEnhancedEffect(() => { + syncHeight(); + }); + + const handleChange = (event: React.ChangeEvent) => { + if (!isControlled) { + syncHeight(); + } + + if (onChange) { + onChange(event); + } + }; + + return ( + +