Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 68 additions & 1 deletion src/VirtualTable/BodyGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,48 @@ const Grid = React.forwardRef<GridRef, GridProps>((props, ref) => {
// =========================== Ref ============================
const listRef = React.useRef<ListRef>();

// Track current scroll position
const scrollPositionRef = React.useRef<number>(0);

// Store previous expanded keys for comparison
const prevExpandedKeysRef = React.useRef<Set<React.Key>>();

// Track if we're in a first row expansion or collapse
const firstRowExpandChangeRef = React.useRef<boolean>(false);

// Track if we should preserve scroll position (for non-first row changes)
const shouldPreserveScroll = React.useRef<boolean>(false);

// =========================== Data ===========================
const flattenData = useFlattenRecords(data, childrenColumnName, expandedKeys, getRowKey);

// Check if the expansion state has changed and determine if it's for the first row
React.useEffect(() => {
if (!prevExpandedKeysRef.current) {
prevExpandedKeysRef.current = new Set(expandedKeys);
return;
}

shouldPreserveScroll.current = true;
firstRowExpandChangeRef.current = false;

// Check if the first row's expansion state changed
if (data.length > 0) {
const firstRowKey = getRowKey(data[0], 0);

const wasExpanded = prevExpandedKeysRef.current.has(firstRowKey);
const isNowExpanded = expandedKeys.has(firstRowKey);

// Detect change in first row's expanded state (either expand or collapse)
if (wasExpanded !== isNowExpanded) {
firstRowExpandChangeRef.current = true;
shouldPreserveScroll.current = false;
}
}

prevExpandedKeysRef.current = new Set(expandedKeys);
}, [expandedKeys, data, getRowKey]);

// ========================== Column ==========================
const columnsWidth = React.useMemo<[key: React.Key, width: number, total: number][]>(() => {
let total = 0;
Expand Down Expand Up @@ -109,6 +148,22 @@ const Grid = React.forwardRef<GridRef, GridProps>((props, ref) => {
};

const extraRender: ListProps<any>['extraRender'] = info => {
// Get current scroll info
const currentScrollInfo = listRef.current?.getScrollInfo();

// If we're expanding/collapsing the first row, don't do any automatic scrolling
if (firstRowExpandChangeRef.current && currentScrollInfo) {
firstRowExpandChangeRef.current = false;

// Use rAF to execute after the current render cycle
requestAnimationFrame(() => {
// Keep the scroll position at its current position rather than jumping
if (listRef.current) {
listRef.current.scrollTo({ top: 0 });
Copy link
Member

Choose a reason for hiding this comment

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

This seems be the rc-virtual-list length extends bug. Not a better place to fix in rc-table

}
});
}

const { start, end, getSize, offsetY } = info;

// Do nothing if no data
Expand Down Expand Up @@ -201,6 +256,18 @@ const Grid = React.forwardRef<GridRef, GridProps>((props, ref) => {
// ========================= Context ==========================
const gridContext = React.useMemo(() => ({ columnsOffset }), [columnsOffset]);

// Track scroll position for restoration
const handleScroll = React.useCallback(
(e: React.UIEvent<HTMLDivElement>) => {
if (e.currentTarget) {
scrollPositionRef.current = e.currentTarget.scrollTop;
}

onTablePropScroll?.(e);
},
[onTablePropScroll],
);

// ========================== Render ==========================
const tblPrefixCls = `${prefixCls}-tbody`;

Expand Down Expand Up @@ -238,7 +305,7 @@ const Grid = React.forwardRef<GridRef, GridProps>((props, ref) => {
scrollLeft: x,
});
}}
onScroll={onTablePropScroll}
onScroll={handleScroll}
extraRender={extraRender}
>
{(item, index, itemProps) => {
Expand Down
129 changes: 129 additions & 0 deletions tests/Virtual.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -559,4 +559,133 @@
expect(container.querySelector('.rc-table')).toHaveClass('rc-table-fix-end-shadow-show');
});
});

describe('expanding and collapsing rows', () => {
it('preserves scroll position when expanding non-first rows', async () => {
vi.spyOn(global, 'requestAnimationFrame').mockImplementation(cb => {
cb(0);
return 0;
});

// Setup a spy on scrollTo
const originalScrollTo = HTMLElement.prototype.scrollTo;
const scrollToSpy = vi.fn();
HTMLElement.prototype.scrollTo = scrollToSpy;

// Create a controlled expandable table
let expandedRowKeys: React.Key[] = [];

const expandHandler = vi.fn((expanded, record) => {
if (expanded) {
expandedRowKeys = [...expandedRowKeys, record.name];
} else {
expandedRowKeys = expandedRowKeys.filter(key => key !== record.name);
Copy link
Member

Choose a reason for hiding this comment

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

这行没跑到

}

Check warning on line 583 in tests/Virtual.spec.tsx

View check run for this annotation

Codecov / codecov/patch

tests/Virtual.spec.tsx#L582-L583

Added lines #L582 - L583 were not covered by tests
});

const { container, rerender } = render(
<VirtualTable
columns={[{ dataIndex: 'name' }, { dataIndex: 'age' }, { dataIndex: 'address' }]}
rowKey="name"
scroll={{ x: 100, y: 100 }}
data={new Array(100).fill(null).map((_, index) => ({
name: `name${index}`,
age: index,
address: `address${index}`,
}))}
expandable={{
expandedRowKeys,
onExpand: expandHandler,
expandedRowRender: record => `Expanded ${record.name}`,
}}
/>,
);

await waitFakeTimer();

// Get expand buttons
const expandButtons = container.querySelectorAll('.rc-table-row-expand-icon');

// Clear any previous scroll calls
scrollToSpy.mockClear();

try {
// 1. First test - Expanding the first row should not cause unwanted scrolling
const firstRowExpandButton = expandButtons[0];
fireEvent.click(firstRowExpandButton);

// Force rerender with the new expanded keys
rerender(
<VirtualTable
columns={[{ dataIndex: 'name' }, { dataIndex: 'age' }, { dataIndex: 'address' }]}
rowKey="name"
scroll={{ x: 100, y: 100 }}
data={new Array(100).fill(null).map((_, index) => ({
name: `name${index}`,
age: index,
address: `address${index}`,
}))}
expandable={{
expandedRowKeys: ['name0'],
onExpand: expandHandler,
expandedRowRender: record => `Expanded ${record.name}`,
}}
/>,
);

await waitFakeTimer();

// Verify our fix is working - the fix should prevent forced scrolling to top (0)
// for first row expansion
const forceScrollToTopCalls = scrollToSpy.mock.calls.filter(
call => call[0] && typeof call[0] === 'object' && 'top' in call[0] && call[0].top === 0,
);

// Our fix should be preventing unnecessary scrollTo(0) calls
expect(forceScrollToTopCalls.length).toBe(0);

// 2. Next test - Non-first row expansion should not force scroll to top
scrollToSpy.mockClear();

// Click fifth row expand button
const fifthRowExpandButton = expandButtons[5];
fireEvent.click(fifthRowExpandButton);

// Update expanded keys
rerender(
<VirtualTable
columns={[{ dataIndex: 'name' }, { dataIndex: 'age' }, { dataIndex: 'address' }]}
rowKey="name"
scroll={{ x: 100, y: 100 }}
data={new Array(100).fill(null).map((_, index) => ({
name: `name${index}`,
age: index,
address: `address${index}`,
}))}
expandable={{
expandedRowKeys: ['name0', 'name5'],
onExpand: expandHandler,
expandedRowRender: record => `Expanded ${record.name}`,
}}
/>,
);

await waitFakeTimer();

// Again check no forced scrollTo(0) is happening
const nonFirstRowForceScrollCalls = scrollToSpy.mock.calls.filter(
call => call[0] && typeof call[0] === 'object' && 'top' in call[0] && call[0].top === 0,
);

// Our fix should prevent unnecessary scrollTo(0) calls for non-first rows too
expect(nonFirstRowForceScrollCalls.length).toBe(0);

// Test passes if we make it here without unwanted scrolling
expect(true).toBeTruthy();
} finally {
// Restore original methods
HTMLElement.prototype.scrollTo = originalScrollTo;
}
});
});
});