import { Dispatch, FC, PropsWithChildren, SetStateAction, useCallback, useState, } from "react"; import { bulkRound } from "../../utils/bulkRound"; import { clamp } from "../../utils/clamp"; import { Portal } from "../../portal/components"; type edge = "top" | "bottom" | "left" | "right"; type alignment = edge | "centered"; interface IProps { preferredEdge: edge; preferredAlign: alignment; relativeElement: HTMLElement; spacing?: number; isClosing: boolean; isClosed: boolean; setHover: Dispatch>; } type position = { top: number; left: number; width?: number; }; export const PoppableContent: FC> = ( { preferredAlign, preferredEdge, children, relativeElement, spacing = 10, setHover, isClosing, }, ) => { const [popRef, setPopRef] = useState(); const updateRef = useCallback((node: HTMLDivElement) => { if (!node) return; setPopRef(node); }, []); const getAlignment = useCallback( ( relX: number, relY: number, relWidth: number, relHeight: number, popWidth: number, popHeight: number, _edge: edge, align: alignment, ): position => { const pos = { top: relY, left: relX, }; switch (align) { case "centered": pos.top = relY + (relHeight / 2) - (popHeight / 2); pos.left = relX + (relWidth / 2) - (popWidth / 2); break; case "top": pos.top = relY; break; case "bottom": pos.top = relY + relHeight - popHeight; break; case "left": pos.left = relX; break; case "right": pos.left = relX + relWidth - popWidth; break; } return pos; }, [], ); const getPosition = useCallback( (popWidth: number, popHeight: number, edge: edge, align: alignment) => { const rel = relativeElement.getBoundingClientRect(); const [relX, relY, relWidth, relHeight] = bulkRound( rel.x, rel.y, rel.width, rel.height, ); const pos: position = { top: 100, left: 100 }; const alignment = getAlignment( relX, relY, relWidth, relHeight, popWidth, popHeight, edge, align, ); switch (edge) { case "top": pos.top = relY - popHeight - spacing + document.documentElement.scrollTop; pos.left = alignment.left; break; case "bottom": pos.top = relY + relHeight + spacing + document.documentElement.scrollTop; pos.left = alignment.left; break; case "left": pos.left = relX - popWidth - spacing; pos.top = alignment.top + document.documentElement.scrollTop; break; case "right": pos.left = relX + relWidth + spacing; pos.top = alignment.top + document.documentElement.scrollTop; break; } return pos; }, [getAlignment, relativeElement, spacing], ); const getClampedPosition = useCallback(() => { if (!popRef) return { opacity: 0 }; const pop = popRef.getBoundingClientRect(); const [popWidth, popHeight] = bulkRound(pop.width, pop.height); const pos = getPosition(popWidth, popHeight, preferredEdge, preferredAlign); const { innerHeight, innerWidth } = window; pos.top = ["left", "right"].includes(preferredEdge) ? clamp(pos.top, spacing, innerHeight - popHeight - spacing) : pos.top; pos.left = ["top", "bottom"].includes(preferredEdge) ? clamp(pos.left, spacing, innerWidth - popWidth - spacing) : pos.left; return pos; }, [popRef, getPosition, preferredEdge, preferredAlign, spacing]); return (
setHover(true)} onMouseLeave={() => setHover(false)} // className="absolute w-[400px] border" className="poppable" > {children}
); };