UNPKG

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