UNPKG

13.7 kBJavaScriptView Raw
1import React, { useRef, useEffect, useState } from 'react';
2import ReactDom from 'react-dom';
3import cx from 'classnames';
4import noScroll from 'no-scroll';
5
6function _extends() {
7 _extends = Object.assign || function (target) {
8 for (var i = 1; i < arguments.length; i++) {
9 var source = arguments[i];
10
11 for (var key in source) {
12 if (Object.prototype.hasOwnProperty.call(source, key)) {
13 target[key] = source[key];
14 }
15 }
16 }
17
18 return target;
19 };
20
21 return _extends.apply(this, arguments);
22}
23
24var CloseIcon = function CloseIcon(_ref) {
25 var classes = _ref.classes,
26 classNames = _ref.classNames,
27 styles = _ref.styles,
28 id = _ref.id,
29 closeIcon = _ref.closeIcon,
30 onClickCloseIcon = _ref.onClickCloseIcon;
31 return React.createElement("button", {
32 id: id,
33 className: cx(classes.closeButton, classNames === null || classNames === void 0 ? void 0 : classNames.closeButton),
34 style: styles === null || styles === void 0 ? void 0 : styles.closeButton,
35 onClick: onClickCloseIcon,
36 "data-testid": "close-button"
37 }, closeIcon ? closeIcon : React.createElement("svg", {
38 className: classNames === null || classNames === void 0 ? void 0 : classNames.closeIcon,
39 style: styles === null || styles === void 0 ? void 0 : styles.closeIcon,
40 xmlns: "http://www.w3.org/2000/svg",
41 width: 28,
42 height: 28,
43 viewBox: "0 0 36 36",
44 "data-testid": "close-icon"
45 }, React.createElement("path", {
46 d: "M28.5 9.62L26.38 7.5 18 15.88 9.62 7.5 7.5 9.62 15.88 18 7.5 26.38l2.12 2.12L18 20.12l8.38 8.38 2.12-2.12L20.12 18z"
47 })));
48};
49
50var _modals = [];
51/**
52 * Handle the order of the modals.
53 * Inspired by the material-ui implementation.
54 */
55
56var modalManager = {
57 /**
58 * Return the modals array
59 */
60 modals: function modals() {
61 return _modals;
62 },
63
64 /**
65 * Register a new modal
66 */
67 add: function add(newModal, blockScroll) {
68 if (_modals.findIndex(function (modal) {
69 return modal.element === newModal;
70 }) === -1) {
71 _modals.push({
72 element: newModal,
73 blockScroll: blockScroll
74 });
75 }
76 },
77
78 /**
79 * Remove a modal
80 */
81 remove: function remove(oldModal) {
82 var index = _modals.findIndex(function (modal) {
83 return modal.element === oldModal;
84 });
85
86 if (index !== -1) {
87 _modals.splice(index, 1);
88 }
89 },
90
91 /**
92 * Check if the modal is the first one on the screen
93 */
94 isTopModal: function isTopModal(modal) {
95 var _modals2;
96
97 return !!_modals.length && ((_modals2 = _modals[_modals.length - 1]) === null || _modals2 === void 0 ? void 0 : _modals2.element) === modal;
98 }
99};
100
101var isBrowser = typeof window !== 'undefined';
102var blockNoScroll = function blockNoScroll() {
103 noScroll.on();
104};
105var unblockNoScroll = function unblockNoScroll() {
106 // Restore the scroll only if there is no modal on the screen
107 // We filter the modals that are not affecting the scroll
108 var modals = modalManager.modals().filter(function (modal) {
109 return modal.blockScroll;
110 });
111
112 if (modals.length === 0) {
113 noScroll.off();
114 }
115};
116
117// https://github.com/alexandrzavalii/focus-trap-js/blob/master/src/index.js v1.0.9
118var candidateSelectors = ['input', 'select', 'textarea', 'a[href]', 'button', '[tabindex]', 'audio[controls]', 'video[controls]', '[contenteditable]:not([contenteditable="false"])'];
119
120function isHidden(node) {
121 // offsetParent being null will allow detecting cases where an element is invisible or inside an invisible element,
122 // as long as the element does not use position: fixed. For them, their visibility has to be checked directly as well.
123 return node.offsetParent === null || getComputedStyle(node).visibility === 'hidden';
124}
125
126function getAllTabbingElements(parentElem) {
127 var tabbableNodes = parentElem.querySelectorAll(candidateSelectors.join(','));
128 var onlyTabbable = [];
129
130 for (var i = 0; i < tabbableNodes.length; i++) {
131 var node = tabbableNodes[i];
132
133 if (!node.disabled && getTabindex(node) > -1 && !isHidden(node)) {
134 onlyTabbable.push(node);
135 }
136 }
137
138 return onlyTabbable;
139}
140function tabTrappingKey(event, parentElem) {
141 // check if current event keyCode is tab
142 if (!event || event.key !== 'Tab') return;
143
144 if (!parentElem || !parentElem.contains) {
145 if (process && process.env.NODE_ENV === 'development') {
146 console.warn('focus-trap-js: parent element is not defined');
147 }
148
149 return false;
150 }
151
152 if (!parentElem.contains(event.target)) {
153 return false;
154 }
155
156 var allTabbingElements = getAllTabbingElements(parentElem);
157 var firstFocusableElement = allTabbingElements[0];
158 var lastFocusableElement = allTabbingElements[allTabbingElements.length - 1];
159
160 if (event.shiftKey && event.target === firstFocusableElement) {
161 lastFocusableElement.focus();
162 event.preventDefault();
163 return true;
164 } else if (!event.shiftKey && event.target === lastFocusableElement) {
165 firstFocusableElement.focus();
166 event.preventDefault();
167 return true;
168 }
169
170 return false;
171}
172
173function getTabindex(node) {
174 var tabindexAttr = parseInt(node.getAttribute('tabindex'), 10);
175 if (!isNaN(tabindexAttr)) return tabindexAttr; // Browsers do not return tabIndex correctly for contentEditable nodes;
176 // so if they don't have a tabindex attribute specifically set, assume it's 0.
177
178 if (isContentEditable(node)) return 0;
179 return node.tabIndex;
180}
181
182function isContentEditable(node) {
183 return node.getAttribute('contentEditable');
184}
185
186var FocusTrap = function FocusTrap(_ref) {
187 var container = _ref.container;
188 var refLastFocus = useRef();
189 /**
190 * Handle focus lock on the modal
191 */
192
193 useEffect(function () {
194 var handleKeyEvent = function handleKeyEvent(event) {
195 if (container === null || container === void 0 ? void 0 : container.current) {
196 tabTrappingKey(event, container.current);
197 }
198 };
199
200 if (isBrowser) {
201 document.addEventListener('keydown', handleKeyEvent);
202 } // On mount we focus on the first focusable element in the modal if there is one
203
204
205 if (isBrowser && (container === null || container === void 0 ? void 0 : container.current)) {
206 var allTabbingElements = getAllTabbingElements(container.current);
207
208 if (allTabbingElements[0]) {
209 // First we save the last focused element
210 // only if it's a focusable element
211 if (candidateSelectors.findIndex(function (selector) {
212 var _document$activeEleme;
213
214 return (_document$activeEleme = document.activeElement) === null || _document$activeEleme === void 0 ? void 0 : _document$activeEleme.matches(selector);
215 }) !== -1) {
216 refLastFocus.current = document.activeElement;
217 }
218
219 allTabbingElements[0].focus();
220 }
221 }
222
223 return function () {
224 if (isBrowser) {
225 var _refLastFocus$current;
226
227 document.removeEventListener('keydown', handleKeyEvent); // On unmount we restore the focus to the last focused element
228
229 (_refLastFocus$current = refLastFocus.current) === null || _refLastFocus$current === void 0 ? void 0 : _refLastFocus$current.focus();
230 }
231 };
232 }, [container]);
233 return null;
234};
235
236var classes = {
237 overlay: 'react-responsive-modal-overlay',
238 modal: 'react-responsive-modal-modal',
239 modalCenter: 'react-responsive-modal-modalCenter',
240 closeButton: 'react-responsive-modal-closeButton',
241 animationIn: 'react-responsive-modal-fadeIn',
242 animationOut: 'react-responsive-modal-fadeOut'
243};
244var Modal = function Modal(_ref) {
245 var _classNames$animation, _classNames$animation2;
246
247 var open = _ref.open,
248 center = _ref.center,
249 _ref$blockScroll = _ref.blockScroll,
250 blockScroll = _ref$blockScroll === void 0 ? true : _ref$blockScroll,
251 _ref$closeOnEsc = _ref.closeOnEsc,
252 closeOnEsc = _ref$closeOnEsc === void 0 ? true : _ref$closeOnEsc,
253 _ref$closeOnOverlayCl = _ref.closeOnOverlayClick,
254 closeOnOverlayClick = _ref$closeOnOverlayCl === void 0 ? true : _ref$closeOnOverlayCl,
255 container = _ref.container,
256 _ref$showCloseIcon = _ref.showCloseIcon,
257 showCloseIcon = _ref$showCloseIcon === void 0 ? true : _ref$showCloseIcon,
258 closeIconId = _ref.closeIconId,
259 closeIcon = _ref.closeIcon,
260 _ref$focusTrapped = _ref.focusTrapped,
261 focusTrapped = _ref$focusTrapped === void 0 ? true : _ref$focusTrapped,
262 _ref$animationDuratio = _ref.animationDuration,
263 animationDuration = _ref$animationDuratio === void 0 ? 500 : _ref$animationDuratio,
264 classNames = _ref.classNames,
265 styles = _ref.styles,
266 _ref$role = _ref.role,
267 role = _ref$role === void 0 ? 'dialog' : _ref$role,
268 ariaDescribedby = _ref.ariaDescribedby,
269 ariaLabelledby = _ref.ariaLabelledby,
270 modalId = _ref.modalId,
271 onClose = _ref.onClose,
272 onEscKeyDown = _ref.onEscKeyDown,
273 onOverlayClick = _ref.onOverlayClick,
274 onAnimationEnd = _ref.onAnimationEnd,
275 children = _ref.children;
276 var refModal = useRef(null);
277 var refShouldClose = useRef(null);
278 var refContainer = useRef(null); // Lazily create the ref instance
279 // https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily
280
281 if (refContainer.current === null && isBrowser) {
282 refContainer.current = document.createElement('div');
283 }
284
285 var _useState = useState(open),
286 showPortal = _useState[0],
287 setShowPortal = _useState[1];
288
289 var handleOpen = function handleOpen() {
290 modalManager.add(refContainer.current, blockScroll);
291
292 if (blockScroll) {
293 blockNoScroll();
294 }
295
296 if (refContainer.current && !container && !document.body.contains(refContainer.current)) {
297 document.body.appendChild(refContainer.current);
298 }
299
300 document.addEventListener('keydown', handleKeydown);
301 };
302
303 var handleClose = function handleClose() {
304 modalManager.remove(refContainer.current);
305
306 if (blockScroll) {
307 unblockNoScroll();
308 }
309
310 if (refContainer.current && !container && document.body.contains(refContainer.current)) {
311 document.body.removeChild(refContainer.current);
312 }
313
314 document.removeEventListener('keydown', handleKeydown);
315 };
316
317 var handleKeydown = function handleKeydown(event) {
318 // Only the last modal need to be escaped when pressing the esc key
319 if (event.keyCode !== 27 || !modalManager.isTopModal(refContainer.current)) {
320 return;
321 }
322
323 if (onEscKeyDown) {
324 onEscKeyDown(event);
325 }
326
327 if (closeOnEsc) {
328 onClose();
329 }
330 };
331
332 useEffect(function () {
333 // When the modal is rendered first time we want to block the scroll
334 if (open) {
335 handleOpen();
336 }
337
338 return function () {
339 // When the component is unmounted directly we want to unblock the scroll
340 if (showPortal) {
341 handleClose();
342 }
343 };
344 }, []);
345 useEffect(function () {
346 // If the open prop is changing, we need to open the modal
347 if (open && !showPortal) {
348 setShowPortal(true);
349 handleOpen();
350 }
351 }, [open]);
352
353 var handleClickOverlay = function handleClickOverlay(event) {
354 if (refShouldClose.current === null) {
355 refShouldClose.current = true;
356 }
357
358 if (!refShouldClose.current) {
359 refShouldClose.current = null;
360 return;
361 }
362
363 if (onOverlayClick) {
364 onOverlayClick(event);
365 }
366
367 if (closeOnOverlayClick) {
368 onClose();
369 }
370
371 refShouldClose.current = null;
372 };
373
374 var handleModalEvent = function handleModalEvent() {
375 refShouldClose.current = false;
376 };
377
378 var handleClickCloseIcon = function handleClickCloseIcon() {
379 onClose();
380 };
381
382 var handleAnimationEnd = function handleAnimationEnd() {
383 if (!open) {
384 setShowPortal(false);
385 handleClose();
386 }
387
388 if (blockScroll) {
389 unblockNoScroll();
390 }
391
392 if (onAnimationEnd) {
393 onAnimationEnd();
394 }
395 };
396
397 return showPortal ? ReactDom.createPortal(React.createElement("div", {
398 style: _extends({
399 animation: (open ? (_classNames$animation = classNames === null || classNames === void 0 ? void 0 : classNames.animationIn) !== null && _classNames$animation !== void 0 ? _classNames$animation : classes.animationIn : (_classNames$animation2 = classNames === null || classNames === void 0 ? void 0 : classNames.animationOut) !== null && _classNames$animation2 !== void 0 ? _classNames$animation2 : classes.animationOut) + " " + animationDuration + "ms"
400 }, styles === null || styles === void 0 ? void 0 : styles.overlay),
401 className: cx(classes.overlay, classNames === null || classNames === void 0 ? void 0 : classNames.overlay),
402 onClick: handleClickOverlay,
403 onAnimationEnd: handleAnimationEnd,
404 "data-testid": "overlay"
405 }, React.createElement("div", {
406 ref: refModal,
407 className: cx(classes.modal, center && classes.modalCenter, classNames === null || classNames === void 0 ? void 0 : classNames.modal),
408 style: styles === null || styles === void 0 ? void 0 : styles.modal,
409 onMouseDown: handleModalEvent,
410 onMouseUp: handleModalEvent,
411 onClick: handleModalEvent,
412 id: modalId,
413 role: role,
414 "aria-modal": "true",
415 "aria-labelledby": ariaLabelledby,
416 "aria-describedby": ariaDescribedby,
417 "data-testid": "modal"
418 }, focusTrapped && React.createElement(FocusTrap, {
419 container: refModal
420 }), children, showCloseIcon && React.createElement(CloseIcon, {
421 classes: classes,
422 classNames: classNames,
423 styles: styles,
424 closeIcon: closeIcon,
425 onClickCloseIcon: handleClickCloseIcon,
426 id: closeIconId
427 }))), container || refContainer.current) : null;
428};
429
430export default Modal;
431export { Modal };
432//# sourceMappingURL=react-responsive-modal.esm.js.map