import classNames from "classnames"; import type { CSSProperties, KeyboardEvent, ReactNode } from "react"; import { useRef, useState } from "react"; import { generateId } from "../../lib/generateId"; import { Portal } from "../Portal"; export interface TooltipProps { children: ReactNode; content: ReactNode; tabIndex?: number; size?: "md" | "lg"; className?: string; } const hiddenStyles: CSSProperties = { left: -99999, top: -99999, visibility: "hidden", pointerEvents: "none", opacity: 0, }; type TooltipPosition = "top" | "bottom"; interface TooltipOpenState { styles: CSSProperties; position: TooltipPosition; } export function Tooltip({ children, className, content, tabIndex, size = "md" }: TooltipProps) { const [openState, setOpenState] = useState(null); const triggerRef = useRef(null); const tooltipRef = useRef(null); const showTimeout = useRef(undefined); const handleOpenImmediate = () => { if (triggerRef.current == null || tooltipRef.current == null) return; clearTimeout(showTimeout.current); const triggerRect = triggerRef.current.getBoundingClientRect(); const tooltipRect = tooltipRef.current.getBoundingClientRect(); const viewportHeight = document.documentElement.clientHeight; const margin = 8; const spaceAbove = Math.max(0, triggerRect.top - margin); const spaceBelow = Math.max(0, viewportHeight - triggerRect.bottom - margin); const preferBottom = spaceAbove < tooltipRect.height + margin && spaceBelow > spaceAbove; const position: TooltipPosition = preferBottom ? "bottom" : "top"; const styles: CSSProperties = { left: Math.max(0, triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2), maxHeight: position === "top" ? spaceAbove : spaceBelow, ...(position === "top" ? { bottom: viewportHeight - triggerRect.top } : { top: triggerRect.bottom }), }; setOpenState({ styles, position }); }; const handleOpen = () => { clearTimeout(showTimeout.current); showTimeout.current = setTimeout(handleOpenImmediate, 500); }; const handleClose = () => { clearTimeout(showTimeout.current); setOpenState(null); }; const handleToggleImmediate = () => { if (openState) handleClose(); else handleOpenImmediate(); }; const handleKeyDown = (e: KeyboardEvent) => { if (openState && e.key === "Escape") { e.preventDefault(); e.stopPropagation(); handleClose(); } }; const id = useRef(`tooltip-${generateId()}`); return ( <> {/* oxlint-disable-next-line jsx-a11y/prefer-tag-over-role -- Needs to be usable in other buttons */} {children} ); } function Triangle({ className, position }: { className?: string; position: "top" | "bottom" }) { const isBottom = position === "bottom"; return ( Triangle ); }