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
JavaScript
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