From e463d060a940f0f68ac98c62b95f4df9d61e3510 Mon Sep 17 00:00:00 2001 From: jiangrong-devops <18707886349@163.com> Date: Wed, 13 May 2026 17:55:24 +0800 Subject: [PATCH 1/3] fix(table): fix row hover with expanded offset rowSpan --- src/Body/BodyRow.tsx | 6 +++- src/Cell/index.tsx | 8 +++-- src/VirtualTable/VirtualCell.tsx | 3 +- tests/ExpandedOffset.spec.tsx | 2 +- tests/Hover.spec.tsx | 60 ++++++++++++++++++++++++++++++++ 5 files changed, 74 insertions(+), 5 deletions(-) diff --git a/src/Body/BodyRow.tsx b/src/Body/BodyRow.tsx index aae9f4c7b..7a091a1bc 100644 --- a/src/Body/BodyRow.tsx +++ b/src/Body/BodyRow.tsx @@ -85,6 +85,7 @@ export function getCellProps( } 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, }; } @@ -187,7 +190,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, @@ -216,6 +219,7 @@ const BodyRow = ( {...fixedInfo} appendNode={appendCellNode} additionalProps={additionalCellProps} + hoverRowSpan={hoverRowSpan} /> ); })} diff --git a/src/Cell/index.tsx b/src/Cell/index.tsx index 3d07772b5..b557dab69 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 = 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/ExpandedOffset.spec.tsx b/tests/ExpandedOffset.spec.tsx index 12d345369..5f20106f9 100644 --- a/tests/ExpandedOffset.spec.tsx +++ b/tests/ExpandedOffset.spec.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { spyElementPrototypes } from 'rc-util/lib/test/domHook'; +import { spyElementPrototypes } from '@rc-component/util/lib/test/domHook'; import { render, act } from '@testing-library/react'; import { _rs } from '@rc-component/resize-observer'; import Table, { type ColumnsType } from '../src'; diff --git a/tests/Hover.spec.tsx b/tests/Hover.spec.tsx index 813f09a6b..341440b1a 100644 --- a/tests/Hover.spec.tsx +++ b/tests/Hover.spec.tsx @@ -6,6 +6,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' }, @@ -128,6 +129,65 @@ 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); + }); + describe('perf', () => { it('legacy mode should render every time', () => { let renderTimes = 0; From 2b7deac96513d3ebd42413c84b27782b1cf3ad2c Mon Sep 17 00:00:00 2001 From: jiangrong-devops <18707886349@163.com> Date: Fri, 31 Jul 2026 20:21:08 +0800 Subject: [PATCH 2/3] fix(table): preserve hover rowSpan with expanded offset Respect legacy render rowSpan when calculating hover ranges and avoid mutating objects returned by onCell. Add regression coverage for legacy cells, stable props, StrictMode, and expanded row rerenders. --- src/Body/BodyRow.tsx | 2 +- src/Cell/index.tsx | 2 +- tests/Hover.spec.tsx | 100 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 2 deletions(-) diff --git a/src/Body/BodyRow.tsx b/src/Body/BodyRow.tsx index 7a091a1bc..d64c01863 100644 --- a/src/Body/BodyRow.tsx +++ b/src/Body/BodyRow.tsx @@ -84,7 +84,7 @@ export function getCellProps( ); } - const additionalCellProps = column.onCell?.(record, index) || {}; + const additionalCellProps = { ...(column.onCell?.(record, index) || {}) }; let hoverRowSpan: number | undefined; // Expandable row has offset diff --git a/src/Cell/index.tsx b/src/Cell/index.tsx index b557dab69..659e63970 100644 --- a/src/Cell/index.tsx +++ b/src/Cell/index.tsx @@ -186,7 +186,7 @@ const Cell = (props: CellProps) => { // ================ RowSpan & ColSpan ================= const mergedColSpan = legacyCellProps?.colSpan ?? additionalProps.colSpan ?? colSpan ?? 1; const mergedRowSpan = legacyCellProps?.rowSpan ?? additionalProps.rowSpan ?? rowSpan ?? 1; - const mergedHoverRowSpan = hoverRowSpan ?? mergedRowSpan; + const mergedHoverRowSpan = legacyCellProps?.rowSpan ?? hoverRowSpan ?? mergedRowSpan; // ====================== Hover ======================= const [hovering, onHover] = useHoverState(index, mergedHoverRowSpan); diff --git a/tests/Hover.spec.tsx b/tests/Hover.spec.tsx index 305c0a6d3..db5894587 100644 --- a/tests/Hover.spec.tsx +++ b/tests/Hover.spec.tsx @@ -187,6 +187,106 @@ describe('Table.Hover', () => { 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, + 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; From 03329fcdc666b526c57232f87b7169a18537be77 Mon Sep 17 00:00:00 2001 From: mwbdtth <448847482@qq.com> Date: Sat, 1 Aug 2026 11:57:51 +0800 Subject: [PATCH 3/3] Update tests/Hover.spec.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- tests/Hover.spec.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Hover.spec.tsx b/tests/Hover.spec.tsx index db5894587..8a47175eb 100644 --- a/tests/Hover.spec.tsx +++ b/tests/Hover.spec.tsx @@ -211,6 +211,7 @@ describe('Table.Hover', () => { ]} expandable={{ expandedRowOffset: 1, + defaultExpandAllRows: true, expandedRowRender: record => expanded {record.key}, }} />,