☰
Zag scroll-area 补丁解析:`data-dragging` 双轴误标问题的轴分离实现与 E2E 验证
2026/10/13 3:08:25 网站建设 项目流程
  • 前端
  • UI组件

【免费下载链接】zag

Build your design system in React, Solid, Vue, Svelte or Vanilla. Powered by finite state machines

项目地址:https://gitcode.com/gh_mirrors/za/zag
点击查看免费下载

这篇指南围绕.changeset/scroll-area-dragging-axis.md记录的补丁展开:修复@zag-js/scroll-area中“只拖动单轴时data-dragging却被同时标记到两个滚动条和两个拇指上”的缺陷。读完你可以理解该 changeset 在 monorepo 版本体系中的位置、data-dragging属性在状态机到 connect 层的完整产生链路,以及如何用 Playwright 断言锁定这类行为回归。

一、这个 changeset 说了什么

Zag 是一个 monorepo,.changeset/目录由@changesets/cli驱动(见 .changeset/README.md)。每份 changeset 文件由 YAML frontmatter + 一句话变更说明组成,发布时会据此更新各包CHANGELOG.md并决定版本号。本次补丁文件 scroll-area-dragging-axis.md 的完整内容即:

--- "@zag-js/scroll-area": patch --- Fixed issue where `data-dragging` was set on both scrollbars and thumbs when only one axis was dragged.

要点:

  • 受影响包为@zag-js/scroll-area,级别为patch(缺陷修复,无 API 变更)。
  • 缺陷现象:Scroll Area 同时存在水平/垂直两条滚动轴时,用户只拖动其中一轴的 thumb/scrollbar,但data-dragging属性被同时挂在了两条滚动条与两个拇指(thumb)上。
  • 由于data-dragging是官方提供给样式层的 CSS 钩子(用于在拖拽中强调 thumb、放大滚动条等),双轴误标会导致未拖动的轴也被应用“拖拽中”样式,属于可视化状态错误。

结合 .changeset/config.json 可以看到"fixed": [["@zag-js/**"]],即所有@zag-js/*包采用 lockstep(锁步)版本策略——这意味着该 patch 发布时全部机器包会同批升版,升级任一包时不会遇到版本不一致问题。

二、data-dragging在 Scroll Area 中的正常形态

Scroll Area 的 DOM 解剖由 scroll-area.anatomy.ts 定义,共六个部件:root、viewport、content、scrollbar、thumb、corner。其中水平、垂直两条轴各自渲染一个scrollbar+ 一个thumb,通过data-orientation="horizontal" | "vertical"区分。

按官方 data attribute 文档(data-attr.json 中scroll-area条目,L1635-L1650):

部件与拖拽/滚动状态相关的属性语义
Scrollbardata-dragging拖拽状态存在时挂出
Scrollbardata-scrolling该轴正在滚动时挂出
Scrollbardata-hover悬浮时挂出
Thumbdata-dragging拖拽状态存在时挂出
Thumbdata-hover悬浮时挂出

官方组件文档 scroll-area.mdx 也给出了配套样式写法(节选):

[data-part="scrollbar"][data-scrolling] { /* styles when actively scrolling */ } [data-part="thumb"][data-scrolling] { /* styles for thumb when scrolling */ }

data-dragging与此同理,是纯 CSS 侧的状态钩子。它由@zag-js/scroll-area包在较早期版本引入(见 scroll-area CHANGELOG,PR #2652:“Adddata-draggingattribute to scroll area parts for ease of styling”),而本次 changeset 修复的正是它引入后暴露出的轴区分缺陷。

三、修复后的源码实现:draggingOrientation的比较逻辑

3.1 connect 层:单变量收敛两轴状态

修复的核心在 scroll-area.connect.ts。connect 函数把无框架依赖的 service 连接成可渲染的 props API,开头即有一个关键推导(L17):

const draggingOrientation = state.matches("dragging") ? refs.get("orientation") : null

其语义:

  • 状态机不在dragging状态时,draggingOrientation为null——所有部件都不挂data-dragging;
  • 处于dragging状态时,取refs中记录的本次拖动方向("vertical"或"horizontal"),只与当前部件的方向做等值比较。

随后在两个部件的 props 生成处分别使用它(L139、L182):

// getScrollbarProps(props = {}) const { orientation = "vertical" } = props return normalize.element({ ... "data-dragging": dataAttr(draggingOrientation === orientation), ... })
// getThumbProps(props = {}) const { orientation = "vertical" } = props return normalize.element({ ... "data-dragging": dataAttr(draggingOrientation === orientation), ... })

dataAttr()(来自@zag-js/dom-query)是 Zag 各机器通用的布尔到 data 属性转换器:条件成立时输出空字符串(属性存在),否则属性被省略。因此当且仅当“正在拖动的轴 === 当前部件所属轴”时,data-dragging才会出现在该部件上——这正是 changeset 所述缺陷的修复点:把“拖拽中”从全局信号收敛为按轴比较的信号。

程序化读取状态的路径getScrollbarState()同样遵循该语义(L42-L51):

getScrollbarState(props) { const { orientation = "vertical" } = props const horizontal = orientation === "horizontal" return { hovering, dragging: draggingOrientation === orientation, // 与属性口径一致 scrolling: context.get(horizontal ? "scrollingX" : "scrollingY"), hidden: horizontal ? hiddenState.scrollbarXHidden : hiddenState.scrollbarYHidden, } }

也就是说,api.getScrollbarState({ orientation }).dragging与 DOM 上的data-dragging永远同真同假,JS 逻辑与 CSS 钩子不会出现口径分裂。

3.2 machine 层:orientationref 的写入与清除

属性能否按轴区分,取决于状态机是否正确记录“正在拖的是哪根轴”。在 scroll-area.machine.ts 中:

  1. 进入拖拽(L99-L110):idle状态监听两类事件,任一命中都迁移到dragging,且事件携带orientation:
idle: { on: { "scrollbar.pointerdown": { target: "dragging", actions: ["scrollToPointer", "startDragging"], // 点击轨道:先跳转到指针位置 }, "thumb.pointerdown": { target: "dragging", actions: ["startDragging"], // 直接抓拇指 }, }, },
  1. 记录轴向:startDraggingaction(L384-L394)把事件的orientation写进 refs,并快照指针起点与scrollTop/scrollLeft:
startDragging({ event, refs, scope }) { refs.set("startX", event.point.x) refs.set("startY", event.point.y) refs.set("orientation", event.orientation) ... }
  1. 拖拽中:dragging状态挂trackPointerMoveeffect(L112-L127),指针移动换算成viewportEl.scrollTop/scrollLeft的增量(setDraggingScroll,L396-L447),并按轴分别维护scrollingX/scrollingY及 1000ms 的滚动超时(SCROLL_TIMEOUT)。
  2. 退出拖拽:thumb.pointerup/scrollbar.pointerup迁回idle,stopDragging(L449-L451)将refs.orientation置回null——此时 connect 层推导出的draggingOrientation归 null,所有部件的data-dragging一并消失。

值得注意的是refs的初始值orientation: "vertical"(L51)只是一个占位默认:在idle状态下state.matches("dragging")为 false,connect 层根本不会去读它;真正有意义的取值只发生在startDragging之后。

从源码结构看,本次修复的关键在于让 connect 层能拿到每一次拖动会话所属的轴,并只在同轴部件上挂属性;修复前的实现缺少这种按轴区分的比较(具体旧实现形态以 changeset 的缺陷描述为准:双轴同时被标记)。

3.3 类型契约

scroll-area.types.ts 中的ScrollAreaSchema.state: "idle" | "dragging"(L69)与ScrollAreaRefs.orientation: Orientation | null(L52)共同构成了这条链路:状态名告诉 connect “是否在拖”,ref 告诉它“拖的是哪根轴”。getScrollbarProps({ orientation })的orientation参数(默认"vertical")则是部件与轴对应的入口。

四、E2E 测试如何锁定该修复

e2e/scroll-area.e2e.ts 针对本修复提供了专门的回归用例(L8-L19),使用 Playwright 驱动 examples 站点的/scroll-area/basic页面(对应 basic.tsx,其中条件渲染api.hasOverflowY && .../api.hasOverflowX && ...保证两轴同时存在):

test("dragging the vertical thumb only marks the vertical axis", async ({ page }) => { await page.goto("/scroll-area/basic") const box = (await thumb(page, "vertical").boundingBox())! await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2) await page.mouse.down() await expect(thumb(page, "vertical")).toHaveAttribute("data-dragging", "") await expect(scrollbar(page, "vertical")).toHaveAttribute("data-dragging", "") await expect(thumb(page, "horizontal")).not.toHaveAttribute("data-dragging") await expect(scrollbar(page, "horizontal")).not.toHaveAttribute("data-dragging") })

断言恰好覆盖四个部件:垂直 thumb 与垂直 scrollbar 上属性存在(空字符串值),水平 thumb 与水平 scrollbar 上属性不存在。定位器按[data-scope=scroll-area][data-part=...][data-orientation=...]组合,与 anatomy 产出的data-part/data-orientation属性一一对应。若回归到修复前行为(双轴同标),最后两条not.toHaveAttribute断言会立刻失败。

五、样式实践:如何正确使用data-dragging

修复后,可以放心按轴编写“拖拽中”样式。结合data-part、data-orientation、data-dragging、data-hover、data-scrolling的典型用法:

/* 垂直轴:拖拽中放大拇指、加深颜色 */ [data-part="thumb"][data-orientation="vertical"][data-dragging] { background: var(--color-primary); opacity: 1; } /* 两条轴通用:拖拽中加粗滚动条 */ [data-part="scrollbar"][data-dragging] { opacity: 1; } [data-part="scrollbar"][data-hover] { opacity: 0.6; /* 悬浮淡显 */ }

配套可用信号(见 scroll-area.mdx):

  • data-scrolling:用户正在滚动该轴(1s 超时自动清除),可用于滚动条高亮;
  • data-hover:指针处于 Scroll Area 内;
  • data-overflow-x/data-overflow-y:对应轴是否有溢出(决定该轴 scrollbar 是否渲染);
  • viewport 上的data-at-top/data-at-bottom/data-at-left/data-at-right以及--scroll-area-overflow-*-start/endCSS 变量,可用于到边渐隐等效果。

在框架侧(以 React 为例,basic.tsx):

const service = useMachine(scrollArea.machine, { id: useId() }) const api = scrollArea.connect(service, normalizeProps) {api.hasOverflowY && ( <div {...api.getScrollbarProps({ orientation: "vertical" })}> <div {...api.getThumbProps({ orientation: "vertical" })} /> </div> )}

只要向getScrollbarProps/getThumbProps传入正确的orientation,data-dragging就会按上述轴比较逻辑正确输出;需要纯逻辑判断时改用api.getScrollbarState({ orientation: "vertical" }).dragging。

六、小结与文件索引

关注点文件
补丁记录(本文主体).changeset/scroll-area-dragging-axis.md
changeset 版本策略(lockstep).changeset/config.json
data-dragging输出逻辑scroll-area.connect.ts(L139、L182 为两个挂载点)
状态机与orientationrefscroll-area.machine.ts(startDraggingL384、stopDraggingL449)
类型契约scroll-area.types.ts
回归测试e2e/scroll-area.e2e.ts
data attribute 官方口径data-attr.json
属性/状态样式文档scroll-area.mdx

一句话总结:这个 patch changeset 修复的是@zag-js/scroll-area中data-dragging未按轴区分的问题;修复手段是让状态机在拖拽会话中记录orientation,connect 层以draggingOrientation === orientation的比较结果驱动属性输出,并辅以 E2E 四部件断言防回归。对使用者而言,升级后即可安全地依赖data-dragging对单轴做 CSS 强调样式,且getScrollbarState().dragging与 DOM 属性保持同一口径。

  • 前端
  • UI组件

【免费下载链接】zag

Build your design system in React, Solid, Vue, Svelte or Vanilla. Powered by finite state machines

项目地址:https://gitcode.com/gh_mirrors/za/zag
点击查看免费下载

相关推荐

上一篇:Valdi 原生上下文(Native Context):从 Native 向组件树注入服务与数据的完整指南
下一篇:CoffeeScript 1.2.0 发布解读:`--watch`/`--join` 协同、`throw` 表达式化与安全闭包注释外移

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询