UNPKG

react-micro-modal

Version:
271 lines (257 loc) 12.5 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react'), require('react-dom')) : typeof define === 'function' && define.amd ? define(['exports', 'react', 'react-dom'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ReactMicroModal = {}, global.React, global.ReactDOM)); }(this, (function (exports, React, reactDom) { 'use strict'; function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var React__default = /*#__PURE__*/_interopDefaultLegacy(React); /*! ***************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; } const FOCUSABLE_SELETORS = [ 'a[href]', 'area[href]', 'input:not([disabled]):not([type="hidden"]):not([aria-hidden])', 'select:not([disabled]):not([aria-hidden])', 'textarea:not([disabled]):not([aria-hidden])', 'button:not([disabled]):not([aria-hidden])', 'iframe', 'object', 'embed', '[contenteditable]', '[tabindex]:not([tabindex^="-"])', ]; const getFocusableNodes = (element) => { return Object.values(element.querySelectorAll(FOCUSABLE_SELETORS)); }; const focusFirstNode = (element) => { const focusableNodes = getFocusableNodes(element); let focusedElement; if (focusableNodes.length) { [focusedElement] = focusableNodes; focusedElement.focus(); } return focusedElement; }; const handleTabPress = (element, event) => { const focusableNodes = getFocusableNodes(element); if (!focusableNodes.length) { return undefined; } const focusedElement = focusableNodes[0]; if (!element.contains(document.activeElement)) { focusedElement.focus(); event.preventDefault(); } else { const focusedItemIndex = focusableNodes.indexOf(document.activeElement); if (event.shiftKey && focusedItemIndex === 0) { focusableNodes[focusableNodes.length - 1].focus(); event.preventDefault(); } if (!event.shiftKey && focusedItemIndex === focusableNodes.length - 1) { focusedElement.focus(); event.preventDefault(); } return focusableNodes[focusedItemIndex]; } return focusedElement; }; const BASE_CLASS_NAME = 'react-micro-modal'; const PORTAL_CLASS_NAME = `${BASE_CLASS_NAME}--portal`; const OVERLAY_BASE_STYLE = { position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0, 0, 0, 0.6)', display: 'flex', justifyContent: 'center', alignItems: 'center', }; const DIALOG_BASE_STYLE = { backgroundColor: '#fff', padding: '30px', maxWidth: '500px', maxHeight: '100vh', borderRadius: '4px', overflowY: 'auto', boxSizing: 'border-box', }; const createPortalRoot = () => { const root = document.createElement('div'); root.className = PORTAL_CLASS_NAME; return root; }; const ModalPortal = ({ parent, children }) => { const node = React.useMemo(() => createPortalRoot(), []); const getParent = () => { var _a; return (_a = parent === null || parent === void 0 ? void 0 : parent()) !== null && _a !== void 0 ? _a : document.body; }; React.useEffect(() => { getParent().appendChild(node); return () => { getParent().removeChild(node); }; }, []); return reactDom.createPortal(children, node); }; const ESCAPE_KEY = 'Escape'; const TAB_KEY = 'Tab'; const getOverlayAnimationName = (ariaHidden) => { return ariaHidden === 'false' ? 'modal-fade-in' : 'modal-fade-out'; }; const getDialogAnimationName = (ariaHidden) => { return ariaHidden === 'false' ? 'modal-slide-in' : 'modal-slide-out'; }; const openContainerRefStack = []; function getLastOpenContainer() { return openContainerRefStack[openContainerRefStack.length - 1]; } const MicroModal = (_a) => { var { trigger, children, open: isOpenParam, handleClose: handleCloseParam, parent: parentSelector, openInitially, closeOnAnimationEnd, closeOnEscapePress = true, closeOnOverlayClick = true, disableFirstElementFocus } = _a, _b = _a.overrides, _c = _b === void 0 ? {} : _b, _d = _c.Root, _e = _d === void 0 ? { style: {} } : _d, { style: rootStyleOverrides } = _e, rootOverrides = __rest(_e, ["style"]), _f = _c.Overlay, _g = _f === void 0 ? { style: {}, className: '', } : _f, { style: overlayStyleOverrides } = _g, overlayOverrides = __rest(_g, ["style"]), _h = _c.Dialog, _j = _h === void 0 ? { style: {}, } : _h, { style: dialogStyleOverrides } = _j, dialogOverrides = __rest(_j, ["style"]); const rootRef = React.useRef(null); const dialogRef = React.useRef(null); const lastActiveElement = React.useRef(null); const [isModalOpen, setIsModalOpen] = React.useState(openInitially !== null && openInitially !== void 0 ? openInitially : false); const [isClosing, setIsClosing] = React.useState(false); const ariaHidden = isModalOpen && !isClosing ? 'false' : 'true'; const isControlled = React.useMemo(() => isOpenParam !== undefined, [isOpenParam]); const isMounted = React.useRef(false); const open = React.useCallback(() => { setIsModalOpen(true); setIsClosing(false); }, [setIsModalOpen, setIsClosing]); const close = React.useCallback(() => { setIsModalOpen(false); setIsClosing(false); }, [setIsModalOpen, setIsClosing]); const closeOrStartAnimating = React.useCallback(() => { if (closeOnAnimationEnd) { setIsClosing(true); } else { close(); } }, [close, closeOnAnimationEnd]); const handleClose = React.useCallback(() => { if (isControlled) { if (handleCloseParam) { handleCloseParam(); } else { // eslint-disable-next-line no-console console.warn('[react-micro-modal]: cannot close modal -- handleClose prop is required in controlled mode'); } } else { closeOrStartAnimating(); } }, [isControlled, handleCloseParam, closeOrStartAnimating]); const onKeydown = React.useCallback((event) => { if (dialogRef === getLastOpenContainer()) { if (event.key === ESCAPE_KEY && closeOnEscapePress) { event.stopPropagation(); handleClose(); } if (event.key === TAB_KEY && dialogRef.current) { handleTabPress(dialogRef.current, event); } } }, [handleClose]); const onClick = React.useCallback((event) => { if (event.target && dialogRef.current && !dialogRef.current.contains(event.target)) { handleClose(); event.preventDefault(); } }, [handleClose]); // Controlled component React.useEffect(() => { if (isMounted.current) { if (isOpenParam === true) { open(); } else if (isOpenParam === false) { closeOrStartAnimating(); } } else { isMounted.current = true; } }, [isOpenParam]); // Animate React.useEffect(() => { const containerElement = dialogRef.current; if (isClosing && containerElement) { containerElement.addEventListener('animationend', function handler() { close(); containerElement.removeEventListener('animationend', handler, false); }); } }, [isClosing]); // Stack & focus React.useEffect(() => { var _a, _b; if (isModalOpen) { lastActiveElement.current = document.activeElement; openContainerRefStack.push(dialogRef); document.addEventListener('keydown', onKeydown); if (closeOnOverlayClick) { (_a = rootRef.current) === null || _a === void 0 ? void 0 : _a.addEventListener('click', onClick); } if (!disableFirstElementFocus && dialogRef.current) { focusFirstNode(dialogRef.current); } } else { document.removeEventListener('keydown', onKeydown); if (closeOnOverlayClick) { (_b = rootRef.current) === null || _b === void 0 ? void 0 : _b.removeEventListener('click', onClick); } openContainerRefStack.pop(); if (lastActiveElement.current) { lastActiveElement.current.focus(); lastActiveElement.current = null; } } }, [isModalOpen]); return (React__default['default'].createElement(React__default['default'].Fragment, null, trigger !== undefined && trigger(open), React__default['default'].createElement(ModalPortal, { parent: parentSelector }, React__default['default'].createElement("div", Object.assign({ ref: rootRef, "aria-hidden": ariaHidden, style: Object.assign({ display: isModalOpen ? 'block' : 'none' }, rootStyleOverrides) }, rootOverrides), React__default['default'].createElement("div", Object.assign({ style: Object.assign(Object.assign(Object.assign({}, OVERLAY_BASE_STYLE), { animation: `${getOverlayAnimationName(ariaHidden)} 0.3s cubic-bezier(0, 0, 0.2, 1)` }), overlayStyleOverrides) }, overlayOverrides), React__default['default'].createElement("div", Object.assign({ ref: dialogRef, role: "dialog", "aria-modal": "true", style: Object.assign(Object.assign(Object.assign({}, DIALOG_BASE_STYLE), { animation: `${getDialogAnimationName(ariaHidden)} 0.3s cubic-bezier(0, 0, 0.2, 1)` }), dialogStyleOverrides) }, dialogOverrides), isModalOpen ? children(handleClose) : null)))))); }; exports.default = MicroModal; Object.defineProperty(exports, '__esModule', { value: true }); }))); //# sourceMappingURL=index.umd.development.js.map