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 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
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>
8 changes: 8 additions & 0 deletions docs/demo/expandedSticky.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: expandedSticky
nav:
title: Demo
path: /demo
---

<code src="../examples/expandedSticky.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;
57 changes: 57 additions & 0 deletions docs/examples/expandedSticky.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { useState } from 'react';
import type { ColumnType } from 'rc-table';
import Table from 'rc-table';
import '../../assets/index.less';

const Demo = () => {
const [expandedRowKeys, setExpandedRowKeys] = useState<readonly React.Key[]>([]);

const columns: ColumnType<Record<string, any>>[] = [
// { title: '分割', dataIndex: 'ca' },
{
title: '手机号',
dataIndex: 'a',
width: 100,
fixed: 'left',
onCell: (_, index) => {
const props: React.TdHTMLAttributes<HTMLTableCellElement> = {};
if (index === 0) props.rowSpan = 1;
if (index === 1) props.rowSpan = 2;
if (index === 2) props.rowSpan = 0;
return props;
},
},
Table.EXPAND_COLUMN,
{ title: 'Name', dataIndex: 'c' },
{ title: 'Address', fixed: 'right', dataIndex: 'd', width: 200 },
];

return (
<div
style={{
height: 10000,
}}
>
<h2>expanded & sticky</h2>
<Table<Record<string, any>>
rowKey="key"
sticky
scroll={{ x: 800 }}
columns={columns}
data={[
{ key: 'a', a: '12313132132', c: '小二', d: '文零西路' },
{ key: 'b', a: '13812340987', c: '张三', d: '文一西路' },
{ key: 'c', a: '13812340987', c: '张夫', d: '文二西路' },
]}
expandable={{
expandedRowKeys,
onExpandedRowsChange: keys => setExpandedRowKeys(keys),
expandedRowRender: record => <p style={{ margin: 0 }}>{record.key}</p>,
}}
className="table"
/>
</div>
);
};

export default Demo;
76 changes: 63 additions & 13 deletions src/Body/BodyRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import useRowInfo from '../hooks/useRowInfo';
import type { ColumnType, CustomizeComponent } from '../interface';
import ExpandedRow from './ExpandedRow';
import { computedExpandedClassName } from '../utils/expandUtil';
import { TableProps } from '..';
import type { TableProps } from '..';

export interface BodyRowProps<RecordType> {
record: RecordType;
Expand All @@ -22,6 +22,7 @@ export interface BodyRowProps<RecordType> {
scopeCellComponent: CustomizeComponent;
indent?: number;
rowKey: React.Key;
rowKeys: React.Key[];
}

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

const key = columnsKey[colIndex];
Expand Down Expand Up @@ -74,6 +78,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 && rowSpan !== undefined) {
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;
}
}

return {
Expand All @@ -84,9 +103,31 @@ export function getCellProps<RecordType>(
};
}

// ==================================================================================
// == getCellProps ==
// ==================================================================================
const getOffsetData = (
columnsData: {
column: ColumnType<any>;
cell: { additionalCellProps: React.TdHTMLAttributes<HTMLElement> };
}[],
) => {
let offsetWidth = 0;
let offsetColumn = 0;
let isRowSpanEnd = false;
columnsData.forEach(item => {
if (!isRowSpanEnd) {
const { column, cell } = item;
if (cell.additionalCellProps.rowSpan !== undefined) {
offsetColumn += 1;
if (typeof column.width === 'number') {
offsetWidth = offsetWidth + (column.width ?? 0);
}
} else {
isRowSpanEnd = true;
}
}
});
return { offsetWidth, offsetColumn };
};

function BodyRow<RecordType extends { children?: readonly RecordType[] }>(
props: BodyRowProps<RecordType>,
) {
Expand All @@ -107,9 +148,11 @@ function BodyRow<RecordType extends { children?: readonly RecordType[] }>(
rowComponent: RowComponent,
cellComponent,
scopeCellComponent,
rowKeys,
} = props;

const rowInfo = useRowInfo(record, rowKey, index, indent);

const {
prefixCls,
flattenColumns,
Expand All @@ -134,6 +177,17 @@ function BodyRow<RecordType extends { children?: readonly RecordType[] }>(
// 此时如果 level > 1 则说明是 expandedRow, 一样需要附加 computedExpandedRowClassName
const expandedClsName = computedExpandedClassName(expandedRowClassName, record, index, indent);

const { columnsData, offsetData } = React.useMemo(() => {
// eslint-disable-next-line @typescript-eslint/no-shadow
const columnsData = flattenColumns.map((column: ColumnType<RecordType>, colIndex) => {
const cell = getCellProps(rowInfo, column, colIndex, indent, index, rowKeys);
return { column, cell };
});
// eslint-disable-next-line @typescript-eslint/no-shadow
const offsetData = getOffsetData(columnsData);
return { columnsData, offsetData };
}, [flattenColumns, indent, index, rowInfo, rowKeys]);

// ======================== Base tr row ========================
const baseRowNode = (
<RowComponent
Expand All @@ -155,16 +209,11 @@ function BodyRow<RecordType extends { children?: readonly RecordType[] }>(
...styles.row,
}}
>
{flattenColumns.map((column: ColumnType<RecordType>, colIndex) => {
{columnsData.map(item => {
const { column, cell } = item;
const { render, dataIndex, className: columnClassName } = column;

const { key, fixedInfo, appendCellNode, additionalCellProps } = getCellProps(
rowInfo,
column,
colIndex,
indent,
index,
);
const { key, fixedInfo, appendCellNode, additionalCellProps } = cell;

return (
<Cell<RecordType>
Expand Down Expand Up @@ -207,7 +256,8 @@ function BodyRow<RecordType extends { children?: readonly RecordType[] }>(
prefixCls={prefixCls}
component={RowComponent}
cellComponent={cellComponent}
colSpan={flattenColumns.length}
offsetWidth={offsetData.offsetWidth}
colSpan={flattenColumns.length - offsetData.offsetColumn}
isEmpty={false}
Comment on lines +259 to 261
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

确保 colSpan 不会为负值

当计算展开行的 colSpan 时,需要确保结果不会为负值:

<ExpandedRow
  expanded={expanded}
  className={cls(
    `${prefixCls}-expanded-row`,
    `${prefixCls}-expanded-row-level-${indent + 1}`,
    expandedClsName,
  )}
  prefixCls={prefixCls}
  component={RowComponent}
  cellComponent={cellComponent}
  offsetWidth={offsetData.offsetWidth}
- colSpan={flattenColumns.length - offsetData.offsetColumn}
+ colSpan={Math.max(1, flattenColumns.length - offsetData.offsetColumn)}
  isEmpty={false}
>

offsetData.offsetColumn 大于或等于 flattenColumns.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
offsetWidth={offsetData.offsetWidth}
colSpan={flattenColumns.length - offsetData.offsetColumn}
isEmpty={false}
<ExpandedRow
expanded={expanded}
className={cls(
`${prefixCls}-expanded-row`,
`${prefixCls}-expanded-row-level-${indent + 1}`,
expandedClsName,
)}
prefixCls={prefixCls}
component={RowComponent}
cellComponent={cellComponent}
offsetWidth={offsetData.offsetWidth}
colSpan={Math.max(
1,
flattenColumns.length - offsetData.offsetColumn
)}
isEmpty={false}
>
🤖 Prompt for AI Agents
In src/Body/BodyRow.tsx around lines 259 to 261, the calculation of colSpan as
flattenColumns.length minus offsetData.offsetColumn can result in zero or
negative values if offsetData.offsetColumn is greater than or equal to
flattenColumns.length. To fix this, add a check to ensure colSpan is never less
than 1 by using a conditional or Math.max to set a minimum value of 1 for
colSpan.

>
{expandContent}
Expand Down
4 changes: 3 additions & 1 deletion src/Body/ExpandedRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface ExpandedRowProps {
children: React.ReactNode;
colSpan: number;
isEmpty: boolean;
offsetWidth?: number;
}

function ExpandedRow(props: ExpandedRowProps) {
Expand All @@ -30,6 +31,7 @@ function ExpandedRow(props: ExpandedRowProps) {
expanded,
colSpan,
isEmpty,
offsetWidth = 0,
} = props;

const { scrollbarSize, fixHeader, fixColumn, componentWidth, horizonScroll } = useContext(
Expand All @@ -44,7 +46,7 @@ function ExpandedRow(props: ExpandedRowProps) {
contentNode = (
<div
style={{
width: componentWidth - (fixHeader && !isEmpty ? scrollbarSize : 0),
width: componentWidth - offsetWidth - (fixHeader && !isEmpty ? scrollbarSize : 0),
position: 'sticky',
left: 0,
overflow: 'hidden',
Expand Down
19 changes: 12 additions & 7 deletions src/Body/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,14 @@ function Body<RecordType>(props: BodyProps<RecordType>) {
const { body: bodyCls = {} } = classNames || {};
const { body: bodyStyles = {} } = styles || {};

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 @@ -66,16 +72,15 @@ 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
classNames={bodyCls}
styles={bodyStyles}
key={key}
rowKey={key}
key={rowKey}
rowKey={rowKey}
rowKeys={rowKeys}
record={record}
index={idx}
renderIndex={renderIndex}
Expand Down
6 changes: 5 additions & 1 deletion src/Panel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ export interface TitleProps {
}

function Panel({ className, style, children }: TitleProps) {
return <div className={className} style={style}>{children}</div>;
return (
<div className={className} style={style}>
{children}
</div>
);
}

export default Panel;
2 changes: 2 additions & 0 deletions src/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,8 @@ function Table<RecordType extends DefaultRecordType>(
mergedChildrenColumnName,

rowHoverable,
classNames,
styles,
],
);

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
2 changes: 1 addition & 1 deletion src/context/TableContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type {
TriggerEventHandler,
} from '../interface';
import type { FixedInfo } from '../utils/fixUtil';
import { TableProps } from '../Table';
import type { TableProps } from '../Table';

const { makeImmutable, responseImmutable, useImmutableMark } = createImmutable();
export { makeImmutable, responseImmutable, useImmutableMark };
Expand Down
Loading