@nex-ui/react
Version:
🎉 A beautiful, modern, and reliable React component library.
196 lines (193 loc) • 6.98 kB
JavaScript
"use client";
import { jsx } from 'react/jsx-runtime';
import { defineRecipe } from '@nex-ui/system';
import { useState, useRef, useEffect } from 'react';
import { useEvent } from '@nex-ui/hooks';
import { addEventListener, ownerWindow } from '@nex-ui/utils';
import { usePopper } from './PopperContext.mjs';
import { useSlot } from '../utils/useSlot.mjs';
import { computePosition } from '../utils/computePosition/computePosition.mjs';
import { getOverflowAncestors } from '../utils/computePosition/getOverflowAncestors.mjs';
import { Portal } from '../utils/portal/Portal.mjs';
import { PresenceMotion } from '../utils/PresenceMotion.mjs';
const recipe = defineRecipe({
base: {
pos: 'absolute',
w: 'max-content',
left: 'var(--popper-x)',
top: 'var(--popper-y)'
}
});
const style = recipe();
const PopperRoot = (inProps)=>{
const { open, referenceRef, setOpen, popperRootRef } = usePopper();
const { children, container, flip = {
mainAxis: true,
crossAxis: true
}, offset = 5, shift = true, keepMounted = false, closeOnDetached = true, closeOnEscape = true, placement = 'top', ...props } = inProps;
const [styleVariables, setStyleVariables] = useState(undefined);
const unsubscribeRef = useRef(undefined);
// To avoid multiple calculations on the initial render,
// because ResizeObserver is triggered when observing starts.
const initialRender = useRef(true);
// Portal renders asynchronously. Use this variable to avoid multiple handler registrations.
const initialized = useRef(false);
const [PopperMotion, getPopperMotionProps] = useSlot({
style,
elementType: PresenceMotion,
externalForwardedProps: props,
shouldForwardComponent: false,
additionalProps: {
open,
keepMounted,
ref: popperRootRef,
style: styleVariables
},
a11y: {
'aria-hidden': open ? undefined : true
},
dataAttrs: {
placement,
keepMounted,
closeOnEscape,
state: open ? 'open' : 'closed'
}
});
const setPosition = useEvent(()=>{
// istanbul ignore if
if (!referenceRef.current || !popperRootRef.current) return;
const { x, y } = computePosition(referenceRef.current, popperRootRef.current, {
placement,
offset,
flip,
shift
});
const newStyleVars = {
'--popper-x': x + 'px',
'--popper-y': y + 'px'
};
setStyleVariables(newStyleVars);
});
// istanbul ignore next
const resetPosition = useEvent(()=>{
if (!referenceRef.current || !popperRootRef.current) {
setStyleVariables(undefined);
return;
}
setPosition();
});
// istanbul ignore next
const observeReferenceIntersection = useEvent(()=>{
// istanbul ignore if
if (!referenceRef.current || !closeOnDetached) return;
function handleIntersect(entries) {
entries.forEach((entry)=>{
if (!entry.isIntersecting) {
setOpen(false);
}
});
}
const observer = new IntersectionObserver(handleIntersect);
observer.observe(referenceRef.current);
return ()=>{
observer.disconnect();
};
});
// istanbul ignore next
const observeElementResizeChanges = useEvent(()=>{
// istanbul ignore if
if (!referenceRef.current || !popperRootRef.current) return;
const resizeObserver = new ResizeObserver((entries)=>{
if (initialRender.current) {
initialRender.current = false;
return;
}
for (const entry of entries){
if (entry.target === referenceRef.current || entry.target === popperRootRef.current) {
resetPosition();
}
}
});
resizeObserver.observe(referenceRef.current);
resizeObserver.observe(popperRootRef.current);
return ()=>{
resizeObserver.disconnect();
initialRender.current = true;
};
});
// istanbul ignore next
const subscribeAncestorScrollEvents = useEvent(()=>{
if (!popperRootRef.current) return;
const ancestors = getOverflowAncestors(popperRootRef.current);
const unsubscribeScroll = ancestors.map((ancestor)=>{
return addEventListener(ancestor, 'scroll', resetPosition);
});
return ()=>{
unsubscribeScroll.forEach((unsub)=>unsub());
};
});
const subscribeWindowResizeEvent = useEvent(()=>{
const win = ownerWindow(referenceRef.current);
return addEventListener(win, 'resize', resetPosition);
});
const subscribeEscapeEvent = useEvent(()=>{
if (!closeOnEscape || !open) return;
const win = ownerWindow(referenceRef.current);
return addEventListener(win, 'keydown', (e)=>{
if (e.key === 'Escape') {
setOpen(false);
}
});
});
const handleMount = useEvent(()=>{
// istanbul ignore if
if (initialized.current) return;
setPosition();
const unobserveElementResizeChanges = observeElementResizeChanges();
const unsubscribeAncestorScrollEvents = subscribeAncestorScrollEvents();
const unsubscribeWindowResizeEvent = subscribeWindowResizeEvent();
const unobserveReferenceIntersection = observeReferenceIntersection();
const unsubscribeEscapeEvent = subscribeEscapeEvent();
unsubscribeRef.current = ()=>{
unobserveElementResizeChanges?.();
unsubscribeAncestorScrollEvents?.();
unsubscribeWindowResizeEvent?.();
unobserveReferenceIntersection?.();
unsubscribeEscapeEvent?.();
};
initialized.current = true;
});
const handleUnmount = useEvent(()=>{
if (initialized.current === false) return;
unsubscribeRef.current?.();
initialized.current = false;
});
const onMount = useEvent(()=>{
// Mainly handle the case where open defaults to true.
if (open) handleMount();
});
const onUnmount = useEvent(()=>{
handleUnmount();
});
useEffect(()=>{
if (!open || !popperRootRef.current) return;
handleMount();
return handleUnmount;
}, [
handleMount,
handleUnmount,
open,
popperRootRef
]);
return /*#__PURE__*/ jsx(Portal, {
onMount: onMount,
onUnmount: onUnmount,
container: container,
children: /*#__PURE__*/ jsx(PopperMotion, {
...getPopperMotionProps(),
children: children
})
});
};
PopperRoot.displayName = 'PopperRoot';
export { PopperRoot };