UNPKG

@nex-ui/react

Version:

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

196 lines (193 loc) • 6.98 kB
"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 };