From 7f921098e73ae154b219f560b662df74f188c5e6 Mon Sep 17 00:00:00 2001 From: fansaly Date: Wed, 8 May 2024 21:08:49 +0800 Subject: [PATCH 1/4] feat: grouping columns specified colSpan & rowSpan --- ...uping-columns-specified-colSpan-rowSpan.md | 8 + ...ping-columns-specified-colSpan-rowSpan.tsx | 148 ++++++++++ src/Header/Header.tsx | 3 +- src/utils/convertUtil.ts | 162 +++++++++++ tests/GroupingColumns.spec.jsx | 254 ++++++++++++++++++ 5 files changed, 574 insertions(+), 1 deletion(-) create mode 100644 docs/demo/grouping-columns-specified-colSpan-rowSpan.md create mode 100644 docs/examples/grouping-columns-specified-colSpan-rowSpan.tsx create mode 100644 src/utils/convertUtil.ts diff --git a/docs/demo/grouping-columns-specified-colSpan-rowSpan.md b/docs/demo/grouping-columns-specified-colSpan-rowSpan.md new file mode 100644 index 000000000..076b2d2cb --- /dev/null +++ b/docs/demo/grouping-columns-specified-colSpan-rowSpan.md @@ -0,0 +1,8 @@ +--- +title: grouping-columns-specified-colSpan-rowSpan.tsx +nav: + title: Demo + path: /demo +--- + + diff --git a/docs/examples/grouping-columns-specified-colSpan-rowSpan.tsx b/docs/examples/grouping-columns-specified-colSpan-rowSpan.tsx new file mode 100644 index 000000000..95639d0ff --- /dev/null +++ b/docs/examples/grouping-columns-specified-colSpan-rowSpan.tsx @@ -0,0 +1,148 @@ +import React from 'react'; +import type { TableProps } from 'rc-table'; +import Table from 'rc-table'; +import '../../assets/index.less'; + +const columns: TableProps['columns'] = [ + { + title: '姓名', + dataIndex: 'name', + key: 'name', + }, + { + title: '出勤', + rowSpan: 3, + children: [ + { + title: '出勤', + dataIndex: 'attendance', + key: 'attendance', + }, + { + title: '迟到', + dataIndex: 'late', + key: 'late', + }, + { + title: '请假', + dataIndex: 'leave', + key: 'leave', + }, + ], + }, + { + title: '其它', + children: [ + { + title: '年龄', + dataIndex: 'age', + key: 'age', + }, + { + title: '住址', + children: [ + { + title: '街道', + dataIndex: 'street', + key: 'street', + }, + { + title: '小区', + children: [ + { + title: '单元', + dataIndex: 'building', + key: 'building', + }, + { + title: '门牌', + dataIndex: 'number', + key: 'number', + }, + ], + }, + ], + }, + ], + }, + { + title: '技能', + rowSpan: 2, + children: [ + { + title: '前端', + dataIndex: 'frontend', + key: 'frontend', + }, + { + title: '后端', + dataIndex: 'backend', + key: 'backend', + }, + ], + }, + { + title: '公司', + children: [ + { + title: '地址', + dataIndex: 'companyAddress', + key: 'companyAddress', + }, + { + title: '名称', + dataIndex: 'companyName', + key: 'companyName', + }, + ], + }, + { + title: '性别', + dataIndex: 'gender', + key: 'gender', + }, +]; + +const data = [ + { + key: '1', + name: '胡彦斌', + attendance: 20, + late: 0, + leave: 1, + age: 32, + street: '拱墅区和睦街道', + building: 1, + number: 2033, + frontend: 'S', + backend: 'S', + companyAddress: '西湖区湖底公园', + companyName: '湖底有限公司', + gender: '男', + }, + { + key: '2', + name: '胡彦祖', + attendance: 20, + late: 0, + leave: 1, + age: 42, + street: '拱墅区和睦街道', + building: 3, + number: 2035, + frontend: 'S', + backend: 'S', + companyAddress: '西湖区湖底公园', + companyName: '湖底有限公司', + gender: '男', + }, +]; + +const Demo = () => ( +
+

grouping columns specified colSpan & rowSpan

+ + +); + +export default Demo; diff --git a/src/Header/Header.tsx b/src/Header/Header.tsx index f21b817b2..071625626 100644 --- a/src/Header/Header.tsx +++ b/src/Header/Header.tsx @@ -2,6 +2,7 @@ import { useContext } from '@rc-component/context'; import * as React from 'react'; import TableContext, { responseImmutable } from '../context/TableContext'; import devRenderTimes from '../hooks/useRenderTimes'; +import { convertColumns } from '../utils/convertUtil'; import type { CellType, ColumnGroupType, @@ -67,7 +68,7 @@ function parseHeaderRows( } // Generate `rows` cell data - fillRowCells(rootColumns, 0); + fillRowCells(convertColumns>(rootColumns), 0); // Handle `rowSpan` const rowCount = rows.length; diff --git a/src/utils/convertUtil.ts b/src/utils/convertUtil.ts new file mode 100644 index 000000000..eb7e0ae5a --- /dev/null +++ b/src/utils/convertUtil.ts @@ -0,0 +1,162 @@ +interface Column { + [key: string | symbol]: any; +} + +interface Options { + children: string; + colSpan: string; + rowSpan: string; + hidden: string; +} + +export function convertColumns( + columns: Columns, + options: Partial = {}, +) { + if (!Array.isArray(columns) || columns.length === 0) { + return [] as unknown as Columns; + } + + const defaultOptions = { + children: 'children', + colSpan: 'colSpan', + rowSpan: 'rowSpan', + hidden: 'hidden', + }; + const { + children: childrenProp, + colSpan: colSpanProp, + rowSpan: rowSpanProp, + hidden: hiddenProp, + } = Object.assign({}, defaultOptions, options); + + let specified = false; + let tree = columns.map(item => ({ ...item }) as Column); + + let depthCurr = 0; + let depthNext = 0; + const nodePos: { + index: number; + total: number; + }[] = [ + { + index: tree.length, + total: tree.length, + }, + ]; + const rowSpans: number[] = []; + const columnsMap = new Map(); + const treeMap = new Map(); + const branchLastSet = new Set(); + + while (tree.length > 0) { + depthCurr = depthNext; + + nodePos.splice(depthCurr + 1); + rowSpans.splice(depthCurr); + + nodePos[depthCurr].index--; + + if (nodePos[depthCurr].index <= 0) { + depthNext = 0; + + for (let i = nodePos.length - 1; i >= 0; i--) { + if (nodePos[i].index > 0) { + depthNext = i; + break; + } + } + } + + const node = tree.shift(); + + if (!node || typeof node !== 'object' || node[hiddenProp]) { + continue; + } + + const colSpanSpecified = node[colSpanProp]; + const rowSpanSpecified = node[rowSpanProp]; + const colSpan = node[colSpanProp] ?? 1; + const rowSpan = node[rowSpanProp] ?? 1; + node[colSpanProp] = colSpan; + node[rowSpanProp] = rowSpan; + + if (!specified && (colSpan > 1 || rowSpan > 1)) { + specified = true; + } + + const parentsRowCount = rowSpans.reduce((acc, num) => acc + num, 0); + if (!columnsMap.has(parentsRowCount)) { + columnsMap.set(parentsRowCount, []); + } + columnsMap.get(parentsRowCount).push(node); + + let leaf = node[childrenProp]; + delete node[childrenProp]; + + if (Array.isArray(leaf) && leaf.length > 0) { + depthNext = depthCurr + 1; + nodePos[depthNext] = { index: leaf.length, total: leaf.length }; + rowSpans[depthCurr] = rowSpan; + + leaf = leaf.map(item => ({ ...item }) as Column); + node.colSpanSpecified = colSpanSpecified; + if (!treeMap.has(node)) { + treeMap.set(node, []); + } + treeMap.get(node).push(...leaf); + tree = [...leaf, ...tree]; + } else { + node.rowSpanSpecified = rowSpanSpecified; + node.parentsRowCount = parentsRowCount; + branchLastSet.add(node); + } + } + + if (!specified) { + return columns; + } + + // correct colSpan of parent column in default state + [...treeMap.keys()].reverse().forEach(column => { + const { colSpanSpecified } = column; + delete column.colSpanSpecified; + + if (column[hiddenProp] || Number.isInteger(colSpanSpecified)) { + return; + } + + const children = treeMap.get(column); + column[colSpanProp] = children.reduce((acc, item) => { + return item[hiddenProp] ? acc : acc + item[colSpanProp]; + }, 0); + }); + + let rowCountMax = 0; + branchLastSet.forEach(column => { + const rowCount = column[rowSpanProp] + column.parentsRowCount; + if (rowCount > rowCountMax) { + rowCountMax = rowCount; + } + }); + + // correct rowSpan of column in default state + branchLastSet.forEach(column => { + const { rowSpanSpecified, parentsRowCount } = column; + + if (!Number.isInteger(rowSpanSpecified)) { + column[rowSpanProp] = rowCountMax - parentsRowCount; + } + + delete column.rowSpanSpecified; + delete column.parentsRowCount; + }); + + const keys = [...columnsMap.keys()].sort(); + for (let i = keys.length - 1; i >= 1; i--) { + const parent = columnsMap.get(keys[i - 1]); + parent[0][childrenProp] = columnsMap.get(keys[i]); + } + + return columnsMap.get(0) as unknown as Columns; +} diff --git a/tests/GroupingColumns.spec.jsx b/tests/GroupingColumns.spec.jsx index 822fb4e7f..bc8038f3a 100644 --- a/tests/GroupingColumns.spec.jsx +++ b/tests/GroupingColumns.spec.jsx @@ -155,6 +155,260 @@ describe('Table with grouping columns', () => { expect(titleA.prop('rowSpan')).toBe(3); }); + it('more strange layout', () => { + /** + * +---+---+-----------+-----------+-----------+ + * | A | B | C | | E | + * +---+---+---+-------+ D +-----------+ + * | | G | H | | I | + * | +---+---+---+---+---+---+---+---+---+ + * | F | | | | | N | O | P | | + * | | J | K | L | M +---+---+---+ Q | + * | | | | | | R | S | T | | + * +-------+---+---+---+---+---+---+---+---+---+ + */ + + const columns = [ + { + title: 'A', + className: 'title-a', + colSpan: 1, + children: [ + { + title: 'F', + className: 'title-f', + colSpan: 2, + dataIndex: 'F', + key: 'F', + onCell: () => ({ colSpan: 2 }), + }, + ], + }, + { + title: 'B', + className: 'title-b', + rowSpan: 1, + onCell: () => ({ colSpan: 0 }), + }, + { + title: 'C', + className: 'title-c', + children: [ + { + title: 'G', + className: 'title-g', + children: [ + { + title: 'J', + className: 'title-j', + dataIndex: 'J', + key: 'J', + }, + ], + }, + { + title: 'H', + className: 'title-h', + colSpan: 2, + children: [ + { + title: 'K', + className: 'title-k', + dataIndex: 'K', + key: 'K', + }, + { + title: 'L', + className: 'title-l', + colSpan: 2, + dataIndex: 'L', + key: 'L', + onCell: () => ({ colSpan: 2 }), + }, + ], + }, + ], + }, + { + title: 'D', + className: 'title-d', + colSpan: 3, + rowSpan: 2, + children: [ + { + title: 'M', + className: 'title-m', + dataIndex: 'M', + key: 'M', + }, + { + title: 'N', + className: 'title-n', + children: [ + { + title: 'R', + className: 'title-r', + dataIndex: 'R', + key: 'R', + }, + ], + }, + ], + }, + { + title: 'E', + className: 'title-e', + children: [ + { + title: 'I', + className: 'title-i', + children: [ + { + title: 'O', + className: 'title-o', + children: [ + { + title: 'S', + className: 'title-s', + dataIndex: 'S', + key: 'S', + }, + ], + }, + { + title: 'P', + className: 'title-p', + children: [ + { + title: 'T', + className: 'title-t', + dataIndex: 'T', + key: 'T', + }, + ], + }, + { + title: 'Q', + className: 'title-q', + dataIndex: 'Q', + key: 'Q', + }, + ], + }, + ], + }, + ]; + + const data = [ + { + key: '1', + F: 'F-1', + J: 'J-1', + K: 'K-1', + L: 'L-1', + M: 'M-1', + R: 'R-1', + S: 'S-1', + T: 'T-1', + Q: 'Q-1', + }, + { + key: '2', + F: 'F-2', + J: 'J-2', + K: 'K-2', + L: 'L-2', + M: 'M-2', + R: 'R-2', + S: 'S-2', + T: 'T-2', + Q: 'Q-2', + }, + ]; + + const wrapper = mount(
); + + const titleA = wrapper.find('th.title-a'); + expect(titleA.prop('colSpan')).toBe(null); + expect(titleA.prop('rowSpan')).toBe(null); + + const titleB = wrapper.find('th.title-b'); + expect(titleB.prop('colSpan')).toBe(null); + expect(titleB.prop('rowSpan')).toBe(null); + + const titleC = wrapper.find('th.title-c'); + expect(titleC.prop('colSpan')).toBe(3); + expect(titleC.prop('rowSpan')).toBe(null); + + const titleD = wrapper.find('th.title-d'); + expect(titleD.prop('colSpan')).toBe(3); + expect(titleD.prop('rowSpan')).toBe(2); + + const titleE = wrapper.find('th.title-e'); + expect(titleE.prop('colSpan')).toBe(3); + expect(titleE.prop('rowSpan')).toBe(null); + + const titleF = wrapper.find('th.title-f'); + expect(titleF.prop('colSpan')).toBe(2); + expect(titleF.prop('rowSpan')).toBe(3); + + const titleG = wrapper.find('th.title-g'); + expect(titleG.prop('colSpan')).toBe(null); + expect(titleG.prop('rowSpan')).toBe(null); + + const titleH = wrapper.find('th.title-h'); + expect(titleH.prop('colSpan')).toBe(2); + expect(titleH.prop('rowSpan')).toBe(null); + + const titleI = wrapper.find('th.title-i'); + expect(titleI.prop('colSpan')).toBe(3); + expect(titleI.prop('rowSpan')).toBe(null); + + const titleJ = wrapper.find('th.title-j'); + expect(titleJ.prop('colSpan')).toBe(null); + expect(titleJ.prop('rowSpan')).toBe(2); + + const titleK = wrapper.find('th.title-k'); + expect(titleK.prop('colSpan')).toBe(null); + expect(titleK.prop('rowSpan')).toBe(2); + + const titleL = wrapper.find('th.title-l'); + expect(titleL.prop('colSpan')).toBe(2); + expect(titleL.prop('rowSpan')).toBe(2); + + const titleM = wrapper.find('th.title-m'); + expect(titleM.prop('colSpan')).toBe(null); + expect(titleM.prop('rowSpan')).toBe(2); + + const titleN = wrapper.find('th.title-n'); + expect(titleN.prop('colSpan')).toBe(null); + expect(titleN.prop('rowSpan')).toBe(null); + + const titleO = wrapper.find('th.title-o'); + expect(titleO.prop('colSpan')).toBe(null); + expect(titleO.prop('rowSpan')).toBe(null); + + const titleP = wrapper.find('th.title-p'); + expect(titleP.prop('colSpan')).toBe(null); + expect(titleP.prop('rowSpan')).toBe(null); + + const titleQ = wrapper.find('th.title-q'); + expect(titleQ.prop('colSpan')).toBe(null); + expect(titleQ.prop('rowSpan')).toBe(2); + + const titleR = wrapper.find('th.title-r'); + expect(titleR.prop('colSpan')).toBe(null); + expect(titleR.prop('rowSpan')).toBe(null); + + const titleS = wrapper.find('th.title-s'); + expect(titleS.prop('colSpan')).toBe(null); + expect(titleS.prop('rowSpan')).toBe(null); + + const titleT = wrapper.find('th.title-t'); + expect(titleT.prop('colSpan')).toBe(null); + expect(titleT.prop('rowSpan')).toBe(null); + }); + it('hidden column', () => { const columns = [ { From 33674f319a65604b956148158ff7a21ab2670fbc Mon Sep 17 00:00:00 2001 From: fansaly Date: Tue, 14 May 2024 11:05:02 +0800 Subject: [PATCH 2/4] refactor: use built-in types --- src/Header/Header.tsx | 2 +- src/utils/convertUtil.ts | 95 ++++++++++++++++------------------------ 2 files changed, 38 insertions(+), 59 deletions(-) diff --git a/src/Header/Header.tsx b/src/Header/Header.tsx index 071625626..9c8564f67 100644 --- a/src/Header/Header.tsx +++ b/src/Header/Header.tsx @@ -68,7 +68,7 @@ function parseHeaderRows( } // Generate `rows` cell data - fillRowCells(convertColumns>(rootColumns), 0); + fillRowCells(convertColumns(rootColumns), 0); // Handle `rowSpan` const rowCount = rows.length; diff --git a/src/utils/convertUtil.ts b/src/utils/convertUtil.ts index eb7e0ae5a..142b4141a 100644 --- a/src/utils/convertUtil.ts +++ b/src/utils/convertUtil.ts @@ -1,53 +1,32 @@ -interface Column { - [key: string | symbol]: any; -} +import type { ColumnsType, ColumnType } from '../interface'; -interface Options { - children: string; - colSpan: string; - rowSpan: string; - hidden: string; +interface Column extends ColumnType { + colSpanSpecified?: number; + rowSpanSpecified?: number; + parentsRowCount?: number; + children?: Column[]; } -export function convertColumns( - columns: Columns, - options: Partial = {}, -) { +export function convertColumns>( + columns: ColumnsType, +): ColumnsType { if (!Array.isArray(columns) || columns.length === 0) { - return [] as unknown as Columns; + return []; } - const defaultOptions = { - children: 'children', - colSpan: 'colSpan', - rowSpan: 'rowSpan', - hidden: 'hidden', - }; - const { - children: childrenProp, - colSpan: colSpanProp, - rowSpan: rowSpanProp, - hidden: hiddenProp, - } = Object.assign({}, defaultOptions, options); - - let specified = false; - let tree = columns.map(item => ({ ...item }) as Column); - let depthCurr = 0; let depthNext = 0; const nodePos: { index: number; total: number; - }[] = [ - { - index: tree.length, - total: tree.length, - }, - ]; + }[] = [{ index: columns.length, total: columns.length }]; const rowSpans: number[] = []; - const columnsMap = new Map(); - const treeMap = new Map(); - const branchLastSet = new Set(); + const columnsMap = new Map[]>(); + const treeMap = new Map, Column[]>(); + const lastSet = new Set>(); + + let specified = false; + let tree: Column[] = columns.map(item => ({ ...item })); while (tree.length > 0) { depthCurr = depthNext; @@ -70,16 +49,16 @@ export function convertColumns( const node = tree.shift(); - if (!node || typeof node !== 'object' || node[hiddenProp]) { + if (!node || typeof node !== 'object' || node.hidden) { continue; } - const colSpanSpecified = node[colSpanProp]; - const rowSpanSpecified = node[rowSpanProp]; - const colSpan = node[colSpanProp] ?? 1; - const rowSpan = node[rowSpanProp] ?? 1; - node[colSpanProp] = colSpan; - node[rowSpanProp] = rowSpan; + const colSpanSpecified = node.colSpan; + const rowSpanSpecified = node.rowSpan; + const colSpan = node.colSpan ?? 1; + const rowSpan = node.rowSpan ?? 1; + node.colSpan = colSpan; + node.rowSpan = rowSpan; if (!specified && (colSpan > 1 || rowSpan > 1)) { specified = true; @@ -91,15 +70,15 @@ export function convertColumns( } columnsMap.get(parentsRowCount).push(node); - let leaf = node[childrenProp]; - delete node[childrenProp]; + let leaf = node.children; + delete node.children; if (Array.isArray(leaf) && leaf.length > 0) { depthNext = depthCurr + 1; nodePos[depthNext] = { index: leaf.length, total: leaf.length }; rowSpans[depthCurr] = rowSpan; - leaf = leaf.map(item => ({ ...item }) as Column); + leaf = leaf.map(item => ({ ...item })); node.colSpanSpecified = colSpanSpecified; if (!treeMap.has(node)) { treeMap.set(node, []); @@ -109,7 +88,7 @@ export function convertColumns( } else { node.rowSpanSpecified = rowSpanSpecified; node.parentsRowCount = parentsRowCount; - branchLastSet.add(node); + lastSet.add(node); } } @@ -122,30 +101,30 @@ export function convertColumns( const { colSpanSpecified } = column; delete column.colSpanSpecified; - if (column[hiddenProp] || Number.isInteger(colSpanSpecified)) { + if (column.hidden || Number.isInteger(colSpanSpecified)) { return; } const children = treeMap.get(column); - column[colSpanProp] = children.reduce((acc, item) => { - return item[hiddenProp] ? acc : acc + item[colSpanProp]; + column.colSpan = children.reduce((acc, item) => { + return item.hidden ? acc : acc + item.colSpan; }, 0); }); let rowCountMax = 0; - branchLastSet.forEach(column => { - const rowCount = column[rowSpanProp] + column.parentsRowCount; + lastSet.forEach(column => { + const rowCount = column.rowSpan + column.parentsRowCount; if (rowCount > rowCountMax) { rowCountMax = rowCount; } }); // correct rowSpan of column in default state - branchLastSet.forEach(column => { + lastSet.forEach(column => { const { rowSpanSpecified, parentsRowCount } = column; if (!Number.isInteger(rowSpanSpecified)) { - column[rowSpanProp] = rowCountMax - parentsRowCount; + column.rowSpan = rowCountMax - parentsRowCount; } delete column.rowSpanSpecified; @@ -155,8 +134,8 @@ export function convertColumns( const keys = [...columnsMap.keys()].sort(); for (let i = keys.length - 1; i >= 1; i--) { const parent = columnsMap.get(keys[i - 1]); - parent[0][childrenProp] = columnsMap.get(keys[i]); + parent[0].children = columnsMap.get(keys[i]); } - return columnsMap.get(0) as unknown as Columns; + return columnsMap.get(0) as unknown as ColumnsType; } From c6b0734a774a568ca9353ebb4be081d54963b725 Mon Sep 17 00:00:00 2001 From: fansaly Date: Tue, 14 May 2024 11:20:32 +0800 Subject: [PATCH 3/4] fix: fixed colStart and colEnd of head cell column in strange layout --- src/FixedHolder/index.tsx | 29 +++-- src/Header/Header.tsx | 80 +------------- src/Table.tsx | 7 +- src/context/TableContext.tsx | 2 + src/hooks/useColumns/index.tsx | 50 +++------ src/interface.ts | 12 ++- src/utils/convertUtil.ts | 187 +++++++++++++++++++++++++-------- tests/Table.spec.jsx | 12 ++- 8 files changed, 210 insertions(+), 169 deletions(-) diff --git a/src/FixedHolder/index.tsx b/src/FixedHolder/index.tsx index 954cfaac4..50b9d33fa 100644 --- a/src/FixedHolder/index.tsx +++ b/src/FixedHolder/index.tsx @@ -7,7 +7,7 @@ import ColGroup from '../ColGroup'; import TableContext from '../context/TableContext'; import type { HeaderProps } from '../Header/Header'; import devRenderTimes from '../hooks/useRenderTimes'; -import type { ColumnsType, ColumnType, Direction } from '../interface'; +import type { CellType, ColumnsType, ColumnType, Direction } from '../interface'; function useColumnWidth(colWidths: readonly number[], columCount: number) { return useMemo(() => { @@ -48,6 +48,7 @@ const FixedHolder = React.forwardRef>((pro className, noData, columns, + headCells, flattenColumns, colWidths, columCount, @@ -63,12 +64,10 @@ const FixedHolder = React.forwardRef>((pro ...restProps } = props; - const { prefixCls, scrollbarSize, isSticky, getComponent } = useContext(TableContext, [ - 'prefixCls', - 'scrollbarSize', - 'isSticky', - 'getComponent', - ]); + const { prefixCls, headMatrix, scrollbarSize, isSticky, getComponent } = useContext( + TableContext, + ['prefixCls', 'headMatrix', 'scrollbarSize', 'isSticky', 'getComponent'], + ); const TableComponent = getComponent(['header', 'table'], 'table'); const combinationScrollBarSize = isSticky && !fixHeader ? 0 : scrollbarSize; @@ -111,12 +110,27 @@ const FixedHolder = React.forwardRef>((pro className: `${prefixCls}-cell-scrollbar`, }), }; + const ScrollBarColumnCell: CellType = { + column: ScrollBarColumn, + colSpan: 1, + colStart: headMatrix[0], + colEnd: headMatrix[0], + rowSpan: headMatrix[1], + }; const columnsWithScrollbar = useMemo>( () => (combinationScrollBarSize ? [...columns, ScrollBarColumn] : columns), [combinationScrollBarSize, columns], ); + const headCellsWithScrollbar = useMemo[][]>(() => { + if (combinationScrollBarSize) { + const [cell, ...cells] = headCells; + return [[...cell, ScrollBarColumnCell], ...cells]; + } + return headCells; + }, [combinationScrollBarSize, headCells]); + const flattenColumnsWithScrollbar = useMemo( () => (combinationScrollBarSize ? [...flattenColumns, ScrollBarColumn] : flattenColumns), [combinationScrollBarSize, flattenColumns], @@ -165,6 +179,7 @@ const FixedHolder = React.forwardRef>((pro ...restProps, stickyOffsets: headerStickyOffsets, columns: columnsWithScrollbar, + headCells: headCellsWithScrollbar, flattenColumns: flattenColumnsWithScrollbar, })} diff --git a/src/Header/Header.tsx b/src/Header/Header.tsx index 9c8564f67..8d705e0d5 100644 --- a/src/Header/Header.tsx +++ b/src/Header/Header.tsx @@ -2,10 +2,8 @@ import { useContext } from '@rc-component/context'; import * as React from 'react'; import TableContext, { responseImmutable } from '../context/TableContext'; import devRenderTimes from '../hooks/useRenderTimes'; -import { convertColumns } from '../utils/convertUtil'; import type { CellType, - ColumnGroupType, ColumnsType, ColumnType, GetComponentProps, @@ -13,79 +11,9 @@ import type { } from '../interface'; import HeaderRow from './HeaderRow'; -function parseHeaderRows( - rootColumns: ColumnsType, -): CellType[][] { - const rows: CellType[][] = []; - - function fillRowCells( - columns: ColumnsType, - colIndex: number, - rowIndex: number = 0, - ): number[] { - // Init rows - rows[rowIndex] = rows[rowIndex] || []; - - let currentColIndex = colIndex; - const colSpans: number[] = columns.filter(Boolean).map(column => { - const cell: CellType = { - key: column.key, - className: column.className || '', - children: column.title, - column, - colStart: currentColIndex, - }; - - let colSpan: number = 1; - - const subColumns = (column as ColumnGroupType).children; - if (subColumns && subColumns.length > 0) { - colSpan = fillRowCells(subColumns, currentColIndex, rowIndex + 1).reduce( - (total, count) => total + count, - 0, - ); - cell.hasSubColumns = true; - } - - if ('colSpan' in column) { - ({ colSpan } = column); - } - - if ('rowSpan' in column) { - cell.rowSpan = column.rowSpan; - } - - cell.colSpan = colSpan; - cell.colEnd = cell.colStart + colSpan - 1; - rows[rowIndex].push(cell); - - currentColIndex += colSpan; - - return colSpan; - }); - - return colSpans; - } - - // Generate `rows` cell data - fillRowCells(convertColumns(rootColumns), 0); - - // Handle `rowSpan` - const rowCount = rows.length; - for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) { - rows[rowIndex].forEach(cell => { - if (!('rowSpan' in cell) && !cell.hasSubColumns) { - // eslint-disable-next-line no-param-reassign - cell.rowSpan = rowCount - rowIndex; - } - }); - } - - return rows; -} - export interface HeaderProps { columns: ColumnsType; + headCells: CellType[][]; flattenColumns: readonly ColumnType[]; stickyOffsets: StickyOffsets; onHeaderRow: GetComponentProps[]>; @@ -96,18 +24,16 @@ const Header = (props: HeaderProps) => { devRenderTimes(props); } - const { stickyOffsets, columns, flattenColumns, onHeaderRow } = props; + const { stickyOffsets, headCells, flattenColumns, onHeaderRow } = props; const { prefixCls, getComponent } = useContext(TableContext, ['prefixCls', 'getComponent']); - const rows = React.useMemo[][]>(() => parseHeaderRows(columns), [columns]); - const WrapperComponent = getComponent(['header', 'wrapper'], 'thead'); const trComponent = getComponent(['header', 'row'], 'tr'); const thComponent = getComponent(['header', 'cell'], 'th'); return ( - {rows.map((row, rowIndex) => { + {headCells.map((row, rowIndex) => { const rowNode = ( ( const scrollX = scroll?.x; const [componentWidth, setComponentWidth] = React.useState(0); - const [columns, flattenColumns, flattenScrollX, hasGapFixed] = useColumns( + const [columns, headCells, headMatrix, flattenColumns, flattenScrollX, hasGapFixed] = useColumns( { ...props, ...expandableConfig, @@ -312,9 +312,10 @@ function Table( const columnContext = React.useMemo( () => ({ columns, + headCells, flattenColumns, }), - [columns, flattenColumns], + [columns, headCells, flattenColumns], ); // ======================= Refs ======================= @@ -825,6 +826,7 @@ function Table( // Column columns, + headMatrix, flattenColumns, onColumnResize, @@ -874,6 +876,7 @@ function Table( // Column columns, + headMatrix, flattenColumns, onColumnResize, diff --git a/src/context/TableContext.tsx b/src/context/TableContext.tsx index fd810b378..192379711 100644 --- a/src/context/TableContext.tsx +++ b/src/context/TableContext.tsx @@ -2,6 +2,7 @@ import { createContext, createImmutable } from '@rc-component/context'; import type { ColumnsType, ColumnType, + HeadMatrix, Direction, ExpandableType, ExpandedRowRender, @@ -54,6 +55,7 @@ export interface TableContextProps { // Column columns: ColumnsType; + headMatrix: HeadMatrix; flattenColumns: readonly ColumnType[]; onColumnResize: (columnKey: React.Key, width: number) => void; diff --git a/src/hooks/useColumns/index.tsx b/src/hooks/useColumns/index.tsx index 99490db32..69427ffed 100644 --- a/src/hooks/useColumns/index.tsx +++ b/src/hooks/useColumns/index.tsx @@ -3,16 +3,19 @@ import warning from 'rc-util/lib/warning'; import * as React from 'react'; import { EXPAND_COLUMN } from '../../constant'; import type { + CellType, ColumnGroupType, ColumnsType, ColumnType, Direction, + HeadMatrix, FixedType, GetRowKey, Key, RenderExpandIcon, TriggerEventHandler, } from '../../interface'; +import { convertColumns } from '../../utils/convertUtil'; import { INTERNAL_COL_DEFINE } from '../../utils/legacyUtil'; import useWidthColumns from './useWidthColumns'; @@ -55,39 +58,6 @@ function filterHiddenColumns( }); } -function flatColumns( - columns: ColumnsType, - parentKey = 'key', -): ColumnType[] { - return columns - .filter(column => column && typeof column === 'object') - .reduce((list, column, index) => { - const { fixed } = column; - // Convert `fixed='true'` to `fixed='left'` instead - const parsedFixed = fixed === true ? 'left' : fixed; - const mergedKey = `${parentKey}-${index}`; - - const subColumns = (column as ColumnGroupType).children; - if (subColumns && subColumns.length > 0) { - return [ - ...list, - ...flatColumns(subColumns, mergedKey).map(subColum => ({ - fixed: parsedFixed, - ...subColum, - })), - ]; - } - return [ - ...list, - { - key: mergedKey, - ...column, - fixed: parsedFixed, - }, - ]; - }, []); -} - function revertForRtl(columns: ColumnsType): ColumnsType { return columns.map(column => { const { fixed, ...restProps } = column; @@ -150,6 +120,8 @@ function useColumns( transformColumns: (columns: ColumnsType) => ColumnsType, ): [ columns: ColumnsType, + headCells: CellType[][], + headMatrix: HeadMatrix, flattenColumns: readonly ColumnType[], realScrollWidth: undefined | number, hasGapFixed: boolean, @@ -263,13 +235,17 @@ function useColumns( return finalColumns; }, [transformColumns, withExpandColumns, direction]); + const [headCells, headMatrix, lastColumns] = React.useMemo(() => { + return convertColumns(mergedColumns); + }, [mergedColumns]); + // ========================== Flatten ========================= const flattenColumns = React.useMemo(() => { if (direction === 'rtl') { - return revertForRtl(flatColumns(mergedColumns)); + return revertForRtl(lastColumns); } - return flatColumns(mergedColumns); - }, [mergedColumns, direction, scrollWidth]); + return lastColumns; + }, [lastColumns, direction, scrollWidth]); // ========================= Gap Fixed ======================== const hasGapFixed = React.useMemo(() => { @@ -313,7 +289,7 @@ function useColumns( clientWidth, ); - return [mergedColumns, filledColumns, realScrollWidth, hasGapFixed]; + return [mergedColumns, headCells, headMatrix, filledColumns, realScrollWidth, hasGapFixed]; } export default useColumns; diff --git a/src/interface.ts b/src/interface.ts index 06213c043..a38201ca6 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -26,6 +26,12 @@ export type DefaultRecordType = Record; export type TableLayout = 'auto' | 'fixed'; +/** + * 0 => horizontal count of table head cells + * 1 => vertical count of table head cells + */ +export type HeadMatrix = [number, number]; + export type ScrollConfig = { index?: number; key?: Key; @@ -70,7 +76,11 @@ export type Direction = 'ltr' | 'rtl'; // SpecialString will be removed in antd@6 export type SpecialString = T | (string & {}); -export type DataIndex = DeepNamePath | SpecialString | number | (SpecialString | number)[]; +export type DataIndex = + | DeepNamePath + | SpecialString + | number + | (SpecialString | number)[]; export type CellEllipsisType = { showTitle?: boolean } | boolean; diff --git a/src/utils/convertUtil.ts b/src/utils/convertUtil.ts index 142b4141a..996305a05 100644 --- a/src/utils/convertUtil.ts +++ b/src/utils/convertUtil.ts @@ -1,17 +1,22 @@ -import type { ColumnsType, ColumnType } from '../interface'; +import type { CellType, ColumnsType, ColumnType, FixedType, HeadMatrix, Key } from '../interface'; interface Column extends ColumnType { colSpanSpecified?: number; rowSpanSpecified?: number; - parentsRowCount?: number; + colStart?: number; + colEnd?: number; children?: Column[]; } export function convertColumns>( columns: ColumnsType, -): ColumnsType { +): [ + headCells: CellType[][], + headMatrix: HeadMatrix, + lastColumns: readonly ColumnType[], +] { if (!Array.isArray(columns) || columns.length === 0) { - return []; + return [[[]], [0, 0], []]; } let depthCurr = 0; @@ -20,13 +25,23 @@ export function convertColumns>( index: number; total: number; }[] = [{ index: columns.length, total: columns.length }]; + const pKeys: Key[] = []; const rowSpans: number[] = []; const columnsMap = new Map[]>(); + const flatMap = new Map< + Key, + /** + * 0 => current column + * 1 => parent key + * 2 => parents row count + */ + [Column, undefined | Key, undefined | number] + >(); const treeMap = new Map, Column[]>(); const lastSet = new Set>(); - let specified = false; - let tree: Column[] = columns.map(item => ({ ...item })); + let rowCountMax = 0; + let tree: Column[] = [...columns]; while (tree.length > 0) { depthCurr = depthNext; @@ -50,6 +65,18 @@ export function convertColumns>( const node = tree.shift(); if (!node || typeof node !== 'object' || node.hidden) { + const { index, total } = nodePos[depthCurr]; + const currIndex = total - 1 - index; + const parentKey = pKeys[depthCurr - 1]; + + if (parentKey === undefined) { + columns.splice(currIndex, 1); + } else { + const parent = flatMap.get(parentKey)?.[0]; + parent?.children?.splice(currIndex, 1); + } + + nodePos[depthCurr].total--; continue; } @@ -59,41 +86,47 @@ export function convertColumns>( const rowSpan = node.rowSpan ?? 1; node.colSpan = colSpan; node.rowSpan = rowSpan; - - if (!specified && (colSpan > 1 || rowSpan > 1)) { - specified = true; - } + node.fixed = node.fixed === true ? 'left' : node.fixed; const parentsRowCount = rowSpans.reduce((acc, num) => acc + num, 0); if (!columnsMap.has(parentsRowCount)) { columnsMap.set(parentsRowCount, []); } - columnsMap.get(parentsRowCount).push(node); - - let leaf = node.children; - delete node.children; - + columnsMap.get(parentsRowCount)!.push(node); + // // mark vertical position of cell in table head matrix + // node.rowStart = parentsRowCount; + // node.rowEnd = node.rowStart + rowSpan - 1; + + const pathKey = nodePos.reduce((acc, { index, total }) => { + return `${acc}-${total - 1 - index}`; + }, 'key'); + node.key = node.key && !flatMap.has(node.key) ? node.key : pathKey; + flatMap.set(node.key, [node, pKeys[depthCurr - 1], parentsRowCount]); + + const leaf = node.children; if (Array.isArray(leaf) && leaf.length > 0) { depthNext = depthCurr + 1; nodePos[depthNext] = { index: leaf.length, total: leaf.length }; rowSpans[depthCurr] = rowSpan; + pKeys[depthCurr] = node.key; - leaf = leaf.map(item => ({ ...item })); node.colSpanSpecified = colSpanSpecified; if (!treeMap.has(node)) { treeMap.set(node, []); } - treeMap.get(node).push(...leaf); + treeMap.get(node)!.push(...leaf); tree = [...leaf, ...tree]; } else { + delete node.children; node.rowSpanSpecified = rowSpanSpecified; - node.parentsRowCount = parentsRowCount; lastSet.add(node); - } - } - if (!specified) { - return columns; + // correct vertical cells count of table head matrix + const rowCount = node.rowSpan + parentsRowCount; + if (rowCount > rowCountMax) { + rowCountMax = rowCount; + } + } } // correct colSpan of parent column in default state @@ -101,41 +134,107 @@ export function convertColumns>( const { colSpanSpecified } = column; delete column.colSpanSpecified; - if (column.hidden || Number.isInteger(colSpanSpecified)) { + if (Number.isInteger(colSpanSpecified)) { return; } const children = treeMap.get(column); - column.colSpan = children.reduce((acc, item) => { - return item.hidden ? acc : acc + item.colSpan; - }, 0); - }); - - let rowCountMax = 0; - lastSet.forEach(column => { - const rowCount = column.rowSpan + column.parentsRowCount; - if (rowCount > rowCountMax) { - rowCountMax = rowCount; - } + column.colSpan = children!.reduce((acc, { colSpan = 1 }) => acc + colSpan, 0); }); - // correct rowSpan of column in default state + const lastColumns: ColumnType[] = []; lastSet.forEach(column => { - const { rowSpanSpecified, parentsRowCount } = column; + // correct rowSpan of column in default state + const { rowSpanSpecified } = column; + const parentsRowCount = column.key ? flatMap.get(column.key)?.[2] ?? 0 : 0; + delete column.rowSpanSpecified; if (!Number.isInteger(rowSpanSpecified)) { column.rowSpan = rowCountMax - parentsRowCount; + // // correct vertical position of cell at last of branch + // column.rowEnd = column.rowStart ?? 0 + column.rowSpan - 1; } - delete column.rowSpanSpecified; - delete column.parentsRowCount; + // collect column at last of branch and correct fixed prop + let size = flatMap.size; + let key = column.key; + const columnsFixed: (undefined | FixedType)[] = [column.fixed]; + + while (size-- > 0) { + const parentKey = key ? flatMap.get(key)?.[1] : null; + + if (!parentKey) { + break; + } + + const parentColumn = flatMap.get(parentKey)?.[0]; + key = parentColumn?.key; + columnsFixed.unshift(parentColumn?.fixed); + } + + let prev = 0, + next = 1; + while (next < columnsFixed.length) { + if (columnsFixed[prev]) { + columnsFixed[next] = columnsFixed[prev]; + } + prev++; + next++; + } + + column.fixed = columnsFixed.pop(); + lastColumns.push(column as ColumnType); }); - const keys = [...columnsMap.keys()].sort(); - for (let i = keys.length - 1; i >= 1; i--) { - const parent = columnsMap.get(keys[i - 1]); - parent[0].children = columnsMap.get(keys[i]); - } + let colCountMax = 0; + const headCells: CellType[][] = []; + // generate data of table head and handle horizontal related of table head matrix + [...columnsMap.keys()].sort().forEach(key => { + const group: Column[] = columnsMap.get(key) ?? []; + + const cells = group.map((column, index) => { + // mark horizontal position of cell in table head matrix + const parentKey = column.key ? flatMap.get(column.key)?.[1] : null; + const parentColumn = parentKey ? flatMap.get(parentKey)?.[0] : null; + const parentColStart = parentColumn?.colStart ?? 0; + + const previousIndex = index - 1; + const previousColumn = previousIndex >= 0 ? group[previousIndex] : null; + const previousColEnd = previousColumn?.colEnd ?? -1; + + const colSpan = column.colSpan ?? 1; + const rowSpan = column.rowSpan ?? 1; + const colStart = Math.max(parentColStart, previousColEnd + 1); + const colEnd = colStart + colSpan - 1; + + // correct horizontal cells count of table head matrix + if (colEnd + 1 > colCountMax) { + colCountMax = colEnd + 1; + } + + // avoid fixed columns rendering bug, but does not exist on type ColumnType + column.colStart = colStart; + column.colEnd = colEnd; + + // table head cell data + const item = { + column, + key: column.key, + className: column.className || '', + children: column.title, + colSpan, + rowSpan, + colStart, + colEnd, + }; + + return item as CellType; + }); + + headCells.push(cells); + }); + + const headMatrix: HeadMatrix = [colCountMax, rowCountMax]; - return columnsMap.get(0) as unknown as ColumnsType; + return [headCells, headMatrix, lastColumns]; } diff --git a/tests/Table.spec.jsx b/tests/Table.spec.jsx index 872cd89c2..48dde20d3 100644 --- a/tests/Table.spec.jsx +++ b/tests/Table.spec.jsx @@ -581,7 +581,17 @@ describe('Table.Basic', () => { expect(wrapper.find('thead tr').props().id).toEqual('header-row-0'); expect(onHeaderRow).toHaveBeenCalledWith( - [{ title: 'Name', dataIndex: 'name', key: 'name' }], + [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + colSpan: 1, + rowSpan: 1, + colStart: 0, + colEnd: 0, + }, + ], 0, ); }); From 8253dda2c1046c4b00555470b38167cad3b03fee Mon Sep 17 00:00:00 2001 From: fansaly Date: Tue, 14 May 2024 11:25:33 +0800 Subject: [PATCH 4/4] fix: fixed colspan when empty data --- src/Body/BodyRow.tsx | 3 ++- src/Body/index.tsx | 4 +++- src/hooks/useRowInfo.tsx | 2 ++ tests/GroupingColumns.spec.jsx | 3 ++- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Body/BodyRow.tsx b/src/Body/BodyRow.tsx index 8cee145bf..846a111b4 100644 --- a/src/Body/BodyRow.tsx +++ b/src/Body/BodyRow.tsx @@ -106,6 +106,7 @@ function BodyRow( const rowInfo = useRowInfo(record, rowKey, index, indent); const { prefixCls, + headMatrix, flattenColumns, expandedRowClassName, expandedRowRender, @@ -197,7 +198,7 @@ function BodyRow( prefixCls={prefixCls} component={RowComponent} cellComponent={cellComponent} - colSpan={flattenColumns.length} + colSpan={headMatrix[0]} isEmpty={false} > {expandContent} diff --git a/src/Body/index.tsx b/src/Body/index.tsx index 3173fa22d..72d16c391 100644 --- a/src/Body/index.tsx +++ b/src/Body/index.tsx @@ -26,6 +26,7 @@ function Body(props: BodyProps) { prefixCls, getComponent, onColumnResize, + headMatrix, flattenColumns, getRowKey, expandedKeys, @@ -35,6 +36,7 @@ function Body(props: BodyProps) { 'prefixCls', 'getComponent', 'onColumnResize', + 'headMatrix', 'flattenColumns', 'getRowKey', 'expandedKeys', @@ -86,7 +88,7 @@ function Body(props: BodyProps) { prefixCls={prefixCls} component={trComponent} cellComponent={tdComponent} - colSpan={flattenColumns.length} + colSpan={headMatrix[0]} isEmpty > {emptyNode} diff --git a/src/hooks/useRowInfo.tsx b/src/hooks/useRowInfo.tsx index bba123a89..5b4085081 100644 --- a/src/hooks/useRowInfo.tsx +++ b/src/hooks/useRowInfo.tsx @@ -14,6 +14,7 @@ export default function useRowInfo( TableContextProps, | 'prefixCls' | 'fixedInfoList' + | 'headMatrix' | 'flattenColumns' | 'expandableType' | 'expandRowByClick' @@ -40,6 +41,7 @@ export default function useRowInfo( const context: TableContextProps = useContext(TableContext, [ 'prefixCls', 'fixedInfoList', + 'headMatrix', 'flattenColumns', 'expandableType', 'expandRowByClick', diff --git a/tests/GroupingColumns.spec.jsx b/tests/GroupingColumns.spec.jsx index bc8038f3a..2fd3d9867 100644 --- a/tests/GroupingColumns.spec.jsx +++ b/tests/GroupingColumns.spec.jsx @@ -326,7 +326,8 @@ describe('Table with grouping columns', () => { }, ]; - const wrapper = mount(
); + const wrapper = mount(
); + expect(wrapper.find('.rc-table-placeholder .rc-table-cell').prop('colSpan')).toEqual(11); const titleA = wrapper.find('th.title-a'); expect(titleA.prop('colSpan')).toBe(null);