Skip to content

Commit 22bede1

Browse files
committed
feat: support expand all in table header
1 parent adf0701 commit 22bede1

8 files changed

Lines changed: 318 additions & 6 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,16 @@ Then open `http://localhost:8000`.
8383
| expandable.expandedRowKeys | String[] | | current expanded rows keys |
8484
| expandable.expandedRowRender | Function(recode, index, indent, expanded):ReactNode | | Content render to expanded row |
8585
| expandable.expandedRowClassName | `string` \| `(recode, index, indent) => string` | | get expanded row's className |
86+
| expandable.columnTitle | ReactNode \| Function(originalNode) | | Customize expand column title |
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 |
8889
| expandable.expandIcon | props => ReactNode | | Customize expand icon |
90+
| expandable.expandAllIcon | props => ReactNode | | Customize the icon rendered when `showExpandAll` is enabled |
8991
| expandable.indentSize | Number | 15 | indentSize for every level of data.i.children, better using with column.width specified |
9092
| expandable.rowExpandable | (record) => boolean | | Config row support expandable |
93+
| expandable.showExpandAll | Boolean | false | Show expand all icon in the expand column header when using `expandedRowRender` |
9194
| expandable.onExpand | Function(expanded, record) | | function to call when click expand icon |
95+
| expandable.onExpandAll | Function(expanded, records) | | function to call when click expand all icon |
9296
| expandable.onExpandedRowsChange | Function(expandedRows) | | function to call when the expanded rows change |
9397
| expandable.fixed | String \| Boolean | - | this expand icon will be fixed when table scroll horizontally: true or `left` or `right` and `expandIconColumnIndex` need to stay first or last |
9498
| rowKey | string or Function(record, index):string | 'key' | If rowKey is string, `record[rowKey]` will be used as key. If rowKey is function, the return value of `rowKey(record, index)` will be use as key. |

README.zh-CN.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,16 @@ npm start
8383
| expandable.expandedRowKeys | String[] | | 当前扩展行键 |
8484
| expandable.expandedRowRender | Function(recode, index, indent, expanded):ReactNode | | 内容渲染到扩展行 |
8585
| expandable.expandedRowClassName | `string` \| `(recode, index, indent) => string` | | 获取扩展行的 className |
86+
| expandable.columnTitle | ReactNode \| Function(originalNode) | | 自定义展开列表头 |
8687
| expandable.expandRowByClick | boolean | | 支持点击行展开 |
8788
| expandable.expandIconColumnIndex | Number | 0 | ExpandIconAsCell 为 false 时将插入哪一列的 ExpandIcon 索引 |
8889
| expandable.expandIcon | props => ReactNode | | 自定义展开图标 |
90+
| expandable.expandAllIcon | props => ReactNode | | 自定义启用 `showExpandAll` 时显示的全部展开图标 |
8991
| expandable.indentSize | Number | 15 | 每一级 `data[i].children` 的缩进尺寸,建议配合指定的 `column.width` 使用 |
9092
| expandable.rowExpandable | (record) => boolean | | 配置行支持可扩展 |
93+
| expandable.showExpandAll | Boolean | false | 使用 `expandedRowRender` 时在展开列的表头中显示全部展开图标 |
9194
| expandable.onExpand | Function(expanded, record) | | 单击展开图标时调用的函数 |
95+
| expandable.onExpandAll | Function(expanded, records) | | 单击全部展开图标时调用的函数 |
9296
| expandable.onExpandedRowsChange | Function(expandedRows) | | 扩展行更改时调用的函数 |
9397
| expandable.fixed | String \| Boolean | - | 当表格水平滚动时,此展开图标将被修复: true 或 `left``right``expandIconColumnIndex` 需要保留在第一个或最后一个 |
9498
| rowKey | string or Function(record, index):string | 'key' | 如果 rowKey 是字符串,则 `record[rowKey]` 将用作键。如果 rowKey 是函数,则 `rowKey(record, index)` 的返回值将用作 key。 |

src/Table.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,10 @@ const Table = <RecordType extends DefaultRecordType>(
312312
expandableType,
313313
mergedExpandedKeys,
314314
mergedExpandIcon,
315+
mergedExpandAllIcon,
315316
mergedChildrenColumnName,
316317
onTriggerExpand,
318+
expandAllInfo,
317319
] = useExpand(props, mergedData, getRowKey);
318320

319321
// ====================== Column ======================
@@ -331,6 +333,8 @@ const Table = <RecordType extends DefaultRecordType>(
331333
// https://github.com/ant-design/ant-design/issues/23894
332334
onTriggerExpand,
333335
expandIcon: mergedExpandIcon,
336+
expandAllIcon: mergedExpandAllIcon,
337+
expandAllInfo,
334338
expandIconColumnIndex: expandableConfig.expandIconColumnIndex,
335339
direction,
336340
scrollWidth: useInternalHooks && tailor && typeof scrollX === 'number' ? scrollX : null,

src/hooks/useColumns/index.tsx

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import type {
99
FixedType,
1010
GetRowKey,
1111
Key,
12+
RenderExpandAllIcon,
13+
RenderExpandAllIconProps,
1214
RenderExpandIcon,
1315
TriggerEventHandler,
1416
} from '../../interface';
@@ -102,6 +104,8 @@ function useColumns<RecordType>(
102104
getRowKey,
103105
onTriggerExpand,
104106
expandIcon,
107+
expandAllIcon,
108+
expandAllInfo,
105109
rowExpandable,
106110
expandIconColumnIndex,
107111
expandedRowOffset = 0,
@@ -117,10 +121,12 @@ function useColumns<RecordType>(
117121
children?: React.ReactNode;
118122
expandable: boolean;
119123
expandedKeys: Set<Key>;
120-
columnTitle?: React.ReactNode;
124+
columnTitle?: React.ReactNode | ((originalNode: React.ReactNode) => React.ReactNode);
121125
getRowKey: GetRowKey<RecordType>;
122126
onTriggerExpand: TriggerEventHandler<RecordType>;
123127
expandIcon?: RenderExpandIcon<RecordType>;
128+
expandAllIcon?: RenderExpandAllIcon;
129+
expandAllInfo: Omit<RenderExpandAllIconProps, 'prefixCls'>;
124130
rowExpandable?: (record: RecordType) => boolean;
125131
expandIconColumnIndex?: number;
126132
direction?: Direction;
@@ -190,13 +196,22 @@ function useColumns<RecordType>(
190196
fixedColumn = prevColumn ? prevColumn.fixed : null;
191197
}
192198

199+
const expandAllNode = expandAllIcon?.({
200+
prefixCls,
201+
...expandAllInfo,
202+
});
203+
const mergedColumnTitle =
204+
typeof columnTitle === 'function'
205+
? columnTitle(expandAllNode)
206+
: (columnTitle ?? expandAllNode);
207+
193208
// >>> Create expandable column
194209
const expandColumn = {
195210
[INTERNAL_COL_DEFINE]: {
196211
className: `${prefixCls}-expand-icon-col`,
197212
columnType: 'EXPAND_COLUMN',
198213
},
199-
title: columnTitle,
214+
title: mergedColumnTitle,
200215
fixed: fixedColumn,
201216
className: `${prefixCls}-row-expand-icon-cell`,
202217
width: columnWidth,
@@ -238,7 +253,18 @@ function useColumns<RecordType>(
238253

239254
return baseColumns.filter(col => col !== EXPAND_COLUMN);
240255
// eslint-disable-next-line react-hooks/exhaustive-deps
241-
}, [expandable, baseColumns, getRowKey, expandedKeys, expandIcon, direction, expandedRowOffset]);
256+
}, [
257+
expandable,
258+
baseColumns,
259+
getRowKey,
260+
expandedKeys,
261+
expandIcon,
262+
expandAllIcon,
263+
expandAllInfo,
264+
columnTitle,
265+
direction,
266+
expandedRowOffset,
267+
]);
242268

243269
// ========================= Transform ========================
244270
const mergedColumns = React.useMemo(() => {

src/hooks/useExpand.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import type {
66
ExpandableType,
77
GetRowKey,
88
Key,
9+
RenderExpandAllIcon,
10+
RenderExpandAllIconProps,
911
RenderExpandIcon,
1012
TriggerEventHandler,
1113
} from '../interface';
1214
import type { TableProps } from '../Table';
13-
import { findAllChildrenKeys, renderExpandIcon } from '../utils/expandUtil';
15+
import { findAllChildrenKeys, renderExpandAllIcon, renderExpandIcon } from '../utils/expandUtil';
1416
import { getExpandableProps } from '../utils/legacyUtil';
1517

1618
export default function useExpand<RecordType>(
@@ -22,20 +24,26 @@ export default function useExpand<RecordType>(
2224
expandableType: ExpandableType,
2325
expandedKeys: Set<Key>,
2426
expandIcon: RenderExpandIcon<RecordType>,
27+
expandAllIcon: RenderExpandAllIcon | undefined,
2528
childrenColumnName: string,
2629
onTriggerExpand: TriggerEventHandler<RecordType>,
30+
expandAllInfo: Omit<RenderExpandAllIconProps, 'prefixCls'>,
2731
] {
2832
const expandableConfig = getExpandableProps(props);
2933

3034
const {
3135
expandIcon,
36+
expandAllIcon,
3237
expandedRowKeys,
3338
defaultExpandedRowKeys,
3439
defaultExpandAllRows,
3540
expandedRowRender,
3641
onExpand,
42+
onExpandAll,
3743
onExpandedRowsChange,
3844
childrenColumnName,
45+
rowExpandable,
46+
showExpandAll,
3947
} = expandableConfig;
4048

4149
const mergedExpandIcon = expandIcon || renderExpandIcon;
@@ -66,6 +74,8 @@ export default function useExpand<RecordType>(
6674
/* eslint-enable */
6775
return false;
6876
}, [!!expandedRowRender, mergedData]);
77+
const mergedExpandAllIcon =
78+
showExpandAll && expandableType === 'row' ? expandAllIcon || renderExpandAllIcon : undefined;
6979

7080
const [innerExpandedKeys, setInnerExpandedKeys] = React.useState(() => {
7181
if (defaultExpandedRowKeys) {
@@ -81,6 +91,25 @@ export default function useExpand<RecordType>(
8191
[expandedRowKeys, innerExpandedKeys],
8292
);
8393

94+
const expandableRows = React.useMemo(() => {
95+
if (!showExpandAll || expandableType !== 'row') {
96+
return [];
97+
}
98+
99+
return mergedData.reduce<{ key: Key; record: RecordType }[]>((rows, record, index) => {
100+
if (!rowExpandable || rowExpandable(record)) {
101+
rows.push({
102+
key: getRowKey(record, index),
103+
record,
104+
});
105+
}
106+
return rows;
107+
}, []);
108+
}, [expandableType, getRowKey, mergedData, rowExpandable, showExpandAll]);
109+
110+
const allExpanded =
111+
expandableRows.length > 0 && expandableRows.every(({ key }) => mergedExpandedKeys.has(key));
112+
84113
const onTriggerExpand: TriggerEventHandler<RecordType> = React.useCallback(
85114
(record: RecordType) => {
86115
const key = getRowKey(record, mergedData.indexOf(record));
@@ -105,6 +134,40 @@ export default function useExpand<RecordType>(
105134
[getRowKey, mergedExpandedKeys, mergedData, onExpand, onExpandedRowsChange],
106135
);
107136

137+
const onTriggerExpandAll: React.MouseEventHandler<HTMLElement> = React.useCallback(() => {
138+
if (!expandableRows.length) {
139+
return;
140+
}
141+
142+
const nextExpanded = !allExpanded;
143+
const nextExpandedKeys = new Set(mergedExpandedKeys);
144+
145+
expandableRows.forEach(({ key }) => {
146+
if (nextExpanded) {
147+
nextExpandedKeys.add(key);
148+
} else {
149+
nextExpandedKeys.delete(key);
150+
}
151+
});
152+
153+
const keys = [...nextExpandedKeys];
154+
setInnerExpandedKeys(keys);
155+
onExpandAll?.(
156+
nextExpanded,
157+
expandableRows.map(({ record }) => record),
158+
);
159+
onExpandedRowsChange?.(keys);
160+
}, [allExpanded, expandableRows, mergedExpandedKeys, onExpandAll, onExpandedRowsChange]);
161+
162+
const expandAllInfo = React.useMemo<Omit<RenderExpandAllIconProps, 'prefixCls'>>(
163+
() => ({
164+
expanded: allExpanded,
165+
expandable: expandableRows.length > 0,
166+
onExpand: onTriggerExpandAll,
167+
}),
168+
[allExpanded, expandableRows.length, onTriggerExpandAll],
169+
);
170+
108171
// Warning if use `expandedRowRender` and nest children in the same time
109172
if (
110173
process.env.NODE_ENV !== 'production' &&
@@ -121,7 +184,9 @@ export default function useExpand<RecordType>(
121184
expandableType,
122185
mergedExpandedKeys,
123186
mergedExpandIcon,
187+
mergedExpandAllIcon,
124188
mergedChildrenColumnName,
125189
onTriggerExpand,
190+
expandAllInfo,
126191
];
127192
}

src/interface.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,20 +253,32 @@ export type RenderExpandIcon<RecordType> = (
253253
props: RenderExpandIconProps<RecordType>,
254254
) => React.ReactNode;
255255

256+
export interface RenderExpandAllIconProps {
257+
prefixCls: string;
258+
expanded: boolean;
259+
expandable: boolean;
260+
onExpand: React.MouseEventHandler<HTMLElement>;
261+
}
262+
263+
export type RenderExpandAllIcon = (props: RenderExpandAllIconProps) => React.ReactNode;
264+
256265
export interface ExpandableConfig<RecordType> {
257266
expandedRowKeys?: readonly Key[];
258267
defaultExpandedRowKeys?: readonly Key[];
259268
expandedRowRender?: ExpandedRowRender<RecordType>;
260-
columnTitle?: React.ReactNode;
269+
columnTitle?: React.ReactNode | ((originalNode: React.ReactNode) => React.ReactNode);
261270
expandRowByClick?: boolean;
262271
expandIcon?: RenderExpandIcon<RecordType>;
272+
expandAllIcon?: RenderExpandAllIcon;
263273
onExpand?: (expanded: boolean, record: RecordType) => void;
274+
onExpandAll?: (expanded: boolean, records: readonly RecordType[]) => void;
264275
onExpandedRowsChange?: (expandedKeys: readonly Key[]) => void;
265276
defaultExpandAllRows?: boolean;
266277
indentSize?: number;
267278
/** @deprecated Please use `EXPAND_COLUMN` in `columns` directly */
268279
expandIconColumnIndex?: number;
269280
showExpandColumn?: boolean;
281+
showExpandAll?: boolean;
270282
expandedRowClassName?: string | RowClassName<RecordType>;
271283
childrenColumnName?: string;
272284
rowExpandable?: (record: RecordType) => boolean;

src/utils/expandUtil.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import * as React from 'react';
22
import { clsx } from 'clsx';
3-
import type { RenderExpandIconProps, Key, GetRowKey, ExpandableConfig } from '../interface';
3+
import type {
4+
RenderExpandAllIconProps,
5+
RenderExpandIconProps,
6+
Key,
7+
GetRowKey,
8+
ExpandableConfig,
9+
} from '../interface';
410

511
export function renderExpandIcon<RecordType>({
612
prefixCls,
@@ -31,6 +37,34 @@ export function renderExpandIcon<RecordType>({
3137
);
3238
}
3339

40+
export function renderExpandAllIcon({
41+
prefixCls,
42+
onExpand,
43+
expanded,
44+
expandable,
45+
}: RenderExpandAllIconProps) {
46+
const expandClassName = `${prefixCls}-row-expand-icon`;
47+
48+
if (!expandable) {
49+
return <span className={clsx(expandClassName, `${prefixCls}-row-spaced`)} />;
50+
}
51+
52+
const onClick: React.MouseEventHandler<HTMLElement> = event => {
53+
onExpand(event);
54+
event.stopPropagation();
55+
};
56+
57+
return (
58+
<span
59+
className={clsx(expandClassName, {
60+
[`${prefixCls}-row-expanded`]: expanded,
61+
[`${prefixCls}-row-collapsed`]: !expanded,
62+
})}
63+
onClick={onClick}
64+
/>
65+
);
66+
}
67+
3468
export function findAllChildrenKeys<RecordType>(
3569
data: readonly RecordType[],
3670
getRowKey: GetRowKey<RecordType>,

0 commit comments

Comments
 (0)