diff --git a/README.md b/README.md index ff5581766..e7c335710 100644 --- a/README.md +++ b/README.md @@ -84,12 +84,15 @@ 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.expandIcon | props => ReactNode | | Deprecated. Use `components.ExpandIcon`; retained as a row-only fallback | | 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. | @@ -105,6 +108,7 @@ Then open `http://localhost:8000`. | emptyText | React.Node or Function | `No Data` | Display text when data is empty | | columns | Object[] | | The columns config of table, see table below | | components | Object | | Override table elements, see [#171](https://github.com/react-component/table/pull/171) for more details | +| components.ExpandIcon | `React.ComponentType` | | Customize both row and expand-all icons. Receives `type` (`row` or `all`), `expanded`, `expandable`, `onClick`, and `record` for rows only | | sticky | boolean \| {offsetHeader?: number, offsetScroll?: number, getContainer?: () => Window \| HTMLElement } | false | stick header and scroll bar | | summary | (data: readonly RecordType[]) => React.ReactNode | - | `summary` attribute in `table` component is used to define the summary row. | | rowHoverable | boolean | true | Table hover interaction | diff --git a/README.zh-CN.md b/README.zh-CN.md index 0b4abb851..5c06515fd 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -84,12 +84,15 @@ 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.expandIcon | props => ReactNode | | 已废弃。请使用 `components.ExpandIcon`;此配置仅作为行展开图标的后备方案保留 | | 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。 | @@ -105,6 +108,7 @@ npm start | emptyText | React.Node or Function | `No Data` | 数据为空时显示文本 | | columns | Object[] | | 表的列配置见下表 | | components | Object | | 覆盖表元素,请参阅 [#171](https://github.com/react-component/table/pull/171) 了解更多详细信息 | +| components.ExpandIcon | `React.ComponentType` | | 自定义行展开和全部展开图标。接收 `type`(`row` 或 `all`)、`expanded`、`expandable`、`onClick`,仅行图标接收 `record` | | sticky | boolean \| {offsetHeader?: number, offsetScroll?: number, getContainer?: () => Window \| HTMLElement } | false | 粘贴标题和滚动条 | | summary | (data: readonly RecordType[]) => React.ReactNode | - | `table` 组件中的 `summary` 属性用于定义汇总行。 | | rowHoverable | boolean | true | 表格 hover 交互 | 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/Body/BodyRow.tsx b/src/Body/BodyRow.tsx index 64b8a096a..d7e7d51c1 100644 --- a/src/Body/BodyRow.tsx +++ b/src/Body/BodyRow.tsx @@ -7,7 +7,7 @@ import useRowInfo from '../hooks/useRowInfo'; import type { ColumnType, CustomizeComponent } from '../interface'; import ExpandedRow from './ExpandedRow'; import type { ExpandedRowProps } from './ExpandedRow'; -import { computedExpandedClassName } from '../utils/expandUtil'; +import { computedExpandedClassName, renderRowExpandIcon } from '../utils/expandUtil'; import type { TableProps } from '..'; export interface BodyRowProps { @@ -54,6 +54,7 @@ export function getCellProps( nestExpandable, indentSize, expandIcon, + ExpandIcon, expanded, hasNestChildren, onTriggerExpand, @@ -73,7 +74,7 @@ export function getCellProps( style={{ paddingLeft: `${indentSize * indent}px` }} className={`${prefixCls}-row-indent indent-level-${indent}`} /> - {expandIcon({ + {renderRowExpandIcon(ExpandIcon, expandIcon, { prefixCls, expanded, expandable: hasNestChildren, diff --git a/src/Table.tsx b/src/Table.tsx index 664ad785b..ebaf206c4 100644 --- a/src/Table.tsx +++ b/src/Table.tsx @@ -314,6 +314,7 @@ const Table = ( mergedExpandIcon, mergedChildrenColumnName, onTriggerExpand, + expandAllInfo, ] = useExpand(props, mergedData, getRowKey); // ====================== Column ====================== @@ -331,6 +332,8 @@ const Table = ( // https://github.com/ant-design/ant-design/issues/23894 onTriggerExpand, expandIcon: mergedExpandIcon, + ExpandIcon: components?.ExpandIcon, + expandAllInfo, expandIconColumnIndex: expandableConfig.expandIconColumnIndex, direction, scrollWidth: useInternalHooks && tailor && typeof scrollX === 'number' ? scrollX : null, @@ -907,6 +910,7 @@ const Table = ( rowClassName, expandedRowClassName: expandableConfig.expandedRowClassName, expandIcon: mergedExpandIcon, + ExpandIcon: components?.ExpandIcon, expandableType, expandRowByClick: expandableConfig.expandRowByClick, expandedRowRender: expandableConfig.expandedRowRender, @@ -965,6 +969,7 @@ const Table = ( rowClassName, expandableConfig.expandedRowClassName, mergedExpandIcon, + components?.ExpandIcon, expandableType, expandableConfig.expandRowByClick, expandableConfig.expandedRowRender, diff --git a/src/context/TableContext.tsx b/src/context/TableContext.tsx index 636702a48..8371016ed 100644 --- a/src/context/TableContext.tsx +++ b/src/context/TableContext.tsx @@ -5,6 +5,7 @@ import type { Direction, ExpandableConfig, ExpandableType, + ExpandIconComponent, ExpandedRowRender, GetComponent, GetComponentProps, @@ -55,6 +56,7 @@ export interface TableContextProps { expandedRowRender: ExpandedRowRender; forceRender: boolean; expandIcon: RenderExpandIcon; + ExpandIcon?: ExpandIconComponent; onTriggerExpand: TriggerEventHandler; expandIconColumnIndex: number; allColumnsFixedLeft: boolean; diff --git a/src/hooks/useColumns/index.tsx b/src/hooks/useColumns/index.tsx index 64db0dacb..efe09a3f3 100644 --- a/src/hooks/useColumns/index.tsx +++ b/src/hooks/useColumns/index.tsx @@ -6,12 +6,15 @@ import type { ColumnsType, ColumnType, Direction, + ExpandIconComponent, + ExpandIconProps, FixedType, GetRowKey, Key, RenderExpandIcon, TriggerEventHandler, } from '../../interface'; +import { DefaultExpandIcon, renderRowExpandIcon } from '../../utils/expandUtil'; import { INTERNAL_COL_DEFINE } from '../../utils/legacyUtil'; import useWidthColumns from './useWidthColumns'; @@ -102,6 +105,8 @@ function useColumns( getRowKey, onTriggerExpand, expandIcon, + ExpandIcon, + expandAllInfo, rowExpandable, expandIconColumnIndex, expandedRowOffset = 0, @@ -117,10 +122,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; + expandIcon: RenderExpandIcon; + ExpandIcon?: ExpandIconComponent; + expandAllInfo?: Pick, 'expanded' | 'expandable' | 'onClick'>; rowExpandable?: (record: RecordType) => boolean; expandIconColumnIndex?: number; direction?: Direction; @@ -190,13 +197,22 @@ function useColumns( fixedColumn = prevColumn ? prevColumn.fixed : null; } + const MergedExpandIcon = ExpandIcon || DefaultExpandIcon; + const expandAllNode = expandAllInfo ? ( + + ) : undefined; + 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, @@ -205,7 +221,7 @@ function useColumns( const expanded = expandedKeys.has(rowKey); const recordExpandable = rowExpandable ? rowExpandable(record) : true; - const icon = expandIcon({ + const icon = renderRowExpandIcon(ExpandIcon, expandIcon, { prefixCls, expanded, expandable: recordExpandable, @@ -238,7 +254,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, + ExpandIcon, + expandAllInfo, + columnTitle, + direction, + expandedRowOffset, + ]); // ========================= Transform ======================== const mergedColumns = React.useMemo(() => { diff --git a/src/hooks/useExpand.ts b/src/hooks/useExpand.ts index dc391c99c..7161dfc7b 100644 --- a/src/hooks/useExpand.ts +++ b/src/hooks/useExpand.ts @@ -4,6 +4,7 @@ import { INTERNAL_HOOKS } from '../constant'; import type { ExpandableConfig, ExpandableType, + ExpandIconProps, GetRowKey, Key, RenderExpandIcon, @@ -13,6 +14,11 @@ import type { TableProps } from '../Table'; import { findAllChildrenKeys, renderExpandIcon } from '../utils/expandUtil'; import { getExpandableProps } from '../utils/legacyUtil'; +type ExpandAllInfo = Pick< + ExpandIconProps, + 'expanded' | 'expandable' | 'onClick' +>; + export default function useExpand( props: TableProps, mergedData: readonly RecordType[], @@ -24,6 +30,7 @@ export default function useExpand( expandIcon: RenderExpandIcon, childrenColumnName: string, onTriggerExpand: TriggerEventHandler, + expandAllInfo: ExpandAllInfo | undefined, ] { const expandableConfig = getExpandableProps(props); @@ -34,8 +41,11 @@ export default function useExpand( defaultExpandAllRows, expandedRowRender, onExpand, + onExpandAll, onExpandedRowsChange, childrenColumnName, + rowExpandable, + showExpandAll, } = expandableConfig; const mergedExpandIcon = expandIcon || renderExpandIcon; @@ -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,47 @@ export default function useExpand( [getRowKey, mergedExpandedKeys, mergedData, onExpand, onExpandedRowsChange], ); + const onTriggerExpandAll: React.MouseEventHandler = React.useCallback( + event => { + event.stopPropagation(); + 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 | undefined>( + () => + showExpandAll && expandableType === 'row' + ? { + expanded: allExpanded, + expandable: expandableRows.length > 0, + onClick: onTriggerExpandAll, + } + : undefined, + [allExpanded, expandableRows.length, expandableType, onTriggerExpandAll, showExpandAll], + ); + // Warning if use `expandedRowRender` and nest children in the same time if ( process.env.NODE_ENV !== 'production' && @@ -123,5 +193,6 @@ export default function useExpand( mergedExpandIcon, mergedChildrenColumnName, onTriggerExpand, + expandAllInfo, ]; } diff --git a/src/hooks/useRowInfo.tsx b/src/hooks/useRowInfo.tsx index cdba4059e..db54cd517 100644 --- a/src/hooks/useRowInfo.tsx +++ b/src/hooks/useRowInfo.tsx @@ -22,6 +22,7 @@ export default function useRowInfo( | 'expandedRowClassName' | 'indentSize' | 'expandIcon' + | 'ExpandIcon' | 'expandedRowRender' | 'forceRender' | 'expandIconColumnIndex' @@ -49,6 +50,7 @@ export default function useRowInfo( 'expandedRowClassName', 'indentSize', 'expandIcon', + 'ExpandIcon', 'expandedRowRender', 'forceRender', 'expandIconColumnIndex', diff --git a/src/index.ts b/src/index.ts index a66a9ad49..026ceac85 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import type { ColumnType, ColumnsType, DefaultRecordType, + ExpandIconProps, ExpandableConfig, FixedType, GetComponentProps, @@ -37,6 +38,7 @@ export { type ColumnType, type ColumnsType, type DefaultRecordType, + type ExpandIconProps, type ExpandableConfig, type FixedType, type GetComponentProps, diff --git a/src/interface.ts b/src/interface.ts index bea4a689e..0f3f71dac 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -178,6 +178,7 @@ export type CustomizeScrollBody = ( ) => React.ReactNode; export interface TableComponents { + ExpandIcon?: ExpandIconComponent; table?: CustomizeComponent; header?: { table?: CustomizeComponent; @@ -211,7 +212,7 @@ export interface LegacyExpandableProps { expandedRowRender?: ExpandedRowRender; /** @deprecated Use `expandable.expandRowByClick` instead */ expandRowByClick?: boolean; - /** @deprecated Use `expandable.expandIcon` instead */ + /** @deprecated Use `components.ExpandIcon` instead */ expandIcon?: RenderExpandIcon; /** @deprecated Use `expandable.onExpand` instead */ onExpand?: (expanded: boolean, record: RecordType) => void; @@ -249,21 +250,42 @@ export type RenderExpandIcon = ( props: RenderExpandIconProps, ) => React.ReactNode; +export type ExpandIconProps = { + prefixCls: string; + expanded: boolean; + expandable: boolean; + onClick: React.MouseEventHandler; +} & ( + | { + type: 'row'; + record: RecordType; + } + | { + type: 'all'; + record?: never; + } +); + +export type ExpandIconComponent = React.ComponentType>; + 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; + /** @deprecated Please use `components.ExpandIcon` instead. This only customizes row icons. */ expandIcon?: RenderExpandIcon; 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..17bc13fd4 100644 --- a/src/utils/expandUtil.tsx +++ b/src/utils/expandUtil.tsx @@ -1,6 +1,47 @@ import * as React from 'react'; import { clsx } from 'clsx'; -import type { RenderExpandIconProps, Key, GetRowKey, ExpandableConfig } from '../interface'; +import type { + ExpandIconComponent, + ExpandIconProps, + RenderExpandIconProps, + RenderExpandIcon, + Key, + GetRowKey, + ExpandableConfig, +} from '../interface'; + +export function DefaultExpandIcon({ + prefixCls, + type, + onClick, + expanded, + expandable, +}: ExpandIconProps) { + const expandClassName = `${prefixCls}-row-expand-icon`; + + if (!expandable) { + return ; + } + + const className = clsx(expandClassName, { + [`${prefixCls}-row-expanded`]: expanded, + [`${prefixCls}-row-collapsed`]: !expanded, + }); + + if (type === 'all') { + return ( +