| import { useState, useRef, useCallback, useEffect, type ReactNode } from "react"; |
| import { computePosition, offset, flip, shift, type Placement } from "@floating-ui/dom"; |
|
|
| interface TooltipProps { |
| title: string; |
| placement?: Placement; |
| children: ReactNode; |
| } |
|
|
| export function Tooltip({ title, placement = "top", children }: TooltipProps) { |
| const triggerRef = useRef<HTMLSpanElement>(null); |
| const tooltipRef = useRef<HTMLDivElement>(null); |
| const [open, setOpen] = useState(false); |
|
|
| const reposition = useCallback(() => { |
| const trigger = triggerRef.current; |
| const tip = tooltipRef.current; |
| if (!trigger || !tip) return; |
|
|
| computePosition(trigger, tip, { |
| placement, |
| strategy: "fixed", |
| middleware: [offset(6), flip(), shift({ padding: 8 })], |
| }).then(({ x, y }) => { |
| tip.style.left = `${x}px`; |
| tip.style.top = `${y}px`; |
| }); |
| }, [placement]); |
|
|
| useEffect(() => { |
| if (open) reposition(); |
| }, [open, reposition]); |
|
|
| if (!title) return <>{children}</>; |
|
|
| return ( |
| <> |
| <span |
| ref={triggerRef} |
| onMouseEnter={() => setOpen(true)} |
| onMouseLeave={() => setOpen(false)} |
| onFocus={() => setOpen(true)} |
| onBlur={() => setOpen(false)} |
| style={{ display: "inline-flex" }} |
| > |
| {children} |
| </span> |
| {open && ( |
| <div ref={tooltipRef} className="ed-tooltip" role="tooltip"> |
| {title} |
| </div> |
| )} |
| </> |
| ); |
| } |
|
|