Skip to content

Commit 4a3373a

Browse files
nikzandaNicolò ZandarinzombieJ
authored
feat: support force rendering expanded rows
* feat: support force rendering expanded rows * test: cover collapsing force-rendered rows * fix: prevent duplicate force-rendered content in virtual tables * ci: fix React Doctor workflow * ci: update React Doctor action --------- Co-authored-by: Nicolò Zandarin <nicolo.zandarin@atroos.com> Co-authored-by: 二货机器人 <smith3816@gmail.com>
1 parent adf0701 commit 4a3373a

11 files changed

Lines changed: 239 additions & 13 deletions

File tree

.github/workflows/react-doctor.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ name: React Doctor
33
on:
44
pull_request:
55
branches: [master]
6+
push:
7+
branches: [master]
68

79
permissions:
810
contents: read
911
pull-requests: write
12+
issues: write
13+
statuses: write
1014

1115
concurrency:
1216
group: react-doctor-${{ github.event.pull_request.number || github.ref }}
@@ -18,8 +22,7 @@ jobs:
1822
steps:
1923
- uses: actions/checkout@v7
2024
with:
25+
fetch-depth: 0
2126
persist-credentials: false
2227

23-
- uses: millionco/react-doctor@ced746f518f11e8283d488c4ff31c44e478bb0e5
24-
with:
25-
github-token: ${{ secrets.GITHUB_TOKEN }}
28+
- uses: millionco/react-doctor@01820bb4fd4d0a4aebcd8df2b2a143a098649cb2

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ Then open `http://localhost:8000`.
8282
| expandable.defaultExpandedRowKeys | String[] | [] | initial expanded rows keys |
8383
| expandable.expandedRowKeys | String[] | | current expanded rows keys |
8484
| expandable.expandedRowRender | Function(recode, index, indent, expanded):ReactNode | | Content render to expanded row |
85+
| expandable.forceRender | Boolean | false | Force render expanded row content before expansion. In virtual mode, only rows currently mounted by the virtual list are force-rendered; off-screen rows may still be unmounted |
8586
| expandable.expandedRowClassName | `string` \| `(recode, index, indent) => string` | | get expanded row's className |
8687
| expandable.expandRowByClick | boolean | | Support expand by click row |
8788
| expandable.expandIconColumnIndex | Number | 0 | The index of expandIcon which column will be inserted when expandIconAsCell is false |

README.zh-CN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ npm start
8282
| expandable.defaultExpandedRowKeys | String[] | [] | 初始扩展行键 |
8383
| expandable.expandedRowKeys | String[] | | 当前扩展行键 |
8484
| expandable.expandedRowRender | Function(recode, index, indent, expanded):ReactNode | | 内容渲染到扩展行 |
85+
| expandable.forceRender | Boolean | false | 在展开前强制渲染展开行内容。虚拟模式下,仅强制渲染虚拟列表当前挂载的行;屏幕外的行仍可能被卸载 |
8586
| expandable.expandedRowClassName | `string` \| `(recode, index, indent) => string` | | 获取扩展行的 className |
8687
| expandable.expandRowByClick | boolean | | 支持点击行展开 |
8788
| expandable.expandIconColumnIndex | Number | 0 | ExpandIconAsCell 为 false 时将插入哪一列的 ExpandIcon 索引 |

src/Body/BodyRow.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,14 +146,15 @@ const BodyRow = <RecordType extends { children?: readonly RecordType[] }>(
146146
flattenColumns,
147147
expandedRowClassName,
148148
expandedRowRender,
149+
forceRender,
149150
rowProps,
150151

151152
// Misc
152153
expanded,
153154
rowSupportExpand,
154155
} = rowInfo;
155156

156-
// Force render expand row if expanded before
157+
// Keep the expanded row mounted after it has been expanded
157158
const expandedRef = React.useRef(false);
158159
expandedRef.current ||= expanded;
159160

@@ -224,7 +225,7 @@ const BodyRow = <RecordType extends { children?: readonly RecordType[] }>(
224225

225226
// ======================== Expand Row =========================
226227
let expandRowNode: React.ReactElement<ExpandedRowProps>;
227-
if (rowSupportExpand && (expandedRef.current || expanded)) {
228+
if (rowSupportExpand && (forceRender || expandedRef.current || expanded)) {
228229
const expandContent = expandedRowRender(record, index, indent + 1, expanded);
229230

230231
expandRowNode = (

src/Table.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -910,6 +910,7 @@ const Table = <RecordType extends DefaultRecordType>(
910910
expandableType,
911911
expandRowByClick: expandableConfig.expandRowByClick,
912912
expandedRowRender: expandableConfig.expandedRowRender,
913+
forceRender: expandableConfig.forceRender ?? false,
913914
expandedRowOffset: expandableConfig.expandedRowOffset,
914915
onTriggerExpand,
915916
expandIconColumnIndex: expandableConfig.expandIconColumnIndex,
@@ -967,6 +968,7 @@ const Table = <RecordType extends DefaultRecordType>(
967968
expandableType,
968969
expandableConfig.expandRowByClick,
969970
expandableConfig.expandedRowRender,
971+
expandableConfig.forceRender,
970972
expandableConfig.expandedRowOffset,
971973
onTriggerExpand,
972974
expandableConfig.expandIconColumnIndex,

src/VirtualTable/BodyLine.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,19 @@ const BodyLine = React.forwardRef<HTMLDivElement, BodyLineProps>((props, ref) =>
4343
const cellComponent = getComponent(['body', 'cell'], 'div');
4444

4545
// ========================== Expand ==========================
46-
const { rowSupportExpand, expanded, rowProps, expandedRowRender, expandedRowClassName } = rowInfo;
46+
const {
47+
rowSupportExpand,
48+
expanded,
49+
rowProps,
50+
expandedRowRender,
51+
expandedRowClassName,
52+
forceRender,
53+
} = rowInfo;
4754

4855
const expandedClsName = computedExpandedClassName(expandedRowClassName, record, index, indent);
4956

5057
let expandRowNode: React.ReactElement<any>;
51-
if (rowSupportExpand && expanded) {
58+
if (!extra && rowSupportExpand && (forceRender || expanded)) {
5259
const expandContent = expandedRowRender(record, index, indent + 1, expanded);
5360

5461
let additionalProps: React.TdHTMLAttributes<HTMLElement> = {};
@@ -69,6 +76,7 @@ const BodyLine = React.forwardRef<HTMLDivElement, BodyLineProps>((props, ref) =>
6976
`${prefixCls}-expanded-row-level-${indent + 1}`,
7077
expandedClsName,
7178
)}
79+
style={{ display: expanded ? undefined : 'none' }}
7280
>
7381
<Cell
7482
component={cellComponent}

src/context/TableContext.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export interface TableContextProps<RecordType = any> {
5353
expandableType: ExpandableType;
5454
expandRowByClick: boolean;
5555
expandedRowRender: ExpandedRowRender<RecordType>;
56+
forceRender: boolean;
5657
expandIcon: RenderExpandIcon<RecordType>;
5758
onTriggerExpand: TriggerEventHandler<RecordType>;
5859
expandIconColumnIndex: number;

src/hooks/useRowInfo.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export default function useRowInfo<RecordType>(
2323
| 'indentSize'
2424
| 'expandIcon'
2525
| 'expandedRowRender'
26+
| 'forceRender'
2627
| 'expandIconColumnIndex'
2728
| 'expandedKeys'
2829
| 'childrenColumnName'
@@ -49,6 +50,7 @@ export default function useRowInfo<RecordType>(
4950
'indentSize',
5051
'expandIcon',
5152
'expandedRowRender',
53+
'forceRender',
5254
'expandIconColumnIndex',
5355
'expandedKeys',
5456
'childrenColumnName',

src/interface.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,7 @@ export type Direction = 'ltr' | 'rtl';
9090
export type SpecialString<T> = T | (string & NonNullable<unknown>);
9191

9292
export type DataIndex<T = any> =
93-
| DeepNamePath<T>
94-
| SpecialString<T>
95-
| number
96-
| (SpecialString<T> | number)[];
93+
DeepNamePath<T> | SpecialString<T> | number | (SpecialString<T> | number)[];
9794

9895
export type CellEllipsisType = { showTitle?: boolean } | boolean;
9996

@@ -139,8 +136,7 @@ export interface ColumnType<RecordType> extends ColumnSharedType<RecordType> {
139136
}
140137

141138
export type ColumnsType<RecordType = unknown> = readonly (
142-
| ColumnGroupType<RecordType>
143-
| ColumnType<RecordType>
139+
ColumnGroupType<RecordType> | ColumnType<RecordType>
144140
)[];
145141

146142
export type GetRowKey<RecordType> = (record: RecordType, index?: number) => Key;
@@ -257,6 +253,7 @@ export interface ExpandableConfig<RecordType> {
257253
expandedRowKeys?: readonly Key[];
258254
defaultExpandedRowKeys?: readonly Key[];
259255
expandedRowRender?: ExpandedRowRender<RecordType>;
256+
forceRender?: boolean;
260257
columnTitle?: React.ReactNode;
261258
expandRowByClick?: boolean;
262259
expandIcon?: RenderExpandIcon<RecordType>;

tests/ExpandRow.spec.jsx

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,96 @@ describe('Table.Expand', () => {
3939
expect(rowRender).toHaveBeenLastCalledWith(sampleData[0], 0, 1, false);
4040
});
4141

42+
it('force renders expanded rows before expansion', () => {
43+
const rowRender = vi.fn(record => <div>{record.name} expanded row</div>);
44+
const { container } = render(
45+
createTable({
46+
expandable: {
47+
expandedRowRender: rowRender,
48+
forceRender: true,
49+
},
50+
}),
51+
);
52+
53+
const expandedRows = container.querySelectorAll('.rc-table-expanded-row');
54+
55+
expect(rowRender).toHaveBeenCalledTimes(2);
56+
expect(rowRender).toHaveBeenNthCalledWith(1, sampleData[0], 0, 1, false);
57+
expect(rowRender).toHaveBeenNthCalledWith(2, sampleData[1], 1, 1, false);
58+
expect(expandedRows).toHaveLength(2);
59+
expect(expandedRows[0]).toHaveStyle({ display: 'none' });
60+
expect(expandedRows[1]).toHaveStyle({ display: 'none' });
61+
62+
fireEvent.click(container.querySelector('.rc-table-row-expand-icon'));
63+
64+
expect(expandedRows[0]).not.toHaveStyle({ display: 'none' });
65+
expect(expandedRows[1]).toHaveStyle({ display: 'none' });
66+
67+
fireEvent.click(container.querySelector('.rc-table-row-expand-icon'));
68+
69+
expect(expandedRows[0]).toHaveStyle({ display: 'none' });
70+
expect(expandedRows[1]).toHaveStyle({ display: 'none' });
71+
});
72+
73+
it('keeps force-rendered expanded content mounted across expand and collapse', () => {
74+
const onMount = vi.fn();
75+
const onUnmount = vi.fn();
76+
77+
class StatefulContent extends React.Component {
78+
state = { count: 0 };
79+
80+
componentDidMount() {
81+
onMount();
82+
}
83+
84+
componentWillUnmount() {
85+
onUnmount();
86+
}
87+
88+
render() {
89+
return (
90+
<button
91+
type="button"
92+
className="stateful-expanded-content"
93+
onClick={() => this.setState(({ count }) => ({ count: count + 1 }))}
94+
>
95+
{this.state.count}
96+
</button>
97+
);
98+
}
99+
}
100+
101+
const { container } = render(
102+
createTable({
103+
data: [sampleData[0]],
104+
expandable: {
105+
expandedRowRender: () => <StatefulContent />,
106+
forceRender: true,
107+
},
108+
}),
109+
);
110+
111+
const expandIcon = container.querySelector('.rc-table-row-expand-icon');
112+
const content = container.querySelector('.stateful-expanded-content');
113+
114+
expect(onMount).toHaveBeenCalledTimes(1);
115+
expect(onUnmount).not.toHaveBeenCalled();
116+
117+
fireEvent.click(expandIcon);
118+
fireEvent.click(content);
119+
expect(content).toHaveTextContent('1');
120+
121+
fireEvent.click(expandIcon);
122+
expect(container.querySelector('.stateful-expanded-content')).toBe(content);
123+
expect(onUnmount).not.toHaveBeenCalled();
124+
125+
fireEvent.click(expandIcon);
126+
expect(container.querySelector('.stateful-expanded-content')).toBe(content);
127+
expect(content).toHaveTextContent('1');
128+
expect(onMount).toHaveBeenCalledTimes(1);
129+
expect(onUnmount).not.toHaveBeenCalled();
130+
});
131+
42132
it('renders tree row correctly', () => {
43133
const data = [
44134
{

0 commit comments

Comments
 (0)