close
Skip to content

fix: prevent scrolling to top when clicking node#1018

Merged
zombieJ merged 4 commits into
react-component:masterfrom
aojunhao123:fix/a11y-20260307
Mar 10, 2026
Merged

fix: prevent scrolling to top when clicking node#1018
zombieJ merged 4 commits into
react-component:masterfrom
aojunhao123:fix/a11y-20260307

Conversation

@aojunhao123
Copy link
Copy Markdown
Contributor

@aojunhao123 aojunhao123 commented Mar 7, 2026

之前在这个 pr 里重构了一下 tree 的 a11y 实现,a11y规范中有个约定就是:tree 接收到焦点时,focus ring 给到被选中的第一个节点,如果没有被选中的节点则 fallback 到第一个节点。然后这里就出问题了:初始态一棵无选中节点的树 -> 用户点击节点,触发容器 focus -> onFocus 中根据 selectedKey 判断是否要 fallback,但此时还没触发点击事件,selectedKeynull,于是 fallback 到第一个树节点,onActiveChange 会触发 scrollTo({ firstNodeKey }) -> 滚动到顶部

解法就是利用 mousedown 判断一下焦点触发来源,鼠标点击触发的 focus 不走 a11y 焦点规范。

btw,TDD 是对的

update:目前 tree 的 a11y 还有点小缺陷,比如:

  1. 用户先鼠标选中某个节点,然后再使用键盘导航,此时 focus ring 应该从选中项切换到下一个,也就是键盘和鼠标的操作要能够自然切换
  2. 虚拟滚动情况下读屏器无法读出完整的节点层级和数量,这个要通过 aria 来解
  3. 不支持 type-ahead,这个不知道中文场景下会不会有特殊 case 想起来 antd 本身是支持 search 功能的,type-ahead 在使用的功能性上还是太弱了,而且 w3c 官方对它的实现规范目前其实还有不明确的地方,比如这个 issue。综上,个人感觉 search 是 type-ahead 的 promax 版,所以没必要提供 type-ahead 能力了,鸡肋

先 mark 一下,后面有空了一个个补上。都做完了的话 tree 的 a11y 应该基本上是完善了(大概

Summary by CodeRabbit

  • 新功能

    • 为树组件新增 onMouseDown/onMouseUp 鼠标事件回调并改进基于鼠标的聚焦追踪,鼠标交互更可控。
    • 节点列表增加同样的鼠标事件支持,事件可向外转发。
  • 问题修复

    • 修复在点击节点并聚焦时导致的不必要滚动到顶部行为。
  • 测试

    • 优化断言并新增覆盖点击聚焦不触发滚动的测试。
  • 文档

    • 示例中调整了树组件初始渲染高度。

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 7, 2026

@aojunhao123 is attempting to deploy a commit to the React Component Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 7, 2026

Walkthrough

引入鼠标交互追踪(focusedByMouse 与计时器),在 Tree/NodeList 上新增并透传 onMouseDownonMouseUp 公共回调;调整聚焦逻辑以忽略由鼠标触发的 focus;新增/修正相关测试并在示例中调整高度属性。

Changes

Cohort / File(s) Summary
Tree 组件与聚焦逻辑
src/Tree.tsx
新增 TreePropsonMouseDown?onMouseUp?,引入 focusedByMouse 状态与计时器(raf),在 onMouseDown/onMouseUp/onBlur/onFocus 中处理聚焦来源并将鼠标事件转发给外部回调;卸载时清理计时器。
节点列表事件透传
src/NodeList.tsx
NodeListProps 新增 onMouseDown?onMouseUp?,并将它们透传给内部 VirtualList/容器,从而支持在节点列表层级捕获鼠标按下/抬起事件。
测试与示例
tests/Tree.spec.tsx, docs/examples/basic.jsx
测试中将 Jest 断言改为 toHaveBeenCalled();新增“点击节点且树已聚焦时不滚动到顶部”测试;示例中为第一个 Tree 实例添加 height={150}

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Tree as Tree Component
    participant NodeList as NodeList / VirtualList
    participant External as External Callback

    User->>NodeList: mouseDown on node
    NodeList->>Tree: onMouseDown event propagated
    Tree->>Tree: set focusedByMouse = true (start raf timer)
    Tree->>External: call props.onMouseDown?()
    Note over Tree: If focus event occurs while focusedByMouse === true\nonFocus ignores activate-like behavior
    User->>NodeList: mouseUp on node
    NodeList->>Tree: onMouseUp event propagated
    Tree->>External: call props.onMouseUp?()
    Tree->>Tree: onBlur -> focusedByMouse = false (clear timer)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • refactor: better a11y #1001: 修改/重构与焦点处理、active/聚焦行为相关的代码路径,可能与本 PR 的聚焦变更产生交互。

Suggested reviewers

  • afc163
  • zombieJ

Poem

🐇 鼠标轻点不惊慌,
焦点明晰藏时光,
计时悄守不回荡,
节点稳住不乱忙,
兔儿跳跃来庆藏。

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main fix: preventing unwanted scrolling to top when clicking tree nodes, which directly addresses the core issue.
Linked Issues check ✅ Passed Code changes implement the required fix for #1017 by tracking mouse focus origin and preventing auto-scroll when focus comes from mouse clicks on unselected nodes.
Out of Scope Changes check ✅ Passed The height prop addition in basic.jsx example is a minor documentation change unrelated to the core fix but does not impede the PR's primary objective of fixing the scroll-to-top issue.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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 and usage tips.

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求修复了一个在点击树节点时可能导致树滚动到顶部的错误。该问题源于之前对树的可访问性重构,当树没有选中节点时,onFocus 事件会错误地触发回退滚动行为。通过引入一个机制来区分鼠标和键盘触发的焦点事件,本修复确保了只有在键盘导航时才执行回退滚动,从而避免了不必要的滚动。

Highlights

  • 解决意外滚动问题: 解决了点击树节点时意外滚动到顶部的问题,特别是在没有初始选中节点的情况下。
  • 区分焦点来源: 引入了区分鼠标和键盘焦点的机制,以防止 onFocus 事件过早触发 scrollTo
  • 新增鼠标事件处理: 新增了 onMouseDown 事件处理程序和 focusedByMouse 状态,并使用 raf (requestAnimationFrame) 辅助管理此状态的重置。
  • 新增测试用例: 增加了新的测试用例以验证此修复,确保点击节点时不会触发不必要的滚动。

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • src/Tree.tsx
    • 引入了 raf 工具函数。
    • TreeProps 中添加了 onMouseDown 属性。
    • 新增了 focusedByMouse 状态和 pointerTimer,用于管理鼠标触发的焦点。
    • 在组件卸载时取消了 pointerTimer
    • 实现了 onMouseDown 事件处理程序,用于设置 focusedByMouse 并在短时间后通过 raf 重置。
    • 修改了 onFocus 逻辑,使其仅在非鼠标触发焦点时执行回退滚动。
    • onMouseDown 属性传递给了 NodeList 组件。
  • tests/Tree.spec.tsx
    • 将现有测试中的 toBeCalled() 更新为 toHaveBeenCalled()
    • 添加了一个新的测试用例,以验证当点击节点且树获得焦点时,scrollTo 不会被调用。
Activity
  • 此拉取请求旨在修复 BUG: @rc-component/tree@1.2.3 can't select tree node when virtual(height) enabled #1017
  • 该问题是由于之前对树的可访问性实现进行重构后引入的。
  • 具体来说,当一棵没有选中节点的树(activeKey 为 null)被点击时,容器会获得焦点,onFocus 事件会根据 selectedKey 判断是否需要回退,但此时点击事件尚未触发,导致回退到第一个树节点并立即调用 scrollTo,从而滚动到顶部。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 7, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.84%. Comparing base (c95025e) to head (1039693).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1018   +/-   ##
=======================================
  Coverage   99.84%   99.84%           
=======================================
  Files          14       14           
  Lines        1293     1303   +10     
  Branches      396      396           
=======================================
+ Hits         1291     1301   +10     
  Misses          2        2           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

这个 PR 很好地解决了在未选中状态下点击节点时,树会滚动到顶部的问题。通过在 onMouseDown 中设置 focusedByMouse 标志,并在 onFocus 中检查该标志来区分鼠标触发的聚焦和键盘触发的聚焦,这是一个可靠的解决方案。使用 requestAnimationFrame 来重置标志也是合适的。新增的测试用例也正确地验证了此修复。我有几个关于定时器取消逻辑的小建议,以提高代码的健壮性。

Comment thread src/Tree.tsx Outdated
Comment thread src/Tree.tsx Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/Tree.spec.tsx (1)

1415-1433: 测试逻辑正确,建议添加 spy 清理。

测试用例正确模拟了问题场景:鼠标点击触发的 focus 不应引发 scrollTo。建议在测试结束时恢复 spy,避免潜在的测试污染:

♻️ 建议添加 spy 清理
     // Simulate pointer focus without existing selectedKeys
     fireEvent.mouseDown(treeContainer);
     fireEvent.focus(treeContainer);

     expect(scrollToSpy).not.toHaveBeenCalled();
+
+    scrollToSpy.mockRestore();
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Tree.spec.tsx` around lines 1415 - 1433, The test creates a spy on
treeRef.current.scrollTo (stored as scrollToSpy) but never restores it, which
can leak into other tests; update the test file (tests/Tree.spec.tsx) to restore
the spy after use—either call scrollToSpy.mockRestore() at the end of this it
block or add a global afterEach that calls jest.restoreAllMocks()—so that the
scrollTo spy is cleaned up and does not affect other tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Tree.tsx`:
- Line 1540: Add an explicit onMouseDown prop to the NodeListProps interface to
restore type safety: declare onMouseDown?:
React.MouseEventHandler<HTMLDivElement>; in the NodeListProps definition
(alongside existing onKeyDown/onFocus/onBlur), and ensure it is forwarded via
domProps to VirtualList where onMouseDown={this.onMouseDown} is used so the
component and callers get proper typings.

---

Nitpick comments:
In `@tests/Tree.spec.tsx`:
- Around line 1415-1433: The test creates a spy on treeRef.current.scrollTo
(stored as scrollToSpy) but never restores it, which can leak into other tests;
update the test file (tests/Tree.spec.tsx) to restore the spy after use—either
call scrollToSpy.mockRestore() at the end of this it block or add a global
afterEach that calls jest.restoreAllMocks()—so that the scrollTo spy is cleaned
up and does not affect other tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d482dc13-e90f-4ca5-943c-890a53bfd16f

📥 Commits

Reviewing files that changed from the base of the PR and between c95025e and 7b0ec5b.

📒 Files selected for processing (2)
  • src/Tree.tsx
  • tests/Tree.spec.tsx

Comment thread src/Tree.tsx
Comment thread tests/Tree.spec.tsx
Comment on lines +1415 to +1439
it('should not scroll to top when click node and tree is focused', () => {
jest.useFakeTimers();
const data = [
{ key: '0', title: '0' },
{ key: '1', title: '1' },
{ key: '2', title: '2' },
];
const treeRef = React.createRef<any>();
const { container } = render(<Tree ref={treeRef} treeData={data} />);
const treeContainer = container.querySelector('.rc-tree-list');
const scrollToSpy = jest.spyOn(treeRef.current, 'scrollTo');

// Simulate pointer focus without existing selectedKeys
fireEvent.mouseDown(treeContainer);
expect(treeRef.current.focusedByMouse).toBe(true);
fireEvent.focus(treeContainer);

act(() => {
jest.runAllTimers();
});

expect(treeRef.current.focusedByMouse).toBe(false);
expect(scrollToSpy).not.toHaveBeenCalled();
jest.useRealTimers();
});
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsdom 里模拟不了真实滚动行为,所以这里的 case 测了内部的实现细节

Comment thread src/Tree.tsx
const { activeKey, selectedKeys, flattenNodes } = this.state;

if (!disabled && activeKey === null) {
if (!this.focusedByMouse && !disabled && activeKey === null) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

如果点击事件区分才执行下面逻辑的话,看起来下面逻辑就是不充分的。那岂不也可以直接去掉交给点击和键盘自行操作么

@aojunhao123
Copy link
Copy Markdown
Contributor Author

同意用 raf 去猜时序确实比较 hack 且容易引发玄学问题。梳理了一下 DOM 的原生事件流,打算废弃定时器,改用原生的生命周期事件来管理:

  1. onMouseDown 标记 focusedByMouse = true
  2. onMouseUp 中重置为 false(利用 mousedown -> focus -> mouseup 的严格同步顺序,精准跳过 focus 时的滚动逻辑)。
  3. 同时在 onBlur 中也重置为 false,兜底防止用户鼠标按住拖拽到组件外松开导致的事件丢失。
    这样既能精准区分焦点来源,又完全剥离了定时器的时序风险。更新了一版,你看这个方案如何?

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
docs/examples/basic.jsx (1)

103-109: 建议把回归场景放到独立示例里,不要改写 simple 示例语义。

这里给第一个 Tree 直接加 height={150},会把“simple”示例也切成虚拟滚动版本;但它仍然带着 defaultSelectedKeys,并不对应 #1017 的“初始无选中节点”前提。更稳妥的是保留这个基础示例不变,另外新增一个专门的 virtual/no-selected 示例来承载这次回归场景。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/examples/basic.jsx` around lines 103 - 109, The first Tree in the
"simple" example was changed to use height={150}, turning it into a virtualized
tree while still carrying defaultSelectedKeys which violates the regression test
premise; revert the height={150} addition in the existing Tree (the one using
ref={this.setTreeRef}, className="myCls", showLine, checkable, defaultExpandAll,
and any defaultSelectedKeys) to preserve the original "simple" semantics, and
instead add a new dedicated example (e.g., virtual-no-selected) that renders a
Tree with height={150} (and omits defaultSelectedKeys) to reproduce the
regression scenario.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@docs/examples/basic.jsx`:
- Around line 103-109: The first Tree in the "simple" example was changed to use
height={150}, turning it into a virtualized tree while still carrying
defaultSelectedKeys which violates the regression test premise; revert the
height={150} addition in the existing Tree (the one using ref={this.setTreeRef},
className="myCls", showLine, checkable, defaultExpandAll, and any
defaultSelectedKeys) to preserve the original "simple" semantics, and instead
add a new dedicated example (e.g., virtual-no-selected) that renders a Tree with
height={150} (and omits defaultSelectedKeys) to reproduce the regression
scenario.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4d649eee-f814-407b-b7e9-ccb1c2ff6a3a

📥 Commits

Reviewing files that changed from the base of the PR and between 56efd2a and 1039693.

📒 Files selected for processing (4)
  • docs/examples/basic.jsx
  • src/NodeList.tsx
  • src/Tree.tsx
  • tests/Tree.spec.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/Tree.spec.tsx
  • src/Tree.tsx

@aojunhao123
Copy link
Copy Markdown
Contributor Author

aojunhao123 commented Mar 9, 2026

这 cursor 怎么未经允许擅自 commit 并且评论…🌚

@zombieJ zombieJ merged commit 1519688 into react-component:master Mar 10, 2026
7 of 8 checks passed
This was referenced Mar 29, 2026
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.

BUG: @rc-component/tree@1.2.3 can't select tree node when virtual(height) enabled

2 participants