diff --git a/docs/demo/colspan-rowspan-legacy.md b/docs/demo/colspan-rowspan-legacy.md new file mode 100644 index 000000000..11809cef6 --- /dev/null +++ b/docs/demo/colspan-rowspan-legacy.md @@ -0,0 +1,3 @@ +## colspan-rowspan-legacy + + diff --git a/docs/examples/colspan-rowspan-legacy.tsx b/docs/examples/colspan-rowspan-legacy.tsx new file mode 100644 index 000000000..c67773d3c --- /dev/null +++ b/docs/examples/colspan-rowspan-legacy.tsx @@ -0,0 +1,150 @@ +import React from 'react'; +import Table from 'rc-table'; +import '../../assets/index.less'; +import { ColumnsType, RenderedCell } from '@/interface'; + +interface RecordType { + a?: string; + b?: string; + c?: string; + d?: string; + e?: string; + key?: string; +} + +const columns: ColumnsType = [ + { + title: '手机号', + dataIndex: 'a', + colSpan: 2, + width: 100, + key: 'a', + render(o, row, index) { + const obj: RenderedCell = { + children: o, + props: {}, + }; + // 设置第一行为链接 + if (index === 0) { + obj.children = {o}; + } + // 第5行合并两列 + if (index === 4) { + obj.props.colSpan = 2; + } + + if (index === 5) { + obj.props.colSpan = 6; + } + return obj; + }, + }, + { + title: '电话', + dataIndex: 'b', + colSpan: 0, + width: 100, + key: 'b', + render(o, row, index) { + const obj: RenderedCell = { + children: o, + props: {}, + }; + // 列合并掉的表格设置colSpan=0,不会去渲染 + if (index === 4 || index === 5) { + obj.props.colSpan = 0; + } + return obj; + }, + }, + { + title: 'Name', + dataIndex: 'c', + width: 100, + key: 'c', + render(o, row, index) { + const obj: RenderedCell = { + children: o, + props: {}, + }; + + if (index === 5) { + obj.props.colSpan = 0; + } + return obj; + }, + }, + { + title: 'Address', + dataIndex: 'd', + width: 200, + key: 'd', + render(o, row, index) { + const obj: RenderedCell = { + children: o, + props: {}, + }; + if (index === 0) { + obj.props.rowSpan = 2; + } + if (index === 1 || index === 5) { + obj.props.rowSpan = 0; + } + + if (index === 5) { + obj.props.colSpan = 0; + } + + return obj; + }, + }, + { + title: 'Gender', + dataIndex: 'e', + width: 200, + key: 'e', + render(o, row, index) { + const obj: RenderedCell = { + children: o, + props: {}, + }; + if (index === 5) { + obj.props.colSpan = 0; + } + return obj; + }, + }, + { + title: 'Operations', + dataIndex: '', + key: 'f', + render(o, row, index) { + if (index === 5) { + return { + props: { + colSpan: 0, + }, + }; + } + return Operations; + }, + }, +]; + +const data: RecordType[] = [ + { a: '13812340987', b: '0571-12345678', c: '张三', d: '文一西路', e: 'Male', key: '1' }, + { a: '13812340986', b: '0571-98787658', c: '张夫人', d: '文一西路', e: 'Female', key: '2' }, + { a: '13812988888', b: '0571-099877', c: '李四', d: '文二西路', e: 'Male', key: '3' }, + { a: '1381200008888', b: '0571-099877', c: '王五', d: '文二西路', e: 'Male', key: '4' }, + { a: '0571-88888110', c: '李警官', d: '武林门', e: 'Male', key: '5' }, + { a: '资料统计完毕于xxxx年xxx月xxx日', key: '6' }, +]; + +const Demo = () => ( +
+

colSpan & rowSpan

+ + +); + +export default Demo; diff --git a/docs/examples/colspan-rowspan.tsx b/docs/examples/colspan-rowspan.tsx index c67773d3c..d78ab1a8f 100644 --- a/docs/examples/colspan-rowspan.tsx +++ b/docs/examples/colspan-rowspan.tsx @@ -20,23 +20,21 @@ const columns: ColumnsType = [ width: 100, key: 'a', render(o, row, index) { - const obj: RenderedCell = { - children: o, - props: {}, - }; - // 设置第一行为链接 - if (index === 0) { - obj.children = {o}; - } + return index === 0 ? {o} : o; + }, + onCell: (_, index) => { + const props: React.TdHTMLAttributes = {}; + // 第5行合并两列 if (index === 4) { - obj.props.colSpan = 2; + props.colSpan = 2; } if (index === 5) { - obj.props.colSpan = 6; + props.colSpan = 6; } - return obj; + + return props; }, }, { @@ -45,16 +43,12 @@ const columns: ColumnsType = [ colSpan: 0, width: 100, key: 'b', - render(o, row, index) { - const obj: RenderedCell = { - children: o, - props: {}, - }; + onCell(_, index) { // 列合并掉的表格设置colSpan=0,不会去渲染 if (index === 4 || index === 5) { - obj.props.colSpan = 0; + return { colSpan: 0 }; } - return obj; + return {}; }, }, { @@ -62,16 +56,11 @@ const columns: ColumnsType = [ dataIndex: 'c', width: 100, key: 'c', - render(o, row, index) { - const obj: RenderedCell = { - children: o, - props: {}, - }; - + onCell(_, index) { if (index === 5) { - obj.props.colSpan = 0; + return { colSpan: 0 }; } - return obj; + return {}; }, }, { @@ -79,23 +68,19 @@ const columns: ColumnsType = [ dataIndex: 'd', width: 200, key: 'd', - render(o, row, index) { - const obj: RenderedCell = { - children: o, - props: {}, - }; + onCell(_, index) { + const props: React.TdHTMLAttributes = {}; if (index === 0) { - obj.props.rowSpan = 2; + props.rowSpan = 2; } if (index === 1 || index === 5) { - obj.props.rowSpan = 0; + props.rowSpan = 0; } if (index === 5) { - obj.props.colSpan = 0; + props.colSpan = 0; } - - return obj; + return props; }, }, { @@ -103,30 +88,27 @@ const columns: ColumnsType = [ dataIndex: 'e', width: 200, key: 'e', - render(o, row, index) { - const obj: RenderedCell = { - children: o, - props: {}, - }; + onCell(_, index) { if (index === 5) { - obj.props.colSpan = 0; + return { colSpan: 0 }; } - return obj; + return {}; }, }, { title: 'Operations', dataIndex: '', key: 'f', - render(o, row, index) { + render() { + return Operations; + }, + onCell(_, index) { if (index === 5) { return { - props: { - colSpan: 0, - }, + colSpan: 0, }; } - return Operations; + return {}; }, }, ]; diff --git a/package.json b/package.json index 52337a35a..a66ed9485 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "@types/jest": "^26.0.3", "@types/react": "^17.0.35", "@types/react-dom": "^17.0.10", + "@types/shallowequal": "^1.1.1", "@umijs/fabric": "^2.0.0", "cross-env": "^7.0.0", "dumi": "^1.1.9", diff --git a/src/Body/index.tsx b/src/Body/index.tsx index 8d070f1d3..640cc2345 100644 --- a/src/Body/index.tsx +++ b/src/Body/index.tsx @@ -31,8 +31,6 @@ function Body({ emptyNode, childrenColumnName, }: BodyProps) { - const [startRow, setStartRow] = React.useState(-1); - const [endRow, setEndRow] = React.useState(-1); const { onColumnResize } = React.useContext(ResizeContext); const { prefixCls, getComponent } = React.useContext(TableContext); const { flattenColumns } = React.useContext(BodyContext); @@ -40,6 +38,10 @@ function Body({ const flattenData: { record: RecordType; indent: number; index: number }[] = useFlattenRecords(data, childrenColumnName, expandedKeys, getRowKey); + // ====================== Hover ======================= + const [startRow, setStartRow] = React.useState(-1); + const [endRow, setEndRow] = React.useState(-1); + const onHover = React.useCallback((start: number, end: number) => { setStartRow(start); setEndRow(end); @@ -50,7 +52,8 @@ function Body({ [onHover, startRow, endRow], ); - return React.useMemo(() => { + // ====================== Render ====================== + const bodyNode = React.useMemo(() => { const WrapperComponent = getComponent(['body', 'wrapper'], 'tbody'); const trComponent = getComponent(['body', 'row'], 'tr'); const tdComponent = getComponent(['body', 'cell'], 'td'); @@ -98,20 +101,18 @@ function Body({ const columnsKey = getColumnsKey(flattenColumns); return ( - - - {/* Measure body column width with additional hidden col */} - {measureColumnWidth && ( - - )} - - {rows} - - + + {/* Measure body column width with additional hidden col */} + {measureColumnWidth && ( + + )} + + {rows} + ); }, [ data, @@ -127,8 +128,9 @@ function Body({ onColumnResize, rowExpandable, flattenData, - hoverContext, ]); + + return {bodyNode}; } const MemoBody = React.memo(Body); diff --git a/src/Cell/index.tsx b/src/Cell/index.tsx index fffda2850..6e70895cf 100644 --- a/src/Cell/index.tsx +++ b/src/Cell/index.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import classNames from 'classnames'; +import shallowEqual from 'shallowequal'; import { supportRef } from 'rc-util/lib/ref'; import type { DataIndex, @@ -15,6 +16,7 @@ import { getPathValue, validateValue } from '../utils/valueUtil'; import StickyContext from '../context/StickyContext'; import HoverContext from '../context/HoverContext'; import type { HoverContextProps } from '../context/HoverContext'; +import warning from 'rc-util/lib/warning'; /** Check if cell is in hover range */ function inHoverRange(cellStartRow: number, cellRowSpan: number, startRow: number, endRow: number) { @@ -36,7 +38,8 @@ function isRefComponent(component: CustomizeComponent) { return supportRef(component); } -interface InternalCellProps extends HoverContextProps { +interface InternalCellProps + extends Pick { prefixCls?: string; className?: string; record?: RecordType; @@ -64,13 +67,15 @@ interface InternalCellProps extends HoverC // ====================== Private Props ====================== /** @private Used for `expandable` with nest tree */ appendNode?: React.ReactNode; - additionalProps?: React.HTMLAttributes; + additionalProps?: React.TdHTMLAttributes; /** @private Fixed for user use `shouldCellUpdate` which block the render */ expanded?: boolean; rowType?: 'header' | 'body' | 'footer'; isSticky?: boolean; + + hovering?: boolean; } export type CellProps = Omit< @@ -104,13 +109,10 @@ function Cell( isSticky, // Hover - startRow, - endRow, + hovering, onHover, - - // MISC - shouldCellUpdate, - }: InternalCellProps, + }: // MISC + InternalCellProps, ref: React.Ref, ): React.ReactElement { const cellPrefixCls = `${prefixCls}-cell`; @@ -132,6 +134,12 @@ function Cell( const renderData = render(value, record, index); if (isRenderCell(renderData)) { + if (process.env.NODE_ENV !== 'production') { + warning( + false, + '`columns.render` return cell props is deprecated with perf issue, please use `onCell` instead.', + ); + } childNode = renderData.children; cellProps = renderData.props; } else { @@ -189,9 +197,7 @@ function Cell( } // ====================== Hover ======================= - const hovering = inHoverRange(index, mergedRowSpan, startRow, endRow); - - const onMouseEnter: React.MouseEventHandler = event => { + const onMouseEnter: React.MouseEventHandler = event => { if (record) { onHover(index, index + mergedRowSpan - 1); } @@ -199,7 +205,7 @@ function Cell( additionalProps?.onMouseEnter?.(event); }; - const onMouseLeave: React.MouseEventHandler = event => { + const onMouseLeave: React.MouseEventHandler = event => { if (record) { onHover(-1, -1); } @@ -239,7 +245,7 @@ function Cell( [`${cellPrefixCls}-ellipsis`]: ellipsis, [`${cellPrefixCls}-with-append`]: appendNode, [`${cellPrefixCls}-fix-sticky`]: (isFixLeft || isFixRight) && isSticky && supportSticky, - [`${cellPrefixCls}-row-hover`]: !shouldCellUpdate && hovering, // Not patch style if using shouldCellUpdate + [`${cellPrefixCls}-row-hover`]: !cellProps && hovering, }, additionalProps.className, cellClassName, @@ -261,7 +267,7 @@ function Cell( const RefCell = React.forwardRef>(Cell); RefCell.displayName = 'Cell'; -const comparePropList: (keyof InternalCellProps)[] = ['expanded', 'className']; +const comparePropList: (keyof InternalCellProps)[] = ['expanded', 'className', 'hovering']; const MemoCell = React.memo( RefCell, @@ -275,15 +281,31 @@ const MemoCell = React.memo( ); } - return false; + return shallowEqual(prev, next); }, ); /** Inject hover data here, we still wish MemoCell keep simple `shouldCellUpdate` logic */ const WrappedCell = React.forwardRef((props: CellProps, ref: React.Ref) => { const { onHover, startRow, endRow } = React.useContext(HoverContext); + const { index, additionalProps = {}, colSpan, rowSpan } = props; + const { colSpan: cellColSpan, rowSpan: cellRowSpan } = additionalProps; - return ; + const mergedColSpan = colSpan ?? cellColSpan; + const mergedRowSpan = rowSpan ?? cellRowSpan; + + const hovering = inHoverRange(index, mergedRowSpan || 1, startRow, endRow); + + return ( + + ); }); WrappedCell.displayName = 'WrappedCell'; diff --git a/src/interface.ts b/src/interface.ts index e762b10e9..a0d146537 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -107,7 +107,7 @@ export interface StickyOffsets { export type GetComponentProps = ( data: DataType, index?: number, -) => React.HTMLAttributes; +) => React.HTMLAttributes | React.TdHTMLAttributes; type Component

= | React.ComponentType

diff --git a/tests/Hover.spec.tsx b/tests/Hover.spec.tsx new file mode 100644 index 000000000..2873713e0 --- /dev/null +++ b/tests/Hover.spec.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { resetWarned } from 'rc-util/lib/warning'; +import Table from '../src'; +import type { TableProps } from '../src/Table'; + +describe('Table.Hover', () => { + const data = [ + { key: 'key0', name: 'Lucy' }, + { key: 'key1', name: 'Jack' }, + ]; + const createTable = (props?: TableProps) => { + const columns = [{ title: 'Name', dataIndex: 'name', key: 'name' }]; + + return

; + }; + + it('basic', () => { + const wrapper = mount(createTable()); + wrapper.find('tbody td').first().simulate('mouseEnter'); + expect(wrapper.exists('.rc-table-cell-row-hover')).toBeTruthy(); + + wrapper.find('tbody td').first().simulate('mouseLeave'); + expect(wrapper.exists('.rc-table-cell-row-hover')).toBeFalsy(); + }); + + it('works on shouldCellUpdate', () => { + const wrapper = mount( + createTable({ + columns: [{ title: 'Name', dataIndex: 'name', key: 'name', shouldCellUpdate: () => false }], + }), + ); + + wrapper.find('tbody td').first().simulate('mouseEnter'); + expect(wrapper.exists('.rc-table-cell-row-hover')).toBeTruthy(); + + wrapper.find('tbody td').first().simulate('mouseLeave'); + expect(wrapper.exists('.rc-table-cell-row-hover')).toBeFalsy(); + }); + + it('warning if use `render` for rowSpan', () => { + resetWarned(); + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + const wrapper = mount( + createTable({ + columns: [ + { + dataIndex: 'name', + render: (name, _, index) => { + if (index === 0) { + return { + children: name, + props: { rowSpan: 2 }, + }; + } + return { + props: { rowSpan: 0 }, + }; + }, + }, + { + dataIndex: 'key', + }, + ], + }), + ); + + // Merge row check + expect(wrapper.find('tbody td')).toHaveLength(3); + + // Hover 0-0 + wrapper.find('tbody td').at(0).simulate('mouseEnter'); + expect(wrapper.find('td.rc-table-cell-row-hover')).toHaveLength(2); + + // Hover 0-1 + wrapper.find('tbody td').at(1).simulate('mouseEnter'); + expect(wrapper.find('td.rc-table-cell-row-hover')).toHaveLength(1); + + // Mouse leave + wrapper.find('tbody td').at(1).simulate('mouseLeave'); + expect(wrapper.exists('.rc-table-cell-row-hover')).toBeFalsy(); + + expect(errorSpy).toHaveBeenCalledWith( + 'Warning: `columns.render` return cell props is deprecated with perf issue, please use `onCell` instead.', + ); + errorSpy.mockRestore(); + }); + + it('onCell should work', () => { + const wrapper = mount( + createTable({ + columns: [ + { + dataIndex: 'name', + onCell: (_, index) => { + if (index === 0) { + return { + rowSpan: 2, + }; + } + return { rowSpan: 0 }; + }, + }, + { + dataIndex: 'key', + }, + ], + }), + ); + + // Merge row check + expect(wrapper.find('tbody td')).toHaveLength(3); + + // Hover 0-0 + wrapper.find('tbody td').at(0).simulate('mouseEnter'); + expect(wrapper.find('td.rc-table-cell-row-hover')).toHaveLength(3); + + // Hover 0-1 + wrapper.find('tbody td').at(1).simulate('mouseEnter'); + expect(wrapper.find('td.rc-table-cell-row-hover')).toHaveLength(2); + + // Mouse leave + wrapper.find('tbody td').at(1).simulate('mouseLeave'); + expect(wrapper.exists('.rc-table-cell-row-hover')).toBeFalsy(); + }); +}); diff --git a/tests/Table.spec.js b/tests/Table.spec.js index d1e989cea..b5a703fb6 100644 --- a/tests/Table.spec.js +++ b/tests/Table.spec.js @@ -774,7 +774,6 @@ describe('Table.Basic', () => { }, internalHooks: INTERNAL_HOOKS, transformColumns: columns => { - console.log(columns); existExpandColumn = columns.some( col => col[INTERNAL_COL_DEFINE]?.columnType === 'EXPAND_COLUMN', ); @@ -959,30 +958,6 @@ describe('Table.Basic', () => { }); }); - describe('hover', () => { - it('basic', () => { - const wrapper = mount(createTable()); - wrapper.find('tbody td').first().simulate('mouseEnter'); - expect(wrapper.exists('.rc-table-cell-row-hover')).toBeTruthy(); - - wrapper.find('tbody td').first().simulate('mouseLeave'); - expect(wrapper.exists('.rc-table-cell-row-hover')).toBeFalsy(); - }); - - it('skip when config should cell update', () => { - const wrapper = mount( - createTable({ - columns: [ - { title: 'Name', dataIndex: 'name', key: 'name', shouldCellUpdate: () => false }, - ], - }), - ); - - wrapper.find('tbody td').first().simulate('mouseEnter'); - expect(wrapper.exists('.rc-table-cell-row-hover')).toBeFalsy(); - }); - }); - it('render index in tree table', () => { const tColumns = [ {