Skip to content

Commit 18e3f2f

Browse files
authored
[DataGrid] Reset row spanning on row expansion change (#20661)
1 parent 601f211 commit 18e3f2f

File tree

2 files changed

+116
-0
lines changed

2 files changed

+116
-0
lines changed
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { RefObject } from '@mui/x-internals/types';
2+
import { createRenderer, act } from '@mui/internal-test-utils';
3+
import { DataGridPro, DataGridProProps, GridApi, useGridApiRef } from '@mui/x-data-grid-pro';
4+
import { unwrapPrivateAPI } from '@mui/x-data-grid-pro/internals';
5+
import { getCell, microtasks } from 'test/utils/helperFn';
6+
import { isJSDOM } from 'test/utils/skipIf';
7+
8+
describe.skipIf(isJSDOM)('<DataGridPro /> - Row spanning', () => {
9+
const { render } = createRenderer();
10+
11+
let apiRef: RefObject<GridApi | null>;
12+
13+
describe('tree data', () => {
14+
// Tree data adds __tree_data_group__ column at index 0
15+
// So salesPerson is at visible column index 1
16+
const SALES_PERSON_COL_INDEX = 1;
17+
18+
// Row IDs match hierarchy paths for tree data (e.g., 'Thomas', 'Thomas.Robert')
19+
const rows = [
20+
{ path: 'Sarah', salesPerson: 'Sarah', role: 'Head of HR' },
21+
{ path: 'Thomas', salesPerson: 'Thomas', role: 'Head of Sales' },
22+
{ path: 'Thomas.Robert', salesPerson: 'Thomas', role: 'Sales Rep' },
23+
{ path: 'Thomas.Karen', salesPerson: 'Thomas', role: 'Sales Rep' },
24+
{ path: 'Thomas.Nancy', salesPerson: 'Thomas', role: 'Sales Rep' },
25+
{ path: 'Mary', salesPerson: 'Mary', role: 'Head of Engineering' },
26+
];
27+
28+
const columns: DataGridProProps['columns'] = [
29+
{ field: 'salesPerson', headerName: 'Sales Person', width: 150 },
30+
{ field: 'role', headerName: 'Role', width: 200 },
31+
];
32+
33+
function TreeDataTest(props: Partial<DataGridProProps>) {
34+
apiRef = useGridApiRef();
35+
return (
36+
<div style={{ width: 500, height: 400 }}>
37+
<DataGridPro
38+
apiRef={apiRef}
39+
rows={rows}
40+
columns={columns}
41+
treeData
42+
getTreeDataPath={(row) => row.path.split('.')}
43+
getRowId={(row) => row.path}
44+
rowSpanning
45+
{...props}
46+
/>
47+
</div>
48+
);
49+
}
50+
51+
it('should recalculate row spanning when expanding a tree node', async () => {
52+
render(<TreeDataTest defaultGroupingExpansionDepth={0} />);
53+
const privateApi = unwrapPrivateAPI(apiRef.current!);
54+
const store = privateApi.virtualizer.store;
55+
56+
// Initially collapsed: Sarah, Thomas, Mary visible - no spanning since all have unique salesPerson
57+
const initialSpannedCells = store.state.rowSpanning.caches.spannedCells;
58+
expect(Object.keys(initialSpannedCells).length).to.equal(0);
59+
60+
// Expand Thomas to show children
61+
act(() => {
62+
apiRef.current?.setRowChildrenExpansion('Thomas', true);
63+
});
64+
await microtasks();
65+
66+
// After expanding: Thomas row should span 4 rows (Thomas + 3 children with same salesPerson)
67+
const expandedSpannedCells = store.state.rowSpanning.caches.spannedCells;
68+
expect(expandedSpannedCells.Thomas).to.deep.equal({ [SALES_PERSON_COL_INDEX]: 4 });
69+
});
70+
71+
it('should recalculate row spanning when collapsing a tree node', async () => {
72+
render(<TreeDataTest defaultGroupingExpansionDepth={-1} />);
73+
const privateApi = unwrapPrivateAPI(apiRef.current!);
74+
const store = privateApi.virtualizer.store;
75+
await microtasks();
76+
77+
// Initially expanded: Thomas should span 4 rows (itself + 3 children)
78+
let spannedCells = store.state.rowSpanning.caches.spannedCells;
79+
expect(spannedCells.Thomas).to.deep.equal({ [SALES_PERSON_COL_INDEX]: 4 });
80+
81+
// Collapse Thomas
82+
act(() => {
83+
apiRef.current?.setRowChildrenExpansion('Thomas', false);
84+
});
85+
await microtasks();
86+
87+
// After collapsing: Thomas should no longer span (only group header visible)
88+
spannedCells = store.state.rowSpanning.caches.spannedCells;
89+
expect(spannedCells.Thomas).to.equal(undefined);
90+
});
91+
92+
it('should update spanned cell height when collapsing', async () => {
93+
render(<TreeDataTest defaultGroupingExpansionDepth={-1} />);
94+
const privateApi = unwrapPrivateAPI(apiRef.current!);
95+
await microtasks();
96+
97+
// Get the row index for Thomas
98+
const thomasRowIndex = privateApi.getRowIndexRelativeToVisibleRows('Thomas');
99+
100+
// When expanded, the salesPerson cell should have height for 4 rows
101+
let spannedCell = getCell(thomasRowIndex, SALES_PERSON_COL_INDEX);
102+
expect(spannedCell).to.have.style('height', `${52 * 4}px`);
103+
104+
// Collapse Thomas
105+
act(() => {
106+
apiRef.current?.setRowChildrenExpansion('Thomas', false);
107+
});
108+
await microtasks();
109+
110+
// After collapsing, the cell should have default height (no span)
111+
spannedCell = getCell(thomasRowIndex, SALES_PERSON_COL_INDEX);
112+
expect(spannedCell.style.height).to.equal('');
113+
});
114+
});
115+
});

packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ export const useGridRowSpanning = (
247247
useGridEvent(apiRef, 'paginationModelChange', runIf(props.rowSpanning, resetRowSpanningState));
248248
useGridEvent(apiRef, 'filteredRowsSet', runIf(props.rowSpanning, resetRowSpanningState));
249249
useGridEvent(apiRef, 'columnsChange', runIf(props.rowSpanning, resetRowSpanningState));
250+
useGridEvent(apiRef, 'rowExpansionChange', runIf(props.rowSpanning, resetRowSpanningState));
250251

251252
React.useEffect(() => {
252253
const store = apiRef.current.virtualizer?.store;

0 commit comments

Comments
 (0)