diff --git a/README.md b/README.md index ff5581766..d41e8f8d1 100644 --- a/README.md +++ b/README.md @@ -84,12 +84,16 @@ Then open `http://localhost:8000`. | expandable.expandedRowRender | Function(recode, index, indent, expanded):ReactNode | | Content render to expanded row | | 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 | | expandable.expandedRowClassName | `string` \| `(recode, index, indent) => string` | | get expanded row's className | +| expandable.columnTitle | ReactNode \| Function(originalNode) | | Customize expand column title | | expandable.expandRowByClick | boolean | | Support expand by click row | | expandable.expandIconColumnIndex | Number | 0 | The index of expandIcon which column will be inserted when expandIconAsCell is false | | expandable.expandIcon | props => ReactNode | | Customize expand icon | +| expandable.expandAllIcon | props => ReactNode | | Customize the icon rendered when `showExpandAll` is enabled | | expandable.indentSize | Number | 15 | indentSize for every level of data.i.children, better using with column.width specified | | expandable.rowExpandable | (record) => boolean | | Config row support expandable | +| expandable.showExpandAll | Boolean | false | Show expand all icon in the expand column header when using `expandedRowRender` | | expandable.onExpand | Function(expanded, record) | | function to call when click expand icon | +| expandable.onExpandAll | Function(expanded, records) | | function to call when click expand all icon | | expandable.onExpandedRowsChange | Function(expandedRows) | | function to call when the expanded rows change | | 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 | | 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. | diff --git a/README.zh-CN.md b/README.zh-CN.md index 0b4abb851..8f2d600e7 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -84,12 +84,16 @@ npm start | expandable.expandedRowRender | Function(recode, index, indent, expanded):ReactNode | | 内容渲染到扩展行 | | expandable.forceRender | Boolean | false | 在展开前强制渲染展开行内容。虚拟模式下,仅强制渲染虚拟列表当前挂载的行;屏幕外的行仍可能被卸载 | | expandable.expandedRowClassName | `string` \| `(recode, index, indent) => string` | | 获取扩展行的 className | +| expandable.columnTitle | ReactNode \| Function(originalNode) | | 自定义展开列表头 | | expandable.expandRowByClick | boolean | | 支持点击行展开 | | expandable.expandIconColumnIndex | Number | 0 | ExpandIconAsCell 为 false 时将插入哪一列的 ExpandIcon 索引 | | expandable.expandIcon | props => ReactNode | | 自定义展开图标 | +| expandable.expandAllIcon | props => ReactNode | | 自定义启用 `showExpandAll` 时显示的全部展开图标 | | expandable.indentSize | Number | 15 | 每一级 `data[i].children` 的缩进尺寸,建议配合指定的 `column.width` 使用 | | expandable.rowExpandable | (record) => boolean | | 配置行支持可扩展 | +| expandable.showExpandAll | Boolean | false | 使用 `expandedRowRender` 时在展开列的表头中显示全部展开图标 | | expandable.onExpand | Function(expanded, record) | | 单击展开图标时调用的函数 | +| expandable.onExpandAll | Function(expanded, records) | | 单击全部展开图标时调用的函数 | | expandable.onExpandedRowsChange | Function(expandedRows) | | 扩展行更改时调用的函数 | | expandable.fixed | String \| Boolean | - | 当表格水平滚动时,此展开图标将被修复: true 或 `left` 或 `right` 和 `expandIconColumnIndex` 需要保留在第一个或最后一个 | | rowKey | string or Function(record, index):string | 'key' | 如果 rowKey 是字符串,则 `record[rowKey]` 将用作键。如果 rowKey 是函数,则 `rowKey(record, index)` 的返回值将用作 key。 | diff --git a/assets/index.less b/assets/index.less index 948062677..3f5ae6745 100644 --- a/assets/index.less +++ b/assets/index.less @@ -230,13 +230,18 @@ } &-row-expand-icon { + box-sizing: inherit; display: inline-block; width: 16px; height: 16px; + padding: 0; color: #aaa; + font: inherit; line-height: 16px; text-align: center; vertical-align: middle; + appearance: none; + background: transparent; border: 1px solid currentColor; cursor: pointer; diff --git a/src/Table.tsx b/src/Table.tsx index 664ad785b..9ce6459cb 100644 --- a/src/Table.tsx +++ b/src/Table.tsx @@ -312,8 +312,10 @@ const Table = ( expandableType, mergedExpandedKeys, mergedExpandIcon, + mergedExpandAllIcon, mergedChildrenColumnName, onTriggerExpand, + expandAllInfo, ] = useExpand(props, mergedData, getRowKey); // ====================== Column ====================== @@ -331,6 +333,8 @@ const Table = ( // https://github.com/ant-design/ant-design/issues/23894 onTriggerExpand, expandIcon: mergedExpandIcon, + expandAllIcon: mergedExpandAllIcon, + expandAllInfo, expandIconColumnIndex: expandableConfig.expandIconColumnIndex, direction, scrollWidth: useInternalHooks && tailor && typeof scrollX === 'number' ? scrollX : null, diff --git a/src/hooks/useColumns/index.tsx b/src/hooks/useColumns/index.tsx index 64db0dacb..73ab04211 100644 --- a/src/hooks/useColumns/index.tsx +++ b/src/hooks/useColumns/index.tsx @@ -9,6 +9,8 @@ import type { FixedType, GetRowKey, Key, + RenderExpandAllIcon, + RenderExpandAllIconProps, RenderExpandIcon, TriggerEventHandler, } from '../../interface'; @@ -102,6 +104,8 @@ function useColumns( getRowKey, onTriggerExpand, expandIcon, + expandAllIcon, + expandAllInfo, rowExpandable, expandIconColumnIndex, expandedRowOffset = 0, @@ -117,10 +121,12 @@ function useColumns( children?: React.ReactNode; expandable: boolean; expandedKeys: Set; - columnTitle?: React.ReactNode; + columnTitle?: React.ReactNode | ((originalNode: React.ReactNode) => React.ReactNode); getRowKey: GetRowKey; onTriggerExpand: TriggerEventHandler; expandIcon?: RenderExpandIcon; + expandAllIcon?: RenderExpandAllIcon; + expandAllInfo: Omit; rowExpandable?: (record: RecordType) => boolean; expandIconColumnIndex?: number; direction?: Direction; @@ -190,13 +196,22 @@ function useColumns( fixedColumn = prevColumn ? prevColumn.fixed : null; } + const expandAllNode = expandAllIcon?.({ + prefixCls, + ...expandAllInfo, + }); + const mergedColumnTitle = + typeof columnTitle === 'function' + ? columnTitle(expandAllNode) + : (columnTitle ?? expandAllNode); + // >>> Create expandable column const expandColumn = { [INTERNAL_COL_DEFINE]: { className: `${prefixCls}-expand-icon-col`, columnType: 'EXPAND_COLUMN', }, - title: columnTitle, + title: mergedColumnTitle, fixed: fixedColumn, className: `${prefixCls}-row-expand-icon-cell`, width: columnWidth, @@ -238,7 +253,18 @@ function useColumns( return baseColumns.filter(col => col !== EXPAND_COLUMN); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [expandable, baseColumns, getRowKey, expandedKeys, expandIcon, direction, expandedRowOffset]); + }, [ + expandable, + baseColumns, + getRowKey, + expandedKeys, + expandIcon, + expandAllIcon, + expandAllInfo, + columnTitle, + direction, + expandedRowOffset, + ]); // ========================= Transform ======================== const mergedColumns = React.useMemo(() => { diff --git a/src/hooks/useExpand.ts b/src/hooks/useExpand.ts index dc391c99c..380d5c240 100644 --- a/src/hooks/useExpand.ts +++ b/src/hooks/useExpand.ts @@ -6,11 +6,13 @@ import type { ExpandableType, GetRowKey, Key, + RenderExpandAllIcon, + RenderExpandAllIconProps, RenderExpandIcon, TriggerEventHandler, } from '../interface'; import type { TableProps } from '../Table'; -import { findAllChildrenKeys, renderExpandIcon } from '../utils/expandUtil'; +import { findAllChildrenKeys, renderExpandAllIcon, renderExpandIcon } from '../utils/expandUtil'; import { getExpandableProps } from '../utils/legacyUtil'; export default function useExpand( @@ -22,20 +24,26 @@ export default function useExpand( expandableType: ExpandableType, expandedKeys: Set, expandIcon: RenderExpandIcon, + expandAllIcon: RenderExpandAllIcon | undefined, childrenColumnName: string, onTriggerExpand: TriggerEventHandler, + expandAllInfo: Omit, ] { const expandableConfig = getExpandableProps(props); const { expandIcon, + expandAllIcon, expandedRowKeys, defaultExpandedRowKeys, defaultExpandAllRows, expandedRowRender, onExpand, + onExpandAll, onExpandedRowsChange, childrenColumnName, + rowExpandable, + showExpandAll, } = expandableConfig; const mergedExpandIcon = expandIcon || renderExpandIcon; @@ -66,6 +74,8 @@ export default function useExpand( /* eslint-enable */ return false; }, [!!expandedRowRender, mergedData]); + const mergedExpandAllIcon = + showExpandAll && expandableType === 'row' ? expandAllIcon || renderExpandAllIcon : undefined; const [innerExpandedKeys, setInnerExpandedKeys] = React.useState(() => { if (defaultExpandedRowKeys) { @@ -81,6 +91,25 @@ export default function useExpand( [expandedRowKeys, innerExpandedKeys], ); + const expandableRows = React.useMemo(() => { + if (!showExpandAll || expandableType !== 'row') { + return []; + } + + return mergedData.reduce<{ key: Key; record: RecordType }[]>((rows, record, index) => { + if (!rowExpandable || rowExpandable(record)) { + rows.push({ + key: getRowKey(record, index), + record, + }); + } + return rows; + }, []); + }, [expandableType, getRowKey, mergedData, rowExpandable, showExpandAll]); + + const allExpanded = + expandableRows.length > 0 && expandableRows.every(({ key }) => mergedExpandedKeys.has(key)); + const onTriggerExpand: TriggerEventHandler = React.useCallback( (record: RecordType) => { const key = getRowKey(record, mergedData.indexOf(record)); @@ -105,6 +134,40 @@ export default function useExpand( [getRowKey, mergedExpandedKeys, mergedData, onExpand, onExpandedRowsChange], ); + const onTriggerExpandAll: React.MouseEventHandler = React.useCallback(() => { + if (!expandableRows.length) { + return; + } + + const nextExpanded = !allExpanded; + const nextExpandedKeys = new Set(mergedExpandedKeys); + + expandableRows.forEach(({ key }) => { + if (nextExpanded) { + nextExpandedKeys.add(key); + } else { + nextExpandedKeys.delete(key); + } + }); + + const keys = [...nextExpandedKeys]; + setInnerExpandedKeys(keys); + onExpandAll?.( + nextExpanded, + expandableRows.map(({ record }) => record), + ); + onExpandedRowsChange?.(keys); + }, [allExpanded, expandableRows, mergedExpandedKeys, onExpandAll, onExpandedRowsChange]); + + const expandAllInfo = React.useMemo>( + () => ({ + expanded: allExpanded, + expandable: expandableRows.length > 0, + onExpand: onTriggerExpandAll, + }), + [allExpanded, expandableRows.length, onTriggerExpandAll], + ); + // Warning if use `expandedRowRender` and nest children in the same time if ( process.env.NODE_ENV !== 'production' && @@ -121,7 +184,9 @@ export default function useExpand( expandableType, mergedExpandedKeys, mergedExpandIcon, + mergedExpandAllIcon, mergedChildrenColumnName, onTriggerExpand, + expandAllInfo, ]; } diff --git a/src/interface.ts b/src/interface.ts index bea4a689e..08d95b5d7 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -249,21 +249,33 @@ export type RenderExpandIcon = ( props: RenderExpandIconProps, ) => React.ReactNode; +export interface RenderExpandAllIconProps { + prefixCls: string; + expanded: boolean; + expandable: boolean; + onExpand: React.MouseEventHandler; +} + +export type RenderExpandAllIcon = (props: RenderExpandAllIconProps) => React.ReactNode; + export interface ExpandableConfig { expandedRowKeys?: readonly Key[]; defaultExpandedRowKeys?: readonly Key[]; expandedRowRender?: ExpandedRowRender; forceRender?: boolean; - columnTitle?: React.ReactNode; + columnTitle?: React.ReactNode | ((originalNode: React.ReactNode) => React.ReactNode); expandRowByClick?: boolean; expandIcon?: RenderExpandIcon; + expandAllIcon?: RenderExpandAllIcon; onExpand?: (expanded: boolean, record: RecordType) => void; + onExpandAll?: (expanded: boolean, records: readonly RecordType[]) => void; onExpandedRowsChange?: (expandedKeys: readonly Key[]) => void; defaultExpandAllRows?: boolean; indentSize?: number; /** @deprecated Please use `EXPAND_COLUMN` in `columns` directly */ expandIconColumnIndex?: number; showExpandColumn?: boolean; + showExpandAll?: boolean; expandedRowClassName?: string | RowClassName; childrenColumnName?: string; rowExpandable?: (record: RecordType) => boolean; diff --git a/src/utils/expandUtil.tsx b/src/utils/expandUtil.tsx index 9a6d5fa98..5d1b21d3c 100644 --- a/src/utils/expandUtil.tsx +++ b/src/utils/expandUtil.tsx @@ -1,6 +1,12 @@ import * as React from 'react'; import { clsx } from 'clsx'; -import type { RenderExpandIconProps, Key, GetRowKey, ExpandableConfig } from '../interface'; +import type { + RenderExpandAllIconProps, + RenderExpandIconProps, + Key, + GetRowKey, + ExpandableConfig, +} from '../interface'; export function renderExpandIcon({ prefixCls, @@ -31,6 +37,37 @@ export function renderExpandIcon({ ); } +export function renderExpandAllIcon({ + prefixCls, + onExpand, + expanded, + expandable, +}: RenderExpandAllIconProps) { + const expandClassName = `${prefixCls}-row-expand-icon`; + + if (!expandable) { + return ; + } + + const onClick: React.MouseEventHandler = event => { + onExpand(event); + event.stopPropagation(); + }; + + return ( +