;
+
+ModalUnstyled.propTypes /* remove-proptypes */ = {
+ // ----------------------------- Warning --------------------------------
+ // | These PropTypes are generated from the TypeScript type definitions |
+ // | To update them edit the d.ts file and run "yarn 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 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({
+ Backdrop: PropTypes.elementType,
+ Root: PropTypes.elementType,
+ }),
+ /**
+ * The props used for each slot inside the Modal.
+ * @default {}
+ */
+ componentsProps: PropTypes.shape({
+ backdrop: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
+ root: PropTypes.oneOfType([PropTypes.func, 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 or unmounted.
+ * @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.
+ * @deprecated Use the `onClose` prop with the `reason` argument to handle the `backdropClick` events.
+ */
+ 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/mui-joy/src/Modal/ModalProps.ts b/packages/mui-joy/src/Modal/ModalProps.ts
new file mode 100644
index 00000000000000..99b0e5639cd4e3
--- /dev/null
+++ b/packages/mui-joy/src/Modal/ModalProps.ts
@@ -0,0 +1,67 @@
+import * as React from 'react';
+import { OverrideProps } from '@mui/types';
+import { SlotComponentProps } from '@mui/base/utils';
+import { ModalUnstyledOwnProps } from '@mui/base/ModalUnstyled';
+import { SxProps } from '../styles/types';
+
+export type ModalSlot = 'root' | 'backdrop';
+
+interface ComponentsProps {
+ root?: SlotComponentProps<'div', { sx?: SxProps }, ModalOwnerState>;
+ backdrop?: SlotComponentProps<'div', { sx?: SxProps }, ModalOwnerState>;
+}
+
+export interface ModalTypeMap {
+ props: P &
+ Pick<
+ ModalUnstyledOwnProps,
+ | 'children'
+ | 'container'
+ | 'disableAutoFocus'
+ | 'disableEnforceFocus'
+ | 'disableEscapeKeyDown'
+ | 'disablePortal'
+ | 'disableRestoreFocus'
+ | 'disableScrollLock'
+ | 'hideBackdrop'
+ | 'keepMounted'
+ | 'open'
+ > & {
+ /**
+ * The props used for each slot inside the Modal.
+ * @default {}
+ */
+ componentsProps?: ComponentsProps;
+ /**
+ * 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"`, `"closeClick"`.
+ */
+ onClose?: {
+ bivarianceHack(event: {}, reason: 'backdropClick' | 'escapeKeyDown' | 'closeClick'): void;
+ }['bivarianceHack'];
+ /**
+ * The system prop that allows defining system overrides as well as additional CSS styles.
+ */
+ sx?: SxProps;
+ };
+ defaultComponent: D;
+}
+
+export type ModalProps<
+ D extends React.ElementType = ModalTypeMap['defaultComponent'],
+ P = { component?: React.ElementType },
+> = OverrideProps, D>;
+
+export interface ModalOwnerState extends ModalProps {
+ disableAutoFocus: boolean;
+ disableEnforceFocus: boolean;
+ disableEscapeKeyDown: boolean;
+ disablePortal: boolean;
+ disableRestoreFocus: boolean;
+ disableScrollLock: boolean;
+ hideBackdrop: boolean;
+ keepMounted: boolean;
+}
diff --git a/packages/mui-joy/src/Modal/index.ts b/packages/mui-joy/src/Modal/index.ts
new file mode 100644
index 00000000000000..426ec988208a72
--- /dev/null
+++ b/packages/mui-joy/src/Modal/index.ts
@@ -0,0 +1,4 @@
+export { default } from './Modal';
+export * from './modalClasses';
+export { default as modalClasses } from './modalClasses';
+export * from './ModalProps';
diff --git a/packages/mui-joy/src/Modal/modalClasses.ts b/packages/mui-joy/src/Modal/modalClasses.ts
new file mode 100644
index 00000000000000..46e3b3e30f47eb
--- /dev/null
+++ b/packages/mui-joy/src/Modal/modalClasses.ts
@@ -0,0 +1,48 @@
+import { generateUtilityClass, generateUtilityClasses } from '../className';
+
+export interface ModalClasses {
+ /** Styles applied to the root element. */
+ root: string;
+ /** Styles applied to the root element if `color="primary"`. */
+ colorPrimary: string;
+ /** Styles applied to the root element if `color="neutral"`. */
+ colorNeutral: string;
+ /** Styles applied to the root element if `color="danger"`. */
+ colorDanger: string;
+ /** Styles applied to the root element if `color="info"`. */
+ colorInfo: string;
+ /** Styles applied to the root element if `color="success"`. */
+ colorSuccess: string;
+ /** Styles applied to the root element if `color="warning"`. */
+ colorWarning: string;
+ /** Styles applied to the root element if `variant="plain"`. */
+ variantPlain: string;
+ /** Styles applied to the root element if `variant="outlined"`. */
+ variantOutlined: string;
+ /** Styles applied to the root element if `variant="soft"`. */
+ variantSoft: string;
+ /** Styles applied to the root element if `variant="solid"`. */
+ variantSolid: string;
+}
+
+export type ModalClassKey = keyof ModalClasses;
+
+export function getModalUtilityClass(slot: string): string {
+ return generateUtilityClass('JoyModal', slot);
+}
+
+const modalClasses: ModalClasses = generateUtilityClasses('JoyModal', [
+ 'root',
+ 'colorPrimary',
+ 'colorNeutral',
+ 'colorDanger',
+ 'colorInfo',
+ 'colorSuccess',
+ 'colorWarning',
+ 'variantPlain',
+ 'variantOutlined',
+ 'variantSoft',
+ 'variantSolid',
+]);
+
+export default modalClasses;
diff --git a/packages/mui-joy/src/ModalClose/ModalClose.test.tsx b/packages/mui-joy/src/ModalClose/ModalClose.test.tsx
new file mode 100644
index 00000000000000..5102ea7571143e
--- /dev/null
+++ b/packages/mui-joy/src/ModalClose/ModalClose.test.tsx
@@ -0,0 +1,101 @@
+import * as React from 'react';
+import { expect } from 'chai';
+import { spy } from 'sinon';
+import { createRenderer, describeConformance, fireEvent } from 'test/utils';
+import { unstable_capitalize as capitalize } from '@mui/utils';
+import { ThemeProvider } from '@mui/joy/styles';
+import Modal from '@mui/joy/Modal';
+import ModalDialog from '@mui/joy/ModalDialog';
+import ModalClose, { modalCloseClasses as classes } from '@mui/joy/ModalClose';
+
+describe('', () => {
+ const { render } = createRenderer();
+
+ describeConformance(, () => ({
+ classes,
+ inheritComponent: 'button',
+ render,
+ ThemeProvider,
+ muiName: 'JoyModalClose',
+ refInstanceof: window.HTMLButtonElement,
+ testVariantProps: { variant: 'solid' },
+ skip: ['classesRoot', 'componentsProp'],
+ }));
+
+ describe('prop: variant', () => {
+ it('plain by default', () => {
+ const { getByRole } = render();
+
+ expect(getByRole('button')).to.have.class(classes.variantPlain);
+ });
+
+ (['plain', 'outlined', 'soft', 'solid'] as const).forEach((variant) => {
+ it(`should render ${variant}`, () => {
+ const { getByRole } = render();
+
+ // @ts-ignore
+ expect(getByRole('button')).to.have.class(classes[`variant${capitalize(variant)}`]);
+ });
+ });
+ });
+
+ describe('prop: color', () => {
+ it('adds a neutral class by default', () => {
+ const { getByRole } = render();
+
+ expect(getByRole('button')).to.have.class(classes.colorNeutral);
+ });
+
+ (['primary', 'success', 'info', 'danger', 'neutral', 'warning'] as const).forEach((color) => {
+ it(`should render ${color}`, () => {
+ const { getByRole } = render(Hello World);
+
+ // @ts-ignore
+ expect(getByRole('button')).to.have.class(classes[`color${capitalize(color)}`]);
+ });
+ });
+ });
+
+ it('inherit `size` from the context', () => {
+ const { getByRole } = render(
+
+
+ ,
+ );
+
+ expect(getByRole('button')).to.have.class(classes.sizeSm);
+ });
+
+ it('inherit `variant` from the context', () => {
+ const { getByRole } = render(
+
+
+ ,
+ );
+
+ expect(getByRole('button')).to.have.class(classes.variantSolid);
+ });
+
+ it('inherit `color` from the context', () => {
+ const { getByRole } = render(
+
+
+ ,
+ );
+
+ expect(getByRole('button')).to.have.class(classes.colorDanger);
+ });
+
+ it('call `onClick` together with close context from Modal', () => {
+ const onClose = spy();
+ const { getByRole } = render(
+
+
+ ,
+ );
+
+ fireEvent.click(getByRole('button'));
+
+ expect(onClose.callCount).to.equal(1);
+ });
+});
diff --git a/packages/mui-joy/src/ModalClose/ModalClose.tsx b/packages/mui-joy/src/ModalClose/ModalClose.tsx
new file mode 100644
index 00000000000000..da77ffde47e4cb
--- /dev/null
+++ b/packages/mui-joy/src/ModalClose/ModalClose.tsx
@@ -0,0 +1,178 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { unstable_composeClasses as composeClasses } from '@mui/base';
+import { OverridableComponent } from '@mui/types';
+import { unstable_capitalize as capitalize } from '@mui/utils';
+import { useSlotProps } from '@mui/base/utils';
+import { useButton } from '@mui/base/ButtonUnstyled';
+import { useThemeProps, styled } from '../styles';
+import { IconButtonRoot } from '../IconButton/IconButton';
+import { getModalCloseUtilityClass } from './modalCloseClasses';
+import { ModalCloseProps, ModalCloseTypeMap } from './ModalCloseProps';
+import CloseIcon from '../internal/svg-icons/Close';
+import CloseModalContext from '../Modal/CloseModalContext';
+import ModalDialogSizeContext from '../ModalDialog/ModalDialogSizeContext';
+import ModalDialogVariantColorContext from '../ModalDialog/ModalDialogVariantColorContext';
+
+const useUtilityClasses = (ownerState: ModalCloseProps & { focusVisible: boolean }) => {
+ const { variant, color, disabled, focusVisible, size } = ownerState;
+
+ const slots = {
+ root: [
+ 'root',
+ disabled && 'disabled',
+ focusVisible && 'focusVisible',
+ variant && `variant${capitalize(variant)}`,
+ color && `color${capitalize(color)}`,
+ size && `size${capitalize(size)}`,
+ ],
+ };
+
+ return composeClasses(slots, getModalCloseUtilityClass, {});
+};
+
+export const ModalCloseRoot = styled(IconButtonRoot, {
+ name: 'JoyModalClose',
+ slot: 'Root',
+ overridesResolver: (props, styles) => styles.root,
+})<{ ownerState: ModalCloseProps }>(({ ownerState, theme }) => ({
+ ...(ownerState.size === 'sm' && {
+ '--IconButton-size': '28px',
+ }),
+ ...(ownerState.size === 'md' && {
+ '--IconButton-size': '36px',
+ }),
+ ...(ownerState.size === 'lg' && {
+ '--IconButton-size': '40px',
+ }),
+ position: 'absolute',
+ top: `var(--ModalClose-inset, ${theme.spacing(1)})`,
+ right: `var(--ModalClose-inset, ${theme.spacing(1)})`,
+ borderRadius: `var(--ModalClose-radius, ${theme.vars.radius.sm})`,
+ // for variant without a background, use `tertiary` text color to reduce the importancy of the close icon.
+ ...(!theme.variants[ownerState.variant!]?.[ownerState.color!]?.backgroundColor && {
+ color: theme.vars.palette.text.secondary,
+ }),
+}));
+
+const modalDialogVariantMapping = {
+ plain: 'plain',
+ outlined: 'plain',
+ soft: 'soft',
+ solid: 'solid',
+} as const;
+
+const ModalClose = React.forwardRef(function ModalClose(inProps, ref) {
+ const props = useThemeProps({
+ props: inProps,
+ name: 'JoyModalClose',
+ });
+
+ const {
+ component = 'button',
+ color: colorProp = 'neutral',
+ variant: variantProp = 'plain',
+ size: sizeProp = 'md',
+ onClick,
+ ...other
+ } = props;
+
+ const closeModalContext = React.useContext(CloseModalContext);
+ const modalDialogVariantColor = React.useContext(ModalDialogVariantColorContext);
+ const color = inProps.color ?? modalDialogVariantColor?.color ?? colorProp;
+ const variant =
+ inProps.variant ?? modalDialogVariantMapping[modalDialogVariantColor?.variant!] ?? variantProp;
+
+ const modalDialogSize = React.useContext(ModalDialogSizeContext);
+ const size = inProps.size ?? modalDialogSize ?? sizeProp;
+
+ const { focusVisible, getRootProps } = useButton({
+ ...props,
+ ref,
+ });
+
+ const ownerState = {
+ ...props,
+ color,
+ component,
+ variant,
+ size,
+ focusVisible,
+ };
+
+ const classes = useUtilityClasses(ownerState);
+
+ const rootProps = useSlotProps({
+ elementType: ModalCloseRoot,
+ getSlotProps: getRootProps,
+ externalSlotProps: {
+ onClick: (event: React.MouseEvent) => {
+ closeModalContext?.(event, 'closeClick');
+ onClick?.(event);
+ },
+ ...other,
+ },
+ additionalProps: {
+ as: component,
+ },
+ className: classes.root,
+ ownerState,
+ });
+
+ return (
+
+
+
+ );
+}) as OverridableComponent;
+
+ModalClose.propTypes /* remove-proptypes */ = {
+ // ----------------------------- Warning --------------------------------
+ // | These PropTypes are generated from the TypeScript type definitions |
+ // | To update them edit TypeScript types and run "yarn proptypes" |
+ // ----------------------------------------------------------------------
+ /**
+ * @ignore
+ */
+ children: PropTypes.node,
+ /**
+ * The color of the component. It supports those theme colors that make sense for this component.
+ * @default 'neutral'
+ */
+ color: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
+ PropTypes.oneOf(['danger', 'info', 'neutral', 'primary', 'success', 'warning']),
+ PropTypes.string,
+ ]),
+ /**
+ * The component used for the root node.
+ * Either a string to use a HTML element or a component.
+ */
+ component: PropTypes.elementType,
+ /**
+ * @ignore
+ */
+ onClick: PropTypes.func,
+ /**
+ * The size of the component.
+ * @default 'md'
+ */
+ size: PropTypes.oneOf(['sm', 'md', 'lg']),
+ /**
+ * The system prop that allows defining system overrides as well as additional CSS styles.
+ */
+ sx: PropTypes.oneOfType([
+ PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])),
+ PropTypes.func,
+ PropTypes.object,
+ ]),
+ /**
+ * The variant to use.
+ * @default 'plain'
+ */
+ variant: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
+ PropTypes.oneOf(['outlined', 'plain', 'soft', 'solid']),
+ PropTypes.string,
+ ]),
+} as any;
+
+export default ModalClose;
diff --git a/packages/mui-joy/src/ModalClose/ModalCloseProps.ts b/packages/mui-joy/src/ModalClose/ModalCloseProps.ts
new file mode 100644
index 00000000000000..3c156fff66a8da
--- /dev/null
+++ b/packages/mui-joy/src/ModalClose/ModalCloseProps.ts
@@ -0,0 +1,41 @@
+import * as React from 'react';
+import { OverridableStringUnion, OverrideProps } from '@mui/types';
+import { ColorPaletteProp, SxProps, VariantProp } from '../styles/types';
+
+export type ModalCloseSlot = 'root';
+
+export interface ModalClosePropsColorOverrides {}
+export interface ModalClosePropsVariantOverrides {}
+export interface ModalClosePropsSizeOverrides {}
+
+export interface ModalCloseTypeMap {
+ props: P & {
+ /**
+ * The color of the component. It supports those theme colors that make sense for this component.
+ * @default 'neutral'
+ */
+ color?: OverridableStringUnion;
+ /**
+ * The size of the component.
+ * @default 'md'
+ */
+ size?: OverridableStringUnion<'sm' | 'md' | 'lg', ModalClosePropsSizeOverrides>;
+ /**
+ * The system prop that allows defining system overrides as well as additional CSS styles.
+ */
+ sx?: SxProps;
+ /**
+ * The variant to use.
+ * @default 'plain'
+ */
+ variant?: OverridableStringUnion;
+ };
+ defaultComponent: D;
+}
+
+export type ModalCloseProps<
+ D extends React.ElementType = ModalCloseTypeMap['defaultComponent'],
+ P = { component?: React.ElementType },
+> = OverrideProps, D>;
+
+export interface ModalCloseOwnerState extends ModalCloseProps {}
diff --git a/packages/mui-joy/src/ModalClose/index.ts b/packages/mui-joy/src/ModalClose/index.ts
new file mode 100644
index 00000000000000..3612cb7bfcfdab
--- /dev/null
+++ b/packages/mui-joy/src/ModalClose/index.ts
@@ -0,0 +1,4 @@
+export { default } from './ModalClose';
+export * from './modalCloseClasses';
+export { default as modalCloseClasses } from './modalCloseClasses';
+export * from './ModalCloseProps';
diff --git a/packages/mui-joy/src/ModalClose/modalCloseClasses.ts b/packages/mui-joy/src/ModalClose/modalCloseClasses.ts
new file mode 100644
index 00000000000000..6b2be6e619c8f1
--- /dev/null
+++ b/packages/mui-joy/src/ModalClose/modalCloseClasses.ts
@@ -0,0 +1,57 @@
+import { generateUtilityClass, generateUtilityClasses } from '../className';
+
+export interface ModalCloseClasses {
+ /** Styles applied to the root element. */
+ root: string;
+ /** Styles applied to the root element if `color="primary"`. */
+ colorPrimary: string;
+ /** Styles applied to the root element if `color="neutral"`. */
+ colorNeutral: string;
+ /** Styles applied to the root element if `color="danger"`. */
+ colorDanger: string;
+ /** Styles applied to the root element if `color="info"`. */
+ colorInfo: string;
+ /** Styles applied to the root element if `color="success"`. */
+ colorSuccess: string;
+ /** Styles applied to the root element if `color="warning"`. */
+ colorWarning: string;
+ /** Styles applied to the root element if `variant="plain"`. */
+ variantPlain: string;
+ /** Styles applied to the root element if `variant="outlined"`. */
+ variantOutlined: string;
+ /** Styles applied to the root element if `variant="soft"`. */
+ variantSoft: string;
+ /** Styles applied to the root element if `variant="solid"`. */
+ variantSolid: string;
+ /** Styles applied to the root element if `size="sm"`. */
+ sizeSm: string;
+ /** Styles applied to the root element if `size="md"`. */
+ sizeMd: string;
+ /** Styles applied to the root element if `size="lg"`. */
+ sizeLg: string;
+}
+
+export type ModalCloseClassKey = keyof ModalCloseClasses;
+
+export function getModalCloseUtilityClass(slot: string): string {
+ return generateUtilityClass('JoyModalClose', slot);
+}
+
+const modalCloseClasses: ModalCloseClasses = generateUtilityClasses('JoyModalClose', [
+ 'root',
+ 'colorPrimary',
+ 'colorNeutral',
+ 'colorDanger',
+ 'colorInfo',
+ 'colorSuccess',
+ 'colorWarning',
+ 'variantPlain',
+ 'variantOutlined',
+ 'variantSoft',
+ 'variantSolid',
+ 'sizeSm',
+ 'sizeMd',
+ 'sizeLg',
+]);
+
+export default modalCloseClasses;
diff --git a/packages/mui-joy/src/ModalDialog/ModalDialog.test.tsx b/packages/mui-joy/src/ModalDialog/ModalDialog.test.tsx
new file mode 100644
index 00000000000000..e6671c5eaafc34
--- /dev/null
+++ b/packages/mui-joy/src/ModalDialog/ModalDialog.test.tsx
@@ -0,0 +1,56 @@
+import * as React from 'react';
+import { expect } from 'chai';
+import { createRenderer, describeConformance } from 'test/utils';
+import { ThemeProvider } from '@mui/joy/styles';
+import ModalDialog, { modalDialogClasses as classes } from '@mui/joy/ModalDialog';
+import { unstable_capitalize as capitalize } from '@mui/utils';
+
+describe('', () => {
+ const { render } = createRenderer();
+
+ describeConformance(, () => ({
+ classes,
+ inheritComponent: 'div',
+ render,
+ ThemeProvider,
+ muiName: 'JoyModalDialog',
+ refInstanceof: window.HTMLDivElement,
+ testComponentPropWith: 'header',
+ testVariantProps: { variant: 'solid' },
+ skip: ['classesRoot', 'componentsProp'],
+ }));
+
+ describe('prop: variant', () => {
+ it('plain by default', () => {
+ const { getByRole } = render();
+
+ expect(getByRole('dialog')).to.have.class(classes.variantOutlined);
+ });
+
+ (['plain', 'outlined', 'soft', 'solid'] as const).forEach((variant) => {
+ it(`should render ${variant}`, () => {
+ const { getByRole } = render();
+
+ // @ts-ignore
+ expect(getByRole('dialog')).to.have.class(classes[`variant${capitalize(variant)}`]);
+ });
+ });
+ });
+
+ describe('prop: color', () => {
+ it('adds a neutral class by default', () => {
+ const { getByRole } = render();
+
+ expect(getByRole('dialog')).to.have.class(classes.colorNeutral);
+ });
+
+ (['primary', 'success', 'info', 'danger', 'neutral', 'warning'] as const).forEach((color) => {
+ it(`should render ${color}`, () => {
+ const { getByRole } = render();
+
+ // @ts-ignore
+ expect(getByRole('dialog')).to.have.class(classes[`color${capitalize(color)}`]);
+ });
+ });
+ });
+});
diff --git a/packages/mui-joy/src/ModalDialog/ModalDialog.tsx b/packages/mui-joy/src/ModalDialog/ModalDialog.tsx
new file mode 100644
index 00000000000000..39a97099181b6e
--- /dev/null
+++ b/packages/mui-joy/src/ModalDialog/ModalDialog.tsx
@@ -0,0 +1,183 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import clsx from 'clsx';
+import { unstable_composeClasses as composeClasses } from '@mui/base';
+import { OverridableComponent } from '@mui/types';
+import { unstable_capitalize as capitalize } from '@mui/utils';
+import { styled, useThemeProps } from '../styles';
+import { SheetRoot } from '../Sheet/Sheet';
+import { getModalDialogUtilityClass } from './modalDialogClasses';
+import { ModalDialogProps, ModalDialogTypeMap } from './ModalDialogProps';
+import ModalDialogSizeContext from './ModalDialogSizeContext';
+import ModalDialogVariantColorContext from './ModalDialogVariantColorContext';
+
+const useUtilityClasses = (ownerState: ModalDialogProps) => {
+ const { variant, color, size, layout } = ownerState;
+
+ const slots = {
+ root: [
+ 'root',
+ variant && `variant${capitalize(variant)}`,
+ color && `color${capitalize(color)}`,
+ size && `size${capitalize(size)}`,
+ layout && `layout${capitalize(layout)}`,
+ ],
+ };
+
+ return composeClasses(slots, getModalDialogUtilityClass, {});
+};
+
+const ModalDialogRoot = styled(SheetRoot, {
+ name: 'JoyModalDialog',
+ slot: 'Root',
+ overridesResolver: (props, styles) => styles.root,
+})<{ ownerState: ModalDialogProps }>(({ theme, ownerState }) => ({
+ '--ModalClose-radius':
+ 'max((var(--ModalDialog-radius) - var(--variant-borderWidth)) - var(--ModalClose-inset), min(var(--ModalClose-inset) / 2, (var(--ModalDialog-radius) - var(--variant-borderWidth)) / 2))',
+ ...(ownerState.size === 'sm' && {
+ '--ModalDialog-padding': theme.spacing(1.25),
+ '--ModalDialog-radius': theme.vars.radius.sm,
+ '--ModalClose-inset': theme.spacing(0.75),
+ fontSize: theme.vars.fontSize.sm,
+ }),
+ ...(ownerState.size === 'md' && {
+ '--ModalDialog-padding': theme.spacing(2),
+ '--ModalDialog-radius': theme.vars.radius.md,
+ '--ModalClose-inset': theme.spacing(1),
+ fontSize: theme.vars.fontSize.md,
+ }),
+ ...(ownerState.size === 'lg' && {
+ '--ModalDialog-padding': theme.spacing(3),
+ '--ModalDialog-radius': theme.vars.radius.md,
+ '--ModalClose-inset': theme.spacing(1.5),
+ fontSize: theme.vars.fontSize.md,
+ }),
+ boxSizing: 'border-box',
+ boxShadow: theme.vars.shadow.md,
+ borderRadius: 'var(--ModalDialog-radius)',
+ fontFamily: theme.vars.fontFamily.body,
+ lineHeight: theme.vars.lineHeight.md,
+ padding: 'var(--ModalDialog-padding)',
+ minWidth: 'min(calc(100vw - 2 * var(--ModalDialog-padding)), var(--ModalDialog-minWidth, 300px))',
+ outline: 'none',
+ position: 'absolute',
+ ...(ownerState.layout === 'fullscreen' && {
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ border: 0,
+ borderRadius: 0,
+ }),
+ ...(ownerState.layout === 'center' && {
+ top: '50%',
+ left: '50%',
+ transform: 'translate(-50%, -50%)',
+ }),
+}));
+
+const ModalDialog = React.forwardRef(function ModalDialog(inProps, ref) {
+ const props = useThemeProps({
+ props: inProps,
+ name: 'JoyModalDialog',
+ });
+
+ const {
+ className,
+ color = 'neutral',
+ component = 'div',
+ variant = 'outlined',
+ size = 'md',
+ layout = 'center',
+ ...other
+ } = props;
+
+ const ownerState = {
+ ...props,
+ color,
+ component,
+ layout,
+ size,
+ variant,
+ };
+
+ const classes = useUtilityClasses(ownerState);
+
+ return (
+
+
+
+
+
+ );
+}) as OverridableComponent;
+
+ModalDialog.propTypes /* remove-proptypes */ = {
+ // ----------------------------- Warning --------------------------------
+ // | These PropTypes are generated from the TypeScript type definitions |
+ // | To update them edit TypeScript types and run "yarn proptypes" |
+ // ----------------------------------------------------------------------
+ /**
+ * The content of the component.
+ */
+ children: PropTypes.node,
+ /**
+ * @ignore
+ */
+ className: PropTypes.string,
+ /**
+ * The color of the component. It supports those theme colors that make sense for this component.
+ * @default 'neutral'
+ */
+ color: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
+ PropTypes.oneOf(['danger', 'info', 'neutral', 'primary', 'success', 'warning']),
+ PropTypes.string,
+ ]),
+ /**
+ * The component used for the root node.
+ * Either a string to use a HTML element or a component.
+ */
+ component: PropTypes.elementType,
+ /**
+ * The layout of the dialog
+ * @default 'center'
+ */
+ layout: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
+ PropTypes.oneOf(['center', 'fullscreen']),
+ PropTypes.string,
+ ]),
+ /**
+ * The size of the component.
+ * @default 'md'
+ */
+ size: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
+ PropTypes.oneOf(['sm', 'md', 'lg']),
+ PropTypes.string,
+ ]),
+ /**
+ * The system prop that allows defining system overrides as well as additional CSS styles.
+ */
+ sx: PropTypes.oneOfType([
+ PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])),
+ PropTypes.func,
+ PropTypes.object,
+ ]),
+ /**
+ * The variant to use.
+ * @default 'plain'
+ */
+ variant: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
+ PropTypes.oneOf(['outlined', 'plain', 'soft', 'solid']),
+ PropTypes.string,
+ ]),
+} as any;
+
+export default ModalDialog;
diff --git a/packages/mui-joy/src/ModalDialog/ModalDialogProps.ts b/packages/mui-joy/src/ModalDialog/ModalDialogProps.ts
new file mode 100644
index 00000000000000..26ed4589bb60ce
--- /dev/null
+++ b/packages/mui-joy/src/ModalDialog/ModalDialogProps.ts
@@ -0,0 +1,51 @@
+import { OverridableStringUnion, OverrideProps } from '@mui/types';
+import * as React from 'react';
+import { ColorPaletteProp, SxProps, VariantProp } from '../styles/types';
+
+export type ModalDialogSlot = 'root';
+
+export interface ModalDialogPropsColorOverrides {}
+export interface ModalDialogPropsVariantOverrides {}
+export interface ModalDialogPropsSizeOverrides {}
+export interface ModalDialogPropsLayoutOverrides {}
+
+export interface ModalDialogTypeMap {
+ props: P & {
+ /**
+ * The content of the component.
+ */
+ children?: React.ReactNode;
+ /**
+ * The color of the component. It supports those theme colors that make sense for this component.
+ * @default 'neutral'
+ */
+ color?: OverridableStringUnion;
+ /**
+ * The layout of the dialog
+ * @default 'center'
+ */
+ layout?: 'center' | 'fullscreen';
+ /**
+ * The size of the component.
+ * @default 'md'
+ */
+ size?: OverridableStringUnion<'sm' | 'md' | 'lg', ModalDialogPropsSizeOverrides>;
+ /**
+ * The system prop that allows defining system overrides as well as additional CSS styles.
+ */
+ sx?: SxProps;
+ /**
+ * The variant to use.
+ * @default 'plain'
+ */
+ variant?: OverridableStringUnion;
+ };
+ defaultComponent: D;
+}
+
+export type ModalDialogProps<
+ D extends React.ElementType = ModalDialogTypeMap['defaultComponent'],
+ P = { component?: React.ElementType },
+> = OverrideProps, D>;
+
+export interface ModalDialogOwnerState extends ModalDialogProps {}
diff --git a/packages/mui-joy/src/ModalDialog/ModalDialogSizeContext.ts b/packages/mui-joy/src/ModalDialog/ModalDialogSizeContext.ts
new file mode 100644
index 00000000000000..061e784aaf6436
--- /dev/null
+++ b/packages/mui-joy/src/ModalDialog/ModalDialogSizeContext.ts
@@ -0,0 +1,6 @@
+import * as React from 'react';
+import { ModalDialogProps } from './ModalDialogProps';
+
+const ModalDialogSizeContext = React.createContext(undefined);
+
+export default ModalDialogSizeContext;
diff --git a/packages/mui-joy/src/ModalDialog/ModalDialogVariantColorContext.ts b/packages/mui-joy/src/ModalDialog/ModalDialogVariantColorContext.ts
new file mode 100644
index 00000000000000..d184b53f198952
--- /dev/null
+++ b/packages/mui-joy/src/ModalDialog/ModalDialogVariantColorContext.ts
@@ -0,0 +1,8 @@
+import * as React from 'react';
+import { ModalDialogProps } from './ModalDialogProps';
+
+const ModalDialogVariantColorContext = React.createContext<
+ undefined | Pick
+>(undefined);
+
+export default ModalDialogVariantColorContext;
diff --git a/packages/mui-joy/src/ModalDialog/index.ts b/packages/mui-joy/src/ModalDialog/index.ts
new file mode 100644
index 00000000000000..c15073416b2509
--- /dev/null
+++ b/packages/mui-joy/src/ModalDialog/index.ts
@@ -0,0 +1,4 @@
+export { default } from './ModalDialog';
+export * from './modalDialogClasses';
+export { default as modalDialogClasses } from './modalDialogClasses';
+export * from './ModalDialogProps';
diff --git a/packages/mui-joy/src/ModalDialog/modalDialogClasses.ts b/packages/mui-joy/src/ModalDialog/modalDialogClasses.ts
new file mode 100644
index 00000000000000..6b83d66ae7fb85
--- /dev/null
+++ b/packages/mui-joy/src/ModalDialog/modalDialogClasses.ts
@@ -0,0 +1,63 @@
+import { generateUtilityClass, generateUtilityClasses } from '../className';
+
+export interface ModalDialogClasses {
+ /** Styles applied to the root element. */
+ root: string;
+ /** Styles applied to the root element if `color="primary"`. */
+ colorPrimary: string;
+ /** Styles applied to the root element if `color="neutral"`. */
+ colorNeutral: string;
+ /** Styles applied to the root element if `color="danger"`. */
+ colorDanger: string;
+ /** Styles applied to the root element if `color="info"`. */
+ colorInfo: string;
+ /** Styles applied to the root element if `color="success"`. */
+ colorSuccess: string;
+ /** Styles applied to the root element if `color="warning"`. */
+ colorWarning: string;
+ /** Styles applied to the root element if `variant="plain"`. */
+ variantPlain: string;
+ /** Styles applied to the root element if `variant="outlined"`. */
+ variantOutlined: string;
+ /** Styles applied to the root element if `variant="soft"`. */
+ variantSoft: string;
+ /** Styles applied to the root element if `variant="solid"`. */
+ variantSolid: string;
+ /** Styles applied to the root element if `size="sm"`. */
+ sizeSm: string;
+ /** Styles applied to the root element if `size="md"`. */
+ sizeMd: string;
+ /** Styles applied to the root element if `size="lg"`. */
+ sizeLg: string;
+ /** Styles applied to the root element if `layout="center"`. */
+ layoutCenter: string;
+ /** Styles applied to the root element if `layout="fullscreen"`. */
+ layoutFullscreen: string;
+}
+
+export type ModalDialogClassKey = keyof ModalDialogClasses;
+
+export function getModalDialogUtilityClass(slot: string): string {
+ return generateUtilityClass('JoyModalDialog', slot);
+}
+
+const modalDialogClasses: ModalDialogClasses = generateUtilityClasses('JoyModalDialog', [
+ 'root',
+ 'colorPrimary',
+ 'colorNeutral',
+ 'colorDanger',
+ 'colorInfo',
+ 'colorSuccess',
+ 'colorWarning',
+ 'variantPlain',
+ 'variantOutlined',
+ 'variantSoft',
+ 'variantSolid',
+ 'sizeSm',
+ 'sizeMd',
+ 'sizeLg',
+ 'layoutCenter',
+ 'layoutFullscreen',
+]);
+
+export default modalDialogClasses;
diff --git a/packages/mui-joy/src/index.ts b/packages/mui-joy/src/index.ts
index da3402db1361c2..52f274426d1b2c 100644
--- a/packages/mui-joy/src/index.ts
+++ b/packages/mui-joy/src/index.ts
@@ -94,6 +94,15 @@ export * from './MenuItem';
export { default as MenuList } from './MenuList';
export * from './MenuList';
+export { default as Modal } from './Modal';
+export * from './Modal';
+
+export { default as ModalClose } from './ModalClose';
+export * from './ModalClose';
+
+export { default as ModalDialog } from './ModalDialog';
+export * from './ModalDialog';
+
export { default as Option } from './Option';
export * from './Option';
diff --git a/packages/mui-joy/src/styles/CssVarsProvider.test.tsx b/packages/mui-joy/src/styles/CssVarsProvider.test.tsx
index b824207a92813e..9376e3e631bbb2 100644
--- a/packages/mui-joy/src/styles/CssVarsProvider.test.tsx
+++ b/packages/mui-joy/src/styles/CssVarsProvider.test.tsx
@@ -334,6 +334,7 @@ describe('[Joy] CssVarsProvider', () => {
level2: 'var(--joy-palette-background-level2)',
level3: 'var(--joy-palette-background-level3)',
tooltip: 'var(--joy-palette-background-tooltip)',
+ backdrop: 'var(--joy-palette-background-backdrop)',
}),
);
expect(screen.getByTestId('palette-focusVisible').textContent).to.equal(
diff --git a/packages/mui-joy/src/styles/components.d.ts b/packages/mui-joy/src/styles/components.d.ts
index 6b929fc16a3d4b..88a996bb9a835b 100644
--- a/packages/mui-joy/src/styles/components.d.ts
+++ b/packages/mui-joy/src/styles/components.d.ts
@@ -79,6 +79,17 @@ import {
import { MenuProps, MenuOwnerState, MenuSlot } from '../Menu/MenuProps';
import { MenuItemProps, MenuItemOwnerState, MenuItemSlot } from '../MenuItem/MenuItemProps';
import { MenuListProps, MenuListOwnerState, MenuListSlot } from '../MenuList/MenuListProps';
+import { ModalProps, ModalOwnerState, ModalSlot } from '../Modal/ModalProps';
+import {
+ ModalCloseProps,
+ ModalCloseOwnerState,
+ ModalCloseSlot,
+} from '../ModalClose/ModalCloseProps';
+import {
+ ModalDialogProps,
+ ModalDialogOwnerState,
+ ModalDialogSlot,
+} from '../ModalDialog/ModalDialogProps';
import { RadioProps, RadioOwnerState, RadioSlot } from '../Radio/RadioProps';
import {
RadioGroupProps,
@@ -293,6 +304,10 @@ export interface Components {
defaultProps?: Partial;
styleOverrides?: OverridesStyleRules;
};
+ JoyTextarea?: {
+ defaultProps?: Partial;
+ styleOverrides?: OverridesStyleRules;
+ };
JoyTextField?: {
defaultProps?: Partial;
styleOverrides?: OverridesStyleRules;
@@ -313,8 +328,16 @@ export interface Components {
defaultProps?: Partial;
styleOverrides?: OverridesStyleRules;
};
- JoyTextarea?: {
- defaultProps?: Partial;
- styleOverrides?: OverridesStyleRules;
+ JoyModal?: {
+ defaultProps?: Partial;
+ styleOverrides?: OverridesStyleRules;
+ };
+ JoyModalClose?: {
+ defaultProps?: Partial;
+ styleOverrides?: OverridesStyleRules;
+ };
+ JoyModalDialog?: {
+ defaultProps?: Partial;
+ styleOverrides?: OverridesStyleRules;
};
}
diff --git a/packages/mui-joy/src/styles/extendTheme.spec.ts b/packages/mui-joy/src/styles/extendTheme.spec.ts
index 3c8a883f9fe5fe..bde020dcb63a63 100644
--- a/packages/mui-joy/src/styles/extendTheme.spec.ts
+++ b/packages/mui-joy/src/styles/extendTheme.spec.ts
@@ -28,6 +28,9 @@ import { ListItemDecoratorOwnerState } from '@mui/joy/ListItemDecorator';
import { MenuOwnerState } from '@mui/joy/Menu';
import { MenuItemOwnerState } from '@mui/joy/MenuItem';
import { MenuListOwnerState } from '@mui/joy/MenuList';
+import { ModalOwnerState } from '@mui/joy/Modal';
+import { ModalCloseOwnerState } from '@mui/joy/ModalClose';
+import { ModalDialogOwnerState } from '@mui/joy/ModalDialog';
import { OptionOwnerState } from '@mui/joy/Option';
import { RadioOwnerState } from '@mui/joy/Radio';
import { RadioGroupOwnerState } from '@mui/joy/RadioGroup';
@@ -502,6 +505,49 @@ extendTheme({
},
},
},
+ JoyModal: {
+ defaultProps: {
+ disableAutoFocus: true,
+ },
+ styleOverrides: {
+ root: ({ ownerState }) => {
+ expectType, typeof ownerState>(ownerState);
+ return {};
+ },
+ backdrop: ({ ownerState }) => {
+ expectType, typeof ownerState>(ownerState);
+ return {};
+ },
+ },
+ },
+ JoyModalClose: {
+ defaultProps: {
+ size: 'sm',
+ variant: 'outlined',
+ color: 'neutral',
+ },
+ styleOverrides: {
+ root: ({ ownerState }) => {
+ expectType, typeof ownerState>(ownerState);
+ return {};
+ },
+ },
+ },
+ JoyModalDialog: {
+ defaultProps: {
+ variant: 'outlined',
+ color: 'neutral',
+ layout: 'center',
+ },
+ styleOverrides: {
+ root: ({ ownerState }) => {
+ expectType, typeof ownerState>(
+ ownerState,
+ );
+ return {};
+ },
+ },
+ },
JoyOption: {
defaultProps: {
variant: 'outlined',
diff --git a/packages/mui-joy/src/styles/extendTheme.ts b/packages/mui-joy/src/styles/extendTheme.ts
index ae59ff82a9dc13..1813252faa9b78 100644
--- a/packages/mui-joy/src/styles/extendTheme.ts
+++ b/packages/mui-joy/src/styles/extendTheme.ts
@@ -233,6 +233,7 @@ export default function extendTheme(themeOptions?: CssVarsThemeOptions): Theme {
level2: getCssVar('palette-neutral-100'),
level3: getCssVar('palette-neutral-200'),
tooltip: getCssVar('palette-neutral-800'),
+ backdrop: 'rgba(255 255 255 / 0.5)',
},
divider: getCssVar('palette-neutral-200'),
focusVisible: getCssVar('palette-primary-200'),
@@ -322,6 +323,7 @@ export default function extendTheme(themeOptions?: CssVarsThemeOptions): Theme {
level2: getCssVar('palette-neutral-700'),
level3: getCssVar('palette-neutral-600'),
tooltip: getCssVar('palette-neutral-600'),
+ backdrop: `rgba(${getCssVar('palette-neutral-darkChannel')} / 0.5)`,
},
divider: getCssVar('palette-neutral-800'),
focusVisible: getCssVar('palette-primary-500'),
diff --git a/packages/mui-joy/src/styles/types/colorSystem.ts b/packages/mui-joy/src/styles/types/colorSystem.ts
index 73caf3601801ee..cb31adff718f05 100644
--- a/packages/mui-joy/src/styles/types/colorSystem.ts
+++ b/packages/mui-joy/src/styles/types/colorSystem.ts
@@ -113,6 +113,7 @@ export interface PaletteBackground {
level2: string;
level3: string;
tooltip: string;
+ backdrop: string;
}
export interface ColorPalettePropOverrides {}