Skip to content

Commit 649752c

Browse files
authored
[virtualizer] Render the rows and columns in view on small scrolls in controlled layout (#23865)
1 parent a6c9d04 commit 649752c

2 files changed

Lines changed: 113 additions & 4 deletions

File tree

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

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
getColumnValues,
1313
getRows,
1414
getColumnHeaderCell,
15+
raf,
1516
} from 'test/utils/helperFn';
1617
import {
1718
useGridApiRef,
@@ -653,6 +654,103 @@ describe('<DataGridPro /> - Rows', () => {
653654
});
654655
});
655656

657+
describe('controlled layout', () => {
658+
const controlledLayout = { virtualizerLayoutMode: 'controlled' as const };
659+
660+
// Lets the browser fire the scroll event and the grid handle it, like a real scroll.
661+
const scrollTo = (position: { top?: number; left?: number }) =>
662+
act(async () => {
663+
grid('virtualScroller')!.scrollTo(position);
664+
await raf();
665+
});
666+
667+
function expectViewportCovered() {
668+
const { viewportInnerSize } = apiRef.current!.state.dimensions;
669+
const viewportTop = grid('columnHeaders')!.getBoundingClientRect().bottom;
670+
const viewportLeft = grid('virtualScroller')!.getBoundingClientRect().left;
671+
672+
const rows = $$('[role="row"][data-rowindex]');
673+
const rowRects = rows.map((row) => row.getBoundingClientRect());
674+
expect(Math.min(...rowRects.map((rect) => rect.top))).to.be.at.most(viewportTop + 0.5);
675+
expect(Math.max(...rowRects.map((rect) => rect.bottom))).to.be.at.least(
676+
viewportTop + viewportInnerSize.height - 0.5,
677+
);
678+
679+
const cellRects = $$(rows[0], '[role="gridcell"]').map((cell) =>
680+
cell.getBoundingClientRect(),
681+
);
682+
expect(Math.min(...cellRects.map((rect) => rect.left))).to.be.at.most(viewportLeft + 0.5);
683+
expect(Math.max(...cellRects.map((rect) => rect.right))).to.be.at.least(
684+
viewportLeft + viewportInnerSize.width - 0.5,
685+
);
686+
}
687+
688+
it('should render the rows in the viewport when scrolling up by less than a row', async () => {
689+
const rowHeight = 50;
690+
render(
691+
<TestCaseVirtualization rowHeight={rowHeight} experimentalFeatures={controlledLayout} />,
692+
);
693+
694+
// Start in the middle of a row, so the steps below reach the previous row before
695+
// they add up to a full row height.
696+
const initialTop = 20 * rowHeight + rowHeight / 2;
697+
await scrollTo({ top: initialTop });
698+
for (let top = initialTop - 10; top >= 18 * rowHeight; top -= 10) {
699+
// eslint-disable-next-line no-await-in-loop
700+
await scrollTo({ top });
701+
expectViewportCovered();
702+
}
703+
});
704+
705+
it('should render the rows in the viewport when scrolling down past rows shorter than the average', async () => {
706+
// Every 10th row is tall, so the other rows are shorter than the average row height
707+
// and a scroll smaller than the average can bring several of them into view.
708+
render(
709+
<TestCaseVirtualization
710+
getRowHeight={({ id }) => (Number(id) % 10 === 0 ? 300 : 25)}
711+
experimentalFeatures={controlledLayout}
712+
/>,
713+
);
714+
715+
for (let top = 0; top <= 600; top += 40) {
716+
// eslint-disable-next-line no-await-in-loop
717+
await scrollTo({ top });
718+
expectViewportCovered();
719+
}
720+
});
721+
722+
it('should render the columns in the viewport when scrolling left by less than a column', async () => {
723+
const columnWidth = 100;
724+
render(<TestCaseVirtualization nbCols={20} experimentalFeatures={controlledLayout} />);
725+
726+
// Start in the middle of a column and step by less than the column threshold.
727+
const initialLeft = 10 * columnWidth + columnWidth / 2;
728+
await scrollTo({ left: initialLeft });
729+
for (let left = initialLeft - 10; left >= 8 * columnWidth; left -= 10) {
730+
// eslint-disable-next-line no-await-in-loop
731+
await scrollTo({ left });
732+
expectViewportCovered();
733+
}
734+
});
735+
736+
it('should render the columns in the viewport when scrolling right past columns narrower than the threshold', async () => {
737+
const columns = Array.from({ length: 60 }, (_, index) => ({
738+
field: `column${index}`,
739+
width: 30,
740+
minWidth: 30,
741+
}));
742+
render(
743+
<TestCaseVirtualization columns={columns} experimentalFeatures={controlledLayout} />,
744+
);
745+
746+
for (let left = 0; left <= 400; left += 10) {
747+
// eslint-disable-next-line no-await-in-loop
748+
await scrollTo({ left });
749+
expectViewportCovered();
750+
}
751+
});
752+
});
753+
656754
it('should render new columns when scrolling past the threshold value', async () => {
657755
const columnWidth = 100;
658756
const columnThresholdPx = 1 * columnWidth;

‎packages/x-virtualizer/src/features/virtualization/virtualization.ts‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,13 @@ function useVirtualization(store: Store<BaseState>, params: ParamsWithDefaults,
440440
// PERF: use the computed minimum column width instead of a static one
441441
const didCrossThreshold =
442442
rowScroll >= averageRowHeight(store, rowHeight) || columnScroll >= MINIMUM_COLUMN_WIDTH;
443-
shouldUpdate = didCrossThreshold || didChangeDirection;
443+
shouldUpdate =
444+
didCrossThreshold ||
445+
didChangeDirection ||
446+
// The controlled layout renders no buffer, so a scroll below the threshold can
447+
// already reach past the rendered rows or columns.
448+
(layoutMode === 'controlled' &&
449+
isLowOnRenderedBuffer(store, params, scrollPosition.current, scrollCache));
444450
}
445451

446452
if (!shouldUpdate) {
@@ -1458,9 +1464,10 @@ function bufferForDirection(
14581464
/**
14591465
* Whether the viewport has consumed more than half of the rendered buffer on a side
14601466
* that can still be extended.
1461-
* Triggers the sticky-mode rendering logic for computing a new render context.
1462-
* Consecutive updates are thereby spaced half a buffer apart instead of one
1467+
* In sticky mode, it spaces consecutive updates half a buffer apart instead of one
14631468
* row apart, leaving the raster pipeline idle time to fill the buffers between them.
1469+
* In controlled mode, where the buffer is empty, it triggers as soon as the viewport
1470+
* reaches past the rendered rows or columns.
14641471
* The visible bounds mirror the ones `computeRenderContext` searches with.
14651472
*/
14661473
function isLowOnRenderedBuffer(
@@ -1495,7 +1502,11 @@ function isLowOnRenderedBuffer(
14951502
const pinnedRightCount = params.pinnedColumns?.right.length ?? 0;
14961503

14971504
const visibleLeft = Math.abs(scrollPosition.left) + dimensions.leftPinnedWidth;
1498-
const visibleRight = visibleLeft + dimensions.viewportInnerSize.width;
1505+
// In controlled mode, the pinned-right section overlays the horizontal window.
1506+
const visibleRight =
1507+
visibleLeft +
1508+
dimensions.viewportInnerSize.width -
1509+
(store.state.virtualization.layoutMode === 'controlled' ? dimensions.rightPinnedWidth : 0);
14991510
const renderedLeft = columnPositions[context.firstColumnIndex] ?? 0;
15001511
const renderedRight = columnPositions[context.lastColumnIndex] ?? dimensions.columnsTotalWidth;
15011512

0 commit comments

Comments
 (0)