import useRelativePosition from "@/app/hooks/useRelativePosition"; import { RefObject, useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; const ArrowIcon = ({ sibling }: { sibling: RefObject }) => { const [color, setColor] = useState(""); useEffect(() => { if (sibling.current) { const { backgroundColor } = window.getComputedStyle(sibling.current); setColor(backgroundColor); } }, []); return ( ); }; const baseZIndex = 100; const popoverRootName = "popoverRoot"; let popoverRoot = document.querySelector( `#${popoverRootName}`, ) as HTMLDivElement; if (!popoverRoot) { popoverRoot = document.createElement("div"); document.body.appendChild(popoverRoot); popoverRoot.style.height = "0px"; popoverRoot.style.width = "100%"; popoverRoot.style.position = "fixed"; popoverRoot.style.bottom = "0"; popoverRoot.style.zIndex = "100"; popoverRoot.id = "popover-root"; } export interface PopoverProps { content?: JSX.Element | string; children?: JSX.Element; show?: boolean; onShow?: (v: boolean) => void; className?: string; popoverClassName?: string; trigger?: "hover" | "click"; placement?: "t" | "lt" | "rt" | "lb" | "rb" | "b" | "l" | "r"; noArrow?: boolean; delayClose?: number; useGlobalRoot?: boolean; } export default function Popover(props: PopoverProps) { const { content, children, show, onShow, className, popoverClassName, trigger = "hover", placement = "t", noArrow = false, delayClose = 0, useGlobalRoot, } = props; const [internalShow, setShow] = useState(false); const { position, getRelativePosition } = useRelativePosition({ delay: 0, }); const popoverCommonClass = `absolute p-2 box-border`; const mergedShow = show ?? internalShow; const { arrowClassName, placementStyle, placementClassName } = useMemo(() => { const arrowCommonClassName = `${ noArrow ? "hidden" : "" } absolute z-10 left-[50%] translate-x-[calc(-50%)]`; let defaultTopPlacement = true; // when users dont config 't' or 'b' const { distanceToBottomBoundary = 0, distanceToLeftBoundary = 0, distanceToRightBoundary = -10000, distanceToTopBoundary = 0, targetH = 0, targetW = 0, } = position?.poi || {}; if (distanceToBottomBoundary > distanceToTopBoundary) { defaultTopPlacement = false; } const placements = { lt: { placementStyle: { bottom: `calc(${distanceToBottomBoundary + targetH}px + 0.5rem)`, left: `calc(${distanceToLeftBoundary}px - ${targetW * 0.02}px)`, }, arrowClassName: `${arrowCommonClassName} bottom-[calc(100%+0.5rem)] translate-y-[calc(100%)] pb-[0.5rem]`, placementClassName: "bottom-[calc(100%+0.5rem)] left-[calc(-2%)]", }, lb: { placementStyle: { top: `calc(-${distanceToBottomBoundary}px + 0.5rem)`, left: `calc(${distanceToLeftBoundary}px - ${targetW * 0.02}px)`, }, arrowClassName: `${arrowCommonClassName} top-[calc(100%+0.5rem)] translate-y-[calc(-100%)] pt-[0.5rem]`, placementClassName: "top-[calc(100%+0.5rem)] left-[calc(-2%)]", }, rt: { placementStyle: { bottom: `calc(${distanceToBottomBoundary + targetH}px + 0.5rem)`, right: `calc(${distanceToRightBoundary}px - ${targetW * 0.02}px)`, }, arrowClassName: `${arrowCommonClassName} bottom-[calc(100%+0.5rem)] translate-y-[calc(100%)] pb-[0.5rem]`, placementClassName: "bottom-[calc(100%+0.5rem)] right-[calc(-2%)]", }, rb: { placementStyle: { top: `calc(-${distanceToBottomBoundary}px + 0.5rem)`, right: `calc(${distanceToRightBoundary}px - ${targetW * 0.02}px)`, }, arrowClassName: `${arrowCommonClassName} top-[calc(100%+0.5rem)] translate-y-[calc(-100%)] pt-[0.5rem]`, placementClassName: "top-[calc(100%+0.5rem)] right-[calc(-2%)]", }, t: { placementStyle: { bottom: `calc(${distanceToBottomBoundary + targetH}px + 0.5rem)`, left: `calc(${distanceToLeftBoundary + targetW / 2}px`, transform: "translateX(-50%)", }, arrowClassName: `${arrowCommonClassName} bottom-[calc(100%+0.5rem)] translate-y-[calc(100%)] pb-[0.5rem]`, placementClassName: "bottom-[calc(100%+0.5rem)] left-[50%] translate-x-[calc(-50%)]", }, b: { placementStyle: { top: `calc(-${distanceToBottomBoundary}px + 0.5rem)`, left: `calc(${distanceToLeftBoundary + targetW / 2}px`, transform: "translateX(-50%)", }, arrowClassName: `${arrowCommonClassName} top-[calc(100%+0.5rem)] translate-y-[calc(-100%)] pt-[0.5rem]`, placementClassName: "top-[calc(100%+0.5rem)] left-[50%] translate-x-[calc(-50%)]", }, }; const getStyle = () => { if (["l", "r"].includes(placement)) { return placements[ `${placement}${defaultTopPlacement ? "t" : "b"}` as | "lt" | "lb" | "rb" | "rt" ]; } return placements[placement as Exclude]; }; return getStyle(); }, [Object.values(position?.poi || {})]); const popoverRef = useRef(null); const closeTimer = useRef(0); if (trigger === "click") { const handleOpen = (e: { currentTarget: any }) => { clearTimeout(closeTimer.current); onShow?.(true); setShow(true); getRelativePosition(e.currentTarget, ""); window.document.documentElement.style.overflow = "hidden"; }; const handleClose = () => { if (delayClose) { closeTimer.current = window.setTimeout(() => { onShow?.(false); setShow(false); }, delayClose); } else { onShow?.(false); setShow(false); } window.document.documentElement.style.overflow = "auto"; }; return (
{ e.preventDefault(); e.stopPropagation(); if (!mergedShow) { handleOpen(e); } else { handleClose(); } }} > {children} {mergedShow && ( <> {!noArrow && (
)} {createPortal(
{content}
, popoverRoot, )} {createPortal(
{ e.preventDefault(); handleClose(); }} >  
, popoverRoot, )} )}
); } if (useGlobalRoot) { return (
{ e.preventDefault(); clearTimeout(closeTimer.current); onShow?.(true); setShow(true); getRelativePosition(e.currentTarget, ""); window.document.documentElement.style.overflow = "hidden"; }} onPointerLeave={(e) => { e.preventDefault(); if (delayClose) { closeTimer.current = window.setTimeout(() => { onShow?.(false); setShow(false); }, delayClose); } else { onShow?.(false); setShow(false); } window.document.documentElement.style.overflow = "auto"; }} > {children} {mergedShow && ( <>
{createPortal(
{content}
, popoverRoot, )} )}
); } return (
{ getRelativePosition(e.currentTarget, ""); e.preventDefault(); e.stopPropagation(); }} > {children}
{content}
); }