@nex-ui/react
Version:
🎉 A beautiful, modern, and reliable React component library.
209 lines (206 loc) • 7.34 kB
JavaScript
"use client";
import { jsx } from 'react/jsx-runtime';
import { ownerDocument, addEventListener, isFunction, ownerWindow } from '@nex-ui/utils';
import { defineRecipe } from '@nex-ui/system';
import { useRef, useId, useMemo, useEffect } from 'react';
import { useEvent } from '@nex-ui/hooks';
import { useModal, ModalProvider } from './ModalContext.mjs';
import { useModalManager } from './ModalManager.mjs';
import { useSlot } from '../utils/useSlot.mjs';
import { PresenceMotion } from '../utils/PresenceMotion.mjs';
import { Portal } from '../utils/portal/Portal.mjs';
const recipe = defineRecipe({
base: {
position: 'fixed',
inset: 0,
zIndex: 'modal'
}
});
const style = recipe();
const ModalRoot = (inProps)=>{
const { children, ...props } = inProps;
const rootRef = useRef(null);
const modalId = useId();
const modalState = useModal();
const modalManager = useModalManager();
const registeredRef = useRef(false);
const resolver = 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({
style,
elementType: 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 = useEvent(()=>modalManager.isTopmostModal(modalId));
// Portal renders asynchronously.
const handlePortalMount = ()=>{
if (open && rootRef.current && isTopmostModal()) {
modalManager.mount(modalId);
}
};
const ctx = useMemo(()=>({
...modalState,
isTopmostModal: isTopmostModal
}), [
modalState,
isTopmostModal
]);
useEffect(()=>{
if (!open || !closeOnEscape) {
return;
}
const doc = ownerDocument(rootRef.current);
const removeListener = addEventListener(doc.body, 'keyup', (e)=>{
if (open && e.key === 'Escape' && isTopmostModal()) {
setOpen(false);
e.stopPropagation();
}
});
return removeListener;
}, [
closeOnEscape,
isTopmostModal,
open,
setOpen
]);
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
]);
useEffect(()=>{
if (open) {
const doc = ownerDocument(rootRef.current);
let prevOverflow = null;
let prevOverflowX = null;
let prevOverflowY = null;
let prevPaddingRight = null;
const resolvedContainer = (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
]);
useEffect(()=>{
if (open) {
if (rootRef.current && isTopmostModal()) {
modalManager.mount(modalId);
}
return ()=>{
modalManager.unregister(modalId);
registeredRef.current = false;
};
}
}, [
isTopmostModal,
modalId,
modalManager,
open
]);
return /*#__PURE__*/ jsx(Portal, {
container: container,
onMount: handlePortalMount,
children: /*#__PURE__*/ jsx(ModalProvider, {
value: ctx,
children: /*#__PURE__*/ jsx(Motion, {
...getMotionProps(),
children: children
})
})
});
};
// Is a vertical scrollbar displayed?
function isOverflowing(container) {
const doc = ownerDocument(container);
if (doc.body === container) {
return ownerWindow(container).innerWidth > doc.documentElement.clientWidth;
}
return container.scrollHeight > container.clientHeight;
}
function getScrollBarWidth(container) {
const doc = ownerDocument(container);
const win = ownerWindow(container);
if (doc.body === container) {
return Math.max(0, win.innerWidth - doc.documentElement.clientWidth);
}
return container.offsetWidth - container.clientWidth;
}
ModalRoot.displayName = 'ModalRoot';
export { ModalRoot, getScrollBarWidth };