Skip to content

Commit 2b96b0a

Browse files
authored
fix(table): guard virtual rowSpan height calc when data shrinks (#1433)
* fix(table): guard virtual rowSpan height calc when data shrinks * test: add VirtualTable test case * test: remove listItemHeight prop * chore: remove unreachable fallback branch in virtual rowSpan logic
1 parent 5254ebc commit 2b96b0a

File tree

2 files changed

+94
-1
lines changed

2 files changed

+94
-1
lines changed

src/VirtualTable/BodyGrid.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,19 @@ const Grid = React.forwardRef<GridRef, GridProps>((props, ref) => {
192192

193193
const getHeight = (rowSpan: number) => {
194194
const endItemIndex = index + rowSpan - 1;
195-
const endItemKey = getRowKey(flattenData[endItemIndex].record, endItemIndex);
195+
const endItem = flattenData[endItemIndex];
196+
197+
if (!endItem || !endItem.record) {
198+
// clamp 到当前可用的最后一行,或退化为默认高度
199+
const safeEndIndex = Math.min(endItemIndex, flattenData.length - 1);
200+
const safeEndItem = flattenData[safeEndIndex];
201+
202+
const endItemKey = getRowKey(safeEndItem.record, safeEndIndex);
203+
const sizeInfo = getSize(rowKey, endItemKey);
204+
return sizeInfo.bottom - sizeInfo.top;
205+
}
206+
207+
const endItemKey = getRowKey(endItem.record, endItemIndex);
196208

197209
const sizeInfo = getSize(rowKey, endItemKey);
198210
return sizeInfo.bottom - sizeInfo.top;

tests/Virtual.spec.tsx

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,4 +636,85 @@ describe('Table.Virtual', () => {
636636
top: 200,
637637
});
638638
});
639+
640+
it('should not crash when pageSize shrinks after scrolled to bottom (rowSpan)', async () => {
641+
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
642+
const tblRef = React.createRef<Reference>();
643+
644+
const makeData = (len: number) =>
645+
Array.from({ length: len }).map((_, id) => ({
646+
id,
647+
firstName: `First_${id.toString(16)}`,
648+
lastName: `Last_${id.toString(16)}`,
649+
age: 20 + (id % 30),
650+
address: `Address_${id}`,
651+
}));
652+
653+
const data = makeData(10000);
654+
655+
const columns = [
656+
{ dataIndex: 'id', width: 100, fixed: 'left' },
657+
{ dataIndex: 'firstName', width: 140, fixed: 'left' },
658+
{ dataIndex: 'lastName', width: 140 },
659+
{
660+
dataIndex: 'group',
661+
width: 160,
662+
render: (_: any, record: any) => `Group ${Math.floor(record.id / 4)}`,
663+
onCell: (record: any) => ({
664+
rowSpan: record.id % 4 === 0 ? 4 : 0,
665+
}),
666+
},
667+
{
668+
dataIndex: 'age',
669+
width: 120,
670+
onCell: (record: any) => ({
671+
colSpan: record.id % 4 === 0 ? 2 : 1,
672+
}),
673+
},
674+
{
675+
dataIndex: 'address',
676+
width: 220,
677+
onCell: (record: any) => ({
678+
colSpan: record.id % 4 === 0 ? 0 : 1,
679+
}),
680+
},
681+
];
682+
683+
const renderTable = (pageSize: number) => (
684+
<VirtualTable
685+
ref={tblRef}
686+
columns={columns as any}
687+
rowKey="id"
688+
scroll={{ x: 900, y: 200 }}
689+
data={data.slice(0, pageSize)}
690+
/>
691+
);
692+
693+
const { rerender } = render(renderTable(200));
694+
695+
await waitFakeTimer();
696+
697+
// 模拟滚到底
698+
act(() => {
699+
tblRef.current?.scrollTo?.({ top: 10 ** 10 });
700+
});
701+
702+
await waitFakeTimer();
703+
704+
// 切到 pageSize=10(slice 模拟分页)
705+
expect(() => {
706+
rerender(renderTable(10));
707+
}).not.toThrow();
708+
709+
// flush timers to cover transient render
710+
vi.runAllTimers();
711+
await waitFakeTimer();
712+
713+
// 不应出现 record undefined 相关错误
714+
const errorText = errSpy.mock.calls.map(args => String(args[0] ?? '')).join('\n');
715+
expect(errorText).not.toContain('Cannot read properties of undefined');
716+
expect(errorText).not.toContain("reading 'record'");
717+
718+
errSpy.mockRestore();
719+
});
639720
});

0 commit comments

Comments
 (0)