Skip to content

Commit 909c702

Browse files
MBilalShafiweb-flow
authored andcommitted
[DataGrid] Wait for rows before reacting on autosizeOnMount (#22698)
1 parent 516809d commit 909c702

3 files changed

Lines changed: 135 additions & 3 deletions

File tree

docs/data/data-grid/column-dimensions/column-dimensions.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ This example uses `ReactDOM.flushSync`. If used incorrectly it can hurt the perf
124124

125125
### Autosizing virtualized columns
126126

127+
Autosizing measures the cells currently rendered in the DOM. When row virtualization is enabled and the rows do not all fit in the viewport, cells from off-screen rows are not measured.
128+
127129
Use `autoSizeColumns({ disableColumnVirtualization: true})` to include columns that are not visible in the DOM due to column virtualization.
128130

129131
:::warning

packages/x-data-grid-pro/src/tests/columns.DataGridPro.test.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import * as React from 'react';
2+
import * as ReactDOM from 'react-dom';
13
import { createRenderer, fireEvent, screen, act, waitFor } from '@mui/internal-test-utils';
24
import { spy } from 'sinon';
35
import { type RefObject } from '@mui/x-internals/types';
@@ -10,6 +12,7 @@ import {
1012
gridColumnFieldsSelector,
1113
type GridApi,
1214
type GridAutosizeOptions,
15+
type GridColDef,
1316
} from '@mui/x-data-grid-pro';
1417
import { useGridPrivateApiContext } from '@mui/x-data-grid-pro/internals';
1518
import { getColumnHeaderCell, getCell, getRow } from 'test/utils/helperFn';
@@ -635,6 +638,63 @@ describe('<DataGridPro /> - Columns', () => {
635638
});
636639
});
637640

641+
// Regression test for https://github.com/mui/mui-x/issues/22505
642+
it('should wait for all rows to be rendered on mount when rows fit the viewport', async () => {
643+
const shortValue = 'Nike';
644+
const wideValue = 'Lululemon Athletica International Collection';
645+
646+
function DeferredCellContent({ value }: { value: string }) {
647+
const [showValue, setShowValue] = React.useState(value === shortValue);
648+
649+
React.useEffect(() => {
650+
if (!showValue) {
651+
// Hack to make the test fail similar to https://github.com/mui/mui-x/issues/22505
652+
// in our test env. We reveal the wide value a couple of microtasks after mount, so
653+
// that the unfixed code (which autosizes on the first microtask) misses it. The
654+
// commit is flushed synchronously so the wide value is reliably in the DOM before
655+
// the autosize `requestAnimationFrame` fires, regardless of the React version's
656+
// scheduler (a plain `setState` here is committed after the frame on React 18).
657+
Promise.resolve().then(() => {
658+
Promise.resolve().then(() => ReactDOM.flushSync(() => setShowValue(true)));
659+
});
660+
}
661+
}, [showValue]);
662+
663+
return <span>{showValue ? value : shortValue}</span>;
664+
}
665+
666+
const autosizeRows = [
667+
{ id: 0, brand: 'Nike' },
668+
{ id: 1, brand: 'Adidas' },
669+
{ id: 2, brand: 'Puma' },
670+
{ id: 3, brand: 'Reebok' },
671+
{ id: 4, brand: 'Asics' },
672+
{ id: 5, brand: 'New Balance' },
673+
{ id: 6, brand: wideValue },
674+
];
675+
const autosizeColumns: GridColDef[] = [
676+
{
677+
field: 'brand',
678+
renderCell: ({ value }) => <DeferredCellContent value={value} />,
679+
},
680+
];
681+
682+
render(
683+
<Test
684+
rows={autosizeRows}
685+
columns={autosizeColumns}
686+
autosizeOnMount
687+
autosizeOptions={{ columns: ['brand'], includeOutliers: true }}
688+
/>,
689+
);
690+
691+
await waitFor(() => {
692+
const wideCell = getCell(6, 0);
693+
expect(wideCell.textContent).to.equal(wideValue);
694+
expect(wideCell.scrollWidth).to.be.at.most(wideCell.clientWidth);
695+
});
696+
});
697+
638698
it('should work with flex columns', async () => {
639699
const { user } = render(
640700
<Test

packages/x-data-grid/src/hooks/features/columnResize/useGridColumnResize.tsx

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ import {
3939
useGridSelector,
4040
useOnMount,
4141
} from '../../utils';
42-
import { gridVirtualizationColumnEnabledSelector } from '../virtualization';
42+
import {
43+
gridRenderContextSelector,
44+
gridVirtualizationColumnEnabledSelector,
45+
} from '../virtualization';
4346
import {
4447
type ControllablePromise,
4548
createControllablePromise,
@@ -51,6 +54,7 @@ import { GridPinnedColumnPosition } from '../columns/gridColumnsInterfaces';
5154
import { gridColumnsStateSelector } from '../columns';
5255
import { gridDimensionsSelector } from '../dimensions';
5356
import { gridHeaderFilteringEnabledSelector } from '../headerFiltering';
57+
import { gridVisibleRowsSelector } from '../pagination';
5458
import type { DataGridProcessedProps } from '../../../models/props/DataGridProps';
5559
import type { GridColumnResizeParams } from '../../../models/params/gridColumnResizeParams';
5660
import type { GridStateColDef } from '../../../models/colDef/gridColDef';
@@ -62,6 +66,30 @@ type AutosizeOptionsRequired = Required<GridAutosizeOptions>;
6266

6367
type ResizeDirection = keyof typeof GridColumnHeaderSeparatorSides;
6468

69+
function isRenderContextReadyForAutosizeOnMount(apiRef: RefObject<GridPrivateApiCommunity>) {
70+
const dimensions = gridDimensionsSelector(apiRef);
71+
if (!dimensions.isReady) {
72+
return false;
73+
}
74+
75+
const { rows } = gridVisibleRowsSelector(apiRef);
76+
if (rows.length === 0) {
77+
return true;
78+
}
79+
80+
const renderContext = gridRenderContextSelector(apiRef);
81+
if (renderContext.lastRowIndex <= renderContext.firstRowIndex) {
82+
return false;
83+
}
84+
85+
// If all rows fit in the viewport, wait for them all; otherwise we can only measure the rendered rows.
86+
if (!dimensions.hasScrollY) {
87+
return renderContext.firstRowIndex === 0 && renderContext.lastRowIndex >= rows.length;
88+
}
89+
90+
return true;
91+
}
92+
6593
function trackFinger(event: any, currentTouchId: number | undefined): CursorCoordinates | boolean {
6694
if (currentTouchId !== undefined && event.changedTouches) {
6795
for (let i = 0; i < event.changedTouches.length; i += 1) {
@@ -851,10 +879,52 @@ export const useGridColumnResize = (
851879

852880
useOnMount(() => {
853881
if (props.autosizeOnMount) {
854-
Promise.resolve().then(() => {
882+
let frameHandle: number | undefined;
883+
let unsubscribeStateChange: (() => void) | undefined;
884+
let unsubscribeRenderedRowsIntervalChange: (() => void) | undefined;
885+
886+
const cleanupListeners = () => {
887+
unsubscribeStateChange?.();
888+
unsubscribeRenderedRowsIntervalChange?.();
889+
unsubscribeStateChange = undefined;
890+
unsubscribeRenderedRowsIntervalChange = undefined;
891+
};
892+
893+
const runAutosize = () => {
894+
frameHandle = undefined;
895+
cleanupListeners();
855896
apiRef.current.autosizeColumns(props.autosizeOptions);
856-
});
897+
};
898+
899+
const scheduleAutosize = () => {
900+
if (frameHandle === undefined) {
901+
frameHandle = requestAnimationFrame(runAutosize);
902+
}
903+
};
904+
905+
const checkRenderContext = () => {
906+
if (isRenderContextReadyForAutosizeOnMount(apiRef)) {
907+
scheduleAutosize();
908+
}
909+
};
910+
911+
unsubscribeStateChange = apiRef.current.subscribeEvent('stateChange', checkRenderContext);
912+
unsubscribeRenderedRowsIntervalChange = apiRef.current.subscribeEvent(
913+
'renderedRowsIntervalChange',
914+
checkRenderContext,
915+
);
916+
917+
checkRenderContext();
918+
919+
return () => {
920+
cleanupListeners();
921+
if (frameHandle !== undefined) {
922+
cancelAnimationFrame(frameHandle);
923+
}
924+
};
857925
}
926+
927+
return undefined;
858928
});
859929

860930
useGridNativeEventListener(

0 commit comments

Comments
 (0)