UNPKG

5.97 kBJavaScriptView Raw
1'use client';
2
3import _extends from "@babel/runtime/helpers/esm/extends";
4import * as React from 'react';
5import { unstable_ownerDocument as ownerDocument, unstable_useForkRef as useForkRef, unstable_useEventCallback as useEventCallback, unstable_createChainedFunction as createChainedFunction } from '@mui/utils';
6import { extractEventHandlers } from '../utils';
7import { ModalManager, ariaHidden } from './ModalManager';
8function getContainer(container) {
9 return typeof container === 'function' ? container() : container;
10}
11function getHasTransition(children) {
12 return children ? children.props.hasOwnProperty('in') : false;
13}
14
15// A modal manager used to track and manage the state of open Modals.
16// Modals don't open on the server so this won't conflict with concurrent requests.
17const defaultManager = new ModalManager();
18/**
19 *
20 * Demos:
21 *
22 * - [Modal](https://mui.com/base-ui/react-modal/#hook)
23 *
24 * API:
25 *
26 * - [useModal API](https://mui.com/base-ui/react-modal/hooks-api/#use-modal)
27 */
28export function useModal(parameters) {
29 const {
30 container,
31 disableEscapeKeyDown = false,
32 disableScrollLock = false,
33 // @ts-ignore internal logic - Base UI supports the manager as a prop too
34 manager = defaultManager,
35 closeAfterTransition = false,
36 onTransitionEnter,
37 onTransitionExited,
38 children,
39 onClose,
40 open,
41 rootRef
42 } = parameters;
43
44 // @ts-ignore internal logic
45 const modal = React.useRef({});
46 const mountNodeRef = React.useRef(null);
47 const modalRef = React.useRef(null);
48 const handleRef = useForkRef(modalRef, rootRef);
49 const [exited, setExited] = React.useState(!open);
50 const hasTransition = getHasTransition(children);
51 let ariaHiddenProp = true;
52 if (parameters['aria-hidden'] === 'false' || parameters['aria-hidden'] === false) {
53 ariaHiddenProp = false;
54 }
55 const getDoc = () => ownerDocument(mountNodeRef.current);
56 const getModal = () => {
57 modal.current.modalRef = modalRef.current;
58 modal.current.mount = mountNodeRef.current;
59 return modal.current;
60 };
61 const handleMounted = () => {
62 manager.mount(getModal(), {
63 disableScrollLock
64 });
65
66 // Fix a bug on Chrome where the scroll isn't initially 0.
67 if (modalRef.current) {
68 modalRef.current.scrollTop = 0;
69 }
70 };
71 const handleOpen = useEventCallback(() => {
72 const resolvedContainer = getContainer(container) || getDoc().body;
73 manager.add(getModal(), resolvedContainer);
74
75 // The element was already mounted.
76 if (modalRef.current) {
77 handleMounted();
78 }
79 });
80 const isTopModal = React.useCallback(() => manager.isTopModal(getModal()), [manager]);
81 const handlePortalRef = useEventCallback(node => {
82 mountNodeRef.current = node;
83 if (!node) {
84 return;
85 }
86 if (open && isTopModal()) {
87 handleMounted();
88 } else if (modalRef.current) {
89 ariaHidden(modalRef.current, ariaHiddenProp);
90 }
91 });
92 const handleClose = React.useCallback(() => {
93 manager.remove(getModal(), ariaHiddenProp);
94 }, [ariaHiddenProp, manager]);
95 React.useEffect(() => {
96 return () => {
97 handleClose();
98 };
99 }, [handleClose]);
100 React.useEffect(() => {
101 if (open) {
102 handleOpen();
103 } else if (!hasTransition || !closeAfterTransition) {
104 handleClose();
105 }
106 }, [open, handleClose, hasTransition, closeAfterTransition, handleOpen]);
107 const createHandleKeyDown = otherHandlers => event => {
108 otherHandlers.onKeyDown?.(event);
109
110 // The handler doesn't take event.defaultPrevented into account:
111 //
112 // event.preventDefault() is meant to stop default behaviors like
113 // clicking a checkbox to check it, hitting a button to submit a form,
114 // and hitting left arrow to move the cursor in a text input etc.
115 // Only special HTML elements have these default behaviors.
116 if (event.key !== 'Escape' || event.which === 229 ||
117 // Wait until IME is settled.
118 !isTopModal()) {
119 return;
120 }
121 if (!disableEscapeKeyDown) {
122 // Swallow the event, in case someone is listening for the escape key on the body.
123 event.stopPropagation();
124 if (onClose) {
125 onClose(event, 'escapeKeyDown');
126 }
127 }
128 };
129 const createHandleBackdropClick = otherHandlers => event => {
130 otherHandlers.onClick?.(event);
131 if (event.target !== event.currentTarget) {
132 return;
133 }
134 if (onClose) {
135 onClose(event, 'backdropClick');
136 }
137 };
138 const getRootProps = (otherHandlers = {}) => {
139 const propsEventHandlers = extractEventHandlers(parameters);
140
141 // The custom event handlers shouldn't be spread on the root element
142 delete propsEventHandlers.onTransitionEnter;
143 delete propsEventHandlers.onTransitionExited;
144 const externalEventHandlers = _extends({}, propsEventHandlers, otherHandlers);
145 return _extends({
146 role: 'presentation'
147 }, externalEventHandlers, {
148 onKeyDown: createHandleKeyDown(externalEventHandlers),
149 ref: handleRef
150 });
151 };
152 const getBackdropProps = (otherHandlers = {}) => {
153 const externalEventHandlers = otherHandlers;
154 return _extends({
155 'aria-hidden': true
156 }, externalEventHandlers, {
157 onClick: createHandleBackdropClick(externalEventHandlers),
158 open
159 });
160 };
161 const getTransitionProps = () => {
162 const handleEnter = () => {
163 setExited(false);
164 if (onTransitionEnter) {
165 onTransitionEnter();
166 }
167 };
168 const handleExited = () => {
169 setExited(true);
170 if (onTransitionExited) {
171 onTransitionExited();
172 }
173 if (closeAfterTransition) {
174 handleClose();
175 }
176 };
177 return {
178 onEnter: createChainedFunction(handleEnter, children?.props.onEnter),
179 onExited: createChainedFunction(handleExited, children?.props.onExited)
180 };
181 };
182 return {
183 getRootProps,
184 getBackdropProps,
185 getTransitionProps,
186 rootRef: handleRef,
187 portalRef: handlePortalRef,
188 isTopModal,
189 exited,
190 hasTransition
191 };
192}
\No newline at end of file