UNPKG

react-responsive-modal

Version:

A simple responsive and accessible react modal

338 lines (329 loc) 12.3 kB
Object.defineProperty(exports, '__esModule', { value: true }); //#region rolldown:runtime var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); //#endregion const react = __toESM(require("react")); const react_dom = __toESM(require("react-dom")); const classnames = __toESM(require("classnames")); const __bedrock_layout_use_forwarded_ref = __toESM(require("@bedrock-layout/use-forwarded-ref")); const react_jsx_runtime = __toESM(require("react/jsx-runtime")); const body_scroll_lock = __toESM(require("body-scroll-lock")); //#region src/CloseIcon.tsx const CloseIcon = ({ classes: classes$1, classNames, styles, id, closeIcon, onClick }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", { id, className: (0, classnames.default)(classes$1.closeButton, classNames?.closeButton), style: styles?.closeButton, onClick, "data-testid": "close-button", children: closeIcon ? closeIcon : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", { className: classNames?.closeIcon, style: styles?.closeIcon, width: 28, height: 28, viewBox: "0 0 36 36", "data-testid": "close-icon", children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("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 CloseIcon_default = CloseIcon; //#endregion //#region src/utils.ts const isBrowser = typeof window !== "undefined"; //#endregion //#region src/lib/focusTrapJs.ts const candidateSelectors = [ "input", "select", "textarea", "a[href]", "button", "[tabindex]", "audio[controls]", "video[controls]", "[contenteditable]:not([contenteditable=\"false\"])" ]; function isHidden(node) { return node.offsetParent === null || getComputedStyle(node).visibility === "hidden"; } function getCheckedRadio(nodes, form) { for (var i = 0; i < nodes.length; i++) if (nodes[i].checked && nodes[i].form === form) return nodes[i]; } function isNotRadioOrTabbableRadio(node) { if (node.tagName !== "INPUT" || node.type !== "radio" || !node.name) return true; var radioScope = node.form || node.ownerDocument; var radioSet = radioScope.querySelectorAll("input[type=\"radio\"][name=\"" + node.name + "\"]"); var checked = getCheckedRadio(radioSet, node.form); return checked === node || checked === void 0 && radioSet[0] === node; } function getAllTabbingElements(parentElem) { var currentActiveElement = document.activeElement; var tabbableNodes = parentElem.querySelectorAll(candidateSelectors.join(",")); var onlyTabbable = []; for (var i = 0; i < tabbableNodes.length; i++) { var node = tabbableNodes[i]; if (currentActiveElement === node || !node.disabled && getTabindex(node) > -1 && !isHidden(node) && isNotRadioOrTabbableRadio(node)) onlyTabbable.push(node); } return onlyTabbable; } function tabTrappingKey(event, parentElem) { 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; if (isContentEditable(node)) return 0; return node.tabIndex; } function isContentEditable(node) { return node.getAttribute("contentEditable"); } //#endregion //#region src/FocusTrap.tsx const FocusTrap = ({ container, initialFocusRef }) => { const refLastFocus = (0, react.useRef)(null); /** * Handle focus lock on the modal */ (0, react.useEffect)(() => { const handleKeyEvent = (event) => { if (container?.current) tabTrappingKey(event, container.current); }; if (isBrowser) document.addEventListener("keydown", handleKeyEvent); if (isBrowser && container?.current) { const savePreviousFocus = () => { if (candidateSelectors.findIndex((selector) => document.activeElement?.matches(selector)) !== -1) refLastFocus.current = document.activeElement; }; if (initialFocusRef) { savePreviousFocus(); requestAnimationFrame(() => { initialFocusRef.current?.focus(); }); } else { const allTabbingElements = getAllTabbingElements(container.current); if (allTabbingElements[0]) { savePreviousFocus(); allTabbingElements[0].focus(); } } } return () => { if (isBrowser) { document.removeEventListener("keydown", handleKeyEvent); refLastFocus.current?.focus(); } }; }, [container, initialFocusRef]); return null; }; //#endregion //#region src/modalManager.ts let modals = []; /** * Handle the order of the modals. * Inspired by the material-ui implementation. */ const modalManager = { add: (newModal) => { modals.push(newModal); }, remove: (oldModal) => { modals = modals.filter((modal) => modal !== oldModal); }, isTopModal: (modal) => !!modals.length && modals[modals.length - 1] === modal }; function useModalManager(ref, open) { (0, react.useEffect)(() => { if (open) modalManager.add(ref); return () => { modalManager.remove(ref); }; }, [open, ref]); } //#endregion //#region src/useScrollLock.ts const useScrollLock = (refModal, open, showPortal, blockScroll, reserveScrollBarGap) => { const oldRef = (0, react.useRef)(null); (0, react.useEffect)(() => { if (open && refModal.current && blockScroll) { oldRef.current = refModal.current; (0, body_scroll_lock.disableBodyScroll)(refModal.current, { reserveScrollBarGap }); } return () => { if (oldRef.current) { (0, body_scroll_lock.enableBodyScroll)(oldRef.current); oldRef.current = null; } }; }, [ open, showPortal, refModal, blockScroll, reserveScrollBarGap ]); }; //#endregion //#region src/index.tsx const classes = { root: "react-responsive-modal-root", overlay: "react-responsive-modal-overlay", overlayAnimationIn: "react-responsive-modal-overlay-in", overlayAnimationOut: "react-responsive-modal-overlay-out", modalContainer: "react-responsive-modal-container", modalContainerCenter: "react-responsive-modal-containerCenter", modal: "react-responsive-modal-modal", modalAnimationIn: "react-responsive-modal-modal-in", modalAnimationOut: "react-responsive-modal-modal-out", closeButton: "react-responsive-modal-closeButton" }; const Modal = react.default.forwardRef(({ open, center, blockScroll = true, closeOnEsc = true, closeOnOverlayClick = true, container, showCloseIcon = true, closeIconId, closeIcon, focusTrapped = true, initialFocusRef = void 0, animationDuration = 300, classNames, styles, role = "dialog", ariaDescribedby, ariaLabelledby, containerId, modalId, onClose, onEscKeyDown, onOverlayClick, onAnimationEnd, children, reserveScrollBarGap }, ref) => { const refDialog = (0, __bedrock_layout_use_forwarded_ref.useForwardedRef)(ref); const refModal = (0, react.useRef)(null); const refShouldClose = (0, react.useRef)(null); const refContainer = (0, react.useRef)(null); if (refContainer.current === null && isBrowser) refContainer.current = document.createElement("div"); const [showPortal, setShowPortal] = (0, react.useState)(false); useModalManager(refModal, open); useScrollLock(refModal, open, showPortal, blockScroll, reserveScrollBarGap); const handleOpen = () => { if (refContainer.current && !container && !document.body.contains(refContainer.current)) document.body.appendChild(refContainer.current); document.addEventListener("keydown", handleKeydown); }; const handleClose = () => { if (refContainer.current && !container && document.body.contains(refContainer.current)) document.body.removeChild(refContainer.current); document.removeEventListener("keydown", handleKeydown); }; const handleKeydown = (event) => { if (event.keyCode !== 27 || !modalManager.isTopModal(refModal)) return; onEscKeyDown?.(event); if (closeOnEsc) onClose(); }; (0, react.useEffect)(() => { return () => { if (showPortal) handleClose(); }; }, [showPortal]); (0, react.useEffect)(() => { if (open && !showPortal) { setShowPortal(true); handleOpen(); } }, [open]); const handleClickOverlay = (event) => { if (refShouldClose.current === null) refShouldClose.current = true; if (!refShouldClose.current) { refShouldClose.current = null; return; } onOverlayClick?.(event); if (closeOnOverlayClick) onClose(); refShouldClose.current = null; }; const handleModalEvent = () => { refShouldClose.current = false; }; const handleAnimationEnd = () => { if (!open) setShowPortal(false); onAnimationEnd?.(); }; const containerModal = container || refContainer.current; const overlayAnimation = open ? classNames?.overlayAnimationIn ?? classes.overlayAnimationIn : classNames?.overlayAnimationOut ?? classes.overlayAnimationOut; const modalAnimation = open ? classNames?.modalAnimationIn ?? classes.modalAnimationIn : classNames?.modalAnimationOut ?? classes.modalAnimationOut; return showPortal && containerModal ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: (0, classnames.default)(classes.root, classNames?.root), style: styles?.root, "data-testid": "root", children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: (0, classnames.default)(classes.overlay, classNames?.overlay), "data-testid": "overlay", "aria-hidden": true, style: { animation: `${overlayAnimation} ${animationDuration}ms`, ...styles?.overlay } }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { ref: refModal, id: containerId, className: (0, classnames.default)(classes.modalContainer, center && classes.modalContainerCenter, classNames?.modalContainer), style: styles?.modalContainer, "data-testid": "modal-container", onClick: handleClickOverlay, children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { ref: refDialog, className: (0, classnames.default)(classes.modal, classNames?.modal), style: { animation: `${modalAnimation} ${animationDuration}ms`, ...styles?.modal }, onMouseDown: handleModalEvent, onMouseUp: handleModalEvent, onClick: handleModalEvent, onAnimationEnd: handleAnimationEnd, id: modalId, role, "aria-modal": "true", "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, "data-testid": "modal", tabIndex: -1, children: [ focusTrapped && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FocusTrap, { container: refDialog, initialFocusRef }), children, showCloseIcon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CloseIcon_default, { classes, classNames, styles, closeIcon, onClick: onClose, id: closeIconId }) ] }) })] }), containerModal) : null; }); var src_default = Modal; //#endregion exports.Modal = Modal; exports.default = src_default; //# sourceMappingURL=index.cjs.map