UNPKG

react-responsive-modal

Version:

A simple responsive and accessible react modal compatible with React 16 and ready for React 17

419 lines (351 loc) 13.2 kB
import React, { useRef, useEffect, useState } from 'react'; import ReactDom from 'react-dom'; import cx from 'classnames'; import noScroll from 'no-scroll'; function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } var CloseIcon = function CloseIcon(_ref) { var classes = _ref.classes, classNames = _ref.classNames, styles = _ref.styles, id = _ref.id, closeIcon = _ref.closeIcon, onClickCloseIcon = _ref.onClickCloseIcon; return React.createElement("button", { id: id, className: cx(classes.closeButton, classNames === null || classNames === void 0 ? void 0 : classNames.closeButton), style: styles === null || styles === void 0 ? void 0 : styles.closeButton, onClick: onClickCloseIcon, "data-testid": "close-button" }, closeIcon ? closeIcon : React.createElement("svg", { className: classNames === null || classNames === void 0 ? void 0 : classNames.closeIcon, style: styles === null || styles === void 0 ? void 0 : styles.closeIcon, xmlns: "http://www.w3.org/2000/svg", width: 28, height: 28, viewBox: "0 0 36 36", "data-testid": "close-icon" }, React.createElement("path", { 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" }))); }; var _modals = []; /** * Handle the order of the modals. * Inspired by the material-ui implementation. */ var modalManager = { /** * Return the modals array */ modals: function modals() { return _modals; }, /** * Register a new modal */ add: function add(modal) { if (_modals.indexOf(modal) === -1) { _modals.push(modal); } }, /** * Remove a modal */ remove: function remove(modal) { var index = _modals.indexOf(modal); if (index !== -1) { _modals.splice(index, 1); } }, /** * Check if the modal is the first one on the screen */ isTopModal: function isTopModal(modal) { return !!_modals.length && _modals[_modals.length - 1] === modal; } }; var isBrowser = typeof window !== 'undefined'; var blockNoScroll = function blockNoScroll() { noScroll.on(); }; var unblockNoScroll = function unblockNoScroll() { // Restore the scroll only if there is no modal on the screen if (modalManager.modals().length === 0) { noScroll.off(); } }; // https://github.com/alexandrzavalii/focus-trap-js/blob/master/src/index.js v1.0.9 var candidateSelectors = ['input', 'select', 'textarea', 'a[href]', 'button', '[tabindex]', 'audio[controls]', 'video[controls]', '[contenteditable]:not([contenteditable="false"])']; function isHidden(node) { // offsetParent being null will allow detecting cases where an element is invisible or inside an invisible element, // as long as the element does not use position: fixed. For them, their visibility has to be checked directly as well. return node.offsetParent === null || getComputedStyle(node).visibility === 'hidden'; } function getAllTabbingElements(parentElem) { var tabbableNodes = parentElem.querySelectorAll(candidateSelectors.join(',')); var onlyTabbable = []; for (var i = 0; i < tabbableNodes.length; i++) { var node = tabbableNodes[i]; if (!node.disabled && getTabindex(node) > -1 && !isHidden(node)) { onlyTabbable.push(node); } } return onlyTabbable; } function tabTrappingKey(event, parentElem) { // check if current event keyCode is tab if (!event || event.key !== 'Tab') return; if (!parentElem || !parentElem.contains) { if (process && process.env.NODE_ENV === 'development') { console.warn('focus-trap-js: parent element is not defined'); } return false; } if (!parentElem.contains(event.target)) { return false; } var allTabbingElements = getAllTabbingElements(parentElem); var firstFocusableElement = allTabbingElements[0]; var lastFocusableElement = allTabbingElements[allTabbingElements.length - 1]; if (event.shiftKey && event.target === firstFocusableElement) { lastFocusableElement.focus(); event.preventDefault(); return true; } else if (!event.shiftKey && event.target === lastFocusableElement) { firstFocusableElement.focus(); event.preventDefault(); return true; } return false; } function getTabindex(node) { var tabindexAttr = parseInt(node.getAttribute('tabindex'), 10); if (!isNaN(tabindexAttr)) return tabindexAttr; // Browsers do not return tabIndex correctly for contentEditable nodes; // so if they don't have a tabindex attribute specifically set, assume it's 0. if (isContentEditable(node)) return 0; return node.tabIndex; } function isContentEditable(node) { return node.getAttribute('contentEditable'); } var FocusTrap = function FocusTrap(_ref) { var container = _ref.container; var refLastFocus = useRef(); /** * Handle focus lock on the modal */ useEffect(function () { var handleKeyEvent = function handleKeyEvent(event) { if (container === null || container === void 0 ? void 0 : container.current) { tabTrappingKey(event, container.current); } }; if (isBrowser) { document.addEventListener('keydown', handleKeyEvent); } // On mount we focus on the first focusable element in the modal if there is one if (isBrowser && (container === null || container === void 0 ? void 0 : container.current)) { var allTabbingElements = getAllTabbingElements(container.current); if (allTabbingElements[0]) { // First we save the last focused element // only if it's a focusable element if (candidateSelectors.findIndex(function (selector) { var _document$activeEleme; return (_document$activeEleme = document.activeElement) === null || _document$activeEleme === void 0 ? void 0 : _document$activeEleme.matches(selector); }) !== -1) { refLastFocus.current = document.activeElement; } allTabbingElements[0].focus(); } } return function () { if (isBrowser) { var _refLastFocus$current; document.removeEventListener('keydown', handleKeyEvent); // On unmount we restore the focus to the last focused element (_refLastFocus$current = refLastFocus.current) === null || _refLastFocus$current === void 0 ? void 0 : _refLastFocus$current.focus(); } }; }, [container]); return null; }; var classes = { overlay: 'react-responsive-modal-overlay', modal: 'react-responsive-modal-modal', modalCenter: 'react-responsive-modal-modalCenter', closeButton: 'react-responsive-modal-closeButton', animationIn: 'react-responsive-modal-fadeIn', animationOut: 'react-responsive-modal-fadeOut' }; var Modal = function Modal(_ref) { var _classNames$animation, _classNames$animation2; var open = _ref.open, center = _ref.center, _ref$blockScroll = _ref.blockScroll, blockScroll = _ref$blockScroll === void 0 ? true : _ref$blockScroll, _ref$closeOnEsc = _ref.closeOnEsc, closeOnEsc = _ref$closeOnEsc === void 0 ? true : _ref$closeOnEsc, _ref$closeOnOverlayCl = _ref.closeOnOverlayClick, closeOnOverlayClick = _ref$closeOnOverlayCl === void 0 ? true : _ref$closeOnOverlayCl, container = _ref.container, _ref$showCloseIcon = _ref.showCloseIcon, showCloseIcon = _ref$showCloseIcon === void 0 ? true : _ref$showCloseIcon, closeIconId = _ref.closeIconId, closeIcon = _ref.closeIcon, _ref$focusTrapped = _ref.focusTrapped, focusTrapped = _ref$focusTrapped === void 0 ? true : _ref$focusTrapped, _ref$animationDuratio = _ref.animationDuration, animationDuration = _ref$animationDuratio === void 0 ? 500 : _ref$animationDuratio, classNames = _ref.classNames, styles = _ref.styles, _ref$role = _ref.role, role = _ref$role === void 0 ? 'dialog' : _ref$role, ariaDescribedby = _ref.ariaDescribedby, ariaLabelledby = _ref.ariaLabelledby, modalId = _ref.modalId, onClose = _ref.onClose, onEscKeyDown = _ref.onEscKeyDown, onOverlayClick = _ref.onOverlayClick, onAnimationEnd = _ref.onAnimationEnd, children = _ref.children; var refModal = useRef(null); var refShouldClose = useRef(null); var refContainer = useRef(null); // Lazily create the ref instance // https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily if (refContainer.current === null && isBrowser) { refContainer.current = document.createElement('div'); } var _useState = useState(open), showPortal = _useState[0], setShowPortal = _useState[1]; var handleOpen = function handleOpen() { modalManager.add(refContainer.current); if (blockScroll) { blockNoScroll(); } if (refContainer.current && !container && !document.body.contains(refContainer.current)) { document.body.appendChild(refContainer.current); } document.addEventListener('keydown', handleKeydown); }; var handleClose = function handleClose() { modalManager.remove(refContainer.current); if (blockScroll) { unblockNoScroll(); } if (refContainer.current && !container && document.body.contains(refContainer.current)) { document.body.removeChild(refContainer.current); } document.removeEventListener('keydown', handleKeydown); }; var handleKeydown = function handleKeydown(event) { // Only the last modal need to be escaped when pressing the esc key if (event.keyCode !== 27 || !modalManager.isTopModal(refContainer.current)) { return; } if (onEscKeyDown) { onEscKeyDown(event); } if (closeOnEsc) { onClose(); } }; useEffect(function () { // When the modal is rendered first time we want to block the scroll if (open) { handleOpen(); } return function () { // When the component is unmounted directly we want to unblock the scroll if (showPortal) { handleClose(); } }; }, []); useEffect(function () { // If the open prop is changing, we need to open the modal if (open && !showPortal) { setShowPortal(true); handleOpen(); } }, [open]); var handleClickOverlay = function handleClickOverlay(event) { if (refShouldClose.current === null) { refShouldClose.current = true; } if (!refShouldClose.current) { refShouldClose.current = null; return; } if (onOverlayClick) { onOverlayClick(event); } if (closeOnOverlayClick) { onClose(); } refShouldClose.current = null; }; var handleModalEvent = function handleModalEvent() { refShouldClose.current = false; }; var handleClickCloseIcon = function handleClickCloseIcon() { onClose(); }; var handleAnimationEnd = function handleAnimationEnd() { if (!open) { setShowPortal(false); handleClose(); } if (blockScroll) { unblockNoScroll(); } if (onAnimationEnd) { onAnimationEnd(); } }; return showPortal ? ReactDom.createPortal(React.createElement("div", { style: _extends({ 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" }, styles === null || styles === void 0 ? void 0 : styles.overlay), className: cx(classes.overlay, classNames === null || classNames === void 0 ? void 0 : classNames.overlay), onClick: handleClickOverlay, onAnimationEnd: handleAnimationEnd, "data-testid": "overlay" }, React.createElement("div", { ref: refModal, className: cx(classes.modal, center && classes.modalCenter, classNames === null || classNames === void 0 ? void 0 : classNames.modal), style: styles === null || styles === void 0 ? void 0 : styles.modal, onMouseDown: handleModalEvent, onMouseUp: handleModalEvent, onClick: handleModalEvent, id: modalId, role: role, "aria-modal": "true", "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, "data-testid": "modal" }, focusTrapped && React.createElement(FocusTrap, { container: refModal }), children, showCloseIcon && React.createElement(CloseIcon, { classes: classes, classNames: classNames, styles: styles, closeIcon: closeIcon, onClickCloseIcon: handleClickCloseIcon, id: closeIconId }))), container || refContainer.current) : null; }; export default Modal; export { Modal }; //# sourceMappingURL=react-responsive-modal.esm.js.map