UNPKG

12.8 kBJavaScriptView Raw
1import _extends from "@babel/runtime/helpers/esm/extends";
2import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
3const _excluded = ["children", "closeAfterTransition", "container", "disableAutoFocus", "disableEnforceFocus", "disableEscapeKeyDown", "disablePortal", "disableRestoreFocus", "disableScrollLock", "hideBackdrop", "keepMounted", "manager", "onBackdropClick", "onClose", "onKeyDown", "open", "onTransitionEnter", "onTransitionExited", "slotProps", "slots"];
4import * as React from 'react';
5import PropTypes from 'prop-types';
6import { elementAcceptingRef, HTMLElementType, unstable_ownerDocument as ownerDocument, unstable_useForkRef as useForkRef, unstable_createChainedFunction as createChainedFunction, unstable_useEventCallback as useEventCallback } from '@mui/utils';
7import composeClasses from '../composeClasses';
8import Portal from '../Portal';
9import ModalManager, { ariaHidden } from './ModalManager';
10import FocusTrap from '../FocusTrap';
11import { getModalUtilityClass } from './modalClasses';
12import { useSlotProps } from '../utils';
13import { useClassNamesOverride } from '../utils/ClassNameConfigurator';
14import { jsx as _jsx } from "react/jsx-runtime";
15import { jsxs as _jsxs } from "react/jsx-runtime";
16const useUtilityClasses = ownerState => {
17 const {
18 open,
19 exited
20 } = ownerState;
21 const slots = {
22 root: ['root', !open && exited && 'hidden'],
23 backdrop: ['backdrop']
24 };
25 return composeClasses(slots, useClassNamesOverride(getModalUtilityClass));
26};
27function getContainer(container) {
28 return typeof container === 'function' ? container() : container;
29}
30function getHasTransition(children) {
31 return children ? children.props.hasOwnProperty('in') : false;
32}
33
34// A modal manager used to track and manage the state of open Modals.
35// Modals don't open on the server so this won't conflict with concurrent requests.
36const defaultManager = new ModalManager();
37
38/**
39 * Modal is a lower-level construct that is leveraged by the following components:
40 *
41 * * [Dialog](https://mui.com/material-ui/api/dialog/)
42 * * [Drawer](https://mui.com/material-ui/api/drawer/)
43 * * [Menu](https://mui.com/material-ui/api/menu/)
44 * * [Popover](https://mui.com/material-ui/api/popover/)
45 *
46 * If you are creating a modal dialog, you probably want to use the [Dialog](https://mui.com/material-ui/api/dialog/) component
47 * rather than directly using Modal.
48 *
49 * This component shares many concepts with [react-overlays](https://react-bootstrap.github.io/react-overlays/#modals).
50 *
51 * Demos:
52 *
53 * - [Modal](https://mui.com/base/react-modal/)
54 *
55 * API:
56 *
57 * - [Modal API](https://mui.com/base/react-modal/components-api/#modal)
58 */
59const Modal = /*#__PURE__*/React.forwardRef(function Modal(props, forwardedRef) {
60 var _props$ariaHidden, _slots$root;
61 const {
62 children,
63 closeAfterTransition = false,
64 container,
65 disableAutoFocus = false,
66 disableEnforceFocus = false,
67 disableEscapeKeyDown = false,
68 disablePortal = false,
69 disableRestoreFocus = false,
70 disableScrollLock = false,
71 hideBackdrop = false,
72 keepMounted = false,
73 // private
74 manager: managerProp = defaultManager,
75 onBackdropClick,
76 onClose,
77 onKeyDown,
78 open,
79 onTransitionEnter,
80 onTransitionExited,
81 slotProps = {},
82 slots = {}
83 } = props,
84 other = _objectWithoutPropertiesLoose(props, _excluded);
85 // TODO: `modal`` must change its type in this file to match the type of methods
86 // provided by `ModalManager`
87 const manager = managerProp;
88 const [exited, setExited] = React.useState(!open);
89 const modal = React.useRef({});
90 const mountNodeRef = React.useRef(null);
91 const modalRef = React.useRef(null);
92 const handleRef = useForkRef(modalRef, forwardedRef);
93 const hasTransition = getHasTransition(children);
94 const ariaHiddenProp = (_props$ariaHidden = props['aria-hidden']) != null ? _props$ariaHidden : true;
95 const getDoc = () => ownerDocument(mountNodeRef.current);
96 const getModal = () => {
97 modal.current.modalRef = modalRef.current;
98 modal.current.mountNode = mountNodeRef.current;
99 return modal.current;
100 };
101 const handleMounted = () => {
102 manager.mount(getModal(), {
103 disableScrollLock
104 });
105
106 // Fix a bug on Chrome where the scroll isn't initially 0.
107 if (modalRef.current) {
108 modalRef.current.scrollTop = 0;
109 }
110 };
111 const handleOpen = useEventCallback(() => {
112 const resolvedContainer = getContainer(container) || getDoc().body;
113 manager.add(getModal(), resolvedContainer);
114
115 // The element was already mounted.
116 if (modalRef.current) {
117 handleMounted();
118 }
119 });
120 const isTopModal = React.useCallback(() => manager.isTopModal(getModal()), [manager]);
121 const handlePortalRef = useEventCallback(node => {
122 mountNodeRef.current = node;
123 if (!node || !modalRef.current) {
124 return;
125 }
126 if (open && isTopModal()) {
127 handleMounted();
128 } else {
129 ariaHidden(modalRef.current, ariaHiddenProp);
130 }
131 });
132 const handleClose = React.useCallback(() => {
133 manager.remove(getModal(), ariaHiddenProp);
134 }, [manager, ariaHiddenProp]);
135 React.useEffect(() => {
136 return () => {
137 handleClose();
138 };
139 }, [handleClose]);
140 React.useEffect(() => {
141 if (open) {
142 handleOpen();
143 } else if (!hasTransition || !closeAfterTransition) {
144 handleClose();
145 }
146 }, [open, handleClose, hasTransition, closeAfterTransition, handleOpen]);
147 const ownerState = _extends({}, props, {
148 closeAfterTransition,
149 disableAutoFocus,
150 disableEnforceFocus,
151 disableEscapeKeyDown,
152 disablePortal,
153 disableRestoreFocus,
154 disableScrollLock,
155 exited,
156 hideBackdrop,
157 keepMounted
158 });
159 const classes = useUtilityClasses(ownerState);
160 const handleEnter = () => {
161 setExited(false);
162 if (onTransitionEnter) {
163 onTransitionEnter();
164 }
165 };
166 const handleExited = () => {
167 setExited(true);
168 if (onTransitionExited) {
169 onTransitionExited();
170 }
171 if (closeAfterTransition) {
172 handleClose();
173 }
174 };
175 const handleBackdropClick = event => {
176 if (event.target !== event.currentTarget) {
177 return;
178 }
179 if (onBackdropClick) {
180 onBackdropClick(event);
181 }
182 if (onClose) {
183 onClose(event, 'backdropClick');
184 }
185 };
186 const handleKeyDown = event => {
187 if (onKeyDown) {
188 onKeyDown(event);
189 }
190
191 // The handler doesn't take event.defaultPrevented into account:
192 //
193 // event.preventDefault() is meant to stop default behaviors like
194 // clicking a checkbox to check it, hitting a button to submit a form,
195 // and hitting left arrow to move the cursor in a text input etc.
196 // Only special HTML elements have these default behaviors.
197 if (event.key !== 'Escape' || !isTopModal()) {
198 return;
199 }
200 if (!disableEscapeKeyDown) {
201 // Swallow the event, in case someone is listening for the escape key on the body.
202 event.stopPropagation();
203 if (onClose) {
204 onClose(event, 'escapeKeyDown');
205 }
206 }
207 };
208 const childProps = {};
209 if (children.props.tabIndex === undefined) {
210 childProps.tabIndex = '-1';
211 }
212
213 // It's a Transition like component
214 if (hasTransition) {
215 childProps.onEnter = createChainedFunction(handleEnter, children.props.onEnter);
216 childProps.onExited = createChainedFunction(handleExited, children.props.onExited);
217 }
218 const Root = (_slots$root = slots.root) != null ? _slots$root : 'div';
219 const rootProps = useSlotProps({
220 elementType: Root,
221 externalSlotProps: slotProps.root,
222 externalForwardedProps: other,
223 additionalProps: {
224 ref: handleRef,
225 role: 'presentation',
226 onKeyDown: handleKeyDown
227 },
228 className: classes.root,
229 ownerState
230 });
231 const BackdropComponent = slots.backdrop;
232 const backdropProps = useSlotProps({
233 elementType: BackdropComponent,
234 externalSlotProps: slotProps.backdrop,
235 additionalProps: {
236 'aria-hidden': true,
237 onClick: handleBackdropClick,
238 open
239 },
240 className: classes.backdrop,
241 ownerState
242 });
243 if (!keepMounted && !open && (!hasTransition || exited)) {
244 return null;
245 }
246 return /*#__PURE__*/_jsx(Portal
247 // @ts-expect-error TODO: include ref to Base UI Portal props
248 , {
249 ref: handlePortalRef,
250 container: container,
251 disablePortal: disablePortal,
252 children: /*#__PURE__*/_jsxs(Root, _extends({}, rootProps, {
253 children: [!hideBackdrop && BackdropComponent ? /*#__PURE__*/_jsx(BackdropComponent, _extends({}, backdropProps)) : null, /*#__PURE__*/_jsx(FocusTrap, {
254 disableEnforceFocus: disableEnforceFocus,
255 disableAutoFocus: disableAutoFocus,
256 disableRestoreFocus: disableRestoreFocus,
257 isEnabled: isTopModal,
258 open: open,
259 children: /*#__PURE__*/React.cloneElement(children, childProps)
260 })]
261 }))
262 });
263});
264process.env.NODE_ENV !== "production" ? Modal.propTypes /* remove-proptypes */ = {
265 // ----------------------------- Warning --------------------------------
266 // | These PropTypes are generated from the TypeScript type definitions |
267 // | To update them edit TypeScript types and run "yarn proptypes" |
268 // ----------------------------------------------------------------------
269 /**
270 * A single child content element.
271 */
272 children: elementAcceptingRef.isRequired,
273 /**
274 * When set to true the Modal waits until a nested Transition is completed before closing.
275 * @default false
276 */
277 closeAfterTransition: PropTypes.bool,
278 /**
279 * An HTML element or function that returns one.
280 * The `container` will have the portal children appended to it.
281 *
282 * By default, it uses the body of the top-level document object,
283 * so it's simply `document.body` most of the time.
284 */
285 container: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([HTMLElementType, PropTypes.func]),
286 /**
287 * If `true`, the modal will not automatically shift focus to itself when it opens, and
288 * replace it to the last focused element when it closes.
289 * This also works correctly with any modal children that have the `disableAutoFocus` prop.
290 *
291 * Generally this should never be set to `true` as it makes the modal less
292 * accessible to assistive technologies, like screen readers.
293 * @default false
294 */
295 disableAutoFocus: PropTypes.bool,
296 /**
297 * If `true`, the modal will not prevent focus from leaving the modal while open.
298 *
299 * Generally this should never be set to `true` as it makes the modal less
300 * accessible to assistive technologies, like screen readers.
301 * @default false
302 */
303 disableEnforceFocus: PropTypes.bool,
304 /**
305 * If `true`, hitting escape will not fire the `onClose` callback.
306 * @default false
307 */
308 disableEscapeKeyDown: PropTypes.bool,
309 /**
310 * The `children` will be under the DOM hierarchy of the parent component.
311 * @default false
312 */
313 disablePortal: PropTypes.bool,
314 /**
315 * If `true`, the modal will not restore focus to previously focused element once
316 * modal is hidden or unmounted.
317 * @default false
318 */
319 disableRestoreFocus: PropTypes.bool,
320 /**
321 * Disable the scroll lock behavior.
322 * @default false
323 */
324 disableScrollLock: PropTypes.bool,
325 /**
326 * If `true`, the backdrop is not rendered.
327 * @default false
328 */
329 hideBackdrop: PropTypes.bool,
330 /**
331 * Always keep the children in the DOM.
332 * This prop can be useful in SEO situation or
333 * when you want to maximize the responsiveness of the Modal.
334 * @default false
335 */
336 keepMounted: PropTypes.bool,
337 /**
338 * Callback fired when the backdrop is clicked.
339 * @deprecated Use the `onClose` prop with the `reason` argument to handle the `backdropClick` events.
340 */
341 onBackdropClick: PropTypes.func,
342 /**
343 * Callback fired when the component requests to be closed.
344 * The `reason` parameter can optionally be used to control the response to `onClose`.
345 *
346 * @param {object} event The event source of the callback.
347 * @param {string} reason Can be: `"escapeKeyDown"`, `"backdropClick"`.
348 */
349 onClose: PropTypes.func,
350 /**
351 * If `true`, the component is shown.
352 */
353 open: PropTypes.bool.isRequired,
354 /**
355 * The props used for each slot inside the Modal.
356 * @default {}
357 */
358 slotProps: PropTypes.shape({
359 backdrop: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
360 root: PropTypes.oneOfType([PropTypes.func, PropTypes.object])
361 }),
362 /**
363 * The components used for each slot inside the Modal.
364 * Either a string to use a HTML element or a component.
365 * @default {}
366 */
367 slots: PropTypes.shape({
368 backdrop: PropTypes.elementType,
369 root: PropTypes.elementType
370 })
371} : void 0;
372export default Modal;
\No newline at end of file