- 前端
- UI组件
【免费下载链接】zag
Build your design system in React, Solid, Vue, Svelte or Vanilla. Powered by finite state machines
这篇指南围绕.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):
| 部件 | 与拖拽/滚动状态相关的属性 | 语义 |
|---|---|---|
Scrollbar | data-dragging | 拖拽状态存在时挂出 |
Scrollbar | data-scrolling | 该轴正在滚动时挂出 |
Scrollbar | data-hover | 悬浮时挂出 |
Thumb | data-dragging | 拖拽状态存在时挂出 |
Thumb | data-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 中:
- 进入拖拽(L99-L110):
idle状态监听两类事件,任一命中都迁移到dragging,且事件携带orientation:
idle: { on: { "scrollbar.pointerdown": { target: "dragging", actions: ["scrollToPointer", "startDragging"], // 点击轨道:先跳转到指针位置 }, "thumb.pointerdown": { target: "dragging", actions: ["startDragging"], // 直接抓拇指 }, }, },- 记录轴向:
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) ... }- 拖拽中:
dragging状态挂trackPointerMoveeffect(L112-L127),指针移动换算成viewportEl.scrollTop/scrollLeft的增量(setDraggingScroll,L396-L447),并按轴分别维护scrollingX/scrollingY及 1000ms 的滚动超时(SCROLL_TIMEOUT)。 - 退出拖拽:
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 为两个挂载点) |
状态机与orientationref | scroll-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
相关推荐
Bokeh 坐标范围与坐标轴完全指南:自动 Range、五大轴类型、双轴与固定轴实战
Bokeh 坐标范围与坐标轴完全指南:自动 Range、五大轴类型、双轴与固定轴实战 Bokeh 在浏览器中交互式渲染数据可视化,其绘图坐标系由 x_range
数据可视化图表库Astryx 布局原语 Center 组件契约解析:单轴/双轴居中、逻辑内边距与主题目标实现
Astryx 布局原语 Center 组件契约解析:单轴/双轴居中、逻辑内边距与主题目标实现 Center 是 Astryx 设计系统中负责将调用方内容在自身盒
设计系统UI组件前端Android折线图多Y轴:MPAndroidChart实现左右双坐标轴完整指南
Android折线图多Y轴:MPAndroidChart实现左右双坐标轴完整指南 MPAndroidChart是一个功能强大的Android图表库,支持多种图表
图表库数据可视化
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考