UNPKG

@flanksource/clicky-ui

Version:

Flanksource Clicky UI — React component library built on shadcn/ui with light/dark and density theming.

171 lines (170 loc) 5.1 kB
import { jsxs, jsx } from "react/jsx-runtime"; import { useState, useRef, useLayoutEffect, useEffect } from "react"; import { createPortal } from "react-dom"; import { cn } from "../lib/utils.js"; const GAP_PX = 6; const HOVER_CLOSE_DELAY_MS = 120; function HoverCard({ trigger, children, placement = "top", delay = 0, arrow = true, className, cardClassName }) { const [open, setOpen] = useState(false); const [position, setPosition] = useState(null); const triggerRef = useRef(null); const cardRef = useRef(null); const openTimerRef = useRef(null); const closeTimerRef = useRef(null); const cancelClose = () => { if (closeTimerRef.current !== null) { window.clearTimeout(closeTimerRef.current); closeTimerRef.current = null; } }; function onEnter() { cancelClose(); if (delay > 0) { if (openTimerRef.current !== null) window.clearTimeout(openTimerRef.current); openTimerRef.current = window.setTimeout(() => setOpen(true), delay); } else { setOpen(true); } } function onLeave() { if (openTimerRef.current !== null) { window.clearTimeout(openTimerRef.current); openTimerRef.current = null; } cancelClose(); closeTimerRef.current = window.setTimeout(() => setOpen(false), HOVER_CLOSE_DELAY_MS); } useLayoutEffect(() => { if (!open) return; const update = () => { const trigger2 = triggerRef.current; const card2 = cardRef.current; if (!trigger2 || !card2) return; const t = trigger2.getBoundingClientRect(); const c = card2.getBoundingClientRect(); let top = 0; let left = 0; switch (placement) { case "top": top = t.top - c.height - GAP_PX; left = t.left + t.width / 2 - c.width / 2; break; case "bottom": top = t.bottom + GAP_PX; left = t.left + t.width / 2 - c.width / 2; break; case "left": top = t.top + t.height / 2 - c.height / 2; left = t.left - c.width - GAP_PX; break; case "right": top = t.top + t.height / 2 - c.height / 2; left = t.right + GAP_PX; break; } const margin = 4; left = Math.min(Math.max(margin, left), window.innerWidth - c.width - margin); top = Math.min(Math.max(margin, top), window.innerHeight - c.height - margin); setPosition({ top, left }); }; update(); window.addEventListener("scroll", update, true); window.addEventListener("resize", update); return () => { window.removeEventListener("scroll", update, true); window.removeEventListener("resize", update); }; }, [open, placement, children]); useEffect(() => { return () => { if (openTimerRef.current !== null) window.clearTimeout(openTimerRef.current); if (closeTimerRef.current !== null) window.clearTimeout(closeTimerRef.current); }; }, []); const card = open && typeof document !== "undefined" ? createPortal( /* @__PURE__ */ jsxs( "div", { ref: cardRef, role: "tooltip", onMouseEnter: cancelClose, onMouseLeave: onLeave, style: position ? { position: "fixed", top: position.top, left: position.left } : { position: "fixed", top: -9999, left: -9999, visibility: "hidden" }, className: cn( "z-[9999] rounded-md border border-border bg-background px-2.5 py-1.5 text-[11px] shadow-lg", "whitespace-nowrap", cardClassName ), children: [ children, arrow && position && /* @__PURE__ */ jsx(Arrow, { placement }) ] } ), document.body ) : null; return /* @__PURE__ */ jsxs( "span", { ref: triggerRef, className: cn("relative inline-flex items-center", className), onMouseEnter: onEnter, onMouseLeave: onLeave, onFocus: onEnter, onBlur: onLeave, children: [ trigger, card ] } ); } function Arrow({ placement }) { const base = "absolute h-1.5 w-1.5 rotate-45 border bg-background border-border"; switch (placement) { case "top": return /* @__PURE__ */ jsx( "span", { "aria-hidden": true, className: cn(base, "left-1/2 -translate-x-1/2 -bottom-0.5 border-l-0 border-t-0") } ); case "bottom": return /* @__PURE__ */ jsx( "span", { "aria-hidden": true, className: cn(base, "left-1/2 -translate-x-1/2 -top-0.5 border-r-0 border-b-0") } ); case "left": return /* @__PURE__ */ jsx( "span", { "aria-hidden": true, className: cn(base, "top-1/2 -translate-y-1/2 -right-0.5 border-l-0 border-b-0") } ); case "right": return /* @__PURE__ */ jsx( "span", { "aria-hidden": true, className: cn(base, "top-1/2 -translate-y-1/2 -left-0.5 border-r-0 border-t-0") } ); } } export { HoverCard }; //# sourceMappingURL=HoverCard.js.map