diff --git a/src/Body/BodyRow.tsx b/src/Body/BodyRow.tsx index 13435f0ab..1d053dd6d 100644 --- a/src/Body/BodyRow.tsx +++ b/src/Body/BodyRow.tsx @@ -84,7 +84,8 @@ export function getCellProps( ); } - const additionalCellProps = column.onCell?.(record, index) || {}; + const additionalCellProps = { ...(column.onCell?.(record, index) || {}) }; + let hoverRowSpan: number | undefined; // Expandable row has offset if (expandedRowOffset) { @@ -93,6 +94,7 @@ export function getCellProps( // For expandable row with rowSpan, // We should increase the rowSpan if the row is expanded if (expandable && rowSpan && colIndex < expandedRowOffset) { + hoverRowSpan = rowSpan; let currentRowSpan = rowSpan; for (let i = index; i < index + rowSpan; i += 1) { @@ -110,6 +112,7 @@ export function getCellProps( fixedInfo, appendCellNode, additionalCellProps: additionalCellProps, + hoverRowSpan, }; } @@ -188,7 +191,7 @@ const BodyRow = ( {flattenColumns.map((column: ColumnType, colIndex) => { const { render, dataIndex, className: columnClassName } = column; - const { key, fixedInfo, appendCellNode, additionalCellProps } = getCellProps( + const { key, fixedInfo, appendCellNode, additionalCellProps, hoverRowSpan } = getCellProps( rowInfo, column, colIndex, @@ -217,6 +220,7 @@ const BodyRow = ( {...fixedInfo} appendNode={appendCellNode} additionalProps={additionalCellProps} + hoverRowSpan={hoverRowSpan} /> ); })} diff --git a/src/Cell/index.tsx b/src/Cell/index.tsx index 3d07772b5..659e63970 100644 --- a/src/Cell/index.tsx +++ b/src/Cell/index.tsx @@ -52,6 +52,8 @@ export interface CellProps { /** @private Used for `expandable` with nest tree */ appendNode?: React.ReactNode; additionalProps?: React.TdHTMLAttributes; + /** @private Keep hover range independent from layout rowSpan patched by expanded row */ + hoverRowSpan?: number; rowType?: 'header' | 'body' | 'footer'; @@ -123,6 +125,7 @@ const Cell = (props: CellProps) => { // Private appendNode, additionalProps = {}, + hoverRowSpan, isSticky, } = props; @@ -183,13 +186,14 @@ const Cell = (props: CellProps) => { // ================ RowSpan & ColSpan ================= const mergedColSpan = legacyCellProps?.colSpan ?? additionalProps.colSpan ?? colSpan ?? 1; const mergedRowSpan = legacyCellProps?.rowSpan ?? additionalProps.rowSpan ?? rowSpan ?? 1; + const mergedHoverRowSpan = legacyCellProps?.rowSpan ?? hoverRowSpan ?? mergedRowSpan; // ====================== Hover ======================= - const [hovering, onHover] = useHoverState(index, mergedRowSpan); + const [hovering, onHover] = useHoverState(index, mergedHoverRowSpan); const onMouseEnter: React.MouseEventHandler = useEvent(event => { if (record) { - onHover(index, index + mergedRowSpan - 1); + onHover(index, index + mergedHoverRowSpan - 1); } additionalProps?.onMouseEnter?.(event); diff --git a/src/VirtualTable/VirtualCell.tsx b/src/VirtualTable/VirtualCell.tsx index ebcfff4ad..3fe2bb180 100644 --- a/src/VirtualTable/VirtualCell.tsx +++ b/src/VirtualTable/VirtualCell.tsx @@ -57,7 +57,7 @@ const VirtualCell = (props: VirtualCellProps) => { const { columnsOffset } = useContext(GridContext, ['columnsOffset']); // TODO: support `expandableRowOffset` - const { key, fixedInfo, appendCellNode, additionalCellProps } = getCellProps( + const { key, fixedInfo, appendCellNode, additionalCellProps, hoverRowSpan } = getCellProps( rowInfo, column, colIndex, @@ -128,6 +128,7 @@ const VirtualCell = (props: VirtualCellProps) => { shouldCellUpdate={column.shouldCellUpdate} {...fixedInfo} appendNode={appendCellNode} + hoverRowSpan={hoverRowSpan} additionalProps={{ ...additionalCellProps, style: mergedStyle, diff --git a/tests/Hover.spec.tsx b/tests/Hover.spec.tsx index 7029feb74..8a47175eb 100644 --- a/tests/Hover.spec.tsx +++ b/tests/Hover.spec.tsx @@ -5,6 +5,7 @@ import Table from '../src'; import type { TableProps } from '../src/Table'; describe('Table.Hover', () => { + const hoverClassName = 'rc-table-cell-row-hover'; const data = [ { key: 'key0', name: 'Lucy' }, { key: 'key1', name: 'Jack' }, @@ -127,6 +128,166 @@ describe('Table.Hover', () => { expect(container.querySelector('.rc-table-cell-row-hover')).toBeFalsy(); }); + it('does not let expanded row offset rowSpan affect hover range', () => { + const { container } = render( + { + if (index === 0) { + return { rowSpan: 2 }; + } + if (index === 1) { + return { rowSpan: 0 }; + } + return {}; + }, + }, + Table.EXPAND_COLUMN, + { + dataIndex: 'name', + }, + ]} + data={[ + { key: 'a', group: 'Group 1', name: 'Alpha' }, + { key: 'b', group: 'Group 1', name: 'Beta' }, + { key: 'c', group: 'Group 2', name: 'Gamma' }, + ]} + expandable={{ + expandedRowOffset: 1, + expandedRowKeys: ['a'], + expandedRowRender: record => expanded {record.key}, + }} + />, + ); + + const getCell = (text: string) => { + const cell = Array.from(container.querySelectorAll('tbody td')).find( + cell => cell.textContent === text, + ); + expect(cell).toBeTruthy(); + return cell!; + }; + + const groupCell = getCell('Group 1'); + const betaCell = getCell('Beta'); + const gammaCell = getCell('Gamma'); + + expect(groupCell.getAttribute('rowspan')).toBe('3'); + + fireEvent.mouseEnter(groupCell); + expect(groupCell.classList.contains(hoverClassName)).toBe(true); + expect(betaCell.classList.contains(hoverClassName)).toBe(true); + expect(gammaCell.classList.contains(hoverClassName)).toBe(false); + + fireEvent.mouseEnter(gammaCell); + expect(groupCell.classList.contains(hoverClassName)).toBe(false); + expect(gammaCell.classList.contains(hoverClassName)).toBe(true); + }); + + it('keeps legacy render rowSpan priority for hover range', () => { + const { container } = render( +
({ + children: value, + props: { rowSpan: index === 0 ? 2 : 0 }, + }), + }, + Table.EXPAND_COLUMN, + { + dataIndex: 'name', + }, + ]} + data={[ + { key: 'a', group: 'Group 1', name: 'Alpha' }, + { key: 'b', group: 'Group 1', name: 'Beta' }, + { key: 'c', group: 'Group 2', name: 'Gamma' }, + ]} + expandable={{ + expandedRowOffset: 1, + defaultExpandAllRows: true, + expandedRowRender: record => expanded {record.key}, + }} + />, + ); + + const getCell = (text: string) => { + const cell = Array.from(container.querySelectorAll('tbody td')).find( + item => item.textContent === text, + ); + expect(cell).toBeTruthy(); + return cell!; + }; + + const groupCell = getCell('Group 1'); + const alphaCell = getCell('Alpha'); + const betaCell = getCell('Beta'); + const gammaCell = getCell('Gamma'); + + expect(groupCell.getAttribute('rowspan')).toBe('2'); + + fireEvent.mouseEnter(groupCell); + expect(alphaCell.classList.contains(hoverClassName)).toBe(true); + expect(betaCell.classList.contains(hoverClassName)).toBe(true); + expect(gammaCell.classList.contains(hoverClassName)).toBe(false); + }); + + it('does not mutate stable onCell props across expanded row renders', () => { + const rowSpanProps = [{ rowSpan: 2 }, { rowSpan: 0 }, {}]; + const dataSource = [ + { key: 'a', group: 'Group 1', name: 'Alpha' }, + { key: 'b', group: 'Group 1', name: 'Beta' }, + { key: 'c', group: 'Group 2', name: 'Gamma' }, + ]; + + const createTableWithExpandedKeys = (expandedRowKeys: React.Key[]) => ( + +
rowSpanProps[index], + }, + Table.EXPAND_COLUMN, + { + dataIndex: 'name', + }, + ]} + data={dataSource} + expandable={{ + expandedRowOffset: 1, + expandedRowKeys, + expandedRowRender: record => expanded {record.key}, + }} + /> + + ); + + const { container, rerender } = render(createTableWithExpandedKeys([])); + const getGroupCell = () => + Array.from(container.querySelectorAll('tbody td')).find( + cell => cell.textContent === 'Group 1', + )!; + + expect(rowSpanProps[0].rowSpan).toBe(2); + expect(getGroupCell().getAttribute('rowspan')).toBe('2'); + + rerender(createTableWithExpandedKeys(['a'])); + expect(rowSpanProps[0].rowSpan).toBe(2); + expect(getGroupCell().getAttribute('rowspan')).toBe('3'); + + rerender(createTableWithExpandedKeys([])); + expect(rowSpanProps[0].rowSpan).toBe(2); + expect(getGroupCell().getAttribute('rowspan')).toBe('2'); + }); + describe('perf', () => { it('legacy mode should render every time', () => { let renderTimes = 0;