From 7bc4891df9c2f6f068514924a01225eae5f0d423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E8=89=B3=E5=85=B5?= Date: Sun, 4 Jan 2026 09:06:42 +0800 Subject: [PATCH 1/4] fix(table): guard virtual rowSpan height calc when data shrinks --- src/VirtualTable/BodyGrid.tsx | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/VirtualTable/BodyGrid.tsx b/src/VirtualTable/BodyGrid.tsx index f249b4ce5..fb7d94923 100644 --- a/src/VirtualTable/BodyGrid.tsx +++ b/src/VirtualTable/BodyGrid.tsx @@ -192,7 +192,25 @@ const Grid = React.forwardRef((props, ref) => { const getHeight = (rowSpan: number) => { const endItemIndex = index + rowSpan - 1; - const endItemKey = getRowKey(flattenData[endItemIndex].record, endItemIndex); + const endItem = flattenData[endItemIndex]; + + if (!endItem || !endItem.record) { + // clamp 到当前可用的最后一行,或退化为默认高度 + const safeEndIndex = Math.min(endItemIndex, flattenData.length - 1); + const safeEndItem = flattenData[safeEndIndex]; + + if (!safeEndItem || !safeEndItem.record) { + // 兜底:没有任何安全 endItem,就返回单行高度 + const single = getSize(rowKey); + return single.bottom - single.top; + } + + const endItemKey = getRowKey(safeEndItem.record, safeEndIndex); + const sizeInfo = getSize(rowKey, endItemKey); + return sizeInfo.bottom - sizeInfo.top; + } + + const endItemKey = getRowKey(endItem.record, endItemIndex); const sizeInfo = getSize(rowKey, endItemKey); return sizeInfo.bottom - sizeInfo.top; From 944127f68d53e46ffeb86b0239af59d66d26c07b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E8=89=B3=E5=85=B5?= Date: Sun, 4 Jan 2026 10:34:39 +0800 Subject: [PATCH 2/4] test: add VirtualTable test case --- tests/Virtual.spec.tsx | 82 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/tests/Virtual.spec.tsx b/tests/Virtual.spec.tsx index 0ff9d11ab..18704df4a 100644 --- a/tests/Virtual.spec.tsx +++ b/tests/Virtual.spec.tsx @@ -636,4 +636,86 @@ describe('Table.Virtual', () => { top: 200, }); }); + + it('should not crash when pageSize shrinks after scrolled to bottom (rowSpan)', async () => { + const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const tblRef = React.createRef(); + + const makeData = (len: number) => + Array.from({ length: len }).map((_, id) => ({ + id, + firstName: `First_${id.toString(16)}`, + lastName: `Last_${id.toString(16)}`, + age: 20 + (id % 30), + address: `Address_${id}`, + })); + + const data = makeData(10000); + + const columns = [ + { dataIndex: 'id', width: 100, fixed: 'left' }, + { dataIndex: 'firstName', width: 140, fixed: 'left' }, + { dataIndex: 'lastName', width: 140 }, + { + dataIndex: 'group', + width: 160, + render: (_: any, record: any) => `Group ${Math.floor(record.id / 4)}`, + onCell: (record: any) => ({ + rowSpan: record.id % 4 === 0 ? 4 : 0, + }), + }, + { + dataIndex: 'age', + width: 120, + onCell: (record: any) => ({ + colSpan: record.id % 4 === 0 ? 2 : 1, + }), + }, + { + dataIndex: 'address', + width: 220, + onCell: (record: any) => ({ + colSpan: record.id % 4 === 0 ? 0 : 1, + }), + }, + ]; + + const renderTable = (pageSize: number) => ( + + ); + + const { rerender } = render(renderTable(200)); + + await waitFakeTimer(); + + // 模拟滚到底 + act(() => { + tblRef.current?.scrollTo?.({ top: 10 ** 10 }); + }); + + await waitFakeTimer(); + + // 切到 pageSize=10(slice 模拟分页) + expect(() => { + rerender(renderTable(10)); + }).not.toThrow(); + + // flush timers to cover transient render + vi.runAllTimers(); + await waitFakeTimer(); + + // 不应出现 record undefined 相关错误 + const errorText = errSpy.mock.calls.map(args => String(args[0] ?? '')).join('\n'); + expect(errorText).not.toContain('Cannot read properties of undefined'); + expect(errorText).not.toContain("reading 'record'"); + + errSpy.mockRestore(); + }); }); From ec5d6802e8d3356c81348a1188faf01e061e9618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E8=89=B3=E5=85=B5?= Date: Sun, 4 Jan 2026 11:29:42 +0800 Subject: [PATCH 3/4] test: remove listItemHeight prop --- tests/Virtual.spec.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Virtual.spec.tsx b/tests/Virtual.spec.tsx index 18704df4a..81aff6aba 100644 --- a/tests/Virtual.spec.tsx +++ b/tests/Virtual.spec.tsx @@ -686,7 +686,6 @@ describe('Table.Virtual', () => { columns={columns as any} rowKey="id" scroll={{ x: 900, y: 200 }} - listItemHeight={20} data={data.slice(0, pageSize)} /> ); From 485def7d6e4c72ebe278d9c1e52dc0fd2624979c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E8=89=B3=E5=85=B5?= Date: Sun, 4 Jan 2026 14:47:40 +0800 Subject: [PATCH 4/4] chore: remove unreachable fallback branch in virtual rowSpan logic --- src/VirtualTable/BodyGrid.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/VirtualTable/BodyGrid.tsx b/src/VirtualTable/BodyGrid.tsx index fb7d94923..a4db84ce4 100644 --- a/src/VirtualTable/BodyGrid.tsx +++ b/src/VirtualTable/BodyGrid.tsx @@ -199,12 +199,6 @@ const Grid = React.forwardRef((props, ref) => { const safeEndIndex = Math.min(endItemIndex, flattenData.length - 1); const safeEndItem = flattenData[safeEndIndex]; - if (!safeEndItem || !safeEndItem.record) { - // 兜底:没有任何安全 endItem,就返回单行高度 - const single = getSize(rowKey); - return single.bottom - single.top; - } - const endItemKey = getRowKey(safeEndItem.record, safeEndIndex); const sizeInfo = getSize(rowKey, endItemKey); return sizeInfo.bottom - sizeInfo.top;