@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
JavaScript
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