Something went wrong. Try again.
[mirror] A local-first desktop markdown writer with grammar checking, at proto integration, and more github.com/stormlightlabs/writer/releases
Something went wrong. Try again.
2.7 kB · 83 lines
TSX
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384import { NO_MOTION_TRANSITION, TOOLTIP } from "$constants";import { useSkipAnimation } from "$hooks/useMotion";import { AnimatePresence, motion } from "motion/react";import type { ReactNode, RefObject } from "react";import { useCallback, useEffect, useMemo, useState } from "react";import { createPortal } from "react-dom";
type TooltipPlacement = "top" | "bottom";
export type TooltipProps = { anchorRef: RefObject<HTMLElement | null>; visible: boolean; children: ReactNode; placement?: TooltipPlacement; offset?: number; className?: string;};
type TooltipPosition = { left: number; top: number };
export function Tooltip( { anchorRef, visible, children, placement = "bottom", offset = 6, className = "" }: TooltipProps,) { const [position, setPosition] = useState<TooltipPosition | null>(null); const skipAnimation = useSkipAnimation(); const transition = useMemo(() => skipAnimation ? NO_MOTION_TRANSITION : TOOLTIP.TRANSITION, [skipAnimation]);
const updatePosition = useCallback(() => { const anchor = anchorRef.current; if (!anchor) { return; }
const rect = anchor.getBoundingClientRect(); const top = placement === "top" ? rect.top - offset : rect.bottom + offset; setPosition({ left: rect.left + rect.width / 2, top }); }, [anchorRef, offset, placement]);
useEffect(() => { if (!visible) { return; }
updatePosition();
const handleViewportChange = () => updatePosition(); window.addEventListener("resize", handleViewportChange); window.addEventListener("scroll", handleViewportChange, true);
return () => { window.removeEventListener("resize", handleViewportChange); window.removeEventListener("scroll", handleViewportChange, true); }; }, [updatePosition, visible]);
const placementClasses = useMemo( () => placement === "top" ? "-translate-x-1/2 -translate-y-full" : "-translate-x-1/2", [placement], );
const style = useMemo(() => position ? { left: position.left, top: position.top } : undefined, [position]);
if (!position || typeof document === "undefined") { return null; }
return createPortal( <AnimatePresence> {visible && ( <motion.div initial={TOOLTIP.INITIAL} animate={TOOLTIP.ANIMATE} exit={TOOLTIP.EXIT} transition={transition} className={`fixed ${placementClasses} px-2 py-1 bg-layer-02 border border-stroke-subtle rounded text-xs text-text-secondary whitespace-nowrap z-1100 shadow-lg pointer-events-none ${className}`} style={style}> {children} </motion.div> )} </AnimatePresence>, document.body, );}