diff --git a/docs/demo/expandedRowSpan.md b/docs/demo/expandedRowSpan.md new file mode 100644 index 00000000..046c3f8b --- /dev/null +++ b/docs/demo/expandedRowSpan.md @@ -0,0 +1,8 @@ +--- +title: expandedRowSpan +nav: + title: Demo + path: /demo +--- + + diff --git a/docs/demo/expandedSticky.md b/docs/demo/expandedSticky.md new file mode 100644 index 00000000..df58f578 --- /dev/null +++ b/docs/demo/expandedSticky.md @@ -0,0 +1,8 @@ +--- +title: expandedSticky +nav: + title: Demo + path: /demo +--- + + diff --git a/docs/examples/expandedRowSpan.tsx b/docs/examples/expandedRowSpan.tsx new file mode 100644 index 00000000..5aeba748 --- /dev/null +++ b/docs/examples/expandedRowSpan.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import Table from 'rc-table'; +import '../../assets/index.less'; +import type { ColumnsType } from '@/interface'; + +const columns: ColumnsType = [ + { + title: '手机号', + dataIndex: 'a', + colSpan: 2, + width: 100, + onCell: (_, index) => { + const props: React.TdHTMLAttributes = {}; + if (index === 0) props.rowSpan = 1; + if (index === 1) props.rowSpan = 4; + if (index === 2) props.rowSpan = 0; + if (index === 3) props.rowSpan = 0; + if (index === 4) props.rowSpan = 0; + if (index === 5) props.rowSpan = undefined; + return props; + }, + }, + { title: '电话', dataIndex: 'b', colSpan: 0, width: 100 }, + Table.EXPAND_COLUMN, + { title: 'Name', dataIndex: 'c', width: 100 }, + { title: 'Address', dataIndex: 'd', width: 200 }, +]; + +const data = [ + { a: '12313132132', b: '0571-43243256', c: '小二', d: '文零西路', e: 'Male', key: 'z' }, + { a: '13812340987', b: '0571-12345678', c: '张三', d: '文一西路', e: 'Male', key: 'a' }, + { a: '13812340987', b: '0571-12345678', c: '张夫人', d: '文一西路', e: 'Female', key: 'b' }, + { a: '13812340987', b: '0571-099877', c: '李四', d: '文二西路', e: 'Male', key: 'c' }, + { a: '13812340987', b: '0571-099877', c: '李四', d: '文二西路', e: 'Male', key: 'd' }, + { a: '1381200008888', b: '0571-099877', c: '王五', d: '文二西路', e: 'Male', key: 'e' }, +]; + +const Demo = () => ( +
+

expanded & rowSpan

+ > + rowKey="key" + columns={columns} + data={data} + expandable={{ expandedRowRender: record =>

{record.key}

}} + className="table" + /> +
+); + +export default Demo; diff --git a/docs/examples/expandedSticky.tsx b/docs/examples/expandedSticky.tsx new file mode 100644 index 00000000..1f516b2b --- /dev/null +++ b/docs/examples/expandedSticky.tsx @@ -0,0 +1,57 @@ +import React, { useState } from 'react'; +import type { ColumnType } from 'rc-table'; +import Table from 'rc-table'; +import '../../assets/index.less'; + +const Demo = () => { + const [expandedRowKeys, setExpandedRowKeys] = useState([]); + + const columns: ColumnType>[] = [ + // { title: '分割', dataIndex: 'ca' }, + { + title: '手机号', + dataIndex: 'a', + width: 100, + fixed: 'left', + onCell: (_, index) => { + const props: React.TdHTMLAttributes = {}; + if (index === 0) props.rowSpan = 1; + if (index === 1) props.rowSpan = 2; + if (index === 2) props.rowSpan = 0; + return props; + }, + }, + Table.EXPAND_COLUMN, + { title: 'Name', dataIndex: 'c' }, + { title: 'Address', fixed: 'right', dataIndex: 'd', width: 200 }, + ]; + + return ( +
+

expanded & sticky

+ > + rowKey="key" + sticky + scroll={{ x: 800 }} + columns={columns} + data={[ + { key: 'a', a: '12313132132', c: '小二', d: '文零西路' }, + { key: 'b', a: '13812340987', c: '张三', d: '文一西路' }, + { key: 'c', a: '13812340987', c: '张夫', d: '文二西路' }, + ]} + expandable={{ + expandedRowKeys, + onExpandedRowsChange: keys => setExpandedRowKeys(keys), + expandedRowRender: record =>

{record.key}

, + }} + className="table" + /> +
+ ); +}; + +export default Demo; diff --git a/src/Body/BodyRow.tsx b/src/Body/BodyRow.tsx index 7e320eaa..97ae9472 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 { computedExpandedClassName } from '../utils/expandUtil'; -import { TableProps } from '..'; +import type { TableProps } from '..'; export interface BodyRowProps { record: RecordType; @@ -22,6 +22,7 @@ export interface BodyRowProps { scopeCellComponent: CustomizeComponent; indent?: number; rowKey: React.Key; + rowKeys: React.Key[]; } // ================================================================================== @@ -33,6 +34,7 @@ export function getCellProps( colIndex: number, indent: number, index: number, + rowKeys: React.Key[], ) { const { record, @@ -46,6 +48,8 @@ export function getCellProps( expanded, hasNestChildren, onTriggerExpand, + expandable, + expandedKeys, } = rowInfo; const key = columnsKey[colIndex]; @@ -74,6 +78,21 @@ export function getCellProps( let additionalCellProps: React.TdHTMLAttributes; if (column.onCell) { additionalCellProps = column.onCell(record, index); + const { rowSpan } = additionalCellProps; + + // For expandable row with rowSpan, + // We should increase the rowSpan if the row is expanded + if (expandable && rowSpan !== undefined) { + let currentRowSpan = rowSpan; + + for (let i = index; i < index + rowSpan; i += 1) { + const rowKey = rowKeys[i]; + if (expandedKeys.has(rowKey)) { + currentRowSpan += 1; + } + } + additionalCellProps.rowSpan = currentRowSpan; + } } return { @@ -84,9 +103,31 @@ export function getCellProps( }; } -// ================================================================================== -// == getCellProps == -// ================================================================================== +const getOffsetData = ( + columnsData: { + column: ColumnType; + cell: { additionalCellProps: React.TdHTMLAttributes }; + }[], +) => { + let offsetWidth = 0; + let offsetColumn = 0; + let isRowSpanEnd = false; + columnsData.forEach(item => { + if (!isRowSpanEnd) { + const { column, cell } = item; + if (cell.additionalCellProps.rowSpan !== undefined) { + offsetColumn += 1; + if (typeof column.width === 'number') { + offsetWidth = offsetWidth + (column.width ?? 0); + } + } else { + isRowSpanEnd = true; + } + } + }); + return { offsetWidth, offsetColumn }; +}; + function BodyRow( props: BodyRowProps, ) { @@ -107,9 +148,11 @@ function BodyRow( rowComponent: RowComponent, cellComponent, scopeCellComponent, + rowKeys, } = props; const rowInfo = useRowInfo(record, rowKey, index, indent); + const { prefixCls, flattenColumns, @@ -134,6 +177,17 @@ function BodyRow( // 此时如果 level > 1 则说明是 expandedRow, 一样需要附加 computedExpandedRowClassName const expandedClsName = computedExpandedClassName(expandedRowClassName, record, index, indent); + const { columnsData, offsetData } = React.useMemo(() => { + // eslint-disable-next-line @typescript-eslint/no-shadow + const columnsData = flattenColumns.map((column: ColumnType, colIndex) => { + const cell = getCellProps(rowInfo, column, colIndex, indent, index, rowKeys); + return { column, cell }; + }); + // eslint-disable-next-line @typescript-eslint/no-shadow + const offsetData = getOffsetData(columnsData); + return { columnsData, offsetData }; + }, [flattenColumns, indent, index, rowInfo, rowKeys]); + // ======================== Base tr row ======================== const baseRowNode = ( ( ...styles.row, }} > - {flattenColumns.map((column: ColumnType, colIndex) => { + {columnsData.map(item => { + const { column, cell } = item; const { render, dataIndex, className: columnClassName } = column; - const { key, fixedInfo, appendCellNode, additionalCellProps } = getCellProps( - rowInfo, - column, - colIndex, - indent, - index, - ); + const { key, fixedInfo, appendCellNode, additionalCellProps } = cell; return ( @@ -207,7 +256,8 @@ function BodyRow( prefixCls={prefixCls} component={RowComponent} cellComponent={cellComponent} - colSpan={flattenColumns.length} + offsetWidth={offsetData.offsetWidth} + colSpan={flattenColumns.length - offsetData.offsetColumn} isEmpty={false} > {expandContent} diff --git a/src/Body/ExpandedRow.tsx b/src/Body/ExpandedRow.tsx index b4009601..77e757c6 100644 --- a/src/Body/ExpandedRow.tsx +++ b/src/Body/ExpandedRow.tsx @@ -14,6 +14,7 @@ export interface ExpandedRowProps { children: React.ReactNode; colSpan: number; isEmpty: boolean; + offsetWidth?: number; } function ExpandedRow(props: ExpandedRowProps) { @@ -30,6 +31,7 @@ function ExpandedRow(props: ExpandedRowProps) { expanded, colSpan, isEmpty, + offsetWidth = 0, } = props; const { scrollbarSize, fixHeader, fixColumn, componentWidth, horizonScroll } = useContext( @@ -44,7 +46,7 @@ function ExpandedRow(props: ExpandedRowProps) { contentNode = (
(props: BodyProps) { const { body: bodyCls = {} } = classNames || {}; const { body: bodyStyles = {} } = styles || {}; - const flattenData: { record: RecordType; indent: number; index: number }[] = - useFlattenRecords(data, childrenColumnName, expandedKeys, getRowKey); + const flattenData = useFlattenRecords( + data, + childrenColumnName, + expandedKeys, + getRowKey, + ); + + const rowKeys = React.useMemo(() => flattenData.map(item => item.rowKey), [flattenData]); // =================== Performance ==================== const perfRef = React.useRef({ @@ -66,16 +72,15 @@ function Body(props: BodyProps) { let rows: React.ReactNode; if (data.length) { rows = flattenData.map((item, idx) => { - const { record, indent, index: renderIndex } = item; - - const key = getRowKey(record, idx); + const { record, indent, index: renderIndex, rowKey } = item; return ( {children}
; + return ( +
+ {children} +
+ ); } export default Panel; diff --git a/src/Table.tsx b/src/Table.tsx index d5a8b196..9c2a682f 100644 --- a/src/Table.tsx +++ b/src/Table.tsx @@ -942,6 +942,8 @@ function Table( mergedChildrenColumnName, rowHoverable, + classNames, + styles, ], ); diff --git a/src/VirtualTable/VirtualCell.tsx b/src/VirtualTable/VirtualCell.tsx index 9b1b3ebe..e4a3c6bf 100644 --- a/src/VirtualTable/VirtualCell.tsx +++ b/src/VirtualTable/VirtualCell.tsx @@ -62,6 +62,7 @@ function VirtualCell(props: VirtualCellProps) { colIndex, indent, index, + [], ); const { style: cellStyle, colSpan = 1, rowSpan = 1 } = additionalCellProps; diff --git a/src/context/TableContext.tsx b/src/context/TableContext.tsx index b9cae4dd..cc1c132e 100644 --- a/src/context/TableContext.tsx +++ b/src/context/TableContext.tsx @@ -14,7 +14,7 @@ import type { TriggerEventHandler, } from '../interface'; import type { FixedInfo } from '../utils/fixUtil'; -import { TableProps } from '../Table'; +import type { TableProps } from '../Table'; const { makeImmutable, responseImmutable, useImmutableMark } = createImmutable(); export { makeImmutable, responseImmutable, useImmutableMark }; diff --git a/src/hooks/useFlattenRecords.ts b/src/hooks/useFlattenRecords.ts index ff67f5d9..bd5e0c54 100644 --- a/src/hooks/useFlattenRecords.ts +++ b/src/hooks/useFlattenRecords.ts @@ -15,6 +15,7 @@ function fillRecords( record, indent, index, + rowKey: getRowKey(record, index), }); const key = getRowKey(record); @@ -41,6 +42,7 @@ export interface FlattenData { record: RecordType; indent: number; index: number; + rowKey: Key; } /** @@ -80,6 +82,7 @@ export default function useFlattenRecords( record: item, indent: 0, index, + rowKey: getRowKey(item, index), }; }); }, [data, childrenColumnName, expandedKeys, getRowKey]); diff --git a/tests/Expanded.spec.tsx b/tests/Expanded.spec.tsx new file mode 100644 index 00000000..b3106cca --- /dev/null +++ b/tests/Expanded.spec.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import { render, act } from '@testing-library/react'; +import Table, { type ColumnsType } from '../src'; +import { spyElementPrototypes } from '@rc-component/util/lib/test/domHook'; + +describe('Table.Expanded', () => { + let domSpy: ReturnType; + + beforeEach(() => { + vi.useFakeTimers(); + }); + + beforeAll(() => { + domSpy = spyElementPrototypes(HTMLElement, { + offsetParent: { + get: () => ({}), + }, + offsetWidth: { + get: () => 1000, + }, + }); + }); + + afterAll(() => { + domSpy.mockRestore(); + }); + + it('expanded + rowSpan', async () => { + const columns: ColumnsType = [ + { + title: 'key', + dataIndex: 'key', + width: 100, + onCell: (_, index) => { + const props: React.TdHTMLAttributes = {}; + if (index === 0) props.rowSpan = 1; + if (index === 1) props.rowSpan = 2; + if (index === 2) props.rowSpan = 0; + if (index === 3) props.rowSpan = undefined; + return props; + }, + }, + Table.EXPAND_COLUMN, + { title: 'key2', dataIndex: 'key2', width: 100 }, + ]; + const data = [{ key: 'a' }, { key: 'b' }, { key: 'c' }, { key: 'd' }]; + const { container } = render( + > + columns={columns} + data={data} + expandable={{ + defaultExpandAllRows: true, + expandedRowRender: record =>
{record.key}
, + }} + />, + ); + + await act(async () => { + vi.runAllTimers(); + await Promise.resolve(); + }); + const trList = container.querySelector('tbody').querySelectorAll('tr'); + // row 1 + expect(trList[0].querySelectorAll('td').length).toBe(3); + expect(trList[0].querySelectorAll('td')[0].getAttribute('rowspan')).toBe('2'); + // expand 1 + expect(trList[1].querySelectorAll('td').length).toBe(1); + expect(trList[1].querySelectorAll('td')[0].getAttribute('colspan')).toBe('2'); + // row 2 + expect(trList[2].querySelectorAll('td').length).toBe(3); + expect(trList[2].querySelectorAll('td')[0].getAttribute('rowspan')).toBe('4'); + // expand 2 + expect(trList[3].querySelectorAll('td').length).toBe(1); + expect(trList[3].querySelectorAll('td')[0].getAttribute('colspan')).toBe('2'); + // row 3 + expect(trList[4].querySelectorAll('td').length).toBe(2); + // expand 3 + expect(trList[5].querySelectorAll('td').length).toBe(1); + expect(trList[5].querySelectorAll('td')[0].getAttribute('colspan')).toBe('2'); + // row 4 + expect(trList[6].querySelectorAll('td').length).toBe(3); + // expand 4 + expect(trList[7].querySelectorAll('td').length).toBe(1); + expect(trList[7].querySelectorAll('td')[0].getAttribute('colspan')).toBe('3'); + }); + + it('expanded + sticky', async () => { + const columns: ColumnsType = [ + { + title: '手机号', + dataIndex: 'a', + width: 100, + fixed: 'left', + onCell: (_, index) => { + const props: React.TdHTMLAttributes = {}; + if (index === 0) props.rowSpan = 1; + if (index === 1) props.rowSpan = 2; + if (index === 2) props.rowSpan = 0; + return props; + }, + }, + Table.EXPAND_COLUMN, + { title: 'Name', dataIndex: 'c' }, + ]; + const data = [{ key: 'a' }, { key: 'b' }, { key: 'c' }, { key: 'd' }]; + const { container } = render( + > + columns={columns} + data={data} + sticky + expandable={{ + defaultExpandAllRows: true, + expandedRowRender: record =>
{record.key}
, + }} + />, + ); + console.log('container', container); + await act(async () => { + vi.runAllTimers(); + await Promise.resolve(); + }); + }); +});