74 lines
2.3 KiB
TypeScript
74 lines
2.3 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { createPortal } from "react-dom";
|
|
|
|
import { MarkdownPreview } from "@/components/markdown/markdown-widgets";
|
|
import { colorModeFromTheme } from "@/components/theme/use-dark-theme";
|
|
|
|
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 colorMode = colorModeFromTheme();
|
|
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>
|
|
);
|
|
}
|