Skip to content

feat: support rowspan expanded #1278

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 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
8 changes: 8 additions & 0 deletions docs/demo/expandedRowSpan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: expandedRowSpan
nav:
title: Demo
path: /demo
---

<code src="../examples/expandedRowSpan.tsx"></code>
51 changes: 51 additions & 0 deletions docs/examples/expandedRowSpan.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import Table from 'rc-table';
import '../../assets/index.less';
import type { ColumnsType } from '@/interface';

const columns: ColumnsType = [
{
title: '手机号',
dataIndex: 'a',
colSpan: 2,
width: 100,
onCell: (_, index) => {
const props: React.TdHTMLAttributes<HTMLTableCellElement> = {};
if (index === 0) props.rowSpan = 1;
if (index === 1) props.rowSpan = 4;
if (index === 2) props.rowSpan = 0;
if (index === 3) props.rowSpan = 0;
if (index === 4) props.rowSpan = 0;
if (index === 5) props.rowSpan = undefined;
return props;
},
},
{ title: '电话', dataIndex: 'b', colSpan: 0, width: 100 },
Table.EXPAND_COLUMN,
{ title: 'Name', dataIndex: 'c', width: 100 },
{ title: 'Address', dataIndex: 'd', width: 200 },
];

const data = [
{ a: '12313132132', b: '0571-43243256', c: '小二', d: '文零西路', e: 'Male', key: 'z' },
{ a: '13812340987', b: '0571-12345678', c: '张三', d: '文一西路', e: 'Male', key: 'a' },
{ a: '13812340987', b: '0571-12345678', c: '张夫人', d: '文一西路', e: 'Female', key: 'b' },
{ a: '13812340987', b: '0571-099877', c: '李四', d: '文二西路', e: 'Male', key: 'c' },
{ a: '13812340987', b: '0571-099877', c: '李四', d: '文二西路', e: 'Male', key: 'd' },
{ a: '1381200008888', b: '0571-099877', c: '王五', d: '文二西路', e: 'Male', key: 'e' },
];

const Demo = () => (
<div>
<h2>expanded & rowSpan</h2>
<Table<Record<string, any>>
rowKey="key"
columns={columns}
data={data}
expandable={{ expandedRowRender: record => <p style={{ margin: 0 }}>{record.key}</p> }}
className="table"
/>
</div>
);

export default Demo;
22 changes: 22 additions & 0 deletions src/Body/BodyRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface BodyRowProps<RecordType> {
scopeCellComponent: CustomizeComponent;
indent?: number;
rowKey: React.Key;
rowKeys: React.Key[];
}

// ==================================================================================
Expand All @@ -30,6 +31,7 @@ export function getCellProps<RecordType>(
colIndex: number,
indent: number,
index: number,
rowKeys: React.Key[],
) {
const {
record,
Expand All @@ -43,6 +45,8 @@ export function getCellProps<RecordType>(
expanded,
hasNestChildren,
onTriggerExpand,
expandable,
expandedKeys,
} = rowInfo;

const key = columnsKey[colIndex];
Expand Down Expand Up @@ -71,6 +75,21 @@ export function getCellProps<RecordType>(
let additionalCellProps: React.TdHTMLAttributes<HTMLElement>;
if (column.onCell) {
additionalCellProps = column.onCell(record, index);
const { rowSpan } = additionalCellProps;

// For expandable row with rowSpan,
// We should increase the rowSpan if the row is expanded
if (expandable) {
let currentRowSpan = rowSpan;

for (let i = index; i < index + rowSpan; i += 1) {
const rowKey = rowKeys[i];
if (expandedKeys.has(rowKey)) {
currentRowSpan += 1;
}
}
additionalCellProps.rowSpan = currentRowSpan;
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

实现行合并与行展开的协同处理

这段代码实现了行展开时动态调整 rowSpan 的核心逻辑,但缺少测试覆盖。

    const { rowSpan } = additionalCellProps;

    // For expandable row with rowSpan,
    // We should increase the rowSpan if the row is expanded
-    if (expandable) {
+    if (expandable && rowSpan > 0) {
      let currentRowSpan = rowSpan;

      for (let i = index; i < index + rowSpan; i += 1) {
+       if (i >= rowKeys.length) break;
        const rowKey = rowKeys[i];
        if (expandedKeys.has(rowKey)) {
          currentRowSpan += 1;
        }
      }
      additionalCellProps.rowSpan = currentRowSpan;
    }

代码存在以下隐患:

  1. 没有检查 rowSpan 是否为有效值 (>0)
  2. 没有检查 i 是否超出 rowKeys 数组边界
  3. 建议为新增的复杂逻辑添加单元测试

🏁 Script executed:

#!/bin/bash
# 检查是否有相关测试文件涵盖 BodyRow 组件中的 rowSpan 功能

echo "查找与 BodyRow 相关的测试文件..."
fd -e test.tsx -e test.ts -e spec.tsx -e spec.ts | grep -i "bodyrow\|body.*row"

echo "查找测试文件中是否包含 rowSpan 相关测试..."
rg -i "rowspan.*test|test.*rowspan" --type ts --type tsx

Length of output: 288


🏁 Script executed:

#!/bin/bash
echo "查找包含 BodyRow 的测试文件..."
rg -l "BodyRow" -g "*.test.ts" -g "*.test.tsx" -g "*.spec.ts" -g "*.spec.tsx"

echo "在测试文件中查找 rowSpan 相关测试..."
rg -n "rowSpan" -g "*.test.ts" -g "*.test.tsx" -g "*.spec.ts" -g "*.spec.tsx"

Length of output: 715


校验 rowSpan 有效性并防止数组越界

src/Body/BodyRow.tsx(约 81–95 行)中,这段逻辑负责在展开行时动态调整 rowSpan,但存在以下隐患需要修复并补充测试:

  1. 未过滤 rowSpan 为 0 或负数的情况
  2. 在循环中可能读取超出 rowKeys 长度的索引
  3. 缺少针对该逻辑的单元测试

建议修改如下:

-   if (expandable) {
+   // 仅对可展开且 rowSpan > 0 的单元格生效
+   if (expandable && rowSpan > 0) {
      let currentRowSpan = rowSpan;

      for (let i = index; i < index + rowSpan; i += 1) {
+       // 防止越界访问
+       if (i >= rowKeys.length) break;
        const rowKey = rowKeys[i];
        if (expandedKeys.has(rowKey)) {
          currentRowSpan += 1;
        }
      }
      additionalCellProps.rowSpan = currentRowSpan;
    }

· 请在 tests/ 下新增 BodyRow.spec.tsx,覆盖以下场景:

  • rowSpan 为 0、1、>1
  • 不同 expandedKeys 集合下的展开行为
  • 边界情况(index + rowSpan 超出 rowKeys.length
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { rowSpan } = additionalCellProps;
// For expandable row with rowSpan,
// We should increase the rowSpan if the row is expanded
if (expandable) {
let currentRowSpan = rowSpan;
for (let i = index; i < index + rowSpan; i += 1) {
const rowKey = rowKeys[i];
if (expandedKeys.has(rowKey)) {
currentRowSpan += 1;
}
}
additionalCellProps.rowSpan = currentRowSpan;
}
const { rowSpan } = additionalCellProps;
// For expandable row with rowSpan,
// We should increase the rowSpan if the row is expanded
// 仅对可展开且 rowSpan > 0 的单元格生效
if (expandable && rowSpan > 0) {
let currentRowSpan = rowSpan;
for (let i = index; i < index + rowSpan; i += 1) {
// 防止越界访问
if (i >= rowKeys.length) break;
const rowKey = rowKeys[i];
if (expandedKeys.has(rowKey)) {
currentRowSpan += 1;
}
}
additionalCellProps.rowSpan = currentRowSpan;
}
🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 86-86: src/Body/BodyRow.tsx#L86
Added line #L86 was not covered by tests


[warning] 88-95: src/Body/BodyRow.tsx#L88-L95
Added lines #L88 - L95 were not covered by tests

🤖 Prompt for AI Agents
In src/Body/BodyRow.tsx around lines 81 to 95, the code adjusts rowSpan when
rows are expanded but lacks validation and boundary checks. Fix this by first
verifying that rowSpan is a positive number greater than zero before proceeding.
Then, in the loop, ensure the index i does not exceed the length of the rowKeys
array to prevent out-of-bounds access. Additionally, create a new test file
tests/BodyRow.spec.tsx to add unit tests covering cases where rowSpan is 0, 1,
or greater than 1, different expandedKeys sets, and boundary conditions where
index plus rowSpan exceeds rowKeys length.

}

return {
Expand Down Expand Up @@ -102,8 +121,10 @@ function BodyRow<RecordType extends { children?: readonly RecordType[] }>(
rowComponent: RowComponent,
cellComponent,
scopeCellComponent,
rowKeys,
} = props;
const rowInfo = useRowInfo(record, rowKey, index, indent);

const {
prefixCls,
flattenColumns,
Expand Down Expand Up @@ -153,6 +174,7 @@ function BodyRow<RecordType extends { children?: readonly RecordType[] }>(
colIndex,
indent,
index,
rowKeys,
);

return (
Expand Down
19 changes: 12 additions & 7 deletions src/Body/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,14 @@ function Body<RecordType>(props: BodyProps<RecordType>) {
'emptyNode',
]);

const flattenData: { record: RecordType; indent: number; index: number }[] =
useFlattenRecords<RecordType>(data, childrenColumnName, expandedKeys, getRowKey);
const flattenData = useFlattenRecords<RecordType>(
data,
childrenColumnName,
expandedKeys,
getRowKey,
);

const rowKeys = React.useMemo(() => flattenData.map(item => item.rowKey), [flattenData]);

// =================== Performance ====================
const perfRef = React.useRef<PerfRecord>({
Expand All @@ -59,14 +65,13 @@ function Body<RecordType>(props: BodyProps<RecordType>) {
let rows: React.ReactNode;
if (data.length) {
rows = flattenData.map((item, idx) => {
const { record, indent, index: renderIndex } = item;

const key = getRowKey(record, idx);
const { record, indent, index: renderIndex, rowKey } = item;

return (
<BodyRow
key={key}
rowKey={key}
key={rowKey}
rowKey={rowKey}
rowKeys={rowKeys}
record={record}
index={idx}
renderIndex={renderIndex}
Expand Down
1 change: 1 addition & 0 deletions src/VirtualTable/VirtualCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ function VirtualCell<RecordType = any>(props: VirtualCellProps<RecordType>) {
colIndex,
indent,
index,
[],
);

const { style: cellStyle, colSpan = 1, rowSpan = 1 } = additionalCellProps;
Expand Down
3 changes: 3 additions & 0 deletions src/hooks/useFlattenRecords.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ function fillRecords<T>(
record,
indent,
index,
rowKey: getRowKey(record, index),
});

const key = getRowKey(record);
Expand All @@ -41,6 +42,7 @@ export interface FlattenData<RecordType> {
record: RecordType;
indent: number;
index: number;
rowKey: Key;
}

/**
Expand Down Expand Up @@ -80,6 +82,7 @@ export default function useFlattenRecords<T>(
record: item,
indent: 0,
index,
rowKey: getRowKey(item, index),
};
});
}, [data, childrenColumnName, expandedKeys, getRowKey]);
Expand Down