import { Fragment, isValidElement, type ComponentProps, type ReactNode } from "react"; import { Focusable, Tooltip as RACTooltip, TooltipTrigger, type Placement } from "react-aria-components"; import { cn } from "@/lib/utils"; /** * Tooltip — 气泡提示(自研,react-aria-components overlay)。 * * - API 对齐 AntD Tooltip(title/placement/children),title 为空时不渲染浮层。 * - 浮层定位、进出场、焦点管理由 RAC 承担(ADR-0008:复杂浮层以 RAC 为无障碍地基)。 * - 样式只吃语义 token:bg-surface-strong(light #fff / dark #2a2a2a)+ text-foreground + * border-border hairline,无裸色;motion 走 RAC 动画类 + motion-reduce 关停。 * - 启用的控件直接接收 RAC 事件、ref 和描述;非交互/禁用内容用 span 接收悬停。 * - placement 映射 AntD → RAC(topLeft→top left …)。 * 对标:AntD Tooltip;boardui base/tooltip。 */ export type TooltipPlacement = "top" | "topLeft" | "topRight" | "bottom" | "bottomLeft" | "bottomRight" | "left" | "right"; const PLACEMENT_MAP: Record = { top: "top", topLeft: "top left", topRight: "top right", bottom: "bottom", bottomLeft: "bottom left", bottomRight: "bottom right", left: "left", right: "right", }; export interface TooltipProps { title?: ReactNode; placement?: TooltipPlacement; /** 悬停延迟 ms(默认 350,贴近 AntD mouseEnterDelay) */ delay?: number; className?: string; children: ReactNode; } export function Tooltip({ title, placement = "top", delay = 350, className, children }: TooltipProps) { if (!title) return <>{children}; const child = isValidElement<{ disabled?: boolean; loading?: boolean; tabIndex?: number }>(children) ? children : null; const directTrigger = child && child.type !== Fragment && !child.props.disabled && !child.props.loading && (typeof child.type !== "string" || ["button", "a", "input", "select", "textarea", "summary"].includes(child.type) || child.props.tabIndex !== undefined); return ( {/* * RAC 1.21 的 TooltipTrigger 通过 context 下发 hover/focus 处理与 triggerRef, * 只有消费该上下文的组件才会真正挂上(原生 /