Organize/components/board/mouse-follow-tooltip.tsx

75 lines
2.3 KiB
TypeScript

"use client";
import { useState } from "react";
import { createPortal } from "react-dom";
import { useTheme } from "next-themes";
import { MarkdownPreview } from "@/components/markdown/markdown-widgets";
const OFFSET = 16;
// Rough size assumptions used to keep the tooltip on-screen -- cheaper than
// measuring the rendered node and repositioning after the fact, and close
// enough for a small hover preview.
const ASSUMED_WIDTH = 288;
const ASSUMED_HEIGHT = 180;
/**
* Wraps its child in a tooltip that tracks the cursor instead of anchoring
* to the trigger element -- rendered via a portal to `document.body` so it
* isn't caught by an ancestor's CSS `transform` (dnd-kit positions dragged
* cards with one, which would otherwise turn `position: fixed` here into
* "fixed to that card" instead of the viewport). Content is rendered as
* markdown, same as the editor it was written in, so links etc. actually
* show up as links in the preview too.
*/
export function MouseFollowTooltip({
content,
children,
}: {
content: string;
children: React.ReactNode;
}) {
const { resolvedTheme } = useTheme();
const colorMode = resolvedTheme === "dark" ? "dark" : "light";
const [pos, setPos] = useState<{ x: number; y: number } | null>(null);
function handleMove(e: React.MouseEvent) {
setPos({ x: e.clientX, y: e.clientY });
}
let left = 0;
let top = 0;
if (pos) {
left = pos.x + OFFSET;
top = pos.y + OFFSET;
if (typeof window !== "undefined") {
if (left + ASSUMED_WIDTH > window.innerWidth) left = pos.x - ASSUMED_WIDTH - OFFSET;
if (top + ASSUMED_HEIGHT > window.innerHeight) top = pos.y - ASSUMED_HEIGHT - OFFSET;
}
}
return (
<span
className="contents"
onMouseEnter={handleMove}
onMouseMove={handleMove}
onMouseLeave={() => setPos(null)}
>
{children}
{pos &&
typeof document !== "undefined" &&
createPortal(
<div
role="tooltip"
data-color-mode={colorMode}
className="pointer-events-none fixed z-50 max-h-44 w-72 overflow-hidden rounded-md border bg-popover p-2.5 text-xs text-popover-foreground shadow-md"
style={{ left, top }}
>
<MarkdownPreview source={content} />
</div>,
document.body
)}
</span>
);
}