UNPKG

@nex-ui/react

Version:

🎉 A beautiful, modern, and reliable React component library.

212 lines (208 loc) • 7.5 kB
"use client"; 'use strict'; var jsxRuntime = require('react/jsx-runtime'); var utils = require('@nex-ui/utils'); var system = require('@nex-ui/system'); var react = require('react'); var hooks = require('@nex-ui/hooks'); var ModalContext = require('./ModalContext.cjs'); var ModalManager = require('./ModalManager.cjs'); var useSlot = require('../utils/useSlot.cjs'); var PresenceMotion = require('../utils/PresenceMotion.cjs'); var Portal = require('../utils/portal/Portal.cjs'); const recipe = system.defineRecipe({ base: { position: 'fixed', inset: 0, zIndex: 'modal' } }); const style = recipe(); const ModalRoot = (inProps)=>{ const { children, ...props } = inProps; const rootRef = react.useRef(null); const modalId = react.useId(); const modalState = ModalContext.useModal(); const modalManager = ModalManager.useModalManager(); const registeredRef = react.useRef(false); const resolver = react.useRef(undefined); const { open, container, keepMounted, setOpen, closeOnEscape, preventScroll } = modalState; if (registeredRef.current === false && open) { modalManager.register(modalId); registeredRef.current = true; } const [Motion, getMotionProps] = useSlot.useSlot({ style, elementType: PresenceMotion.PresenceMotion, externalForwardedProps: props, shouldForwardComponent: false, additionalProps: { open, keepMounted, ref: rootRef, onAnimationComplete: (animation)=>{ if (animation === 'hidden') { resolver.current?.(); } } }, a11y: { // Ignore the user's settings to ensure proper access for assistive technologies. 'aria-hidden': open ? undefined : 'true' }, dataAttrs: { state: open ? 'open' : 'closed', keepMounted } }); const isTopmostModal = hooks.useEvent(()=>modalManager.isTopmostModal(modalId)); // Portal renders asynchronously. const handlePortalMount = ()=>{ if (open && rootRef.current && isTopmostModal()) { modalManager.mount(modalId); } }; const ctx = react.useMemo(()=>({ ...modalState, isTopmostModal: isTopmostModal }), [ modalState, isTopmostModal ]); react.useEffect(()=>{ if (!open || !closeOnEscape) { return; } const doc = utils.ownerDocument(rootRef.current); const removeListener = utils.addEventListener(doc.body, 'keyup', (e)=>{ if (open && e.key === 'Escape' && isTopmostModal()) { setOpen(false); e.stopPropagation(); } }); return removeListener; }, [ closeOnEscape, isTopmostModal, open, setOpen ]); react.useEffect(()=>{ if (open) { const unsubscribe = modalManager.subscribe(()=>{ const ariaHidden = rootRef.current?.getAttribute('aria-hidden'); if (isTopmostModal()) { if (ariaHidden === 'false' || ariaHidden === null) { return; } rootRef.current?.removeAttribute('aria-hidden'); } else { if (ariaHidden === 'true') { return; } rootRef.current?.setAttribute('aria-hidden', 'true'); } }); return ()=>{ unsubscribe(); }; } }, [ modalManager, open, isTopmostModal ]); react.useEffect(()=>{ if (open) { const doc = utils.ownerDocument(rootRef.current); let prevOverflow = null; let prevOverflowX = null; let prevOverflowY = null; let prevPaddingRight = null; const resolvedContainer = (utils.isFunction(container) ? container() : container) || doc.body; const unsubscribe = modalManager.subscribe(()=>{ // Store overflow and paddingRight only if the modal first mounts. if (preventScroll && prevOverflow === null && isTopmostModal()) { // Is vertical scrollbar displayed? if (isOverflowing(resolvedContainer)) { // Avoid scroll content jumping. const scrollBarWidth = getScrollBarWidth(resolvedContainer); prevPaddingRight = resolvedContainer.style.paddingRight; resolvedContainer.style.paddingRight = `${parseInt(prevPaddingRight || '0', 10) + scrollBarWidth}px`; } prevOverflow = resolvedContainer.style.overflow; prevOverflowX = resolvedContainer.style.overflowX; prevOverflowY = resolvedContainer.style.overflowY; resolvedContainer.style.overflow = 'hidden'; } }); return ()=>{ const { promise, resolve } = Promise.withResolvers(); resolver.current = resolve; promise.then(()=>{ if (prevOverflow !== null) { resolvedContainer.style.overflow = prevOverflow; resolvedContainer.style.overflowX = prevOverflowX; resolvedContainer.style.overflowY = prevOverflowY; } if (prevPaddingRight !== null) { resolvedContainer.style.paddingRight = prevPaddingRight; } }); unsubscribe(); }; } }, [ container, isTopmostModal, modalManager, open, preventScroll ]); react.useEffect(()=>{ if (open) { if (rootRef.current && isTopmostModal()) { modalManager.mount(modalId); } return ()=>{ modalManager.unregister(modalId); registeredRef.current = false; }; } }, [ isTopmostModal, modalId, modalManager, open ]); return /*#__PURE__*/ jsxRuntime.jsx(Portal.Portal, { container: container, onMount: handlePortalMount, children: /*#__PURE__*/ jsxRuntime.jsx(ModalContext.ModalProvider, { value: ctx, children: /*#__PURE__*/ jsxRuntime.jsx(Motion, { ...getMotionProps(), children: children }) }) }); }; // Is a vertical scrollbar displayed? function isOverflowing(container) { const doc = utils.ownerDocument(container); if (doc.body === container) { return utils.ownerWindow(container).innerWidth > doc.documentElement.clientWidth; } return container.scrollHeight > container.clientHeight; } function getScrollBarWidth(container) { const doc = utils.ownerDocument(container); const win = utils.ownerWindow(container); if (doc.body === container) { return Math.max(0, win.innerWidth - doc.documentElement.clientWidth); } return container.offsetWidth - container.clientWidth; } ModalRoot.displayName = 'ModalRoot'; exports.ModalRoot = ModalRoot; exports.getScrollBarWidth = getScrollBarWidth;