Skip to content

Commit 2ebb87b

Browse files
jiangrong-devopscoderabbitai[bot]zombieJ
authored
fix(table): fix row hover with expanded offset rowSpan (#1488)
* fix(table): fix row hover with expanded offset rowSpan * 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. * Update tests/Hover.spec.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * refactor(table): clarify origin rowSpan --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: 二货机器人 <smith3816@gmail.com>
1 parent 08188d3 commit 2ebb87b

4 files changed

Lines changed: 175 additions & 5 deletions

File tree

src/Body/BodyRow.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ export function getCellProps<RecordType>(
8484
);
8585
}
8686

87-
const additionalCellProps = column.onCell?.(record, index) || {};
87+
const additionalCellProps = { ...column.onCell?.(record, index) };
88+
let originRowSpan: number | undefined;
8889

8990
// Expandable row has offset
9091
if (expandedRowOffset) {
@@ -93,6 +94,7 @@ export function getCellProps<RecordType>(
9394
// For expandable row with rowSpan,
9495
// We should increase the rowSpan if the row is expanded
9596
if (expandable && rowSpan && colIndex < expandedRowOffset) {
97+
originRowSpan = rowSpan;
9698
let currentRowSpan = rowSpan;
9799

98100
for (let i = index; i < index + rowSpan; i += 1) {
@@ -110,6 +112,7 @@ export function getCellProps<RecordType>(
110112
fixedInfo,
111113
appendCellNode,
112114
additionalCellProps: additionalCellProps,
115+
originRowSpan,
113116
};
114117
}
115118

@@ -188,7 +191,7 @@ const BodyRow = <RecordType extends { children?: readonly RecordType[] }>(
188191
{flattenColumns.map((column: ColumnType<RecordType>, colIndex) => {
189192
const { render, dataIndex, className: columnClassName } = column;
190193

191-
const { key, fixedInfo, appendCellNode, additionalCellProps } = getCellProps(
194+
const { key, fixedInfo, appendCellNode, additionalCellProps, originRowSpan } = getCellProps(
192195
rowInfo,
193196
column,
194197
colIndex,
@@ -217,6 +220,7 @@ const BodyRow = <RecordType extends { children?: readonly RecordType[] }>(
217220
{...fixedInfo}
218221
appendNode={appendCellNode}
219222
additionalProps={additionalCellProps}
223+
originRowSpan={originRowSpan}
220224
/>
221225
);
222226
})}

src/Cell/index.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ export interface CellProps<RecordType extends DefaultRecordType> {
5252
/** @private Used for `expandable` with nest tree */
5353
appendNode?: React.ReactNode;
5454
additionalProps?: React.TdHTMLAttributes<HTMLTableCellElement>;
55+
/** @private Original rowSpan before patching it for expanded rows */
56+
originRowSpan?: number;
5557

5658
rowType?: 'header' | 'body' | 'footer';
5759

@@ -123,6 +125,7 @@ const Cell = <RecordType,>(props: CellProps<RecordType>) => {
123125
// Private
124126
appendNode,
125127
additionalProps = {},
128+
originRowSpan,
126129
isSticky,
127130
} = props;
128131

@@ -183,13 +186,14 @@ const Cell = <RecordType,>(props: CellProps<RecordType>) => {
183186
// ================ RowSpan & ColSpan =================
184187
const mergedColSpan = legacyCellProps?.colSpan ?? additionalProps.colSpan ?? colSpan ?? 1;
185188
const mergedRowSpan = legacyCellProps?.rowSpan ?? additionalProps.rowSpan ?? rowSpan ?? 1;
189+
const mergedHoverRowSpan = legacyCellProps?.rowSpan ?? originRowSpan ?? mergedRowSpan;
186190

187191
// ====================== Hover =======================
188-
const [hovering, onHover] = useHoverState(index, mergedRowSpan);
192+
const [hovering, onHover] = useHoverState(index, mergedHoverRowSpan);
189193

190194
const onMouseEnter: React.MouseEventHandler<HTMLTableCellElement> = useEvent(event => {
191195
if (record) {
192-
onHover(index, index + mergedRowSpan - 1);
196+
onHover(index, index + mergedHoverRowSpan - 1);
193197
}
194198

195199
additionalProps?.onMouseEnter?.(event);

src/VirtualTable/VirtualCell.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ const VirtualCell = <RecordType,>(props: VirtualCellProps<RecordType>) => {
5757
const { columnsOffset } = useContext(GridContext, ['columnsOffset']);
5858

5959
// TODO: support `expandableRowOffset`
60-
const { key, fixedInfo, appendCellNode, additionalCellProps } = getCellProps(
60+
const { key, fixedInfo, appendCellNode, additionalCellProps, originRowSpan } = getCellProps(
6161
rowInfo,
6262
column,
6363
colIndex,
@@ -128,6 +128,7 @@ const VirtualCell = <RecordType,>(props: VirtualCellProps<RecordType>) => {
128128
shouldCellUpdate={column.shouldCellUpdate}
129129
{...fixedInfo}
130130
appendNode={appendCellNode}
131+
originRowSpan={originRowSpan}
131132
additionalProps={{
132133
...additionalCellProps,
133134
style: mergedStyle,

tests/Hover.spec.tsx

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Table from '../src';
55
import type { TableProps } from '../src/Table';
66

77
describe('Table.Hover', () => {
8+
const hoverClassName = 'rc-table-cell-row-hover';
89
const data = [
910
{ key: 'key0', name: 'Lucy' },
1011
{ key: 'key1', name: 'Jack' },
@@ -127,6 +128,166 @@ describe('Table.Hover', () => {
127128
expect(container.querySelector('.rc-table-cell-row-hover')).toBeFalsy();
128129
});
129130

131+
it('does not let expanded row offset rowSpan affect hover range', () => {
132+
const { container } = render(
133+
<Table
134+
rowKey="key"
135+
columns={[
136+
{
137+
dataIndex: 'group',
138+
onCell: (_, index) => {
139+
if (index === 0) {
140+
return { rowSpan: 2 };
141+
}
142+
if (index === 1) {
143+
return { rowSpan: 0 };
144+
}
145+
return {};
146+
},
147+
},
148+
Table.EXPAND_COLUMN,
149+
{
150+
dataIndex: 'name',
151+
},
152+
]}
153+
data={[
154+
{ key: 'a', group: 'Group 1', name: 'Alpha' },
155+
{ key: 'b', group: 'Group 1', name: 'Beta' },
156+
{ key: 'c', group: 'Group 2', name: 'Gamma' },
157+
]}
158+
expandable={{
159+
expandedRowOffset: 1,
160+
expandedRowKeys: ['a'],
161+
expandedRowRender: record => <span>expanded {record.key}</span>,
162+
}}
163+
/>,
164+
);
165+
166+
const getCell = (text: string) => {
167+
const cell = Array.from(container.querySelectorAll<HTMLTableCellElement>('tbody td')).find(
168+
cell => cell.textContent === text,
169+
);
170+
expect(cell).toBeTruthy();
171+
return cell!;
172+
};
173+
174+
const groupCell = getCell('Group 1');
175+
const betaCell = getCell('Beta');
176+
const gammaCell = getCell('Gamma');
177+
178+
expect(groupCell.getAttribute('rowspan')).toBe('3');
179+
180+
fireEvent.mouseEnter(groupCell);
181+
expect(groupCell.classList.contains(hoverClassName)).toBe(true);
182+
expect(betaCell.classList.contains(hoverClassName)).toBe(true);
183+
expect(gammaCell.classList.contains(hoverClassName)).toBe(false);
184+
185+
fireEvent.mouseEnter(gammaCell);
186+
expect(groupCell.classList.contains(hoverClassName)).toBe(false);
187+
expect(gammaCell.classList.contains(hoverClassName)).toBe(true);
188+
});
189+
190+
it('keeps legacy render rowSpan priority for hover range', () => {
191+
const { container } = render(
192+
<Table
193+
rowKey="key"
194+
columns={[
195+
{
196+
dataIndex: 'group',
197+
render: (value, _, index) => ({
198+
children: value,
199+
props: { rowSpan: index === 0 ? 2 : 0 },
200+
}),
201+
},
202+
Table.EXPAND_COLUMN,
203+
{
204+
dataIndex: 'name',
205+
},
206+
]}
207+
data={[
208+
{ key: 'a', group: 'Group 1', name: 'Alpha' },
209+
{ key: 'b', group: 'Group 1', name: 'Beta' },
210+
{ key: 'c', group: 'Group 2', name: 'Gamma' },
211+
]}
212+
expandable={{
213+
expandedRowOffset: 1,
214+
defaultExpandAllRows: true,
215+
expandedRowRender: record => <span>expanded {record.key}</span>,
216+
}}
217+
/>,
218+
);
219+
220+
const getCell = (text: string) => {
221+
const cell = Array.from(container.querySelectorAll<HTMLTableCellElement>('tbody td')).find(
222+
item => item.textContent === text,
223+
);
224+
expect(cell).toBeTruthy();
225+
return cell!;
226+
};
227+
228+
const groupCell = getCell('Group 1');
229+
const alphaCell = getCell('Alpha');
230+
const betaCell = getCell('Beta');
231+
const gammaCell = getCell('Gamma');
232+
233+
expect(groupCell.getAttribute('rowspan')).toBe('2');
234+
235+
fireEvent.mouseEnter(groupCell);
236+
expect(alphaCell.classList.contains(hoverClassName)).toBe(true);
237+
expect(betaCell.classList.contains(hoverClassName)).toBe(true);
238+
expect(gammaCell.classList.contains(hoverClassName)).toBe(false);
239+
});
240+
241+
it('does not mutate stable onCell props across expanded row renders', () => {
242+
const rowSpanProps = [{ rowSpan: 2 }, { rowSpan: 0 }, {}];
243+
const dataSource = [
244+
{ key: 'a', group: 'Group 1', name: 'Alpha' },
245+
{ key: 'b', group: 'Group 1', name: 'Beta' },
246+
{ key: 'c', group: 'Group 2', name: 'Gamma' },
247+
];
248+
249+
const createTableWithExpandedKeys = (expandedRowKeys: React.Key[]) => (
250+
<React.StrictMode>
251+
<Table
252+
rowKey="key"
253+
columns={[
254+
{
255+
dataIndex: 'group',
256+
onCell: (_, index) => rowSpanProps[index],
257+
},
258+
Table.EXPAND_COLUMN,
259+
{
260+
dataIndex: 'name',
261+
},
262+
]}
263+
data={dataSource}
264+
expandable={{
265+
expandedRowOffset: 1,
266+
expandedRowKeys,
267+
expandedRowRender: record => <span>expanded {record.key}</span>,
268+
}}
269+
/>
270+
</React.StrictMode>
271+
);
272+
273+
const { container, rerender } = render(createTableWithExpandedKeys([]));
274+
const getGroupCell = () =>
275+
Array.from(container.querySelectorAll<HTMLTableCellElement>('tbody td')).find(
276+
cell => cell.textContent === 'Group 1',
277+
)!;
278+
279+
expect(rowSpanProps[0].rowSpan).toBe(2);
280+
expect(getGroupCell().getAttribute('rowspan')).toBe('2');
281+
282+
rerender(createTableWithExpandedKeys(['a']));
283+
expect(rowSpanProps[0].rowSpan).toBe(2);
284+
expect(getGroupCell().getAttribute('rowspan')).toBe('3');
285+
286+
rerender(createTableWithExpandedKeys([]));
287+
expect(rowSpanProps[0].rowSpan).toBe(2);
288+
expect(getGroupCell().getAttribute('rowspan')).toBe('2');
289+
});
290+
130291
describe('perf', () => {
131292
it('legacy mode should render every time', () => {
132293
let renderTimes = 0;

0 commit comments

Comments
 (0)