diff --git a/docs/src/pages/components/speed-dial/SpeedDialTooltipOpen.tsx b/docs/src/pages/components/speed-dial/SpeedDialTooltipOpen.tsx
index c96f81cc3a5761..728d311417f545 100644
--- a/docs/src/pages/components/speed-dial/SpeedDialTooltipOpen.tsx
+++ b/docs/src/pages/components/speed-dial/SpeedDialTooltipOpen.tsx
@@ -34,14 +34,8 @@ const actions = [
export default function SpeedDialTooltipOpen() {
const classes = useStyles();
const [open, setOpen] = React.useState(false);
-
- const handleOpen = () => {
- setOpen(true);
- };
-
- const handleClose = () => {
- setOpen(false);
- };
+ const handleOpen = () => setOpen(true);
+ const handleClose = () => setOpen(false);
return (
diff --git a/docs/src/pagesApi.js b/docs/src/pagesApi.js
index 29acd28472bd97..2dd248017b207a 100644
--- a/docs/src/pagesApi.js
+++ b/docs/src/pagesApi.js
@@ -79,6 +79,7 @@ module.exports = [
{ pathname: '/api-docs/mobile-stepper' },
{ pathname: '/api-docs/mobile-time-picker' },
{ pathname: '/api-docs/modal' },
+ { pathname: '/api-docs/modal-unstyled' },
{ pathname: '/api-docs/native-select' },
{ pathname: '/api-docs/no-ssr' },
{ pathname: '/api-docs/outlined-input' },
diff --git a/docs/translations/api-docs/modal-unstyled/modal-unstyled-de.json b/docs/translations/api-docs/modal-unstyled/modal-unstyled-de.json
new file mode 100644
index 00000000000000..f7a5160fc0258b
--- /dev/null
+++ b/docs/translations/api-docs/modal-unstyled/modal-unstyled-de.json
@@ -0,0 +1,33 @@
+{
+ "componentDescription": "Modal is a lower-level construct that is leveraged by the following components:\n\n- [Dialog](/api/dialog/)\n- [Drawer](/api/drawer/)\n- [Menu](/api/menu/)\n- [Popover](/api/popover/)\n\nIf you are creating a modal dialog, you probably want to use the [Dialog](/api/dialog/) component\nrather than directly using Modal.\n\nThis component shares many concepts with [react-overlays](https://react-bootstrap.github.io/react-overlays/#modals).",
+ "propDescriptions": {
+ "BackdropComponent": "A backdrop component. This prop enables custom backdrop rendering.",
+ "BackdropProps": "Props applied to the
Backdrop
element.",
+ "children": "A single child content element.
⚠️
Needs to be able to hold a ref.",
+ "classes": "Override or extend the styles applied to the component. See
CSS API below for more details.",
+ "closeAfterTransition": "When set to true the Modal waits until a nested Transition is completed before closing.",
+ "component": "The component used for the root node. Either a string to use a HTML element or a component.",
+ "components": "The components used for each slot inside the Modal. Either a string to use a HTML element or a component.",
+ "componentsProps": "The props used for each slot inside the Modal.",
+ "container": "An HTML element or function that returns one. The
container
will have the portal children appended to it.
By default, it uses the body of the top-level document object, so it's simply
document.body
most of the time.",
+ "disableAutoFocus": "If
true
, the modal will not automatically shift focus to itself when it opens, and replace it to the last focused element when it closes. This also works correctly with any modal children that have the
disableAutoFocus
prop.
Generally this should never be set to
true
as it makes the modal less accessible to assistive technologies, like screen readers.",
+ "disableEnforceFocus": "If
true
, the modal will not prevent focus from leaving the modal while open.
Generally this should never be set to
true
as it makes the modal less accessible to assistive technologies, like screen readers.",
+ "disableEscapeKeyDown": "If
true
, hitting escape will not fire the
onClose
callback.",
+ "disablePortal": "The
children
will be under the DOM hierarchy of the parent component.",
+ "disableRestoreFocus": "If
true
, the modal will not restore focus to previously focused element once modal is hidden.",
+ "disableScrollLock": "Disable the scroll lock behavior.",
+ "hideBackdrop": "If
true
, the backdrop is not rendered.",
+ "keepMounted": "Always keep the children in the DOM. This prop can be useful in SEO situation or when you want to maximize the responsiveness of the Modal.",
+ "onBackdropClick": "Callback fired when the backdrop is clicked.",
+ "onClose": "Callback fired when the component requests to be closed. The
reason
parameter can optionally be used to control the response to
onClose
.
Signature:function(event: object, reason: string) => void
event: The event source of the callback.
reason: Can be:
"escapeKeyDown"
,
"backdropClick"
.",
+ "open": "If
true
, the component is shown."
+ },
+ "classDescriptions": {
+ "root": { "description": "Styles applied to the root element." },
+ "hidden": {
+ "description": "Styles applied to {{nodeName}} if {{conditions}}.",
+ "nodeName": "the root element",
+ "conditions": "the
Modal
has exited"
+ }
+ }
+}
diff --git a/docs/translations/api-docs/modal-unstyled/modal-unstyled-es.json b/docs/translations/api-docs/modal-unstyled/modal-unstyled-es.json
new file mode 100644
index 00000000000000..f7a5160fc0258b
--- /dev/null
+++ b/docs/translations/api-docs/modal-unstyled/modal-unstyled-es.json
@@ -0,0 +1,33 @@
+{
+ "componentDescription": "Modal is a lower-level construct that is leveraged by the following components:\n\n- [Dialog](/api/dialog/)\n- [Drawer](/api/drawer/)\n- [Menu](/api/menu/)\n- [Popover](/api/popover/)\n\nIf you are creating a modal dialog, you probably want to use the [Dialog](/api/dialog/) component\nrather than directly using Modal.\n\nThis component shares many concepts with [react-overlays](https://react-bootstrap.github.io/react-overlays/#modals).",
+ "propDescriptions": {
+ "BackdropComponent": "A backdrop component. This prop enables custom backdrop rendering.",
+ "BackdropProps": "Props applied to the
Backdrop
element.",
+ "children": "A single child content element.
⚠️
Needs to be able to hold a ref.",
+ "classes": "Override or extend the styles applied to the component. See
CSS API below for more details.",
+ "closeAfterTransition": "When set to true the Modal waits until a nested Transition is completed before closing.",
+ "component": "The component used for the root node. Either a string to use a HTML element or a component.",
+ "components": "The components used for each slot inside the Modal. Either a string to use a HTML element or a component.",
+ "componentsProps": "The props used for each slot inside the Modal.",
+ "container": "An HTML element or function that returns one. The
container
will have the portal children appended to it.
By default, it uses the body of the top-level document object, so it's simply
document.body
most of the time.",
+ "disableAutoFocus": "If
true
, the modal will not automatically shift focus to itself when it opens, and replace it to the last focused element when it closes. This also works correctly with any modal children that have the
disableAutoFocus
prop.
Generally this should never be set to
true
as it makes the modal less accessible to assistive technologies, like screen readers.",
+ "disableEnforceFocus": "If
true
, the modal will not prevent focus from leaving the modal while open.
Generally this should never be set to
true
as it makes the modal less accessible to assistive technologies, like screen readers.",
+ "disableEscapeKeyDown": "If
true
, hitting escape will not fire the
onClose
callback.",
+ "disablePortal": "The
children
will be under the DOM hierarchy of the parent component.",
+ "disableRestoreFocus": "If
true
, the modal will not restore focus to previously focused element once modal is hidden.",
+ "disableScrollLock": "Disable the scroll lock behavior.",
+ "hideBackdrop": "If
true
, the backdrop is not rendered.",
+ "keepMounted": "Always keep the children in the DOM. This prop can be useful in SEO situation or when you want to maximize the responsiveness of the Modal.",
+ "onBackdropClick": "Callback fired when the backdrop is clicked.",
+ "onClose": "Callback fired when the component requests to be closed. The
reason
parameter can optionally be used to control the response to
onClose
.
Signature:function(event: object, reason: string) => void
event: The event source of the callback.
reason: Can be:
"escapeKeyDown"
,
"backdropClick"
.",
+ "open": "If
true
, the component is shown."
+ },
+ "classDescriptions": {
+ "root": { "description": "Styles applied to the root element." },
+ "hidden": {
+ "description": "Styles applied to {{nodeName}} if {{conditions}}.",
+ "nodeName": "the root element",
+ "conditions": "the
Modal
has exited"
+ }
+ }
+}
diff --git a/docs/translations/api-docs/modal-unstyled/modal-unstyled-fr.json b/docs/translations/api-docs/modal-unstyled/modal-unstyled-fr.json
new file mode 100644
index 00000000000000..f7a5160fc0258b
--- /dev/null
+++ b/docs/translations/api-docs/modal-unstyled/modal-unstyled-fr.json
@@ -0,0 +1,33 @@
+{
+ "componentDescription": "Modal is a lower-level construct that is leveraged by the following components:\n\n- [Dialog](/api/dialog/)\n- [Drawer](/api/drawer/)\n- [Menu](/api/menu/)\n- [Popover](/api/popover/)\n\nIf you are creating a modal dialog, you probably want to use the [Dialog](/api/dialog/) component\nrather than directly using Modal.\n\nThis component shares many concepts with [react-overlays](https://react-bootstrap.github.io/react-overlays/#modals).",
+ "propDescriptions": {
+ "BackdropComponent": "A backdrop component. This prop enables custom backdrop rendering.",
+ "BackdropProps": "Props applied to the
Backdrop
element.",
+ "children": "A single child content element.
⚠️
Needs to be able to hold a ref.",
+ "classes": "Override or extend the styles applied to the component. See
CSS API below for more details.",
+ "closeAfterTransition": "When set to true the Modal waits until a nested Transition is completed before closing.",
+ "component": "The component used for the root node. Either a string to use a HTML element or a component.",
+ "components": "The components used for each slot inside the Modal. Either a string to use a HTML element or a component.",
+ "componentsProps": "The props used for each slot inside the Modal.",
+ "container": "An HTML element or function that returns one. The
container
will have the portal children appended to it.
By default, it uses the body of the top-level document object, so it's simply
document.body
most of the time.",
+ "disableAutoFocus": "If
true
, the modal will not automatically shift focus to itself when it opens, and replace it to the last focused element when it closes. This also works correctly with any modal children that have the
disableAutoFocus
prop.
Generally this should never be set to
true
as it makes the modal less accessible to assistive technologies, like screen readers.",
+ "disableEnforceFocus": "If
true
, the modal will not prevent focus from leaving the modal while open.
Generally this should never be set to
true
as it makes the modal less accessible to assistive technologies, like screen readers.",
+ "disableEscapeKeyDown": "If
true
, hitting escape will not fire the
onClose
callback.",
+ "disablePortal": "The
children
will be under the DOM hierarchy of the parent component.",
+ "disableRestoreFocus": "If
true
, the modal will not restore focus to previously focused element once modal is hidden.",
+ "disableScrollLock": "Disable the scroll lock behavior.",
+ "hideBackdrop": "If
true
, the backdrop is not rendered.",
+ "keepMounted": "Always keep the children in the DOM. This prop can be useful in SEO situation or when you want to maximize the responsiveness of the Modal.",
+ "onBackdropClick": "Callback fired when the backdrop is clicked.",
+ "onClose": "Callback fired when the component requests to be closed. The
reason
parameter can optionally be used to control the response to
onClose
.
Signature:function(event: object, reason: string) => void
event: The event source of the callback.
reason: Can be:
"escapeKeyDown"
,
"backdropClick"
.",
+ "open": "If
true
, the component is shown."
+ },
+ "classDescriptions": {
+ "root": { "description": "Styles applied to the root element." },
+ "hidden": {
+ "description": "Styles applied to {{nodeName}} if {{conditions}}.",
+ "nodeName": "the root element",
+ "conditions": "the
Modal
has exited"
+ }
+ }
+}
diff --git a/docs/translations/api-docs/modal-unstyled/modal-unstyled-ja.json b/docs/translations/api-docs/modal-unstyled/modal-unstyled-ja.json
new file mode 100644
index 00000000000000..f7a5160fc0258b
--- /dev/null
+++ b/docs/translations/api-docs/modal-unstyled/modal-unstyled-ja.json
@@ -0,0 +1,33 @@
+{
+ "componentDescription": "Modal is a lower-level construct that is leveraged by the following components:\n\n- [Dialog](/api/dialog/)\n- [Drawer](/api/drawer/)\n- [Menu](/api/menu/)\n- [Popover](/api/popover/)\n\nIf you are creating a modal dialog, you probably want to use the [Dialog](/api/dialog/) component\nrather than directly using Modal.\n\nThis component shares many concepts with [react-overlays](https://react-bootstrap.github.io/react-overlays/#modals).",
+ "propDescriptions": {
+ "BackdropComponent": "A backdrop component. This prop enables custom backdrop rendering.",
+ "BackdropProps": "Props applied to the
Backdrop
element.",
+ "children": "A single child content element.
⚠️
Needs to be able to hold a ref.",
+ "classes": "Override or extend the styles applied to the component. See
CSS API below for more details.",
+ "closeAfterTransition": "When set to true the Modal waits until a nested Transition is completed before closing.",
+ "component": "The component used for the root node. Either a string to use a HTML element or a component.",
+ "components": "The components used for each slot inside the Modal. Either a string to use a HTML element or a component.",
+ "componentsProps": "The props used for each slot inside the Modal.",
+ "container": "An HTML element or function that returns one. The
container
will have the portal children appended to it.
By default, it uses the body of the top-level document object, so it's simply
document.body
most of the time.",
+ "disableAutoFocus": "If
true
, the modal will not automatically shift focus to itself when it opens, and replace it to the last focused element when it closes. This also works correctly with any modal children that have the
disableAutoFocus
prop.
Generally this should never be set to
true
as it makes the modal less accessible to assistive technologies, like screen readers.",
+ "disableEnforceFocus": "If
true
, the modal will not prevent focus from leaving the modal while open.
Generally this should never be set to
true
as it makes the modal less accessible to assistive technologies, like screen readers.",
+ "disableEscapeKeyDown": "If
true
, hitting escape will not fire the
onClose
callback.",
+ "disablePortal": "The
children
will be under the DOM hierarchy of the parent component.",
+ "disableRestoreFocus": "If
true
, the modal will not restore focus to previously focused element once modal is hidden.",
+ "disableScrollLock": "Disable the scroll lock behavior.",
+ "hideBackdrop": "If
true
, the backdrop is not rendered.",
+ "keepMounted": "Always keep the children in the DOM. This prop can be useful in SEO situation or when you want to maximize the responsiveness of the Modal.",
+ "onBackdropClick": "Callback fired when the backdrop is clicked.",
+ "onClose": "Callback fired when the component requests to be closed. The
reason
parameter can optionally be used to control the response to
onClose
.
Signature:function(event: object, reason: string) => void
event: The event source of the callback.
reason: Can be:
"escapeKeyDown"
,
"backdropClick"
.",
+ "open": "If
true
, the component is shown."
+ },
+ "classDescriptions": {
+ "root": { "description": "Styles applied to the root element." },
+ "hidden": {
+ "description": "Styles applied to {{nodeName}} if {{conditions}}.",
+ "nodeName": "the root element",
+ "conditions": "the
Modal
has exited"
+ }
+ }
+}
diff --git a/docs/translations/api-docs/modal-unstyled/modal-unstyled-pt.json b/docs/translations/api-docs/modal-unstyled/modal-unstyled-pt.json
new file mode 100644
index 00000000000000..f7a5160fc0258b
--- /dev/null
+++ b/docs/translations/api-docs/modal-unstyled/modal-unstyled-pt.json
@@ -0,0 +1,33 @@
+{
+ "componentDescription": "Modal is a lower-level construct that is leveraged by the following components:\n\n- [Dialog](/api/dialog/)\n- [Drawer](/api/drawer/)\n- [Menu](/api/menu/)\n- [Popover](/api/popover/)\n\nIf you are creating a modal dialog, you probably want to use the [Dialog](/api/dialog/) component\nrather than directly using Modal.\n\nThis component shares many concepts with [react-overlays](https://react-bootstrap.github.io/react-overlays/#modals).",
+ "propDescriptions": {
+ "BackdropComponent": "A backdrop component. This prop enables custom backdrop rendering.",
+ "BackdropProps": "Props applied to the
Backdrop
element.",
+ "children": "A single child content element.
⚠️
Needs to be able to hold a ref.",
+ "classes": "Override or extend the styles applied to the component. See
CSS API below for more details.",
+ "closeAfterTransition": "When set to true the Modal waits until a nested Transition is completed before closing.",
+ "component": "The component used for the root node. Either a string to use a HTML element or a component.",
+ "components": "The components used for each slot inside the Modal. Either a string to use a HTML element or a component.",
+ "componentsProps": "The props used for each slot inside the Modal.",
+ "container": "An HTML element or function that returns one. The
container
will have the portal children appended to it.
By default, it uses the body of the top-level document object, so it's simply
document.body
most of the time.",
+ "disableAutoFocus": "If
true
, the modal will not automatically shift focus to itself when it opens, and replace it to the last focused element when it closes. This also works correctly with any modal children that have the
disableAutoFocus
prop.
Generally this should never be set to
true
as it makes the modal less accessible to assistive technologies, like screen readers.",
+ "disableEnforceFocus": "If
true
, the modal will not prevent focus from leaving the modal while open.
Generally this should never be set to
true
as it makes the modal less accessible to assistive technologies, like screen readers.",
+ "disableEscapeKeyDown": "If
true
, hitting escape will not fire the
onClose
callback.",
+ "disablePortal": "The
children
will be under the DOM hierarchy of the parent component.",
+ "disableRestoreFocus": "If
true
, the modal will not restore focus to previously focused element once modal is hidden.",
+ "disableScrollLock": "Disable the scroll lock behavior.",
+ "hideBackdrop": "If
true
, the backdrop is not rendered.",
+ "keepMounted": "Always keep the children in the DOM. This prop can be useful in SEO situation or when you want to maximize the responsiveness of the Modal.",
+ "onBackdropClick": "Callback fired when the backdrop is clicked.",
+ "onClose": "Callback fired when the component requests to be closed. The
reason
parameter can optionally be used to control the response to
onClose
.
Signature:function(event: object, reason: string) => void
event: The event source of the callback.
reason: Can be:
"escapeKeyDown"
,
"backdropClick"
.",
+ "open": "If
true
, the component is shown."
+ },
+ "classDescriptions": {
+ "root": { "description": "Styles applied to the root element." },
+ "hidden": {
+ "description": "Styles applied to {{nodeName}} if {{conditions}}.",
+ "nodeName": "the root element",
+ "conditions": "the
Modal
has exited"
+ }
+ }
+}
diff --git a/docs/translations/api-docs/modal-unstyled/modal-unstyled-ru.json b/docs/translations/api-docs/modal-unstyled/modal-unstyled-ru.json
new file mode 100644
index 00000000000000..f7a5160fc0258b
--- /dev/null
+++ b/docs/translations/api-docs/modal-unstyled/modal-unstyled-ru.json
@@ -0,0 +1,33 @@
+{
+ "componentDescription": "Modal is a lower-level construct that is leveraged by the following components:\n\n- [Dialog](/api/dialog/)\n- [Drawer](/api/drawer/)\n- [Menu](/api/menu/)\n- [Popover](/api/popover/)\n\nIf you are creating a modal dialog, you probably want to use the [Dialog](/api/dialog/) component\nrather than directly using Modal.\n\nThis component shares many concepts with [react-overlays](https://react-bootstrap.github.io/react-overlays/#modals).",
+ "propDescriptions": {
+ "BackdropComponent": "A backdrop component. This prop enables custom backdrop rendering.",
+ "BackdropProps": "Props applied to the
Backdrop
element.",
+ "children": "A single child content element.
⚠️
Needs to be able to hold a ref.",
+ "classes": "Override or extend the styles applied to the component. See
CSS API below for more details.",
+ "closeAfterTransition": "When set to true the Modal waits until a nested Transition is completed before closing.",
+ "component": "The component used for the root node. Either a string to use a HTML element or a component.",
+ "components": "The components used for each slot inside the Modal. Either a string to use a HTML element or a component.",
+ "componentsProps": "The props used for each slot inside the Modal.",
+ "container": "An HTML element or function that returns one. The
container
will have the portal children appended to it.
By default, it uses the body of the top-level document object, so it's simply
document.body
most of the time.",
+ "disableAutoFocus": "If
true
, the modal will not automatically shift focus to itself when it opens, and replace it to the last focused element when it closes. This also works correctly with any modal children that have the
disableAutoFocus
prop.
Generally this should never be set to
true
as it makes the modal less accessible to assistive technologies, like screen readers.",
+ "disableEnforceFocus": "If
true
, the modal will not prevent focus from leaving the modal while open.
Generally this should never be set to
true
as it makes the modal less accessible to assistive technologies, like screen readers.",
+ "disableEscapeKeyDown": "If
true
, hitting escape will not fire the
onClose
callback.",
+ "disablePortal": "The
children
will be under the DOM hierarchy of the parent component.",
+ "disableRestoreFocus": "If
true
, the modal will not restore focus to previously focused element once modal is hidden.",
+ "disableScrollLock": "Disable the scroll lock behavior.",
+ "hideBackdrop": "If
true
, the backdrop is not rendered.",
+ "keepMounted": "Always keep the children in the DOM. This prop can be useful in SEO situation or when you want to maximize the responsiveness of the Modal.",
+ "onBackdropClick": "Callback fired when the backdrop is clicked.",
+ "onClose": "Callback fired when the component requests to be closed. The
reason
parameter can optionally be used to control the response to
onClose
.
Signature:function(event: object, reason: string) => void
event: The event source of the callback.
reason: Can be:
"escapeKeyDown"
,
"backdropClick"
.",
+ "open": "If
true
, the component is shown."
+ },
+ "classDescriptions": {
+ "root": { "description": "Styles applied to the root element." },
+ "hidden": {
+ "description": "Styles applied to {{nodeName}} if {{conditions}}.",
+ "nodeName": "the root element",
+ "conditions": "the
Modal
has exited"
+ }
+ }
+}
diff --git a/docs/translations/api-docs/modal-unstyled/modal-unstyled-zh.json b/docs/translations/api-docs/modal-unstyled/modal-unstyled-zh.json
new file mode 100644
index 00000000000000..f7a5160fc0258b
--- /dev/null
+++ b/docs/translations/api-docs/modal-unstyled/modal-unstyled-zh.json
@@ -0,0 +1,33 @@
+{
+ "componentDescription": "Modal is a lower-level construct that is leveraged by the following components:\n\n- [Dialog](/api/dialog/)\n- [Drawer](/api/drawer/)\n- [Menu](/api/menu/)\n- [Popover](/api/popover/)\n\nIf you are creating a modal dialog, you probably want to use the [Dialog](/api/dialog/) component\nrather than directly using Modal.\n\nThis component shares many concepts with [react-overlays](https://react-bootstrap.github.io/react-overlays/#modals).",
+ "propDescriptions": {
+ "BackdropComponent": "A backdrop component. This prop enables custom backdrop rendering.",
+ "BackdropProps": "Props applied to the
Backdrop
element.",
+ "children": "A single child content element.
⚠️
Needs to be able to hold a ref.",
+ "classes": "Override or extend the styles applied to the component. See
CSS API below for more details.",
+ "closeAfterTransition": "When set to true the Modal waits until a nested Transition is completed before closing.",
+ "component": "The component used for the root node. Either a string to use a HTML element or a component.",
+ "components": "The components used for each slot inside the Modal. Either a string to use a HTML element or a component.",
+ "componentsProps": "The props used for each slot inside the Modal.",
+ "container": "An HTML element or function that returns one. The
container
will have the portal children appended to it.
By default, it uses the body of the top-level document object, so it's simply
document.body
most of the time.",
+ "disableAutoFocus": "If
true
, the modal will not automatically shift focus to itself when it opens, and replace it to the last focused element when it closes. This also works correctly with any modal children that have the
disableAutoFocus
prop.
Generally this should never be set to
true
as it makes the modal less accessible to assistive technologies, like screen readers.",
+ "disableEnforceFocus": "If
true
, the modal will not prevent focus from leaving the modal while open.
Generally this should never be set to
true
as it makes the modal less accessible to assistive technologies, like screen readers.",
+ "disableEscapeKeyDown": "If
true
, hitting escape will not fire the
onClose
callback.",
+ "disablePortal": "The
children
will be under the DOM hierarchy of the parent component.",
+ "disableRestoreFocus": "If
true
, the modal will not restore focus to previously focused element once modal is hidden.",
+ "disableScrollLock": "Disable the scroll lock behavior.",
+ "hideBackdrop": "If
true
, the backdrop is not rendered.",
+ "keepMounted": "Always keep the children in the DOM. This prop can be useful in SEO situation or when you want to maximize the responsiveness of the Modal.",
+ "onBackdropClick": "Callback fired when the backdrop is clicked.",
+ "onClose": "Callback fired when the component requests to be closed. The
reason
parameter can optionally be used to control the response to
onClose
.
Signature:function(event: object, reason: string) => void
event: The event source of the callback.
reason: Can be:
"escapeKeyDown"
,
"backdropClick"
.",
+ "open": "If
true
, the component is shown."
+ },
+ "classDescriptions": {
+ "root": { "description": "Styles applied to the root element." },
+ "hidden": {
+ "description": "Styles applied to {{nodeName}} if {{conditions}}.",
+ "nodeName": "the root element",
+ "conditions": "the
Modal
has exited"
+ }
+ }
+}
diff --git a/docs/translations/api-docs/modal-unstyled/modal-unstyled.json b/docs/translations/api-docs/modal-unstyled/modal-unstyled.json
new file mode 100644
index 00000000000000..9c21169bea4ae2
--- /dev/null
+++ b/docs/translations/api-docs/modal-unstyled/modal-unstyled.json
@@ -0,0 +1,33 @@
+{
+ "componentDescription": "Modal is a lower-level construct that is leveraged by the following components:\n\n- [Dialog](/api/dialog/)\n- [Drawer](/api/drawer/)\n- [Menu](/api/menu/)\n- [Popover](/api/popover/)\n\nIf you are creating a modal dialog, you probably want to use the [Dialog](/api/dialog/) component\nrather than directly using Modal.\n\nThis component shares many concepts with [react-overlays](https://react-bootstrap.github.io/react-overlays/#modals).",
+ "propDescriptions": {
+ "BackdropComponent": "A backdrop component. This prop enables custom backdrop rendering.",
+ "BackdropProps": "Props applied to the
BackdropUnstyled
element.",
+ "children": "A single child content element.
⚠️
Needs to be able to hold a ref.",
+ "classes": "Override or extend the styles applied to the component. See
CSS API below for more details.",
+ "closeAfterTransition": "When set to true the Modal waits until a nested Transition is completed before closing.",
+ "component": "The component used for the root node. Either a string to use a HTML element or a component.",
+ "components": "The components used for each slot inside the Modal. Either a string to use a HTML element or a component.",
+ "componentsProps": "The props used for each slot inside the Modal.",
+ "container": "An HTML element or function that returns one. The
container
will have the portal children appended to it.
By default, it uses the body of the top-level document object, so it's simply
document.body
most of the time.",
+ "disableAutoFocus": "If
true
, the modal will not automatically shift focus to itself when it opens, and replace it to the last focused element when it closes. This also works correctly with any modal children that have the
disableAutoFocus
prop.
Generally this should never be set to
true
as it makes the modal less accessible to assistive technologies, like screen readers.",
+ "disableEnforceFocus": "If
true
, the modal will not prevent focus from leaving the modal while open.
Generally this should never be set to
true
as it makes the modal less accessible to assistive technologies, like screen readers.",
+ "disableEscapeKeyDown": "If
true
, hitting escape will not fire the
onClose
callback.",
+ "disablePortal": "The
children
will be under the DOM hierarchy of the parent component.",
+ "disableRestoreFocus": "If
true
, the modal will not restore focus to previously focused element once modal is hidden.",
+ "disableScrollLock": "Disable the scroll lock behavior.",
+ "hideBackdrop": "If
true
, the backdrop is not rendered.",
+ "keepMounted": "Always keep the children in the DOM. This prop can be useful in SEO situation or when you want to maximize the responsiveness of the Modal.",
+ "onBackdropClick": "Callback fired when the backdrop is clicked.",
+ "onClose": "Callback fired when the component requests to be closed. The
reason
parameter can optionally be used to control the response to
onClose
.
Signature:function(event: object, reason: string) => void
event: The event source of the callback.
reason: Can be:
"escapeKeyDown"
,
"backdropClick"
.",
+ "open": "If
true
, the component is shown."
+ },
+ "classDescriptions": {
+ "root": { "description": "Styles applied to the root element." },
+ "hidden": {
+ "description": "Styles applied to {{nodeName}} if {{conditions}}.",
+ "nodeName": "the root element",
+ "conditions": "the
Modal
has exited"
+ }
+ }
+}
diff --git a/docs/translations/api-docs/modal/modal.json b/docs/translations/api-docs/modal/modal.json
index 4f124cd7f68515..4530f11dc4cbbe 100644
--- a/docs/translations/api-docs/modal/modal.json
+++ b/docs/translations/api-docs/modal/modal.json
@@ -4,7 +4,10 @@
"BackdropComponent": "A backdrop component. This prop enables custom backdrop rendering.",
"BackdropProps": "Props applied to the
Backdrop
element.",
"children": "A single child content element.
⚠️
Needs to be able to hold a ref.",
+ "classes": "Override or extend the styles applied to the component. See
CSS API below for more details.",
"closeAfterTransition": "When set to true the Modal waits until a nested Transition is completed before closing.",
+ "components": "The components used for each slot inside the Modal. Either a string to use a HTML element or a component.",
+ "componentsProps": "The props used for each slot inside the Modal.",
"container": "An HTML element or function that returns one. The
container
will have the portal children appended to it.
By default, it uses the body of the top-level document object, so it's simply
document.body
most of the time.",
"disableAutoFocus": "If
true
, the modal will not automatically shift focus to itself when it opens, and replace it to the last focused element when it closes. This also works correctly with any modal children that have the
disableAutoFocus
prop.
Generally this should never be set to
true
as it makes the modal less accessible to assistive technologies, like screen readers.",
"disableEnforceFocus": "If
true
, the modal will not prevent focus from leaving the modal while open.
Generally this should never be set to
true
as it makes the modal less accessible to assistive technologies, like screen readers.",
@@ -16,7 +19,16 @@
"keepMounted": "Always keep the children in the DOM. This prop can be useful in SEO situation or when you want to maximize the responsiveness of the Modal.",
"onBackdropClick": "Callback fired when the backdrop is clicked.",
"onClose": "Callback fired when the component requests to be closed. The
reason
parameter can optionally be used to control the response to
onClose
.
Signature:function(event: object, reason: string) => void
event: The event source of the callback.
reason: Can be:
"escapeKeyDown"
,
"backdropClick"
.",
- "open": "If
true
, the component is shown."
+ "open": "If
true
, the component is shown.",
+ "sx": "The system prop that allows defining system overrides as well as additional CSS styles. See the
`sx` page for more details.",
+ "component": "The component used for the root node. Either a string to use a HTML element or a component."
},
- "classDescriptions": {}
+ "classDescriptions": {
+ "root": { "description": "Styles applied to the root element." },
+ "hidden": {
+ "description": "Styles applied to {{nodeName}} if {{conditions}}.",
+ "nodeName": "the root element",
+ "conditions": "the
Modal
has exited"
+ }
+ }
}
diff --git a/packages/material-ui-unstyled/src/BackdropUnstyled/BackdropUnstyled.d.ts b/packages/material-ui-unstyled/src/BackdropUnstyled/BackdropUnstyled.d.ts
index 20ea84c715e493..0896602cd20102 100644
--- a/packages/material-ui-unstyled/src/BackdropUnstyled/BackdropUnstyled.d.ts
+++ b/packages/material-ui-unstyled/src/BackdropUnstyled/BackdropUnstyled.d.ts
@@ -27,7 +27,6 @@ export interface BackdropUnstyledTypeMap
- {children}
-
+ />
);
});
@@ -68,7 +66,6 @@ BackdropUnstyled.propTypes = {
children: PropTypes.node,
/**
* Override or extend the styles applied to the component.
- * @default {}
*/
classes: PropTypes.object,
/**
diff --git a/packages/material-ui-unstyled/src/BadgeUnstyled/BadgeUnstyled.d.ts b/packages/material-ui-unstyled/src/BadgeUnstyled/BadgeUnstyled.d.ts
index 6d40cd10278ef6..e43160cabceafb 100644
--- a/packages/material-ui-unstyled/src/BadgeUnstyled/BadgeUnstyled.d.ts
+++ b/packages/material-ui-unstyled/src/BadgeUnstyled/BadgeUnstyled.d.ts
@@ -54,7 +54,6 @@ export interface BadgeUnstyledTypeMap
max ? `${max}+` : badgeContent;
}
- const classes = useUtilityClasses({ ...styleProps, classes: classesProp });
+ const classes = useUtilityClasses(styleProps);
const Root = components.Root || component;
const rootProps = componentsProps.root || {};
@@ -149,7 +150,6 @@ BadgeUnstyled.propTypes = {
children: PropTypes.node,
/**
* Override or extend the styles applied to the component.
- * @default {}
*/
classes: PropTypes.object,
/**
diff --git a/packages/material-ui/src/Modal/ModalManager.test.js b/packages/material-ui-unstyled/src/ModalUnstyled/ModalManager.test.js
similarity index 99%
rename from packages/material-ui/src/Modal/ModalManager.test.js
rename to packages/material-ui-unstyled/src/ModalUnstyled/ModalManager.test.js
index 3d523cad4a13e0..8591bea78170db 100644
--- a/packages/material-ui/src/Modal/ModalManager.test.js
+++ b/packages/material-ui-unstyled/src/ModalUnstyled/ModalManager.test.js
@@ -1,5 +1,5 @@
import { expect } from 'chai';
-import getScrollbarSize from '../utils/getScrollbarSize';
+import { unstable_getScrollbarSize as getScrollbarSize } from '@material-ui/utils';
import ModalManager from './ModalManager';
describe('ModalManager', () => {
diff --git a/packages/material-ui/src/Modal/ModalManager.ts b/packages/material-ui-unstyled/src/ModalUnstyled/ModalManager.ts
similarity index 97%
rename from packages/material-ui/src/Modal/ModalManager.ts
rename to packages/material-ui-unstyled/src/ModalUnstyled/ModalManager.ts
index 294a015b542584..69317c0b4496c0 100644
--- a/packages/material-ui/src/Modal/ModalManager.ts
+++ b/packages/material-ui-unstyled/src/ModalUnstyled/ModalManager.ts
@@ -1,6 +1,8 @@
-import getScrollbarSize from '../utils/getScrollbarSize';
-import ownerDocument from '../utils/ownerDocument';
-import ownerWindow from '../utils/ownerWindow';
+import {
+ unstable_ownerWindow as ownerWindow,
+ unstable_ownerDocument as ownerDocument,
+ unstable_getScrollbarSize as getScrollbarSize,
+} from '@material-ui/utils';
export interface ManagedModalProps {
disableScrollLock?: boolean;
diff --git a/packages/material-ui-unstyled/src/ModalUnstyled/ModalUnstyled.d.ts b/packages/material-ui-unstyled/src/ModalUnstyled/ModalUnstyled.d.ts
new file mode 100644
index 00000000000000..97553729948ac1
--- /dev/null
+++ b/packages/material-ui-unstyled/src/ModalUnstyled/ModalUnstyled.d.ts
@@ -0,0 +1,175 @@
+import * as React from 'react';
+import { BackdropUnstyledProps } from '../BackdropUnstyled';
+import { PortalProps } from '../Portal';
+import { OverridableComponent, OverridableTypeMap, OverrideProps } from '../OverridableComponent';
+
+export interface ModalUnstyledTypeMap
{
+ props: P & {
+ /**
+ * A backdrop component. This prop enables custom backdrop rendering.
+ */
+ BackdropComponent?: React.ElementType;
+ /**
+ * Props applied to the [`BackdropUnstyled`](/api/backdrop-unstyled/) element.
+ */
+ BackdropProps?: Partial;
+ /**
+ * A single child content element.
+ */
+ children: React.ReactElement;
+ /**
+ * Override or extend the styles applied to the component.
+ */
+ classes?: {
+ /** Styles applied to the root element. */
+ root?: string;
+ /** Styles applied to the root element if the `Modal` has exited. */
+ hidden?: string;
+ };
+ /**
+ * When set to true the Modal waits until a nested Transition is completed before closing.
+ * @default false
+ */
+ closeAfterTransition?: boolean;
+ /**
+ * The components used for each slot inside the Modal.
+ * Either a string to use a HTML element or a component.
+ * @default {}
+ */
+ components?: {
+ Root?: React.ElementType;
+ };
+ /**
+ * The props used for each slot inside the Modal.
+ * @default {}
+ */
+ componentsProps?: {
+ root?: {
+ as: React.ElementType;
+ styleProps?: Omit['props'], 'components' | 'componentsProps'>;
+ };
+ };
+ /**
+ * An HTML element or function that returns one.
+ * The `container` will have the portal children appended to it.
+ *
+ * By default, it uses the body of the top-level document object,
+ * so it's simply `document.body` most of the time.
+ */
+ container?: PortalProps['container'];
+ /**
+ * If `true`, the modal will not automatically shift focus to itself when it opens, and
+ * replace it to the last focused element when it closes.
+ * This also works correctly with any modal children that have the `disableAutoFocus` prop.
+ *
+ * Generally this should never be set to `true` as it makes the modal less
+ * accessible to assistive technologies, like screen readers.
+ * @default false
+ */
+ disableAutoFocus?: boolean;
+ /**
+ * If `true`, the modal will not prevent focus from leaving the modal while open.
+ *
+ * Generally this should never be set to `true` as it makes the modal less
+ * accessible to assistive technologies, like screen readers.
+ * @default false
+ */
+ disableEnforceFocus?: boolean;
+ /**
+ * If `true`, hitting escape will not fire the `onClose` callback.
+ * @default false
+ */
+ disableEscapeKeyDown?: boolean;
+ /**
+ * The `children` will be under the DOM hierarchy of the parent component.
+ * @default false
+ */
+ disablePortal?: PortalProps['disablePortal'];
+ /**
+ * If `true`, the modal will not restore focus to previously focused element once
+ * modal is hidden.
+ * @default false
+ */
+ disableRestoreFocus?: boolean;
+ /**
+ * Disable the scroll lock behavior.
+ * @default false
+ */
+ disableScrollLock?: boolean;
+ /**
+ * If `true`, the backdrop is not rendered.
+ * @default false
+ */
+ hideBackdrop?: boolean;
+ /**
+ * Always keep the children in the DOM.
+ * This prop can be useful in SEO situation or
+ * when you want to maximize the responsiveness of the Modal.
+ * @default false
+ */
+ keepMounted?: boolean;
+ /**
+ * Callback fired when the backdrop is clicked.
+ */
+ onBackdropClick?: React.ReactEventHandler<{}>;
+ /**
+ * Callback fired when the component requests to be closed.
+ * The `reason` parameter can optionally be used to control the response to `onClose`.
+ *
+ * @param {object} event The event source of the callback.
+ * @param {string} reason Can be: `"escapeKeyDown"`, `"backdropClick"`.
+ */
+ onClose?: {
+ bivarianceHack(event: {}, reason: 'backdropClick' | 'escapeKeyDown'): void;
+ }['bivarianceHack'];
+ /**
+ * If `true`, the component is shown.
+ */
+ open: boolean;
+ };
+ defaultComponent: D;
+}
+
+/**
+ * Utility to create component types that inherit props from ModalUnstyled.
+ */
+export interface ExtendModalUnstyledTypeMap {
+ props: M['props'] & ModalUnstyledTypeMap['props'];
+ defaultComponent: M['defaultComponent'];
+}
+
+export type ExtendModalUnstyled = OverridableComponent<
+ ExtendModalUnstyledTypeMap
+>;
+
+export type ModalUnstyledClassKey = keyof NonNullable;
+
+/**
+ * Modal is a lower-level construct that is leveraged by the following components:
+ *
+ * * [Dialog](https://material-ui.com/api/dialog/)
+ * * [Drawer](https://material-ui.com/api/drawer/)
+ * * [Menu](https://material-ui.com/api/menu/)
+ * * [Popover](https://material-ui.com/api/popover/)
+ *
+ * If you are creating a modal dialog, you probably want to use the [Dialog](https://material-ui.com/api/dialog/) component
+ * rather than directly using Modal.
+ *
+ * This component shares many concepts with [react-overlays](https://react-bootstrap.github.io/react-overlays/#modals).
+ *
+ * Demos:
+ *
+ * - [Modal](https://material-ui.com/components/modal/)
+ *
+ * API:
+ *
+ * - [ModalUnstyled API](https://material-ui.com/api/modal-unstyled/)
+ */
+declare const ModalUnstyled: OverridableComponent;
+
+export type ModalUnstyledProps<
+ D extends React.ElementType = ModalUnstyledTypeMap['defaultComponent'],
+ P = {}
+> = OverrideProps, D>;
+
+export default ModalUnstyled;
diff --git a/packages/material-ui-unstyled/src/ModalUnstyled/ModalUnstyled.js b/packages/material-ui-unstyled/src/ModalUnstyled/ModalUnstyled.js
new file mode 100644
index 00000000000000..561734224729b0
--- /dev/null
+++ b/packages/material-ui-unstyled/src/ModalUnstyled/ModalUnstyled.js
@@ -0,0 +1,419 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import clsx from 'clsx';
+import {
+ elementAcceptingRef,
+ HTMLElementType,
+ unstable_ownerDocument as ownerDocument,
+ unstable_useForkRef as useForkRef,
+ unstable_createChainedFunction as createChainedFunction,
+ unstable_useEventCallback as useEventCallback,
+} from '@material-ui/utils';
+import composeClasses from '../composeClasses';
+import isHostComponent from '../utils/isHostComponent';
+import Portal from '../Portal';
+import ModalManager, { ariaHidden } from './ModalManager';
+import TrapFocus from '../Unstable_TrapFocus';
+import { getModalUtilityClass } from './modalUnstyledClasses';
+
+const useUtilityClasses = (styleProps) => {
+ const { open, exited, classes } = styleProps;
+
+ const slots = {
+ root: ['root', !open && exited && 'hidden'],
+ };
+
+ return composeClasses(slots, getModalUtilityClass, classes);
+};
+
+function getContainer(container) {
+ return typeof container === 'function' ? container() : container;
+}
+
+function getHasTransition(props) {
+ return props.children ? props.children.props.hasOwnProperty('in') : false;
+}
+
+// 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 defaultManager = new ModalManager();
+
+/**
+ * Modal is a lower-level construct that is leveraged by the following components:
+ *
+ * - [Dialog](/api/dialog/)
+ * - [Drawer](/api/drawer/)
+ * - [Menu](/api/menu/)
+ * - [Popover](/api/popover/)
+ *
+ * If you are creating a modal dialog, you probably want to use the [Dialog](/api/dialog/) component
+ * rather than directly using Modal.
+ *
+ * This component shares many concepts with [react-overlays](https://react-bootstrap.github.io/react-overlays/#modals).
+ */
+const ModalUnstyled = React.forwardRef(function ModalUnstyled(props, ref) {
+ const {
+ BackdropComponent,
+ BackdropProps,
+ children,
+ classes: classesProp,
+ className,
+ closeAfterTransition = false,
+ component = 'div',
+ components = {},
+ componentsProps = {},
+ container,
+ disableAutoFocus = false,
+ disableEnforceFocus = false,
+ disableEscapeKeyDown = false,
+ disablePortal = false,
+ disableRestoreFocus = false,
+ disableScrollLock = false,
+ hideBackdrop = false,
+ keepMounted = false,
+ // private
+ // eslint-disable-next-line react/prop-types
+ manager = defaultManager,
+ onBackdropClick,
+ onClose,
+ onKeyDown,
+ open,
+ /* eslint-disable react/prop-types */
+ theme,
+ onTransitionEnter,
+ onTransitionExited,
+ ...other
+ } = props;
+
+ const [exited, setExited] = React.useState(true);
+ const modal = React.useRef({});
+ const mountNodeRef = React.useRef(null);
+ const modalRef = React.useRef(null);
+ const handleRef = useForkRef(modalRef, ref);
+ const hasTransition = getHasTransition(props);
+
+ const getDoc = () => ownerDocument(mountNodeRef.current);
+ const getModal = () => {
+ modal.current.modalRef = modalRef.current;
+ modal.current.mountNode = mountNodeRef.current;
+ return modal.current;
+ };
+
+ const handleMounted = () => {
+ manager.mount(getModal(), { disableScrollLock });
+
+ // Fix a bug on Chrome where the scroll isn't initially 0.
+ modalRef.current.scrollTop = 0;
+ };
+
+ const handleOpen = useEventCallback(() => {
+ const resolvedContainer = getContainer(container) || getDoc().body;
+
+ manager.add(getModal(), resolvedContainer);
+
+ // The element was already mounted.
+ if (modalRef.current) {
+ handleMounted();
+ }
+ });
+
+ const isTopModal = React.useCallback(() => manager.isTopModal(getModal()), [manager]);
+
+ const handlePortalRef = useEventCallback((node) => {
+ mountNodeRef.current = node;
+
+ if (!node) {
+ return;
+ }
+
+ if (open && isTopModal()) {
+ handleMounted();
+ } else {
+ ariaHidden(modalRef.current, true);
+ }
+ });
+
+ const handleClose = React.useCallback(() => {
+ manager.remove(getModal());
+ }, [manager]);
+
+ React.useEffect(() => {
+ return () => {
+ handleClose();
+ };
+ }, [handleClose]);
+
+ React.useEffect(() => {
+ if (open) {
+ handleOpen();
+ } else if (!hasTransition || !closeAfterTransition) {
+ handleClose();
+ }
+ }, [open, handleClose, hasTransition, closeAfterTransition, handleOpen]);
+
+ const styleProps = {
+ ...props,
+ classes: classesProp,
+ closeAfterTransition,
+ disableAutoFocus,
+ disableEnforceFocus,
+ disableEscapeKeyDown,
+ disablePortal,
+ disableRestoreFocus,
+ disableScrollLock,
+ exited,
+ hideBackdrop,
+ keepMounted,
+ };
+
+ const classes = useUtilityClasses(styleProps);
+
+ if (!keepMounted && !open && (!hasTransition || exited)) {
+ return null;
+ }
+
+ const handleEnter = () => {
+ setExited(false);
+
+ if (onTransitionEnter) {
+ onTransitionEnter();
+ }
+ };
+
+ const handleExited = () => {
+ setExited(true);
+
+ if (onTransitionExited) {
+ onTransitionExited();
+ }
+
+ if (closeAfterTransition) {
+ handleClose();
+ }
+ };
+
+ const handleBackdropClick = (event) => {
+ if (event.target !== event.currentTarget) {
+ return;
+ }
+
+ if (onBackdropClick) {
+ onBackdropClick(event);
+ }
+
+ if (onClose) {
+ onClose(event, 'backdropClick');
+ }
+ };
+
+ const handleKeyDown = (event) => {
+ if (onKeyDown) {
+ onKeyDown(event);
+ }
+
+ // The handler doesn't take event.defaultPrevented into account:
+ //
+ // event.preventDefault() is meant to stop default behaviors like
+ // clicking a checkbox to check it, hitting a button to submit a form,
+ // and hitting left arrow to move the cursor in a text input etc.
+ // Only special HTML elements have these default behaviors.
+ if (event.key !== 'Escape' || !isTopModal()) {
+ return;
+ }
+
+ if (!disableEscapeKeyDown) {
+ // Swallow the event, in case someone is listening for the escape key on the body.
+ event.stopPropagation();
+
+ if (onClose) {
+ onClose(event, 'escapeKeyDown');
+ }
+ }
+ };
+
+ const childProps = {};
+ if (children.props.tabIndex === undefined) {
+ childProps.tabIndex = children.props.tabIndex || '-1';
+ }
+
+ // It's a Transition like component
+ if (hasTransition) {
+ childProps.onEnter = createChainedFunction(handleEnter, children.props.onEnter);
+ childProps.onExited = createChainedFunction(handleExited, children.props.onExited);
+ }
+
+ const Root = components.Root || component;
+ const rootProps = componentsProps.root || {};
+
+ return (
+
+ {/*
+ * Marking an element with the role presentation indicates to assistive technology
+ * that this element should be ignored; it exists to support the web application and
+ * is not meant for humans to interact with directly.
+ * https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-static-element-interactions.md
+ */}
+
+ {!hideBackdrop && BackdropComponent ? (
+
+ ) : null}
+
+ {React.cloneElement(children, childProps)}
+
+
+
+ );
+});
+
+ModalUnstyled.propTypes = {
+ // ----------------------------- Warning --------------------------------
+ // | These PropTypes are generated from the TypeScript type definitions |
+ // | To update them edit the d.ts file and run "yarn proptypes" |
+ // ----------------------------------------------------------------------
+ /**
+ * A backdrop component. This prop enables custom backdrop rendering.
+ */
+ BackdropComponent: PropTypes.elementType,
+ /**
+ * Props applied to the [`BackdropUnstyled`](/api/backdrop-unstyled/) element.
+ */
+ BackdropProps: PropTypes.object,
+ /**
+ * A single child content element.
+ */
+ children: elementAcceptingRef.isRequired,
+ /**
+ * Override or extend the styles applied to the component.
+ */
+ classes: PropTypes.object,
+ /**
+ * @ignore
+ */
+ className: PropTypes.string,
+ /**
+ * When set to true the Modal waits until a nested Transition is completed before closing.
+ * @default false
+ */
+ closeAfterTransition: PropTypes.bool,
+ /**
+ * The component used for the root node.
+ * Either a string to use a HTML element or a component.
+ */
+ component: PropTypes.elementType,
+ /**
+ * The components used for each slot inside the Modal.
+ * Either a string to use a HTML element or a component.
+ * @default {}
+ */
+ components: PropTypes.shape({
+ Root: PropTypes.elementType,
+ }),
+ /**
+ * The props used for each slot inside the Modal.
+ * @default {}
+ */
+ componentsProps: PropTypes.object,
+ /**
+ * An HTML element or function that returns one.
+ * The `container` will have the portal children appended to it.
+ *
+ * By default, it uses the body of the top-level document object,
+ * so it's simply `document.body` most of the time.
+ */
+ container: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
+ HTMLElementType,
+ PropTypes.func,
+ ]),
+ /**
+ * If `true`, the modal will not automatically shift focus to itself when it opens, and
+ * replace it to the last focused element when it closes.
+ * This also works correctly with any modal children that have the `disableAutoFocus` prop.
+ *
+ * Generally this should never be set to `true` as it makes the modal less
+ * accessible to assistive technologies, like screen readers.
+ * @default false
+ */
+ disableAutoFocus: PropTypes.bool,
+ /**
+ * If `true`, the modal will not prevent focus from leaving the modal while open.
+ *
+ * Generally this should never be set to `true` as it makes the modal less
+ * accessible to assistive technologies, like screen readers.
+ * @default false
+ */
+ disableEnforceFocus: PropTypes.bool,
+ /**
+ * If `true`, hitting escape will not fire the `onClose` callback.
+ * @default false
+ */
+ disableEscapeKeyDown: PropTypes.bool,
+ /**
+ * The `children` will be under the DOM hierarchy of the parent component.
+ * @default false
+ */
+ disablePortal: PropTypes.bool,
+ /**
+ * If `true`, the modal will not restore focus to previously focused element once
+ * modal is hidden.
+ * @default false
+ */
+ disableRestoreFocus: PropTypes.bool,
+ /**
+ * Disable the scroll lock behavior.
+ * @default false
+ */
+ disableScrollLock: PropTypes.bool,
+ /**
+ * If `true`, the backdrop is not rendered.
+ * @default false
+ */
+ hideBackdrop: PropTypes.bool,
+ /**
+ * Always keep the children in the DOM.
+ * This prop can be useful in SEO situation or
+ * when you want to maximize the responsiveness of the Modal.
+ * @default false
+ */
+ keepMounted: PropTypes.bool,
+ /**
+ * Callback fired when the backdrop is clicked.
+ */
+ onBackdropClick: PropTypes.func,
+ /**
+ * Callback fired when the component requests to be closed.
+ * The `reason` parameter can optionally be used to control the response to `onClose`.
+ *
+ * @param {object} event The event source of the callback.
+ * @param {string} reason Can be: `"escapeKeyDown"`, `"backdropClick"`.
+ */
+ onClose: PropTypes.func,
+ /**
+ * @ignore
+ */
+ onKeyDown: PropTypes.func,
+ /**
+ * If `true`, the component is shown.
+ */
+ open: PropTypes.bool.isRequired,
+};
+
+export default ModalUnstyled;
diff --git a/packages/material-ui-unstyled/src/ModalUnstyled/ModalUnstyled.test.js b/packages/material-ui-unstyled/src/ModalUnstyled/ModalUnstyled.test.js
new file mode 100644
index 00000000000000..071cb48b22428f
--- /dev/null
+++ b/packages/material-ui-unstyled/src/ModalUnstyled/ModalUnstyled.test.js
@@ -0,0 +1,75 @@
+import * as React from 'react';
+import { expect } from 'chai';
+import { createMount, createClientRender, describeConformance } from 'test/utils';
+import ModalUnstyled, {
+ modalUnstyledClasses as classes,
+} from '@material-ui/unstyled/ModalUnstyled';
+
+describe('', () => {
+ const mount = createMount();
+ const render = createClientRender();
+ let savedBodyStyle;
+
+ before(() => {
+ savedBodyStyle = document.body.style;
+ });
+
+ beforeEach(() => {
+ document.body.setAttribute('style', savedBodyStyle);
+ });
+
+ describeConformance(
+
+
+ ,
+ () => ({
+ classes,
+ inheritComponent: 'div',
+ mount,
+ refInstanceof: window.HTMLDivElement,
+ testComponentPropWith: 'div',
+ skip: ['reactTestRenderer'],
+ }),
+ );
+
+ it('forwards style props on the Root component', () => {
+ let styleProps = null;
+ let theme = null;
+
+ const Root = React.forwardRef(
+ ({ styleProps: stylePropsProp, theme: themeProp, ...rest }, ref) => {
+ styleProps = stylePropsProp;
+ theme = themeProp;
+ return ;
+ },
+ );
+
+ render(
+
+
+ ,
+ );
+
+ expect(styleProps).not.to.equal(null);
+ expect(theme).not.to.equal(null);
+ });
+
+ it('does not forward style props as DOM attributes if component slot is primitive', () => {
+ const elementRef = React.createRef();
+ render(
+
+
+ ,
+ );
+
+ const { current: element } = elementRef;
+ expect(element.getAttribute('styleProps')).to.equal(null);
+ expect(element.getAttribute('theme')).to.equal(null);
+ });
+});
diff --git a/packages/material-ui-unstyled/src/ModalUnstyled/index.d.ts b/packages/material-ui-unstyled/src/ModalUnstyled/index.d.ts
new file mode 100644
index 00000000000000..f46c277c538f7f
--- /dev/null
+++ b/packages/material-ui-unstyled/src/ModalUnstyled/index.d.ts
@@ -0,0 +1,8 @@
+export { default } from './ModalUnstyled';
+export * from './ModalUnstyled';
+
+export { default as ModalManager } from './ModalManager';
+export * from './ModalManager';
+
+export { default as modalUnstyledClasses } from './modalUnstyledClasses';
+export * from './modalUnstyledClasses';
diff --git a/packages/material-ui-unstyled/src/ModalUnstyled/index.js b/packages/material-ui-unstyled/src/ModalUnstyled/index.js
new file mode 100644
index 00000000000000..6a1c50b5d29b00
--- /dev/null
+++ b/packages/material-ui-unstyled/src/ModalUnstyled/index.js
@@ -0,0 +1,5 @@
+export { default } from './ModalUnstyled';
+
+export { default as ModalManager } from './ModalManager';
+
+export { default as modalUnstyledClasses, getModalUtilityClass } from './modalUnstyledClasses';
diff --git a/packages/material-ui-unstyled/src/ModalUnstyled/modalUnstyledClasses.d.ts b/packages/material-ui-unstyled/src/ModalUnstyled/modalUnstyledClasses.d.ts
new file mode 100644
index 00000000000000..31bb3cd6f1c179
--- /dev/null
+++ b/packages/material-ui-unstyled/src/ModalUnstyled/modalUnstyledClasses.d.ts
@@ -0,0 +1,9 @@
+import { ModalUnstyledClassKey } from './ModalUnstyled';
+
+export type ModalUnstyledClasses = Record;
+
+declare const modalUnstyledClasses: ModalUnstyledClasses;
+
+export function getModalUtilityClass(slot: string): string;
+
+export default modalUnstyledClasses;
diff --git a/packages/material-ui-unstyled/src/ModalUnstyled/modalUnstyledClasses.js b/packages/material-ui-unstyled/src/ModalUnstyled/modalUnstyledClasses.js
new file mode 100644
index 00000000000000..18bba79a2cbdde
--- /dev/null
+++ b/packages/material-ui-unstyled/src/ModalUnstyled/modalUnstyledClasses.js
@@ -0,0 +1,10 @@
+import generateUtilityClasses from '../generateUtilityClasses';
+import generateUtilityClass from '../generateUtilityClass';
+
+export function getModalUtilityClass(slot) {
+ return generateUtilityClass('MuiModal', slot);
+}
+
+const modalUnstyledClasses = generateUtilityClasses('MuiModal', ['root', 'hidden']);
+
+export default modalUnstyledClasses;
diff --git a/packages/material-ui-unstyled/src/SliderUnstyled/SliderUnstyled.d.ts b/packages/material-ui-unstyled/src/SliderUnstyled/SliderUnstyled.d.ts
index 36776195feae5c..7772164848d353 100644
--- a/packages/material-ui-unstyled/src/SliderUnstyled/SliderUnstyled.d.ts
+++ b/packages/material-ui-unstyled/src/SliderUnstyled/SliderUnstyled.d.ts
@@ -28,7 +28,6 @@ export interface SliderUnstyledTypeMap 0 && marks.some((mark) => mark.label),
max,
min,
orientation,
@@ -600,11 +602,9 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) {
track,
valueLabelDisplay,
valueLabelFormat,
- isRtl,
- marked: marks.length > 0 && marks.some((mark) => mark.label),
};
- const utilityClasses = useUtilityClasses({ ...styleProps, classes: classesProp });
+ const classes = useUtilityClasses(styleProps);
return (
{marks.map((mark, index) => {
@@ -665,8 +665,8 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) {
theme,
})}
style={{ ...style, ...markProps.style }}
- className={clsx(utilityClasses.mark, markProps.className, {
- [utilityClasses.markActive]: markActive,
+ className={clsx(classes.mark, markProps.className, {
+ [classes.markActive]: markActive,
})}
/>
{mark.label != null ? (
@@ -683,8 +683,8 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) {
theme,
})}
style={{ ...style, ...markLabelProps.style }}
- className={clsx(utilityClasses.markLabel, markLabelProps.className, {
- [utilityClasses.markLabelActive]: markActive,
+ className={clsx(classes.markLabel, markLabelProps.className, {
+ [classes.markLabelActive]: markActive,
})}
>
{mark.label}
@@ -713,7 +713,7 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) {
open={open === index || active === index || valueLabelDisplay === 'on'}
disabled={disabled}
{...valueLabelProps}
- className={clsx(utilityClasses.valueLabel, valueLabelProps.className)}
+ className={clsx(classes.valueLabel, valueLabelProps.className)}
{...(!isHostComponent(ValueLabel) && {
styleProps: { ...styleProps, ...valueLabelProps.styleProps },
theme,
@@ -724,9 +724,9 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) {
onMouseOver={handleMouseOver}
onMouseLeave={handleMouseLeave}
{...thumbProps}
- className={clsx(utilityClasses.thumb, thumbProps.className, {
- [utilityClasses.active]: active === index,
- [utilityClasses.focusVisible]: focusVisible === index,
+ className={clsx(classes.thumb, thumbProps.className, {
+ [classes.active]: active === index,
+ [classes.focusVisible]: focusVisible === index,
})}
{...(!isHostComponent(Thumb) && {
styleProps: { ...styleProps, ...thumbProps.styleProps },
@@ -815,7 +815,6 @@ SliderUnstyled.propTypes = {
children: PropTypes.node,
/**
* Override or extend the styles applied to the component.
- * @default {}
*/
classes: PropTypes.object,
/**
diff --git a/packages/material-ui-unstyled/src/index.d.ts b/packages/material-ui-unstyled/src/index.d.ts
index 02de1ea2ba5aa7..5e2c860322674f 100644
--- a/packages/material-ui-unstyled/src/index.d.ts
+++ b/packages/material-ui-unstyled/src/index.d.ts
@@ -4,6 +4,9 @@ export * from './BackdropUnstyled';
export { default as BadgeUnstyled } from './BadgeUnstyled';
export * from './BadgeUnstyled';
+export { default as ModalUnstyled } from './ModalUnstyled';
+export * from './ModalUnstyled';
+
export { default as SliderUnstyled } from './SliderUnstyled';
export * from './SliderUnstyled';
diff --git a/packages/material-ui-unstyled/src/index.js b/packages/material-ui-unstyled/src/index.js
index 89131b6f9afdcf..87391679d5687a 100644
--- a/packages/material-ui-unstyled/src/index.js
+++ b/packages/material-ui-unstyled/src/index.js
@@ -7,6 +7,9 @@ export * from './BadgeUnstyled';
export { default as SliderUnstyled } from './SliderUnstyled';
export * from './SliderUnstyled';
+export { default as ModalUnstyled } from './ModalUnstyled';
+export * from './ModalUnstyled';
+
export { default as Portal } from './Portal';
export { default as Unstable_TrapFocus } from './Unstable_TrapFocus';
diff --git a/packages/material-ui/src/Backdrop/Backdrop.d.ts b/packages/material-ui/src/Backdrop/Backdrop.d.ts
index 77425a752956fc..c605deb49a5f56 100644
--- a/packages/material-ui/src/Backdrop/Backdrop.d.ts
+++ b/packages/material-ui/src/Backdrop/Backdrop.d.ts
@@ -17,7 +17,6 @@ export type BackdropTypeMap<
Partial> & {
/**
* Override or extend the styles applied to the component.
- * @default {}
*/
classes?: BackdropUnstyledTypeMap['props']['classes'];
/**
diff --git a/packages/material-ui/src/Backdrop/Backdrop.js b/packages/material-ui/src/Backdrop/Backdrop.js
index dcefa5b2d45f57..e94eba3d755ed3 100644
--- a/packages/material-ui/src/Backdrop/Backdrop.js
+++ b/packages/material-ui/src/Backdrop/Backdrop.js
@@ -7,11 +7,7 @@ import experimentalStyled from '../styles/experimentalStyled';
import useThemeProps from '../styles/useThemeProps';
import Fade from '../Fade';
-const backdropClasses = {
- ...backdropUnstyledClasses,
-};
-
-export { backdropClasses };
+export const backdropClasses = backdropUnstyledClasses;
const overridesResolver = (props, styles) => {
const { styleProps } = props;
@@ -22,7 +18,7 @@ const overridesResolver = (props, styles) => {
};
const extendUtilityClasses = (styleProps) => {
- const { classes = {} } = styleProps;
+ const { classes } = styleProps;
return classes;
};
@@ -56,9 +52,9 @@ const BackdropRoot = experimentalStyled(
const Backdrop = React.forwardRef(function Backdrop(inProps, ref) {
const props = useThemeProps({ props: inProps, name: 'MuiBackdrop' });
const {
+ children,
components = {},
componentsProps = {},
- children,
className,
invisible = false,
open,
@@ -112,7 +108,6 @@ Backdrop.propTypes = {
children: PropTypes.node,
/**
* Override or extend the styles applied to the component.
- * @default {}
*/
classes: PropTypes.object,
/**
diff --git a/packages/material-ui/src/Badge/Badge.d.ts b/packages/material-ui/src/Badge/Badge.d.ts
index 96c8fe66a88bba..4f52f3f9b25897 100644
--- a/packages/material-ui/src/Badge/Badge.d.ts
+++ b/packages/material-ui/src/Badge/Badge.d.ts
@@ -19,7 +19,6 @@ export type BadgeTypeMap<
props: P & {
/**
* Override or extend the styles applied to the component.
- * @default {}
*/
classes?: BadgeUnstyledTypeMap['props']['classes'] & {
/** Styles applied to the badge `span` element if `color="primary"`. */
diff --git a/packages/material-ui/src/Badge/Badge.js b/packages/material-ui/src/Badge/Badge.js
index 1cb5beea83214f..a3ec7603775d47 100644
--- a/packages/material-ui/src/Badge/Badge.js
+++ b/packages/material-ui/src/Badge/Badge.js
@@ -11,13 +11,11 @@ import styled from '../styles/experimentalStyled';
import useThemeProps from '../styles/useThemeProps';
import capitalize from '../utils/capitalize';
-const badgeClasses = {
+export const badgeClasses = {
...badgeUnstyledClasses,
...generateUtilityClasses('MuiBadge', ['colorError', 'colorPrimary', 'colorSecondary']),
};
-export { badgeClasses };
-
const RADIUS_STANDARD = 10;
const RADIUS_DOT = 4;
@@ -284,7 +282,6 @@ Badge.propTypes = {
children: PropTypes.node,
/**
* Override or extend the styles applied to the component.
- * @default {}
*/
classes: PropTypes.object,
/**
diff --git a/packages/material-ui/src/Modal/Modal.d.ts b/packages/material-ui/src/Modal/Modal.d.ts
index 8011ba3ac3fae8..ab2f0f51063fab 100644
--- a/packages/material-ui/src/Modal/Modal.d.ts
+++ b/packages/material-ui/src/Modal/Modal.d.ts
@@ -1,106 +1,37 @@
import * as React from 'react';
-import { InternalStandardProps as StandardProps } from '..';
+import { SxProps } from '@material-ui/system';
+import {
+ ExtendModalUnstyledTypeMap,
+ ExtendModalUnstyled,
+ ModalUnstyledProps,
+} from '@material-ui/unstyled/ModalUnstyled';
+import { Theme } from '../styles';
import { BackdropProps } from '../Backdrop';
-import { PortalProps } from '../Portal';
-export interface ModalProps
- extends StandardProps, 'children'> {
- /**
- * A backdrop component. This prop enables custom backdrop rendering.
- * @default SimpleBackdrop
- */
- BackdropComponent?: React.ElementType;
- /**
- * Props applied to the [`Backdrop`](/api/backdrop/) element.
- */
- BackdropProps?: Partial;
- /**
- * A single child content element.
- */
- children: React.ReactElement;
- /**
- * When set to true the Modal waits until a nested Transition is completed before closing.
- * @default false
- */
- closeAfterTransition?: boolean;
- /**
- * An HTML element or function that returns one.
- * The `container` will have the portal children appended to it.
- *
- * By default, it uses the body of the top-level document object,
- * so it's simply `document.body` most of the time.
- */
- container?: PortalProps['container'];
- /**
- * If `true`, the modal will not automatically shift focus to itself when it opens, and
- * replace it to the last focused element when it closes.
- * This also works correctly with any modal children that have the `disableAutoFocus` prop.
- *
- * Generally this should never be set to `true` as it makes the modal less
- * accessible to assistive technologies, like screen readers.
- * @default false
- */
- disableAutoFocus?: boolean;
- /**
- * If `true`, the modal will not prevent focus from leaving the modal while open.
- *
- * Generally this should never be set to `true` as it makes the modal less
- * accessible to assistive technologies, like screen readers.
- * @default false
- */
- disableEnforceFocus?: boolean;
- /**
- * If `true`, hitting escape will not fire the `onClose` callback.
- * @default false
- */
- disableEscapeKeyDown?: boolean;
- /**
- * The `children` will be under the DOM hierarchy of the parent component.
- * @default false
- */
- disablePortal?: PortalProps['disablePortal'];
- /**
- * If `true`, the modal will not restore focus to previously focused element once
- * modal is hidden.
- * @default false
- */
- disableRestoreFocus?: boolean;
- /**
- * Disable the scroll lock behavior.
- * @default false
- */
- disableScrollLock?: boolean;
- /**
- * If `true`, the backdrop is not rendered.
- * @default false
- */
- hideBackdrop?: boolean;
- /**
- * Always keep the children in the DOM.
- * This prop can be useful in SEO situation or
- * when you want to maximize the responsiveness of the Modal.
- * @default false
- */
- keepMounted?: boolean;
- /**
- * Callback fired when the backdrop is clicked.
- */
- onBackdropClick?: React.ReactEventHandler<{}>;
- /**
- * Callback fired when the component requests to be closed.
- * The `reason` parameter can optionally be used to control the response to `onClose`.
- *
- * @param {object} event The event source of the callback.
- * @param {string} reason Can be: `"escapeKeyDown"`, `"backdropClick"`.
- */
- onClose?: {
- bivarianceHack(event: {}, reason: 'backdropClick' | 'escapeKeyDown'): void;
- }['bivarianceHack'];
- /**
- * If `true`, the component is shown.
- */
- open: boolean;
-}
+export type ModalTypeMap = ExtendModalUnstyledTypeMap<{
+ props: P & {
+ /**
+ * A backdrop component. This prop enables custom backdrop rendering.
+ * @default Backdrop
+ */
+ BackdropComponent?: React.ElementType;
+ /**
+ * Props applied to the [`Backdrop`](/api/backdrop/) element.
+ */
+ BackdropProps?: Partial;
+ /**
+ * The system prop that allows defining system overrides as well as additional CSS styles.
+ */
+ sx?: SxProps;
+ };
+ defaultComponent: D;
+}>;
+
+type ModalRootProps = NonNullable['root'];
+
+export const ModalRoot: React.FC;
+
+export type ModalClassKey = keyof NonNullable;
/**
* Modal is a lower-level construct that is leveraged by the following components:
@@ -123,6 +54,15 @@ export interface ModalProps
*
* - [Modal API](https://material-ui.com/api/modal/)
*/
-declare const Modal: React.ComponentType;
+declare const Modal: ExtendModalUnstyled;
+
+export type ModalClasses = Record;
+
+export const modalClasses: ModalClasses;
+
+export type ModalProps<
+ D extends React.ElementType = ModalTypeMap['defaultComponent'],
+ P = {}
+> = ModalUnstyledProps;
export default Modal;
diff --git a/packages/material-ui/src/Modal/Modal.js b/packages/material-ui/src/Modal/Modal.js
index f3c334db36ffb1..028d9040f55ad5 100644
--- a/packages/material-ui/src/Modal/Modal.js
+++ b/packages/material-ui/src/Modal/Modal.js
@@ -1,44 +1,48 @@
import * as React from 'react';
import PropTypes from 'prop-types';
-import { getThemeProps, useTheme } from '@material-ui/styles';
-import { elementAcceptingRef, HTMLElementType } from '@material-ui/utils';
-import ownerDocument from '../utils/ownerDocument';
-import Portal from '../Portal';
-import createChainedFunction from '../utils/createChainedFunction';
-import useForkRef from '../utils/useForkRef';
-import useEventCallback from '../utils/useEventCallback';
-import zIndex from '../styles/zIndex';
-import ModalManager, { ariaHidden } from './ModalManager';
-import TrapFocus from '../Unstable_TrapFocus';
-import SimpleBackdrop from './SimpleBackdrop';
+import { isHostComponent } from '@material-ui/unstyled';
+import { deepmerge, elementAcceptingRef, HTMLElementType } from '@material-ui/utils';
+import ModalUnstyled, { modalUnstyledClasses } from '@material-ui/unstyled/ModalUnstyled';
+import experimentalStyled from '../styles/experimentalStyled';
+import useThemeProps from '../styles/useThemeProps';
+import Backdrop from '../Backdrop';
-function getContainer(container) {
- return typeof container === 'function' ? container() : container;
-}
+export const modalClasses = modalUnstyledClasses;
-function getHasTransition(props) {
- return props.children ? props.children.props.hasOwnProperty('in') : false;
-}
+const overridesResolver = (props, styles) => {
+ const { styleProps } = props;
-// 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 defaultManager = new ModalManager();
+ return deepmerge(styles.root || {}, {
+ ...(!styleProps.open && styleProps.exited && styles.hidden),
+ });
+};
-export const styles = (theme) => ({
- /* Styles applied to the root element. */
- root: {
- position: 'fixed',
- zIndex: theme.zIndex.modal,
- right: 0,
- bottom: 0,
- top: 0,
- left: 0,
+const extendUtilityClasses = (styleProps) => {
+ return styleProps.classes;
+};
+
+const ModalRoot = experimentalStyled(
+ 'div',
+ {},
+ {
+ name: 'MuiModal',
+ slot: 'Root',
+ overridesResolver,
},
+)(({ theme, styleProps }) => ({
+ /* Styles applied to the root element. */
+ position: 'fixed',
+ zIndex: theme.zIndex.modal,
+ right: 0,
+ bottom: 0,
+ top: 0,
+ left: 0,
/* Styles applied to the root element if the `Modal` has exited. */
- hidden: {
- visibility: 'hidden',
- },
-});
+ ...(!styleProps.open &&
+ styleProps.exited && {
+ visibility: 'hidden',
+ }),
+}));
/**
* Modal is a lower-level construct that is leveraged by the following components:
@@ -54,14 +58,13 @@ export const styles = (theme) => ({
* This component shares many concepts with [react-overlays](https://react-bootstrap.github.io/react-overlays/#modals).
*/
const Modal = React.forwardRef(function Modal(inProps, ref) {
- const theme = useTheme();
- const props = getThemeProps({ name: 'MuiModal', props: inProps, theme });
+ const props = useThemeProps({ name: 'MuiModal', props: inProps });
const {
- BackdropComponent = SimpleBackdrop,
- BackdropProps,
- children,
+ BackdropComponent = Backdrop,
closeAfterTransition = false,
- container,
+ children,
+ components = {},
+ componentsProps = {},
disableAutoFocus = false,
disableEnforceFocus = false,
disableEscapeKeyDown = false,
@@ -70,184 +73,56 @@ const Modal = React.forwardRef(function Modal(inProps, ref) {
disableScrollLock = false,
hideBackdrop = false,
keepMounted = false,
- // private
- // eslint-disable-next-line react/prop-types
- manager = defaultManager,
- onBackdropClick,
- onClose,
- onKeyDown,
- open,
...other
} = props;
const [exited, setExited] = React.useState(true);
- const modal = React.useRef({});
- const mountNodeRef = React.useRef(null);
- const modalRef = React.useRef(null);
- const handleRef = useForkRef(modalRef, ref);
- const hasTransition = getHasTransition(props);
-
- const getDoc = () => ownerDocument(mountNodeRef.current);
- const getModal = () => {
- modal.current.modalRef = modalRef.current;
- modal.current.mountNode = mountNodeRef.current;
- return modal.current;
- };
-
- const handleMounted = () => {
- manager.mount(getModal(), { disableScrollLock });
-
- // Fix a bug on Chrome where the scroll isn't initially 0.
- modalRef.current.scrollTop = 0;
- };
-
- const handleOpen = useEventCallback(() => {
- const resolvedContainer = getContainer(container) || getDoc().body;
-
- manager.add(getModal(), resolvedContainer);
-
- // The element was already mounted.
- if (modalRef.current) {
- handleMounted();
- }
- });
-
- const isTopModal = React.useCallback(() => manager.isTopModal(getModal()), [manager]);
-
- const handlePortalRef = useEventCallback((node) => {
- mountNodeRef.current = node;
-
- if (!node) {
- return;
- }
-
- if (open && isTopModal()) {
- handleMounted();
- } else {
- ariaHidden(modalRef.current, true);
- }
- });
- const handleClose = React.useCallback(() => {
- manager.remove(getModal());
- }, [manager]);
-
- React.useEffect(() => {
- return () => {
- handleClose();
- };
- }, [handleClose]);
-
- React.useEffect(() => {
- if (open) {
- handleOpen();
- } else if (!hasTransition || !closeAfterTransition) {
- handleClose();
- }
- }, [open, handleClose, hasTransition, closeAfterTransition, handleOpen]);
-
- if (!keepMounted && !open && (!hasTransition || exited)) {
- return null;
- }
-
- const handleEnter = () => {
- setExited(false);
- };
-
- const handleExited = () => {
- setExited(true);
-
- if (closeAfterTransition) {
- handleClose();
- }
+ const commonProps = {
+ BackdropComponent,
+ closeAfterTransition,
+ disableAutoFocus,
+ disableEnforceFocus,
+ disableEscapeKeyDown,
+ disablePortal,
+ disableRestoreFocus,
+ disableScrollLock,
+ hideBackdrop,
+ keepMounted,
+ // private
+ onTransitionEnter: () => setExited(false),
+ onTransitionExited: () => setExited(true),
};
- const handleBackdropClick = (event) => {
- if (event.target !== event.currentTarget) {
- return;
- }
-
- if (onBackdropClick) {
- onBackdropClick(event);
- }
-
- if (onClose) {
- onClose(event, 'backdropClick');
- }
+ const styleProps = {
+ ...props,
+ ...commonProps,
+ exited,
};
- const handleKeyDown = (event) => {
- if (onKeyDown) {
- onKeyDown(event);
- }
-
- // The handler doesn't take event.defaultPrevented into account:
- //
- // event.preventDefault() is meant to stop default behaviors like
- // clicking a checkbox to check it, hitting a button to submit a form,
- // and hitting left arrow to move the cursor in a text input etc.
- // Only special HTML elements have these default behaviors.
- if (event.key !== 'Escape' || !isTopModal()) {
- return;
- }
-
- if (!disableEscapeKeyDown) {
- // Swallow the event, in case someone is listening for the escape key on the body.
- event.stopPropagation();
-
- if (onClose) {
- onClose(event, 'escapeKeyDown');
- }
- }
- };
-
- const inlineStyle = styles(theme || { zIndex });
- const childProps = {};
- if (children.props.tabIndex === undefined) {
- childProps.tabIndex = children.props.tabIndex || '-1';
- }
-
- // It's a Transition like component
- if (hasTransition) {
- childProps.onEnter = createChainedFunction(handleEnter, children.props.onEnter);
- childProps.onExited = createChainedFunction(handleExited, children.props.onExited);
- }
+ const classes = extendUtilityClasses(styleProps);
return (
-
- {/*
- * Marking an element with the role presentation indicates to assistive technology
- * that this element should be ignored; it exists to support the web application and
- * is not meant for humans to interact with directly.
- * https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-static-element-interactions.md
- */}
-
- {hideBackdrop ? null : (
-
- )}
-
-
- {React.cloneElement(children, childProps)}
-
-
-
+
+ {children}
+
);
});
@@ -258,7 +133,7 @@ Modal.propTypes = {
// ----------------------------------------------------------------------
/**
* A backdrop component. This prop enables custom backdrop rendering.
- * @default SimpleBackdrop
+ * @default Backdrop
*/
BackdropComponent: PropTypes.elementType,
/**
@@ -269,11 +144,28 @@ Modal.propTypes = {
* A single child content element.
*/
children: elementAcceptingRef.isRequired,
+ /**
+ * Override or extend the styles applied to the component.
+ */
+ classes: PropTypes.object,
/**
* When set to true the Modal waits until a nested Transition is completed before closing.
* @default false
*/
closeAfterTransition: PropTypes.bool,
+ /**
+ * The components used for each slot inside the Modal.
+ * Either a string to use a HTML element or a component.
+ * @default {}
+ */
+ components: PropTypes.shape({
+ Root: PropTypes.elementType,
+ }),
+ /**
+ * The props used for each slot inside the Modal.
+ * @default {}
+ */
+ componentsProps: PropTypes.object,
/**
* An HTML element or function that returns one.
* The `container` will have the portal children appended to it.
@@ -348,18 +240,14 @@ Modal.propTypes = {
* @param {string} reason Can be: `"escapeKeyDown"`, `"backdropClick"`.
*/
onClose: PropTypes.func,
- /**
- * @ignore
- */
- onKeyDown: PropTypes.func,
/**
* If `true`, the component is shown.
*/
open: PropTypes.bool.isRequired,
/**
- * @ignore
+ * The system prop that allows defining system overrides as well as additional CSS styles.
*/
- style: PropTypes.object,
+ sx: PropTypes.object,
};
export default Modal;
diff --git a/packages/material-ui/src/Modal/Modal.test.js b/packages/material-ui/src/Modal/Modal.test.js
index 0cd3d1b50dcfbd..643560aa239f69 100644
--- a/packages/material-ui/src/Modal/Modal.test.js
+++ b/packages/material-ui/src/Modal/Modal.test.js
@@ -9,17 +9,17 @@ import {
fireEvent,
within,
createMount,
- describeConformance,
+ describeConformanceV5,
} from 'test/utils';
import { createMuiTheme } from '@material-ui/core/styles';
import { ThemeProvider } from '@material-ui/styles';
-import Fade from '../Fade';
-import Backdrop from '../Backdrop';
-import Modal from './Modal';
+import Fade from '@material-ui/core/Fade';
+import Backdrop from '@material-ui/core/Backdrop';
+import Modal, { modalClasses as classes } from '@material-ui/core/Modal';
describe('', () => {
- const mount = createMount({ strict: true });
const render = createClientRender();
+ const mount = createMount({ strict: true });
let savedBodyStyle;
before(() => {
@@ -30,17 +30,25 @@ describe('', () => {
document.body.setAttribute('style', savedBodyStyle);
});
- describeConformance(
+ describeConformanceV5(
,
() => ({
+ classes,
inheritComponent: 'div',
+ render,
mount,
+ muiName: 'MuiModal',
refInstanceof: window.HTMLDivElement,
+ testVariantProps: { hideBackdrop: true },
skip: [
'rootClass',
'componentProp',
+ 'componentsProp',
+ // Can not test this because everything is applied to second element
+ 'themeDefaultProps',
+ 'themeStyleOverrides',
// https://github.com/facebook/react/issues/11565
'reactTestRenderer',
],
diff --git a/packages/material-ui/src/Modal/SimpleBackdrop.js b/packages/material-ui/src/Modal/SimpleBackdrop.js
deleted file mode 100644
index 5d7645952b7def..00000000000000
--- a/packages/material-ui/src/Modal/SimpleBackdrop.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import * as React from 'react';
-import PropTypes from 'prop-types';
-
-export const styles = {
- /* Styles applied to the root element. */
- root: {
- zIndex: -1,
- position: 'fixed',
- right: 0,
- bottom: 0,
- top: 0,
- left: 0,
- backgroundColor: 'rgba(0, 0, 0, 0.5)',
- WebkitTapHighlightColor: 'transparent',
- },
- /* Styles applied to the root element if `invisible={true}`. */
- invisible: {
- backgroundColor: 'transparent',
- },
-};
-
-/**
- * @ignore - internal component.
- */
-const SimpleBackdrop = React.forwardRef(function SimpleBackdrop(props, ref) {
- const { invisible = false, open, ...other } = props;
-
- return open ? (
-
- ) : null;
-});
-
-SimpleBackdrop.propTypes = {
- /**
- * If `true`, the backdrop is invisible.
- * It can be used when rendering a popover or a custom select component.
- */
- invisible: PropTypes.bool,
- /**
- * If `true`, the component is shown.
- */
- open: PropTypes.bool.isRequired,
-};
-
-export default SimpleBackdrop;
diff --git a/packages/material-ui/src/Modal/index.d.ts b/packages/material-ui/src/Modal/index.d.ts
index 4fd1d7c9b6f99b..59d1074f096836 100644
--- a/packages/material-ui/src/Modal/index.d.ts
+++ b/packages/material-ui/src/Modal/index.d.ts
@@ -1,4 +1,4 @@
+export * from '@material-ui/unstyled/ModalUnstyled'; // exporting ModalManager
+
export { default } from './Modal';
export * from './Modal';
-export { default as ModalManager } from './ModalManager';
-export * from './ModalManager';
diff --git a/packages/material-ui/src/Modal/index.js b/packages/material-ui/src/Modal/index.js
index 86979640642557..3e825141545223 100644
--- a/packages/material-ui/src/Modal/index.js
+++ b/packages/material-ui/src/Modal/index.js
@@ -1,2 +1,4 @@
+export * from '@material-ui/unstyled/ModalUnstyled';
+
export { default } from './Modal';
-export { default as ModalManager } from './ModalManager';
+export * from './Modal';
diff --git a/packages/material-ui/src/Slider/Slider.d.ts b/packages/material-ui/src/Slider/Slider.d.ts
index bd7c31bf2c0109..15bfbd97ede072 100644
--- a/packages/material-ui/src/Slider/Slider.d.ts
+++ b/packages/material-ui/src/Slider/Slider.d.ts
@@ -20,7 +20,6 @@ export type SliderTypeMap<
color?: 'primary' | 'secondary';
/**
* Override or extend the styles applied to the component.
- * @default {}
*/
classes?: SliderUnstyledTypeMap['props']['classes'] & {
/** Class name applied to the root element if `color="primary"`. */
diff --git a/packages/material-ui/src/Slider/Slider.js b/packages/material-ui/src/Slider/Slider.js
index ff1f61389903ad..6eebcfece1f80a 100644
--- a/packages/material-ui/src/Slider/Slider.js
+++ b/packages/material-ui/src/Slider/Slider.js
@@ -464,7 +464,6 @@ Slider.propTypes = {
children: PropTypes.node,
/**
* Override or extend the styles applied to the component.
- * @default {}
*/
classes: PropTypes.object,
/**
diff --git a/packages/material-ui/src/styles/components.d.ts b/packages/material-ui/src/styles/components.d.ts
index 4a3400db9ba23d..b32342ffe07ac4 100644
--- a/packages/material-ui/src/styles/components.d.ts
+++ b/packages/material-ui/src/styles/components.d.ts
@@ -279,6 +279,10 @@ export interface Components {
defaultProps?: ComponentsProps['MuiMobileStepper'];
styleOverrides?: ComponentsOverrides['MuiMobileStepper'];
};
+ MuiModal?: {
+ defaultProps?: ComponentsProps['MuiModal'];
+ styleOverrides?: ComponentsOverrides['MuiModal'];
+ };
MuiNativeSelect?: {
defaultProps?: ComponentsProps['MuiNativeSelect'];
styleOverrides?: ComponentsOverrides['MuiNativeSelect'];
diff --git a/packages/material-ui/src/styles/overrides.d.ts b/packages/material-ui/src/styles/overrides.d.ts
index 01d040594dcd31..7f3e97c5615701 100644
--- a/packages/material-ui/src/styles/overrides.d.ts
+++ b/packages/material-ui/src/styles/overrides.d.ts
@@ -64,6 +64,7 @@ import { ListSubheaderClassKey } from '../ListSubheader';
import { MenuClassKey } from '../Menu';
import { MenuItemClassKey } from '../MenuItem';
import { MobileStepperClassKey } from '../MobileStepper';
+import { ModalClassKey } from '../Modal';
import { NativeSelectClassKey } from '../NativeSelect';
import { OutlinedInputClassKey } from '../OutlinedInput';
import { PaginationClassKey } from '../Pagination';
@@ -181,6 +182,7 @@ export interface ComponentNameToClassKey {
MuiMenu: MenuClassKey;
MuiMenuItem: MenuItemClassKey;
MuiMobileStepper: MobileStepperClassKey;
+ MuiModal: ModalClassKey;
MuiNativeSelect: NativeSelectClassKey;
MuiOutlinedInput: OutlinedInputClassKey;
MuiPagination: PaginationClassKey;