67 lines · 2.5 KB
import { useEffect, useRef, useState } from "react";
import { getHovercard } from "~/fns/hovercards";

const SHOW_DELAY_MS = 350;
const HIDE_DELAY_MS = 200;
const KINDS = ["issue", "user", "team", "commit"] as const;

type Kind = (typeof KINDS)[number];
type Card = NonNullable<Awaited<ReturnType<typeof getHovercard>>>;
export type HovercardState = { card: Card; top: number; left: number } | null;

function isKind(value: string | undefined): value is Kind {
  return KINDS.some((kind) => kind === value);
}

/** Watches the document for hovers over `a[data-hovercard]` links and loads preview cards (memoized per ref). */
export function useHovercards() {
  const [state, setState] = useState<HovercardState>(null);
  const cache = useRef(new Map<string, Promise<Card | null>>());
  const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);

  useEffect(() => {
    const target = (event: Event) =>
      event.target instanceof Element ? event.target.closest<HTMLAnchorElement>("a[data-hovercard]") : null;
    const inPopup = (event: Event) =>
      event.target instanceof Element && event.target.closest("[data-hovercard-popup]") !== null;
    const over = (event: MouseEvent) => {
      if (inPopup(event)) {
        clearTimeout(timer.current);
        return;
      }
      const link = target(event);
      if (!link) return;
      const kind = link.dataset.hovercard;
      const ref = link.dataset.ref;
      if (!isKind(kind) || !ref) return;
      clearTimeout(timer.current);
      timer.current = setTimeout(async () => {
        const key = `${kind}:${ref}`;
        let pending = cache.current.get(key);