From 7b0ec5b8c8824c1d8a5129f9bdb3af74179ef13f Mon Sep 17 00:00:00 2001 From: aojunhao123 <1844749591@qq.com> Date: Sat, 7 Mar 2026 18:00:46 +0800 Subject: [PATCH 1/4] fix: prevent scrolling to top when clicking node --- src/Tree.tsx | 19 ++++++++++++++++++- tests/Tree.spec.tsx | 24 ++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/Tree.tsx b/src/Tree.tsx index ca7fcd6b..b4d17244 100644 --- a/src/Tree.tsx +++ b/src/Tree.tsx @@ -4,6 +4,7 @@ import { clsx } from 'clsx'; import pickAttrs from '@rc-component/util/lib/pickAttrs'; import warning from '@rc-component/util/lib/warning'; +import raf from '@rc-component/util/lib/raf'; import * as React from 'react'; import type { @@ -118,6 +119,7 @@ export interface TreeProps { allowDrop?: AllowDrop; titleRender?: (node: TreeDataType) => React.ReactNode; dropIndicatorRender?: (props: DropIndicatorProps) => React.ReactNode; + onMouseDown?: React.MouseEventHandler; onFocus?: React.FocusEventHandler; onBlur?: React.FocusEventHandler; onKeyDown?: React.KeyboardEventHandler; @@ -312,6 +314,9 @@ class Tree extends Rea currentMouseOverDroppableNodeKey = null; + focusedByMouse = false; + pointerTimer: number | null = null; + listRef = React.createRef(); componentDidMount(): void { @@ -337,6 +342,7 @@ class Tree extends Rea componentWillUnmount() { window.removeEventListener('dragend', this.onWindowDragEnd); + raf.cancel(this.pointerTimer); this.destroyed = true; } @@ -1063,11 +1069,21 @@ class Tree extends Rea } }; + onMouseDown: React.MouseEventHandler = event => { + const { onMouseDown } = this.props; + this.focusedByMouse = true; + raf.cancel(this.pointerTimer); + this.pointerTimer = raf(() => { + this.focusedByMouse = false; + }); + onMouseDown?.(event); + }; + onFocus: React.FocusEventHandler = (...args) => { const { onFocus, disabled } = this.props; const { activeKey, selectedKeys, flattenNodes } = this.state; - if (!disabled && activeKey === null) { + if (!this.focusedByMouse && !disabled && activeKey === null) { const visibleSelectedKey = selectedKeys.find(key => { return flattenNodes.some(nodeItem => nodeItem.key === key); }); @@ -1521,6 +1537,7 @@ class Tree extends Rea tabIndex={tabIndex} activeItem={this.getActiveItem()} onFocus={this.onFocus} + onMouseDown={this.onMouseDown} onBlur={this.onBlur} onKeyDown={this.onKeyDown} onActiveChange={this.onActiveChange} diff --git a/tests/Tree.spec.tsx b/tests/Tree.spec.tsx index fd8f6e35..1ded884e 100644 --- a/tests/Tree.spec.tsx +++ b/tests/Tree.spec.tsx @@ -1164,7 +1164,7 @@ describe('Tree Basic', () => { // trigger click to expand node fireEvent.click(container.querySelector('.rc-tree-switcher')); expect(container.querySelector('.rc-tree-switcher')).toHaveClass(OPEN_CLASSNAME); - expect(onExpand).toBeCalled(); + expect(onExpand).toHaveBeenCalled(); // click again onExpand.mockReset(); @@ -1201,7 +1201,7 @@ describe('Tree Basic', () => { , ); fireEvent.scroll(container.querySelector('.rc-tree-list-holder')); - expect(onScroll).toBeCalled(); + expect(onScroll).toHaveBeenCalled(); }); it('should support rootStyle and rootClassName', () => { @@ -1411,4 +1411,24 @@ describe('Tree Basic', () => { expect(title).toHaveClass(testClassNames.itemTitle); expect(item).toHaveStyle(testStyles.item); }); + + it('should not scroll to top when click node and tree is focused', () => { + const data = [ + { key: '0', title: '0' }, + { key: '1', title: '1' }, + { key: '2', title: '2' }, + ]; + const treeRef = React.createRef(); + const { container } = render(); + + const scrollToSpy = jest.spyOn(treeRef.current, 'scrollTo'); + + const treeContainer = container.querySelector('.rc-tree-list'); + + // Simulate pointer focus without existing selectedKeys + fireEvent.mouseDown(treeContainer); + fireEvent.focus(treeContainer); + + expect(scrollToSpy).not.toHaveBeenCalled(); + }); }); From 617d37fada9b9b310a504e4ac61324659c51a478 Mon Sep 17 00:00:00 2001 From: aojunhao123 <1844749591@qq.com> Date: Sat, 7 Mar 2026 18:26:28 +0800 Subject: [PATCH 2/4] update --- tests/Tree.spec.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/Tree.spec.tsx b/tests/Tree.spec.tsx index 1ded884e..e20a27b0 100644 --- a/tests/Tree.spec.tsx +++ b/tests/Tree.spec.tsx @@ -1413,6 +1413,7 @@ describe('Tree Basic', () => { }); 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' }, @@ -1420,15 +1421,20 @@ describe('Tree Basic', () => { ]; const treeRef = React.createRef(); const { container } = render(); - - const scrollToSpy = jest.spyOn(treeRef.current, 'scrollTo'); - 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(); }); }); From 56efd2a7d29ac26d3d9b362ea04fca5b277536a5 Mon Sep 17 00:00:00 2001 From: aojunhao123 <1844749591@qq.com> Date: Sat, 7 Mar 2026 19:18:27 +0800 Subject: [PATCH 3/4] update --- src/Tree.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Tree.tsx b/src/Tree.tsx index b4d17244..c48b8fcd 100644 --- a/src/Tree.tsx +++ b/src/Tree.tsx @@ -315,7 +315,7 @@ class Tree extends Rea currentMouseOverDroppableNodeKey = null; focusedByMouse = false; - pointerTimer: number | null = null; + mouseTimer: number | null = null; listRef = React.createRef(); @@ -342,7 +342,7 @@ class Tree extends Rea componentWillUnmount() { window.removeEventListener('dragend', this.onWindowDragEnd); - raf.cancel(this.pointerTimer); + raf.cancel(this.mouseTimer); this.destroyed = true; } @@ -1072,8 +1072,8 @@ class Tree extends Rea onMouseDown: React.MouseEventHandler = event => { const { onMouseDown } = this.props; this.focusedByMouse = true; - raf.cancel(this.pointerTimer); - this.pointerTimer = raf(() => { + raf.cancel(this.mouseTimer); + this.mouseTimer = raf(() => { this.focusedByMouse = false; }); onMouseDown?.(event); From 1039693f25695b757c358930b4f7e17adee14ced Mon Sep 17 00:00:00 2001 From: aojunhao123 <1844749591@qq.com> Date: Tue, 10 Mar 2026 01:00:47 +0800 Subject: [PATCH 4/4] fix: use onMouseUp and onBlur to reset focusedByMouse flag instead of raf Made-with: Cursor --- docs/examples/basic.jsx | 1 + src/NodeList.tsx | 6 ++++++ src/Tree.tsx | 18 ++++++++++-------- tests/Tree.spec.tsx | 8 ++------ 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/docs/examples/basic.jsx b/docs/examples/basic.jsx index 55e96fc5..ca0c08b9 100644 --- a/docs/examples/basic.jsx +++ b/docs/examples/basic.jsx @@ -105,6 +105,7 @@ class Demo extends React.Component { className="myCls" showLine checkable + height={150} defaultExpandAll defaultExpandedKeys={this.state.defaultExpandedKeys} onExpand={this.onExpand} diff --git a/src/NodeList.tsx b/src/NodeList.tsx index 000ad2f3..8bc45731 100644 --- a/src/NodeList.tsx +++ b/src/NodeList.tsx @@ -85,6 +85,8 @@ interface NodeListProps { onKeyDown?: React.KeyboardEventHandler; onFocus?: React.FocusEventHandler; onBlur?: React.FocusEventHandler; + onMouseDown?: React.MouseEventHandler; + onMouseUp?: React.MouseEventHandler; onActiveChange: (key: Key) => void; onListChangeStart: () => void; @@ -144,6 +146,8 @@ const NodeList = React.forwardRef>((props, ref) onKeyDown, onFocus, onBlur, + onMouseDown, + onMouseUp, onActiveChange, onListChangeStart, @@ -292,6 +296,8 @@ const NodeList = React.forwardRef>((props, ref) onKeyDown={onKeyDown} onFocus={onFocus} onBlur={onBlur} + onMouseDown={onMouseDown} + onMouseUp={onMouseUp} onVisibleChange={originList => { // The best match is using `fullList` - `originList` = `restList` // and check the `restList` to see if has the MOTION_KEY node diff --git a/src/Tree.tsx b/src/Tree.tsx index c48b8fcd..6fdd26d5 100644 --- a/src/Tree.tsx +++ b/src/Tree.tsx @@ -4,7 +4,6 @@ import { clsx } from 'clsx'; import pickAttrs from '@rc-component/util/lib/pickAttrs'; import warning from '@rc-component/util/lib/warning'; -import raf from '@rc-component/util/lib/raf'; import * as React from 'react'; import type { @@ -120,6 +119,7 @@ export interface TreeProps { titleRender?: (node: TreeDataType) => React.ReactNode; dropIndicatorRender?: (props: DropIndicatorProps) => React.ReactNode; onMouseDown?: React.MouseEventHandler; + onMouseUp?: React.MouseEventHandler; onFocus?: React.FocusEventHandler; onBlur?: React.FocusEventHandler; onKeyDown?: React.KeyboardEventHandler; @@ -315,7 +315,6 @@ class Tree extends Rea currentMouseOverDroppableNodeKey = null; focusedByMouse = false; - mouseTimer: number | null = null; listRef = React.createRef(); @@ -342,7 +341,6 @@ class Tree extends Rea componentWillUnmount() { window.removeEventListener('dragend', this.onWindowDragEnd); - raf.cancel(this.mouseTimer); this.destroyed = true; } @@ -1070,15 +1068,17 @@ class Tree extends Rea }; onMouseDown: React.MouseEventHandler = event => { - const { onMouseDown } = this.props; this.focusedByMouse = true; - raf.cancel(this.mouseTimer); - this.mouseTimer = raf(() => { - this.focusedByMouse = false; - }); + const { onMouseDown } = this.props; onMouseDown?.(event); }; + onMouseUp: React.MouseEventHandler = event => { + this.focusedByMouse = false; + const { onMouseUp } = this.props; + onMouseUp?.(event); + }; + onFocus: React.FocusEventHandler = (...args) => { const { onFocus, disabled } = this.props; const { activeKey, selectedKeys, flattenNodes } = this.state; @@ -1099,6 +1099,7 @@ class Tree extends Rea }; onBlur: React.FocusEventHandler = (...args) => { + this.focusedByMouse = false; const { onBlur } = this.props; this.onActiveChange(null); @@ -1538,6 +1539,7 @@ class Tree extends Rea activeItem={this.getActiveItem()} onFocus={this.onFocus} onMouseDown={this.onMouseDown} + onMouseUp={this.onMouseUp} onBlur={this.onBlur} onKeyDown={this.onKeyDown} onActiveChange={this.onActiveChange} diff --git a/tests/Tree.spec.tsx b/tests/Tree.spec.tsx index e20a27b0..004ccda4 100644 --- a/tests/Tree.spec.tsx +++ b/tests/Tree.spec.tsx @@ -1413,7 +1413,6 @@ describe('Tree Basic', () => { }); 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' }, @@ -1428,13 +1427,10 @@ describe('Tree Basic', () => { fireEvent.mouseDown(treeContainer); expect(treeRef.current.focusedByMouse).toBe(true); fireEvent.focus(treeContainer); - - act(() => { - jest.runAllTimers(); - }); + fireEvent.mouseUp(treeContainer); expect(treeRef.current.focusedByMouse).toBe(false); expect(scrollToSpy).not.toHaveBeenCalled(); - jest.useRealTimers(); + scrollToSpy.mockRestore(); }); });