UNPKG

8.13 kBTypeScriptView Raw
1import React, { useEffect, useState, useRef } from 'react';
2import ReactDom from 'react-dom';
3import cx from 'classnames';
4import CloseIcon from './CloseIcon';
5import { FocusTrap } from './FocusTrap';
6import modalManager from './modalManager';
7import { isBrowser, blockNoScroll, unblockNoScroll } from './utils';
8
9const classes = {
10 overlay: 'react-responsive-modal-overlay',
11 modal: 'react-responsive-modal-modal',
12 modalCenter: 'react-responsive-modal-modalCenter',
13 closeButton: 'react-responsive-modal-closeButton',
14 animationIn: 'react-responsive-modal-fadeIn',
15 animationOut: 'react-responsive-modal-fadeOut',
16};
17
18export interface ModalProps {
19 /**
20 * Control if the modal is open or not.
21 */
22 open: boolean;
23 /**
24 * Should the dialog be centered.
25 *
26 * Default to false.
27 */
28 center?: boolean;
29 /**
30 * Is the modal closable when user press esc key.
31 *
32 * Default to true.
33 */
34 closeOnEsc?: boolean;
35 /**
36 * Is the modal closable when user click on overlay.
37 *
38 * Default to true.
39 */
40 closeOnOverlayClick?: boolean;
41 /**
42 * Whether to block scrolling when dialog is open.
43 *
44 * Default to true.
45 */
46 blockScroll?: boolean;
47 /**
48 * Show the close icon.
49 */
50 showCloseIcon?: boolean;
51 /**
52 * id attribute for the close icon button.
53 */
54 closeIconId?: string;
55 /**
56 * Custom icon to render (svg, img, etc...).
57 */
58 closeIcon?: React.ReactNode;
59 /**
60 * When the modal is open, trap focus within it.
61 *
62 * Default to true.
63 */
64 focusTrapped?: boolean;
65 /**
66 * You can specify a container prop which should be of type `Element`.
67 * The portal will be rendered inside that element.
68 * The default behavior will create a div node and render it at the at the end of document.body.
69 */
70 container?: Element;
71 /**
72 * An object containing classNames to style the modal.
73 */
74 classNames?: {
75 overlay?: string;
76 modal?: string;
77 closeButton?: string;
78 closeIcon?: string;
79 animationIn?: string;
80 animationOut?: string;
81 };
82 /**
83 * An object containing the styles objects to style the modal.
84 */
85 styles?: {
86 overlay?: React.CSSProperties;
87 modal?: React.CSSProperties;
88 closeButton?: React.CSSProperties;
89 closeIcon?: React.CSSProperties;
90 };
91 /**
92 * Animation duration in milliseconds.
93 *
94 * Default to 500.
95 */
96 animationDuration?: number;
97 /**
98 * ARIA role for modal
99 *
100 * Default to 'dialog'.
101 */
102 role?: string;
103 /**
104 * ARIA label for modal
105 */
106 ariaLabelledby?: string;
107 /**
108 * ARIA description for modal
109 */
110 ariaDescribedby?: string;
111 /**
112 * id attribute for modal
113 */
114 modalId?: string;
115 /**
116 * Callback fired when the Modal is requested to be closed by a click on the overlay or when user press esc key.
117 */
118 onClose: () => void;
119 /**
120 * Callback fired when the escape key is pressed.
121 */
122 onEscKeyDown?: (event: KeyboardEvent) => void;
123 /**
124 * Callback fired when the overlay is clicked.
125 */
126 onOverlayClick?: (
127 event: React.MouseEvent<HTMLDivElement, MouseEvent>
128 ) => void;
129 /**
130 * Callback fired when the Modal has exited and the animation is finished.
131 */
132 onAnimationEnd?: () => void;
133 children?: React.ReactNode;
134}
135
136export const Modal = ({
137 open,
138 center,
139 blockScroll = true,
140 closeOnEsc = true,
141 closeOnOverlayClick = true,
142 container,
143 showCloseIcon = true,
144 closeIconId,
145 closeIcon,
146 focusTrapped = true,
147 animationDuration = 500,
148 classNames,
149 styles,
150 role = 'dialog',
151 ariaDescribedby,
152 ariaLabelledby,
153 modalId,
154 onClose,
155 onEscKeyDown,
156 onOverlayClick,
157 onAnimationEnd,
158 children,
159}: ModalProps) => {
160 const refModal = useRef<HTMLDivElement>(null);
161 const refShouldClose = useRef<boolean | null>(null);
162 const refContainer = useRef<HTMLDivElement | null>(null);
163 // Lazily create the ref instance
164 // https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily
165 if (refContainer.current === null && isBrowser) {
166 refContainer.current = document.createElement('div');
167 }
168
169 const [showPortal, setShowPortal] = useState(open);
170
171 const handleOpen = () => {
172 modalManager.add(refContainer.current!, blockScroll);
173 if (blockScroll) {
174 blockNoScroll();
175 }
176 if (
177 refContainer.current &&
178 !container &&
179 !document.body.contains(refContainer.current)
180 ) {
181 document.body.appendChild(refContainer.current);
182 }
183 document.addEventListener('keydown', handleKeydown);
184 };
185
186 const handleClose = () => {
187 modalManager.remove(refContainer.current!);
188 if (blockScroll) {
189 unblockNoScroll();
190 }
191 if (
192 refContainer.current &&
193 !container &&
194 document.body.contains(refContainer.current)
195 ) {
196 document.body.removeChild(refContainer.current);
197 }
198 document.removeEventListener('keydown', handleKeydown);
199 };
200
201 const handleKeydown = (event: KeyboardEvent) => {
202 // Only the last modal need to be escaped when pressing the esc key
203 if (
204 event.keyCode !== 27 ||
205 !modalManager.isTopModal(refContainer.current!)
206 ) {
207 return;
208 }
209
210 if (onEscKeyDown) {
211 onEscKeyDown(event);
212 }
213
214 if (closeOnEsc) {
215 onClose();
216 }
217 };
218
219 useEffect(() => {
220 // When the modal is rendered first time we want to block the scroll
221 if (open) {
222 handleOpen();
223 }
224 return () => {
225 // When the component is unmounted directly we want to unblock the scroll
226 if (showPortal) {
227 handleClose();
228 }
229 };
230 }, []);
231
232 useEffect(() => {
233 // If the open prop is changing, we need to open the modal
234 if (open && !showPortal) {
235 setShowPortal(true);
236 handleOpen();
237 }
238 }, [open]);
239
240 const handleClickOverlay = (
241 event: React.MouseEvent<HTMLDivElement, MouseEvent>
242 ) => {
243 if (refShouldClose.current === null) {
244 refShouldClose.current = true;
245 }
246
247 if (!refShouldClose.current) {
248 refShouldClose.current = null;
249 return;
250 }
251
252 if (onOverlayClick) {
253 onOverlayClick(event);
254 }
255
256 if (closeOnOverlayClick) {
257 onClose();
258 }
259
260 refShouldClose.current = null;
261 };
262
263 const handleModalEvent = () => {
264 refShouldClose.current = false;
265 };
266
267 const handleClickCloseIcon = () => {
268 onClose();
269 };
270
271 const handleAnimationEnd = () => {
272 if (!open) {
273 setShowPortal(false);
274 handleClose();
275 }
276
277 if (blockScroll) {
278 unblockNoScroll();
279 }
280
281 if (onAnimationEnd) {
282 onAnimationEnd();
283 }
284 };
285
286 return showPortal
287 ? ReactDom.createPortal(
288 <div
289 style={{
290 animation: `${
291 open
292 ? classNames?.animationIn ?? classes.animationIn
293 : classNames?.animationOut ?? classes.animationOut
294 } ${animationDuration}ms`,
295 ...styles?.overlay,
296 }}
297 className={cx(classes.overlay, classNames?.overlay)}
298 onClick={handleClickOverlay}
299 onAnimationEnd={handleAnimationEnd}
300 data-testid="overlay"
301 >
302 <div
303 ref={refModal}
304 className={cx(
305 classes.modal,
306 center && classes.modalCenter,
307 classNames?.modal
308 )}
309 style={styles?.modal}
310 onMouseDown={handleModalEvent}
311 onMouseUp={handleModalEvent}
312 onClick={handleModalEvent}
313 id={modalId}
314 role={role}
315 aria-modal="true"
316 aria-labelledby={ariaLabelledby}
317 aria-describedby={ariaDescribedby}
318 data-testid="modal"
319 >
320 {focusTrapped && <FocusTrap container={refModal} />}
321 {children}
322 {showCloseIcon && (
323 <CloseIcon
324 classes={classes}
325 classNames={classNames}
326 styles={styles}
327 closeIcon={closeIcon}
328 onClickCloseIcon={handleClickCloseIcon}
329 id={closeIconId}
330 />
331 )}
332 </div>
333 </div>,
334 container || refContainer.current!
335 )
336 : null;
337};
338
339export default Modal;