UNPKG

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