跳到主要内容

Tree 拖拽视觉与行布局对齐新版 Figma(DES-89)

Pre-release · 类型: ✨ 新功能

Linear: DES-89

Figma:Design System 新增独立 Tree 页面(canvas 24402:2236),Tree Item State 轴扩展为 Default / Hover / Selected / Dragged Source / Drop Indicator 5 态,新增 Hierarchy 轴与 Drag Preview / Drop Indicator 元素。

改动文件

  • src/components/Tree/styles.ts
  • src/components/Tree/Tree.tsx
  • src/components/Tree/Tree.test.tsx
  • src/components/Tree/TreeDesignSpec.md
  • packages/design-site/docs/components/patterns/tree.mdx(+ 中文版)

改动内容

1. 行布局 token

  • 行内 gap:gap-(--Spacing_12)gap-(--Spacing_8)
  • 行内边距:px-(--Spacing_8)pl-(--Spacing_8) pr-(--Spacing_4)
  • actions 容器:right-(--Spacing_8)right-(--Spacing_4)

2. 层级缩进

indent 默认 24 → 16;缩进公式从 level * indent 改为 level * indent - 4(首层子级 12px,此后每层 +16px),对齐 Figma Hierarchy 轴(Item 与 Drop Indicator 两组组件均验证为「首层 +12、其后 +16」规律)。

3. Dragged Source 状态

拖拽源行从 opacity-50 改为 1px 内描边 inset-ring inset-ring-(--Separators-Emphasized),内容保持完整不透明(Figma 24448:14150)。

4. Drop 指示器

行间投放指示器从「行内 2px --Labels-Primary 横线」改为「行间 4px gap 内 --Labels-Link ∅8 空心圆点(2px 描边)+ 2px 横线」(Figma Tree Element - Drop Indicator 24459:4888):

  • 容器 -top-1 / -bottom-1 落在 root gap-1 的行间空隙
  • 左缘 = 本级 icon 左缘 + 8px(随 Hierarchy 缩进),右缘距行右 4px

5. 拖拽预览(新增)

dragstart 时克隆行内 icon + title 构建白底浮层(--Grays-White + 0 0 32px var(--Effects-Shadow-Default) 阴影 + 88% 不透明度,宽度与行一致),经 setDragImage 替换浏览器默认行截图(Figma Tree Element - Drag Preivew 24448:14181);截图后下一帧移除节点。

setDragImage 不存在或调用抛错的环境(如 happy-dom 存在该方法但未实现)回退默认拖拽影像并同步移除预览节点,避免泄漏。

追加:视觉对比 case(同日)

Linear: DES-90

新增 Tree.visual.spec.tsx(Playwright CT,9 个 case)+ TreeFixture.visual.tsx(场景 fixture),并在 scripts/visual-diff-config.ts 注册 Tree 的 Figma 节点映射:

  • case:default / expanded / selected / leaf / hierarchy-2 / hover / dragged-source / drop-indicator / drag-preview,与 Figma Tree 页面变体一一对应(映射表见 TreeDesignSpec.md §9)
  • Playwright CT 的 spec 只能传可序列化 props,icon / actions / 拖拽配置收敛进 fixture(沿用 Toast 的 ToastFixture.visual.tsx 模式)
  • 拖拽态用 dispatchEvent + 页面内真实 DataTransfer 驱动(Playwright 无原生 HTML5 DnD);drag preview 因真实节点随 dragstart 下一帧销毁,改为用 TREE_DRAG_PREVIEW_CLASS 静态复刻
  • vitest.config.ts coverage exclude 补充 *.visual.spec.tsx / *.visual.tsx(视觉测试文件不计覆盖率)
  • 已验证:9 case 截图基线稳定(两次运行一致),pnpm visual:gallery 中 Tree 9 个 Figma 节点全部拉图配对成功

追加:超长 title 自动 tooltip(同日)

Linear: DES-91

title 内容超出容器宽度时自动用 Tooltip(悬停延迟 300ms)包裹展示完整内容;未超长不引入 tooltip。

  • 超长判定:克隆 title 节点离屏渲染(fixed + hidden,挂同一父级保留字体级联),用克隆体自然宽度对比容器 clientWidth(+1px 容差);不用 scrollWidth(title 是 mask 渐变非 ellipsis,且内容可为任意 ReactNode)。实现见 utils.measureTitleOverflow
  • 重测时机:挂载后一次 + ResizeObserver 监听容器尺寸变化 + title 内容变化
  • 踩坑记录:RO 必须观察稳定的行容器,且测量回调里实时读 titleRef.current —— tooltip 包裹会重建 title 节点,若 RO 观察 title 节点本身并在闭包捕获旧引用,旧节点卸载时 RO 会测到脱离文档的 0 宽,把 overflowed 错误重置回 false(真实浏览器中 CT 测试抓到的回归)
  • 测试:单测 7 个(测量函数 4 + 组件行为 3,happy-dom 下用原型 getter 注入宽度);CT 行为测试 1 个(真实布局下长 title hover 出 tooltip、短 title 不包 trigger)

根因(测试环境)

design 包 vitest 环境为 happy-dom:e.dataTransfer 是 happy-dom 自有 DataTransfer 实例(非 fireEvent 传入的 mock),其 setDragImage 存在但调用即抛错。首版实现仅做 typeof 守卫导致预览节点泄漏进 document.body,污染后续 39 个用例;改为 try/catch 回退后解决。测试侧通过 vi.spyOn(DataTransfer.prototype, 'setDragImage') 验证预览构建与注入。