UNPKG

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