Skip to content

fix: skip drag-scroll when the target or its ancestor is draggable - #386

Merged
zombieJ merged 1 commit into
react-component:masterfrom
Ye-YiChen:fix-skip-drag-scroll-draggable-ancestor
Sep 17, 2026
Merged

zombieJ merged 1 commit into
react-component:masterfrom
Ye-YiChen:fix-skip-drag-scroll-draggable-ancestor

Conversation

@Ye-YiChen

@Ye-YiChen Ye-YiChen commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

🤔 This is a ...

  • 🆕 New feature
  • 🐞 Bug fix
  • 📝 Site / documentation improvement
  • 📽️ Demo improvement
  • 💄 Component style improvement
  • 🤖 TypeScript definition improvement
  • 📦 Bundle size optimization
  • ⚡️ Performance optimization
  • ⭐️ Feature enhancement
  • 🌐 Internationalization improvement
  • 🛠 Refactoring
  • 🎨 Code style optimization
  • ✅ Test Case
  • 🔀 Branch merge
  • ⏩ Workflow
  • ⌨️ Accessibility improvement
  • ❓ Other (about what?)

🔗 Related Issues

Follow-up of #304 (which fixed ant-design#52774)
Part of ant-design/ant-design#58508 (Tree virtual scroll + drag)

💡 Background and Solution

useScrollDrag skips the drag-scroll when the mousedown target is draggable, so that a native HTML5 drag does not leave the list auto-scrolling after mouseup.

The current check only reads e.target.draggable. In the DOM event path e.target is the deepest node, while draggable is normally set on the ancestor being dragged. So for any draggable element that has children — e.g. <li draggable><span class="rc-tree-title">…</span></li> in rc-tree, or a <tr draggable> in Table — the check never matches, mouseDownLock is set anyway, and the list keeps scrolling after the drag ends.

Fix: walk up from e.target through parentElement and read the draggable IDL attribute, so implicitly draggable ancestors (a[href], img) are covered too — they are draggable without carrying a draggable attribute, so an attribute-based selector would miss them.

The existing test added in #304 fires mousedown directly on the <li draggable> (which has no child element), so e.target === li and the check passes — that is not how the browser dispatches the event, which is why the gap stayed unnoticed.

✅ Verification

Red / green double run on the new cases:

implementation child of <li draggable> child of <a href="#">
before (e.target.draggable) ❌ scrolled 40px ❌ scrolled 40px
attribute selector (closest('[draggable="true"]')) ❌ scrolled 40px
this PR (IDL walk-up)

Full suite: 9 suites / 284 tests passed. tsc --noEmit clean, eslint 0 errors, prettier clean.

📝 Change Log

Language Changelog
🇺🇸 English Fix virtual-list drag scroll not being skipped when draggable is set on an ancestor of the mousedown target
🇨🇳 Chinese 修复 virtual-list 在 draggable 设置在 mousedown 目标祖先节点上时未跳过拖拽滚动的问题

Summary by CodeRabbit

  • Bug 修复

    • 修复列表滚动拖动与可拖拽元素交互时的冲突。
    • 在可拖拽元素或其子元素上按下鼠标时,不再意外触发列表滚动。
  • 测试

    • 增加对显式可拖拽元素及链接元素的回归测试,确保列表滚动位置保持不变。

`useScrollDrag` only checked `e.target.draggable`, but the mousedown target
is the deepest node in the event path while `draggable` is set on the ancestor
being dragged (e.g. `<li draggable><span>...</span></li>` in Tree). The
drag-scroll lock was wrongly set in that case and the list kept scrolling.

Walk up the ancestors and read the IDL attribute, so implicitly draggable
elements (`a[href]`, `img`) are covered as well.
@vercel

vercel Bot commented Sep 17, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the afc163's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Sep 17, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: afeb2bf5-659b-4884-b7cc-6edac6a1b1a3

📥 Commits

Reviewing files that changed from the base of the PR and between a7cdfda and a4edc23.

📒 Files selected for processing (2)
  • src/hooks/useScrollDrag.ts
  • tests/scroll.test.js

Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review.


Walkthrough

本次变更检查事件目标及其祖先的可拖动状态。显式可拖动元素和默认可拖动链接的子节点事件不再触发列表滚动,并新增回归测试。

Changes

滚动拖动处理

Layer / File(s) Summary
检查可拖动祖先
src/hooks/useScrollDrag.ts
新增 isDraggable,向上检查事件目标及其祖先的 draggable 属性。onMouseDown 使用该检查结果。
验证子节点拖动场景
tests/scroll.test.js
新增测试,验证显式 draggable 元素和默认可拖动 <a href> 元素的子节点不会触发列表滚动。

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Suggested reviewers: zombiej

Merge Risk: ⚪ Minimal · up to a4edc

The drag filtering change includes regression coverage for draggable descendants, with no identified issue blocking merge.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确概括了主要变更:当鼠标目标或其祖先元素可拖动时,跳过拖动滚动处理。标题简洁、明确,并与代码及测试变更一致。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

小兔挥爪查祖先,
拖动目标不漏看。
链接子节点安静停,
列表位置保持零。
回归测试蹦蹦跳。

Comment @coderabbitai help to get the list of available commands.

@zombieJ
zombieJ merged commit f068a2b into react-component:master Sep 17, 2026
3 of 4 checks passed
Ye-YiChen pushed a commit to Ye-YiChen/tree that referenced this pull request Sep 17, 2026
A virtual Tree never scrolls when a node is dragged near the top/bottom edge.

The virtual list container is `overflow: hidden`, so the browser's native
drag-to-edge autoscroll never kicks in. The JS fallback in rc-virtual-list
(`useScrollDrag`) cannot cover it either: it runs on `mousemove`, which is not
fired during a native HTML5 drag, and it now returns early whenever the target
or one of its ancestors is `draggable` (react-component/virtual-list#386) so
that it does not fight the native drag.

Add `useDragEdgeScroll` to drive the scrolling from the drag events instead:
`dragover` recomputes the offset from the pointer's distance to the edge,
`dragleave` / `drop` / `dragend` cancel it. `drop` and `dragend` are listened on
the document in the capture phase because `TreeNode` stops their propagation in
the bubble phase.

Gated on `virtual !== false` to match the virtual list's own
`useVirtual = !!(virtual !== false && height && itemHeight)`, so an explicit
`virtual={false}` is still left to the browser.
Ye-YiChen pushed a commit to Ye-YiChen/tree that referenced this pull request Sep 17, 2026
A virtual Tree never scrolls when a node is dragged near the top/bottom edge.

The virtual list container is `overflow: hidden`, so the browser's native
drag-to-edge autoscroll never kicks in. The JS fallback in rc-virtual-list
(`useScrollDrag`) cannot cover it either: it runs on `mousemove`, which is not
fired during a native HTML5 drag, and it now returns early whenever the target
or one of its ancestors is `draggable` (react-component/virtual-list#386) so
that it does not fight the native drag.

Add `useDragEdgeScroll` to drive the scrolling from the drag events instead:
`dragover` recomputes the offset from the pointer's distance to the edge,
`dragleave` / `drop` / `dragend` cancel it. `drop` and `dragend` are listened on
the document in the capture phase because `TreeNode` stops their propagation in
the bubble phase.

Gated on `virtual !== false` to match the virtual list's own
`useVirtual = !!(virtual !== false && height && itemHeight)`, so an explicit
`virtual={false}` is still left to the browser.

Behaviour is covered by `NodeListEdgeScroll.spec.tsx`, which drives the real
Tree: both scroll directions, returning to the idle zone, `drop`, `dragend`
outside the container, `dragleave` (leaving stops, moving between inner nodes
does not), the band boundary where the offset is 0, and re-entering the band
while the loop already runs. `useDragEdgeScroll.spec.tsx` covers the two
defensive branches that cannot be reached through the Tree — an unmounted list
and a list torn down mid-scroll. 100% statement / branch / function / line
coverage on the hook.
Ye-YiChen pushed a commit to Ye-YiChen/tree that referenced this pull request Sep 17, 2026
A virtual Tree never scrolls when a node is dragged near the top/bottom edge.

The virtual list container is `overflow: hidden`, so the browser's native
drag-to-edge autoscroll never kicks in. The JS fallback in rc-virtual-list
(`useScrollDrag`) cannot cover it either: it runs on `mousemove`, which is not
fired during a native HTML5 drag, and it now returns early whenever the target
or one of its ancestors is `draggable` (react-component/virtual-list#386) so
that it does not fight the native drag.

Add `useDragEdgeScroll` to drive the scrolling from the drag events instead:
`dragover` recomputes the offset from the pointer's distance to the edge,
`dragleave` / `drop` / `dragend` cancel it. `drop` and `dragend` are listened on
the document in the capture phase because `TreeNode` stops their propagation in
the bubble phase.

Gated on `virtual !== false` to match the virtual list's own
`useVirtual = !!(virtual !== false && height && itemHeight)`, so an explicit
`virtual={false}` is still left to the browser.

Behaviour is covered by `NodeListEdgeScroll.spec.tsx`, which drives the real
Tree: both scroll directions, returning to the idle zone, `drop`, `dragend`
outside the container, `dragleave` (leaving stops, moving between inner nodes
does not), the band boundary where the offset is 0, and re-entering the band
while the loop already runs. `useDragEdgeScroll.spec.tsx` covers the two
defensive branches that cannot be reached through the Tree — an unmounted list
and a list torn down mid-scroll. 100% statement / branch / function / line
coverage on the hook.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants