修复 position: sticky 容器内拖拽 snap-to-cursor 的垂直偏移:motion(framer-motion)issue-2236 方案解析
前端UI组件【免费下载链接】motionA modern animation library for React and JavaScript项目地址https://gitcode.com/GitHub_Trending/mo/motion点击查看免费下载本文基于仓库 plans/issues/issue-2236.md 计划文档结合framer-motion拖拽引擎源码完整剖析「可拖拽元素位于position: sticky容器内、容器吸顶后 snap-to-cursor 出现垂直偏移」这一 bug 的根因、复现测试设计与修复方案。读完你既能复刻出该 bug 的 Cypress 回归测试也能理解拖拽引擎中「布局测量时机」与「滚动语义」之间的微妙关系并掌握如何用一行「重新测量」消除整类 stale-measurement 缺陷。问题现象吸顶之前正常吸顶之后失准Issue #2236 的场景描述非常精确一个可拖拽元素位于position: sticky容器内页面尚未滚动到吸顶点时通过dragControls.start(event, { snapToCursor: true })触发拖拽元素会精确吸附到指针位置一旦 sticky 容器进入「吸顶stuck」状态再触发拖拽元素在垂直方向上出现明显偏移且不再跟随指针移动。计划文档明确指出snapToCursor: true是「点击即吸附到指针」的唯一入口路径见 VisualElementDragControls.ts 中DragControlOptions接口定义因此修复范围被精准圈定在VisualElementDragControls这一个类内。该 issue 与 #1535 以及若干布局位移变体同属一个「滚动后陈旧测量」问题家族与本仓库的另一个计划 plans/issues/issue-2024.md嵌套滚动容器中 ref 约束偏移共享根因测量发生在挂载时滚动之后不再刷新。根因分析陈旧的页面盒测量遇上 sticky 的滚动语义snap 逻辑读取的测量数据何时被捕获在start()的onSessionStart回调中当前代码 VisualElementDragControls.ts拖拽会话一旦开始就立即调用this.snapToCursor(event)中间没有任何重新测量的机会const onSessionStart (event: PointerEvent) { if (snapToCursor) this.snapToCursor(event) this.stopAnimation() }而snapToCursor内部计算「元素中心对齐指针」所需的盒子数据其测量时机在监听器建立阶段addListeners()中只有当projection尚未拥有layout时才会调用一次projection.updateLayout()当前代码 VisualElementDragControls.ts此后手势开始前再也不会刷新if (projection !projection!.layout) { projection.root projection.root.updateScroll() projection.updateLayout() }计划文档给出的原版公式为axisValue.set(point[axis] - mixNumber(min, max, 0.5) current)其中point取自 event-info.ts 中的pageX/pageY页面坐标系layoutBox则假设是包含当前 transform 的当前页面盒。在计划撰写时的代码树中该布局数据来自projection.layout——即挂载时测量、此后未再刷新的快照。需要说明的是当前仓库树中的snapToCursor实现VisualElementDragControls.ts已改为通过measureViewportBox()视口坐标系实时读取盒子并借getTransformPagePoint()对指针点做变换归一化但「测量与手势开始的时序关系」「页面坐标系与视口坐标系的差异在吸顶元素上不再互相抵消」这两个核心矛盾完全一致本方案针对的就是这套测量时序与坐标系语义。为什么偏偏是吸顶的 sticky 元素出问题关键在于「页面盒 视口盒 根滚动偏移」这一换算关系见 create-projection-node.ts 的measurePageBox()measurePageBox() { const { visualElement } this.options if (!visualElement) return createBox() const box visualElement.measureViewportBox() const wasInScrollRoot this.scroll?.wasRoot || this.path.some(checkNodeWasScrollRoot) if (!wasInScrollRoot) { // Remove viewport scroll to give page-relative coordinates const { scroll } this.root if (scroll) { translateAxis(box.x, scroll.offset.x) translateAxis(box.y, scroll.offset.y) } } return box }据此推导出两种截然不同的滚动行为普通元素视口盒随滚动同步变化加回滚动偏移后得到的页面盒是「滚动不变量」——因此挂载时的测量永远有效吸顶stuck的 sticky 元素视口盒反而变成滚动不变量其真实页面盒会随着越过吸顶点后的每一个滚动像素而改变。于是挂载时缓存的projection.layout恰好陈旧了scrollY - stickPoint那么多像素——这与上报症状吸顶后才出现垂直偏移完全吻合。这也解释了为什么同类的 #2829页面加载即恢复滚动位置问题只影响 ref 约束场景普通元素对根滚动天然免疫而吸顶元素把「视口坐标」与「页面坐标」的差异从常数变成了随滚动变化的变量。现成的修复先例resolveRefConstraints 的根滚动缓存清理方案并非从零发明同一文件里已经存在一模一样的修复模式。resolveRefConstraints()VisualElementDragControls.ts在测量 ref 约束元素之前会先清空投影根节点的滚动缓存并强制重读以绕过按animationId缓存的滚动值对应提交cfccb0300由 issue-2024 / #2829 引入if (projection.root) { projection.root.scroll undefined projection.root.updateScroll() }该注释道出了本质「拖拽挂载时捕获的滚动在文档随后滚动之后可能已陈旧——例如浏览器在刷新时恢复滚动位置或祖先布局副作用在本元素挂载之后才运行」。snap-to-cursor 的修复方案正是把这一「先清缓存、再刷新滚动」的纪律迁移到snapToCursor上。修复方案吸附前重新测量当前页面盒Step 2在 snapToCursor 顶部刷新测量在snapToCursor的eachAxis循环之前插入重新测量逻辑计划文档给出的核心补丁const { projection } this.visualElement if (projection) { if (projection.root) { projection.root.scroll undefined projection.root.updateScroll() } projection.updateLayout() }其原理链如下updateLayout()会重新执行measure(false)见 create-projection-node.ts产出「包含当前 transform、反映当前滚动位置」的页面盒——这正是既有 current公式所假设的数据形态根滚动缓存的清空与resolveRefConstraints()第 420–423 行完全镜像确保measure(false)读到的是实时滚动值而不是按animationId缓存的旧值。一个值得注意的副作用updateLayout()在测量完成后会触发投影节点的measure事件create-projection-node.ts。该事件被addListeners()中的measureDragConstraints监听进而调用resolveRefConstraints()VisualElementDragControls.ts。对吸顶元素而言这同时顺带刷新了 ref 约束——是期望中的收益但计划文档提醒Step 3 的回归套件必须观察是否存在「双重解析」的副作用例如每帧重复解析、约束被反复重置。测试先行Cypress 失败测试的设计与坐标纪律计划的核心执行纪律是「先写失败测试再写修复」对应 issue-2024 计划中的 no-repro 规则。由于该 bug 强依赖 sticky 吸顶后的滚动状态测试页与断言设计都很有讲究。测试页drag-snap-to-cursor-sticky.tsx测试页dev/react/src/tests/drag-snap-to-cursor-sticky.tsx导出App需新建以既有的 drag-snap-to-cursor.tsx外部触发式dragControls.start(e, { snapToCursor: true })的典型示范页为蓝本页面主体内容高 3000px确保可滚动顶部 300px 的 spacer之后是 sticky 容器div style{{ position: sticky, top: 0, height: 200 }}内含一个 200×200 的指针触发区idtriggeronPointerDown{(e) dragControls.start(e, { snapToCursor: true })}motion.div>cy.get(#trigger).trigger(pointerdown, { clientX: 150, clientY: 100, pageX: 150, pageY: 700, force: true, })跟随一次小幅pointermove5, 5同样遵守双坐标系纪律与pointerup.wait(200)用.then()断言拖拽元素getBoundingClientRect()中心点约等于客户端坐标 (155, 105)容差 ±10。修复前预期 y 中心偏差约为「600 减去吸顶距离」需要记录实际差值。计划文档给出明确的验证预期该 spec 在计划基线提交42bfbe3ed上必须失败出现垂直偏移。如果构造不出失败先排查pageY是否真正传到了页面的onPointerDown处理器在页面里console.log验证尝试 2–3 种变体不同滚动量、不同吸顶偏移仍失败则按 no-repro 规则 STOP并判断是合成事件的坐标管线问题还是 Electron/Chromium 的 sticky 行为问题可尝试--browser chrome。回归套件与命令用途命令预期构建yarn build仓库根目录exit 0Cypress React 18/19CLAUDE.md 中的配方--spec cypress/integration/drag-snap-to-cursor-sticky.ts修复后通过Jest 拖拽单测npx jest --config packages/framer-motion/jest.config.json --testPathPatterngestures/drag全部通过回归验证还包括新 spec 在 React 19 上通过既有drag.ts、drag-ref-constraints-absolute-scrolled.ts#2829 的门禁测试与本修复共享代码路径必须保持绿色、drag-ref-constraints-element-resize.ts、drag-tabs.ts在 React 18 上通过。遇到偶发失败先重跑一次同一 spec 连续两次失败即视为真实回归STOP。测试计划要求在同一个 spec 中覆盖两个分支吸顶状态bug 本体与未滚动状态scroll0第二测试防止修复回归正常路径并在 React 18 与 19 双版本上运行。范围边界与停止条件计划文档对改动范围做了严格约束范围内packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts仅snapToCursor和/或onSessionStart新建dev/react/src/tests/drag-snap-to-cursor-sticky.tsx新建packages/framer-motion/cypress/integration/drag-snap-to-cursor-sticky.ts。范围外让投影系统通用地建模 sticky/fixed 定位约束、sticky 下的布局动画——这属于投影层级别的独立项目剩余缺口应在 PR 描述中注明PanSession、动量、约束解析逻辑。停止条件Step 1 无法产出失败测试按 no-repro 规则报告修复需要改动create-projection-node.ts来建模 sticky超出范围上报snapToCursor中的updateLayout()破坏了布局动画类 Cypress speclayout.ts系列——此时备选方案是改用本地测量getBoundingClientRect 实时滚动而不写回projection.layout但这会偏离 current公式的假设需要维护者决策。维护备注与后续演进该修复只解决 snap-to-cursor。sticky 下的拖拽约束仍然近似要等到下一次measure事件才刷新完整的 sticky/fixed 故事属于投影系统应作为已知限制记录在 PR 中。本文件处于计划 019–021拖拽引擎移植到 motion-dom的覆盖范围内re-measure-on-snap行为必须随移植保留——它同时也是计划 020 中 vanilladrag()想要的正确行为。交付完成标准包括新 spec吸顶 未吸顶两分支在 React 18/19 通过、吸顶分支在修复前失败、既有拖拽 Cypress 与 Jest 套件通过、仅改动范围内文件、并在 plans/issues/README.md 中更新本 issue 的状态行。延伸阅读plans/issues/issue-2236.md本方案原始计划文档含完整命令表与 STOP 条件VisualElementDragControls.ts拖拽控制核心snapToCursor/resolveRefConstraints/addListeners均在此create-projection-node.ts投影节点measurePageBox页面盒换算与updateLayout测量事件measure.ts视口盒 / 页面盒测量的底层工具event-info.tspageX/pageY页面坐标系指针信息的来源drag-snap-to-cursor.tsx外部触发式 snap-to-cursor 的既有示范页drag-ref-constraints-absolute-scrolled.tsx?scroll参数 useLayoutEffect滚动模拟模式的出处plans/issues/issue-2024.md同家族「陈旧测量」问题的姊妹计划提供cfccb0300缓存清理先例赞分享前端UI组件【免费下载链接】motionA modern animation library for React and JavaScript项目地址https://gitcode.com/GitHub_Trending/mo/motion点击查看免费下载相关推荐Fluent Bit 内嵌的 lwrb 轻量环形缓冲区库从 v1.0.0 到 v2.0.3 的版本演进与实现原理解析Fluent Bit 内嵌的 lwrb 轻量环形缓冲区库从 v1.0.0 到 v2.0.3 的版本演进与实现原理解析 导读 lwrbLightweight前端UI组件深入 go-openapi/jsonpointernhost 仓库中基于 Go 结构体的 JSON PointerRFC 6901实现解析与实战深入 go openapi/jsonpointernhost 仓库中基于 Go 结构体的 JSON PointerRFC 6901实现解析与实战 导读 本前端UI组件上一篇Vuetify UnoCSS (presetWind4) 集成指南以 TailwindCSS v4 类名按需生成样式下一篇CANN/pypto Matmul性能优化指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

DPDK性能调优实战:绕开BIOS、NUMA、Cache伪共享等90%翻车点

DPDK性能调优实战:绕开BIOS、NUMA、Cache伪共享等90%翻车点

简介:本资源是《深入浅出DPDK》一书的系统性读书笔记PDF,面向网络高性能编程初学者、DPDK开发工程师及NFV/SDN领域技术人员,聚焦解决传统内核态网络栈在万兆以上场景下的中断开销大、吞吐瓶颈等核心问题。笔记完整覆盖DPDK基础原理&#xff0…

2026/9/30 10:53:23 阅读更多 →
伯努利方程详解:从能量守恒到工程应用

伯努利方程详解:从能量守恒到工程应用

想了很久,还是决定把这篇文章写出来。作为一个流体力学方向的老学生,我见过太多人卡在伯努利方程这道坎上,包括我自己当年也在这里栽过跟头。书上的推导看了好几遍,公式背得滚瓜烂熟,可一遇到实际问题,断面…

2026/9/30 10:53:23 阅读更多 →
500-AI-Agents-Projects 简历解析 Agent 实战:基于 LangChain 与 GPT-4o-mini 实现简历结构化抽取与候选人匹配评分

500-AI-Agents-Projects 简历解析 Agent 实战:基于 LangChain 与 GPT-4o-mini 实现简历结构化抽取与候选人匹配评分

示例工程人工智能 【免费下载链接】500-AI-Agents-Projects The 500 AI Agents Projects is a curated collection of AI agent use cases across various industries. It showcases practical applications and provides links to open-source projects for implementation, i…

2026/9/30 10:52:22 阅读更多 →

最新新闻

连点器全攻略:鼠标自动连点、键盘输入与免费工具筛选

连点器全攻略:鼠标自动连点、键盘输入与免费工具筛选

连点器这三个字一出来,很多人第一反应是"游戏挂机脚本",但说实话我用了这么多年,鼠标自动连点真正帮我省下大把时间的场景,反而是那些枯燥到让人怀疑人生的重复工作:一遍遍点"下一步"、批量填表、…

2026/10/1 12:50:58 阅读更多 →
Jev模型结构化输出实战:JSON Schema约束如何让判断题成本直降5倍

Jev模型结构化输出实战:JSON Schema约束如何让判断题成本直降5倍

1. 从一张账单说起:为什么同一个判断题,成本能差 5 倍 先把场景摆出来。我最近在做一个批量内容审核的小工具,核心逻辑特别简单:给一段文本,让模型判断它是否属于某个类别,输出只有两个值——是或否。这种任…

2026/10/1 12:50:58 阅读更多 →
ECC公钥压缩与非压缩格式:汽车电子选型与避坑指南

ECC公钥压缩与非压缩格式:汽车电子选型与避坑指南

1. 汽车电子里的ECC公钥,为什么格式值得拿出来单聊做车载安全的人应该都有感触:ECC(椭圆曲线密码)在车联网和汽车电子里的应用已经非常密集,V2X车路协同、OTA升级签名、安全启动、SecOC报文认证、PKI证书链&#xff0c…

2026/10/1 12:50:58 阅读更多 →
IEC 62351-100-3一致性测试全解析:从安全机制到工程实践

IEC 62351-100-3一致性测试全解析:从安全机制到工程实践

拿到IEC 62351-100-3一致性测试任务的时候,我第一反应是找标准原文、打开产品功能清单,准备一项一项打勾。真正进了实验室才发现,这套测试折腾的根本不是“功能是否正常”,而是“产品对安全机制的理解是否和标准一致”。业务功能正…

2026/10/1 12:50:58 阅读更多 →
Wine+FEX-Emu+DXMT:在Apple Silicon上运行Windows游戏的完整指南

Wine+FEX-Emu+DXMT:在Apple Silicon上运行Windows游戏的完整指南

1. 从“Madeira”这个名字说起:它到底指什么第一次看到“Madeira”这个词,很多人第一反应是葡萄牙那个盛产葡萄酒的海岛,或者是一道叫“马德拉酱汁”的西餐配料。但在技术圈里,尤其是围绕 Wine、FEX-Emu、DXMT 这些关键词的讨论中…

2026/10/1 12:50:58 阅读更多 →
Ubuntu Desktop 安装全流程避坑指南:从分区到驱动配置

Ubuntu Desktop 安装全流程避坑指南:从分区到驱动配置

Ubuntu Desktop 的安装,单看流程图确实简单到让人提不起兴趣:下镜像、写U盘、引导、分区、下一步。但真正动手装过十几台机器之后你会发现,翻车的点几乎全在"下一步"之外——UEFI 和 Legacy 引导混着用导致装完进不去系统、硬盘分区…

2026/10/1 12:49:58 阅读更多 →

日新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/1 0:00:30 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/1 0:00:30 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/1 1:01:17 阅读更多 →

周新闻

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/9/30 13:14:22 阅读更多 →
SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/9/30 18:13:06 阅读更多 →
FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏 【免费下载链接】FireRed-OpenStoryline FireRed-OpenStoryline is an AI video editing agent that transforms manual editing into intention-driven directing through natural language …

2026/9/30 13:14:49 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/1 0:00:30 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/1 0:00:30 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/1 1:01:17 阅读更多 →