# 玻璃提示（Tooltip (WCAG 1.4.13)）

- 类型 / 材质：`tooltip` / `glass-apple`（玻璃）
- 状态：done ｜ 最后提交：2026-11-01
- 灵感来源：Apple HIG Tooltips / WAI-ARIA Tooltip pattern
- 依据标准：`/design#interaction` ｜ 组件页：`/showcase/component/tooltip/glass-apple` ｜ 条目：`/r/tooltip--glass-apple.json`
- 必缺状态：焦点
- 整页场景：
  - 发布记录（`/r/scene--record.json`）

## 规格

- 悬停等 --dwell-tooltip（350ms）才出，键盘聚焦立即出；移出 / 失焦 / Esc 立即关
- 只读信息，不接收焦点；aria-describedby 仅在打开时挂到触发器
- 触发器必须可聚焦且能接 ref；内容只放一句说明
- 浮层 portal 到 body：玻璃祖先的 backdrop-filter 会生成包含块，就地定位会被裁切

## 参数（CSS 变量）

`--color-surface` `--color-text` `--color-text-muted` `--dur-fast` `--ease-standard` `--glass-bg` `--glass-border` `--shadow-lg` `--text-2xs`

## Props 契约

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `onPointerEnter` | `PointerEventHandler<HTMLElement>` | 可选 | — |
| `onPointerLeave` | `PointerEventHandler<HTMLElement>` | 可选 | — |
| `onFocus` | `FocusEventHandler<HTMLElement>` | 可选 | — |
| `onBlur` | `FocusEventHandler<HTMLElement>` | 可选 | — |

## 提示词（可直接粘给智能体）

```
复现「玻璃提示」— Tooltip (WCAG 1.4.13)（提示 / Glass）
灵感来源：Apple HIG Tooltips / WAI-ARIA Tooltip pattern
规格：
- 悬停等 --dwell-tooltip（350ms）才出，键盘聚焦立即出；移出 / 失焦 / Esc 立即关
- 只读信息，不接收焦点；aria-describedby 仅在打开时挂到触发器
- 触发器必须可聚焦且能接 ref；内容只放一句说明
- 浮层 portal 到 body：玻璃祖先的 backdrop-filter 会生成包含块，就地定位会被裁切
参数（CSS 变量）：--color-surface --color-text --color-text-muted --dur-fast --ease-standard --glass-bg --glass-border --shadow-lg --text-2xs
必缺状态：焦点
整页场景：发布记录
无障碍：焦点环 ≥2px 等效周长（SC 2.4.13）；指针目标 ≥24×24（SC 2.5.8）；动效带 prefers-reduced-motion 降级
参考：本站实样 /showcase/component/tooltip/glass-apple · 标准 interaction /design#interaction · 调研笔记 /showcase/component/tooltip/glass-apple#notes
```

## 调研笔记

import {
  cloneElement,
  useEffect,
  useId,
  useRef,
  useState,
  type FocusEventHandler,
  type PointerEventHandler,
  type ReactElement,
  type ReactNode,
} from 'react';
import { useOverlay } from '@/lib/ui-echo/overlay';
import { motionMs } from '@/lib/ui-echo/motion';
import { Portal, layerStyle, useLayerPosition } from '@/lib/ui-echo/layer';

interface TriggerProps {
  'aria-describedby'?: string;
  onPointerEnter?: PointerEventHandler<HTMLElement>;
  onPointerLeave?: PointerEventHandler<HTMLElement>;
  onFocus?: FocusEventHandler<HTMLElement>;
  onBlur?: FocusEventHandler<HTMLElement>;
}

interface Props {
  content: ReactNode;
  /** 必须是可聚焦的元素：按钮、链接或 tabIndex=0 的节点 */
  children: ReactElement<TriggerProps>;
  placement?: 'top' | 'bottom';
  className?: string;
}

export function GlassAppleTooltip({ content, children, placement = 'top', className = '' }: Props) {
  const id = useId();
  const [open, setOpen] = useState(false);
  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
  const { mounted, entering, leaving } = useOverlay(open);
  const { triggerRef, layerRef, pos } = useLayerPosition(mounted, { placement });

  const clear = () => {
    if (timer.current) clearTimeout(timer.current);
    timer.current = null;
  };
  const showNow = () => {
    clear();
    setOpen(true);
  };
  const close = () => {
    clear();
    setOpen(false);
  };
  const showAfterDwell = () => {
    clear();
    timer.current = setTimeout(() => setOpen(true), motionMs('--dwell-tooltip', 350));
  };

  useEffect(() => clear, []);

  // 悬停内容要能用 Esc 关掉（SC 1.4.13），焦点不进入浮层
  useEffect(() => {
    if (!open) return;
    const onKey = (e: KeyboardEvent) => {
      if (e.key === 'Escape') close();
    };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [open]);

  const trigger = cloneElement(children, {
    ref: (node: HTMLElement | null) => {
      triggerRef.current = node;
      const own = (children as { ref?: unknown }).ref;
      if (typeof own === 'function') own(node);
    },
    'aria-describedby': open ? id : undefined,
    onPointerEnter: (e) => {
      showAfterDwell();
      children.props.onPointerEnter?.(e);
    },
    onPointerLeave: (e) => {
      close();
      children.props.onPointerLeave?.(e);
    },
    onFocus: (e) => {
      showNow();
      children.props.onFocus?.(e);
    },
    onBlur: (e) => {
      close();
      children.props.onBlur?.(e);
    },
  } as TriggerProps & { ref: (node: HTMLElement | null) => void });

  return (
    <>
      {trigger}
      {mounted && (
        <Portal>
          <div
            ref={layerRef}
            role="tooltip"
            id={id}
            style={layerStyle(pos)}
            className={`
              z-50 pointer-events-none
              max-w-[240px] px-2.5 py-1.5 rounded-lg
              text-[length:var(--text-2xs)] leading-snug
              bg-[var(--glass-bg)] border border-[var(--glass-border)] backdrop-blur-xl
              shadow-[var(--shadow-lg)] text-[var(--color-text)]
              transition-[opacity,transform] duration-[var(--dur-fast)] ease-[var(--ease-standard)]
              ${entering || leaving ? 'opacity-0 translate-y-0.5' : 'opacity-100 translate-y-0'}
              ${className}
            `}
          >
            {content}
          </div>
        </Portal>
      )}
    </>
  );
}
