+
expanded & sticky
+
>
+ rowKey="key"
+ sticky
+ scroll={{ x: 2000 }}
+ columns={columns}
+ data={data}
+ expandable={{
+ expandedRowOffset: 2,
+ expandedRowKeys,
+ onExpandedRowsChange: keys => setExpandedRowKeys(keys),
+ expandedRowRender: record => expandedRowRender: {record.key}
,
+ }}
+ className="table"
+ />
+
+ );
+};
+
+export default Demo;
diff --git a/src/Body/BodyRow.tsx b/src/Body/BodyRow.tsx
index 46ca866f8..4badca944 100644
--- a/src/Body/BodyRow.tsx
+++ b/src/Body/BodyRow.tsx
@@ -19,6 +19,14 @@ export interface BodyRowProps {
scopeCellComponent: CustomizeComponent;
indent?: number;
rowKey: React.Key;
+ rowKeys: React.Key[];
+
+ // Expanded Row
+ expandedRowInfo?: {
+ offset: number;
+ colSpan: number;
+ sticky: number;
+ };
}
// ==================================================================================
@@ -30,6 +38,8 @@ export function getCellProps(
colIndex: number,
indent: number,
index: number,
+ rowKeys: React.Key[] = [],
+ expandedRowOffset = 0,
) {
const {
record,
@@ -43,6 +53,8 @@ export function getCellProps(
expanded,
hasNestChildren,
onTriggerExpand,
+ expandable,
+ expandedKeys,
} = rowInfo;
const key = columnsKey[colIndex];
@@ -68,16 +80,32 @@ export function getCellProps(
);
}
- let additionalCellProps: React.TdHTMLAttributes;
- if (column.onCell) {
- additionalCellProps = column.onCell(record, index);
+ const additionalCellProps = column.onCell?.(record, index) || {};
+
+ // Expandable row has offset
+ if (expandedRowOffset) {
+ const { rowSpan = 1 } = additionalCellProps;
+
+ // For expandable row with rowSpan,
+ // We should increase the rowSpan if the row is expanded
+ if (expandable && rowSpan && colIndex < expandedRowOffset) {
+ 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 {
key,
fixedInfo,
appendCellNode,
- additionalCellProps: additionalCellProps || {},
+ additionalCellProps: additionalCellProps,
};
}
@@ -98,10 +126,12 @@ function BodyRow(
index,
renderIndex,
rowKey,
+ rowKeys,
indent = 0,
rowComponent: RowComponent,
cellComponent,
scopeCellComponent,
+ expandedRowInfo,
} = props;
const rowInfo = useRowInfo(record, rowKey, index, indent);
const {
@@ -153,6 +183,8 @@ function BodyRow(
colIndex,
indent,
index,
+ rowKeys,
+ expandedRowInfo?.offset,
);
return (
@@ -195,7 +227,8 @@ function BodyRow(
prefixCls={prefixCls}
component={RowComponent}
cellComponent={cellComponent}
- colSpan={flattenColumns.length}
+ colSpan={expandedRowInfo ? expandedRowInfo.colSpan : flattenColumns.length}
+ stickyOffset={expandedRowInfo?.sticky}
isEmpty={false}
>
{expandContent}
diff --git a/src/Body/ExpandedRow.tsx b/src/Body/ExpandedRow.tsx
index b4009601c..425df8647 100644
--- a/src/Body/ExpandedRow.tsx
+++ b/src/Body/ExpandedRow.tsx
@@ -14,6 +14,7 @@ export interface ExpandedRowProps {
children: React.ReactNode;
colSpan: number;
isEmpty: boolean;
+ stickyOffset?: number;
}
function ExpandedRow(props: ExpandedRowProps) {
@@ -30,6 +31,7 @@ function ExpandedRow(props: ExpandedRowProps) {
expanded,
colSpan,
isEmpty,
+ stickyOffset = 0,
} = props;
const { scrollbarSize, fixHeader, fixColumn, componentWidth, horizonScroll } = useContext(
@@ -44,9 +46,9 @@ function ExpandedRow(props: ExpandedRowProps) {
contentNode = (
(props: BodyProps
) {
expandedKeys,
childrenColumnName,
emptyNode,
+ expandedRowOffset = 0,
+ colWidths,
} = useContext(TableContext, [
'prefixCls',
'getComponent',
@@ -40,16 +42,42 @@ function Body(props: BodyProps) {
'expandedKeys',
'childrenColumnName',
'emptyNode',
+ 'expandedRowOffset',
+ 'fixedInfoList',
+ 'colWidths',
]);
- const flattenData: { record: RecordType; indent: number; index: number }[] =
- useFlattenRecords(data, childrenColumnName, expandedKeys, getRowKey);
+ const flattenData = useFlattenRecords(
+ data,
+ childrenColumnName,
+ expandedKeys,
+ getRowKey,
+ );
+ const rowKeys = React.useMemo(() => flattenData.map(item => item.rowKey), [flattenData]);
// =================== Performance ====================
const perfRef = React.useRef({
renderWithProps: false,
});
+ // ===================== Expanded =====================
+ // `expandedRowOffset` data is same for all the rows.
+ // Let's calc on Body side to save performance.
+ const expandedRowInfo = React.useMemo(() => {
+ const expandedColSpan = flattenColumns.length - expandedRowOffset;
+
+ let expandedStickyStart = 0;
+ for (let i = 0; i < expandedRowOffset; i += 1) {
+ expandedStickyStart += colWidths[i] || 0;
+ }
+
+ return {
+ offset: expandedRowOffset,
+ colSpan: expandedColSpan,
+ sticky: expandedStickyStart,
+ };
+ }, [flattenColumns.length, expandedRowOffset, colWidths]);
+
// ====================== Render ======================
const WrapperComponent = getComponent(['body', 'wrapper'], 'tbody');
const trComponent = getComponent(['body', 'row'], 'tr');
@@ -59,14 +87,13 @@ function Body(props: BodyProps) {
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 (
(props: BodyProps) {
cellComponent={tdComponent}
scopeCellComponent={thComponent}
indent={indent}
+ // Expanded row info
+ expandedRowInfo={expandedRowInfo}
/>
);
});
diff --git a/src/Table.tsx b/src/Table.tsx
index 187d0ea32..b61de7520 100644
--- a/src/Table.tsx
+++ b/src/Table.tsx
@@ -822,6 +822,7 @@ function Table(
expandableType,
expandRowByClick: expandableConfig.expandRowByClick,
expandedRowRender: expandableConfig.expandedRowRender,
+ expandedRowOffset: expandableConfig.expandedRowOffset,
onTriggerExpand,
expandIconColumnIndex: expandableConfig.expandIconColumnIndex,
indentSize: expandableConfig.indentSize,
@@ -832,6 +833,7 @@ function Table(
columns,
flattenColumns,
onColumnResize,
+ colWidths,
// Row
hoverStartRow: startRow,
@@ -872,6 +874,7 @@ function Table(
expandableType,
expandableConfig.expandRowByClick,
expandableConfig.expandedRowRender,
+ expandableConfig.expandedRowOffset,
onTriggerExpand,
expandableConfig.expandIconColumnIndex,
expandableConfig.indentSize,
@@ -881,6 +884,7 @@ function Table(
columns,
flattenColumns,
onColumnResize,
+ colWidths,
// Row
startRow,
diff --git a/src/VirtualTable/VirtualCell.tsx b/src/VirtualTable/VirtualCell.tsx
index 9b1b3ebe5..7f0af164c 100644
--- a/src/VirtualTable/VirtualCell.tsx
+++ b/src/VirtualTable/VirtualCell.tsx
@@ -56,6 +56,7 @@ function VirtualCell(props: VirtualCellProps) {
const { columnsOffset } = useContext(GridContext, ['columnsOffset']);
+ // TODO: support `expandableRowOffset`
const { key, fixedInfo, appendCellNode, additionalCellProps } = getCellProps(
rowInfo,
column,
diff --git a/src/context/TableContext.tsx b/src/context/TableContext.tsx
index f566c84f0..d15807dde 100644
--- a/src/context/TableContext.tsx
+++ b/src/context/TableContext.tsx
@@ -3,6 +3,7 @@ import type {
ColumnsType,
ColumnType,
Direction,
+ ExpandableConfig,
ExpandableType,
ExpandedRowRender,
GetComponent,
@@ -56,6 +57,7 @@ export interface TableContextProps {
columns: ColumnsType;
flattenColumns: readonly ColumnType[];
onColumnResize: (columnKey: React.Key, width: number) => void;
+ colWidths: number[];
// Row
hoverStartRow: number;
@@ -68,6 +70,8 @@ export interface TableContextProps {
childrenColumnName: string;
rowHoverable?: boolean;
+
+ expandedRowOffset: ExpandableConfig['expandedRowOffset'];
}
const TableContext = createContext();
diff --git a/src/hooks/useColumns/index.tsx b/src/hooks/useColumns/index.tsx
index 573a44af3..9f3f4b760 100644
--- a/src/hooks/useColumns/index.tsx
+++ b/src/hooks/useColumns/index.tsx
@@ -122,6 +122,7 @@ function useColumns(
expandIcon,
rowExpandable,
expandIconColumnIndex,
+ expandedRowOffset = 0,
direction,
expandRowByClick,
columnWidth,
@@ -146,6 +147,7 @@ function useColumns(
clientWidth: number;
fixed?: FixedType;
scrollWidth?: number;
+ expandedRowOffset?: number;
},
transformColumns: (columns: ColumnsType) => ColumnsType,
): [
@@ -236,7 +238,16 @@ function useColumns(
},
};
- return cloneColumns.map(col => (col === EXPAND_COLUMN ? expandColumn : col));
+ return cloneColumns.map((col, index) => {
+ const column = col === EXPAND_COLUMN ? expandColumn : col;
+ if (index < expandedRowOffset) {
+ return {
+ ...column,
+ fixed: column.fixed || 'left',
+ };
+ }
+ return column;
+ });
}
if (process.env.NODE_ENV !== 'production' && baseColumns.includes(EXPAND_COLUMN)) {
@@ -245,7 +256,7 @@ function useColumns(
return baseColumns.filter(col => col !== EXPAND_COLUMN);
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [expandable, baseColumns, getRowKey, expandedKeys, expandIcon, direction]);
+ }, [expandable, baseColumns, getRowKey, expandedKeys, expandIcon, direction, expandedRowOffset]);
// ========================= Transform ========================
const mergedColumns = React.useMemo(() => {
diff --git a/src/hooks/useFlattenRecords.ts b/src/hooks/useFlattenRecords.ts
index ff67f5d9d..7f6f816db 100644
--- a/src/hooks/useFlattenRecords.ts
+++ b/src/hooks/useFlattenRecords.ts
@@ -11,14 +11,15 @@ function fillRecords(
getRowKey: GetRowKey,
index: number,
) {
+ const key = getRowKey(record, index);
+
list.push({
record,
indent,
index,
+ rowKey: key,
});
- const key = getRowKey(record);
-
const expanded = expandedKeys?.has(key);
if (record && Array.isArray(record[childrenColumnName]) && expanded) {
@@ -41,6 +42,7 @@ export interface FlattenData {
record: RecordType;
indent: number;
index: number;
+ rowKey: Key;
}
/**
@@ -80,6 +82,7 @@ export default function useFlattenRecords(
record: item,
indent: 0,
index,
+ rowKey: getRowKey(item, index),
};
});
}, [data, childrenColumnName, expandedKeys, getRowKey]);
diff --git a/src/interface.ts b/src/interface.ts
index e645b2145..c6d9a1794 100644
--- a/src/interface.ts
+++ b/src/interface.ts
@@ -252,6 +252,7 @@ export interface ExpandableConfig {
rowExpandable?: (record: RecordType) => boolean;
columnWidth?: number | string;
fixed?: FixedType;
+ expandedRowOffset?: number;
}
// =================== Render ===================
diff --git a/tests/ExpandedOffset.spec.tsx b/tests/ExpandedOffset.spec.tsx
new file mode 100644
index 000000000..1c6fa1191
--- /dev/null
+++ b/tests/ExpandedOffset.spec.tsx
@@ -0,0 +1,91 @@
+import React from 'react';
+import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
+import { render, act } from '@testing-library/react';
+import { _rs } from 'rc-resize-observer';
+import Table, { type ColumnsType } from '../src';
+
+async function triggerResize(ele: HTMLElement) {
+ await act(async () => {
+ _rs([{ target: ele }] as any);
+ await Promise.resolve();
+ });
+}
+
+describe('Table.ExpandedOffset', () => {
+ let domSpy: ReturnType;
+
+ beforeEach(() => {
+ vi.useFakeTimers();
+ });
+
+ beforeAll(() => {
+ domSpy = spyElementPrototypes(HTMLElement, {
+ offsetParent: {
+ get: () => ({}),
+ },
+ offsetWidth: {
+ get: () => 50,
+ },
+ });
+ });
+
+ afterAll(() => {
+ domSpy.mockRestore();
+ });
+
+ afterEach(() => {
+ vi.clearAllTimers();
+ vi.useRealTimers();
+ });
+
+ it('expanded + sticky', async () => {
+ const columns: ColumnsType = [
+ {
+ title: 'a',
+ // `fixed` will auto patch to fill the space
+ // fixed: 'left',
+ },
+ Table.EXPAND_COLUMN,
+ { title: 'b' },
+ { title: 'c' },
+ ];
+
+ const data = [{ key: 'a' }];
+ const { container } = render(
+ >
+ columns={columns}
+ data={data}
+ sticky
+ scroll={{ x: 1200 }}
+ expandable={{
+ expandedRowOffset: 1,
+ defaultExpandAllRows: true,
+ expandedRowRender: record => {record.key}
,
+ }}
+ />,
+ );
+
+ await triggerResize(container.querySelector('.rc-table'));
+
+ act(() => {
+ const coll = container.querySelector('.rc-table-resize-collection');
+ if (coll) {
+ triggerResize(coll as HTMLElement);
+ }
+ });
+
+ await act(async () => {
+ vi.runAllTimers();
+ await Promise.resolve();
+ });
+
+ expect(container.querySelector('.rc-table-expanded-row .rc-table-cell')).toHaveAttribute(
+ 'colspan',
+ '3',
+ );
+ expect(container.querySelector('.rc-table-expanded-row .rc-table-cell div')).toHaveStyle({
+ position: 'sticky',
+ left: '50px',
+ });
+ });
+});