From 907646b4d8d8755eff56a61d7d2dca6bf45794bc Mon Sep 17 00:00:00 2001 From: Rom Grk Date: Thu, 8 Feb 2024 10:50:43 -0500 Subject: [PATCH] [DataGrid] Vertical scrolling performance (#11924) Co-authored-by: Andrew Cherniavskyi --- .../x/api/data-grid/data-grid-premium.json | 6 +++ docs/pages/x/api/data-grid/data-grid-pro.json | 6 +++ docs/pages/x/api/data-grid/data-grid.json | 6 +++ .../data-grid-premium/data-grid-premium.json | 1 + .../data-grid-pro/data-grid-pro.json | 1 + .../data-grid/data-grid/data-grid.json | 1 + .../src/tests/rows.DataGridPro.test.tsx | 44 ++++++++----------- .../x-data-grid/src/components/GridRow.tsx | 3 +- .../components/GridScrollbarFillerCell.tsx | 3 -- .../components/containers/GridRootStyles.ts | 6 ++- .../x-data-grid/src/constants/gridClasses.ts | 6 +++ .../virtualization/useGridVirtualScroller.tsx | 23 +++++----- 12 files changed, 62 insertions(+), 44 deletions(-) diff --git a/docs/pages/x/api/data-grid/data-grid-premium.json b/docs/pages/x/api/data-grid/data-grid-premium.json index 90e048ac17a97..2bf7739699c5a 100644 --- a/docs/pages/x/api/data-grid/data-grid-premium.json +++ b/docs/pages/x/api/data-grid/data-grid-premium.json @@ -782,6 +782,12 @@ "description": "Styles applied to the empty cell element.", "isGlobal": false }, + { + "key": "cellOffsetLeft", + "className": "MuiDataGridPremium-cellOffsetLeft", + "description": "", + "isGlobal": false + }, { "key": "cellSkeleton", "className": "MuiDataGridPremium-cellSkeleton", diff --git a/docs/pages/x/api/data-grid/data-grid-pro.json b/docs/pages/x/api/data-grid/data-grid-pro.json index 7b3e2e89f0d4b..0dbb6bdc4e055 100644 --- a/docs/pages/x/api/data-grid/data-grid-pro.json +++ b/docs/pages/x/api/data-grid/data-grid-pro.json @@ -721,6 +721,12 @@ "description": "Styles applied to the empty cell element.", "isGlobal": false }, + { + "key": "cellOffsetLeft", + "className": "MuiDataGridPro-cellOffsetLeft", + "description": "", + "isGlobal": false + }, { "key": "cellSkeleton", "className": "MuiDataGridPro-cellSkeleton", diff --git a/docs/pages/x/api/data-grid/data-grid.json b/docs/pages/x/api/data-grid/data-grid.json index 35526160a411b..7d2e0cc1683fa 100644 --- a/docs/pages/x/api/data-grid/data-grid.json +++ b/docs/pages/x/api/data-grid/data-grid.json @@ -606,6 +606,12 @@ "description": "Styles applied to the empty cell element.", "isGlobal": false }, + { + "key": "cellOffsetLeft", + "className": "MuiDataGrid-cellOffsetLeft", + "description": "", + "isGlobal": false + }, { "key": "cellSkeleton", "className": "MuiDataGrid-cellSkeleton", diff --git a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json index 1cff481853a86..c03719bd0757a 100644 --- a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json +++ b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json @@ -763,6 +763,7 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the empty cell element" }, + "cellOffsetLeft": { "description": "" }, "cellSkeleton": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the skeleton cell element" diff --git a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json index 80d95b46ddb16..bf41dede0b1df 100644 --- a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json +++ b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json @@ -705,6 +705,7 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the empty cell element" }, + "cellOffsetLeft": { "description": "" }, "cellSkeleton": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the skeleton cell element" diff --git a/docs/translations/api-docs/data-grid/data-grid/data-grid.json b/docs/translations/api-docs/data-grid/data-grid/data-grid.json index e39db6e5df53a..3e905560a3a95 100644 --- a/docs/translations/api-docs/data-grid/data-grid/data-grid.json +++ b/docs/translations/api-docs/data-grid/data-grid/data-grid.json @@ -571,6 +571,7 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the empty cell element" }, + "cellOffsetLeft": { "description": "" }, "cellSkeleton": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the skeleton cell element" diff --git a/packages/x-data-grid-pro/src/tests/rows.DataGridPro.test.tsx b/packages/x-data-grid-pro/src/tests/rows.DataGridPro.test.tsx index a84c1f5378f08..1d686619688f0 100644 --- a/packages/x-data-grid-pro/src/tests/rows.DataGridPro.test.tsx +++ b/packages/x-data-grid-pro/src/tests/rows.DataGridPro.test.tsx @@ -4,6 +4,7 @@ import { spy } from 'sinon'; import { expect } from 'chai'; import { $, + $$, grid, getCell, getRow, @@ -450,7 +451,7 @@ describe(' - Rows', () => { virtualScroller.scrollTop = 10e6; // scroll to the bottom act(() => virtualScroller.dispatchEvent(new Event('scroll'))); - const lastCell = $('[role="row"]:last-child [role="gridcell"]:first-child')!; + const lastCell = $$('[role="row"]:last-child [role="gridcell"]')[0]; expect(lastCell).to.have.text('995'); expect(renderingZone.children.length).to.equal( Math.floor((height - 1) / rowHeight) + rowBuffer, @@ -486,11 +487,13 @@ describe(' - Rows', () => { />, ); const firstRow = getRow(0); - expect(firstRow.children).to.have.length(Math.floor(width / columnWidth) + columnBuffer); + expect($$(firstRow, '[role="gridcell"]')).to.have.length( + Math.floor(width / columnWidth) + columnBuffer, + ); const virtualScroller = document.querySelector('.MuiDataGrid-virtualScroller')!; virtualScroller.scrollLeft = 301; act(() => virtualScroller.dispatchEvent(new Event('scroll'))); - expect(firstRow.children).to.have.length( + expect($$(firstRow, '[role="gridcell"]')).to.have.length( columnBuffer + 1 + Math.floor(width / columnWidth) + columnBuffer, ); }); @@ -517,15 +520,14 @@ describe(' - Rows', () => { render( , ); - const virtualScroller = document.querySelector('.MuiDataGrid-virtualScroller')!; - const renderingZone = document.querySelector('.MuiDataGrid-virtualScrollerRenderZone')!; - let firstRow = renderingZone.querySelector('[role="row"]:first-child')!; - let firstColumn = firstRow.firstChild!; + const virtualScroller = grid('virtualScroller')!; + const renderingZone = grid('virtualScrollerRenderZone')!; + const firstRow = $(renderingZone, '[role="row"]:first-child')!; + let firstColumn = $$(firstRow, '[role="gridcell"]')[0]; expect(firstColumn).to.have.attr('data-colindex', '0'); virtualScroller.scrollLeft = columnThreshold * columnWidth; act(() => virtualScroller.dispatchEvent(new Event('scroll'))); - firstRow = renderingZone.querySelector('[role="row"]:first-child')!; - firstColumn = firstRow.firstChild!; + firstColumn = $(renderingZone, '[role="row"] > [role="gridcell"]')!; expect(firstColumn).to.have.attr('data-colindex', '3'); }); @@ -546,16 +548,14 @@ describe(' - Rows', () => { act(() => virtualScroller.dispatchEvent(new Event('scroll'))); const dimensions = apiRef.current.state.dimensions; - const lastCell = document.querySelector( - '[role="row"]:last-child [role="gridcell"]:first-child', - )!; + const lastCell = $$('[role="row"]:last-child [role="gridcell"]')[0]; expect(lastCell).to.have.text('31'); expect(virtualScroller.scrollHeight).to.equal( dimensions.headerHeight + nbRows * rowHeight + dimensions.scrollbarSize, ); }); - it('should not virtualized the last page if smaller than viewport', () => { + it('should not virtualize the last page if smaller than viewport', () => { render( - Rows', () => { height={500} />, ); - const virtualScroller = document.querySelector('.MuiDataGrid-virtualScroller')!; + const virtualScroller = grid('virtualScroller')!; virtualScroller.scrollTop = 10e6; // scroll to the bottom virtualScroller.dispatchEvent(new Event('scroll')); - const lastCell = document.querySelector( - '[role="row"]:last-child [role="gridcell"]:first-child', - )!; + const lastCell = $$('[role="row"]:last-child [role="gridcell"]')[0]; expect(lastCell).to.have.text('99'); expect(virtualScroller.scrollTop).to.equal(0); expect(virtualScroller.scrollHeight).to.equal(virtualScroller.clientHeight); - expect( - document.querySelector('.MuiDataGrid-virtualScrollerRenderZone')!.children, - ).to.have.length(4); + expect(grid('virtualScrollerRenderZone')!.children).to.have.length(4); }); it('should paginate small dataset in auto page-size #1492', () => { @@ -585,18 +581,14 @@ describe(' - Rows', () => { ); const virtualScroller = document.querySelector('.MuiDataGrid-virtualScroller')!; - const lastCell = document.querySelector( - '[role="row"]:last-child [role="gridcell"]:first-child', - )!; + const lastCell = $$('[role="row"]:last-child [role="gridcell"]')[0]; expect(lastCell).to.have.text('6'); const rows = document.querySelectorAll('.MuiDataGrid-row[role="row"]')!; expect(rows.length).to.equal(7); expect(virtualScroller.scrollTop).to.equal(0); expect(virtualScroller.scrollHeight).to.equal(virtualScroller.clientHeight); - expect( - document.querySelector('.MuiDataGrid-virtualScrollerRenderZone')!.children, - ).to.have.length(7); + expect(grid('virtualScrollerRenderZone')!.children).to.have.length(7); }); }); diff --git a/packages/x-data-grid/src/components/GridRow.tsx b/packages/x-data-grid/src/components/GridRow.tsx index 457218861b733..3e5ab5adc2ae1 100644 --- a/packages/x-data-grid/src/components/GridRow.tsx +++ b/packages/x-data-grid/src/components/GridRow.tsx @@ -91,8 +91,6 @@ const useUtilityClasses = (ownerState: OwnerState) => { isLastVisible && 'row--lastVisible', rowHeight === 'auto' && 'row--dynamicHeight', ], - pinnedLeft: ['pinnedLeft'], - pinnedRight: ['pinnedRight'], }; return composeClasses(slots, getDataGridUtilityClass, classes); @@ -517,6 +515,7 @@ const GridRow = React.forwardRef(function GridRow( {...other} > {leftCells} +
{cells} {emptyCellWidth > 0 && } {rightCells.length > 0 &&
} diff --git a/packages/x-data-grid/src/components/GridScrollbarFillerCell.tsx b/packages/x-data-grid/src/components/GridScrollbarFillerCell.tsx index 8be0cf5eb44b5..188a9d78c13bc 100644 --- a/packages/x-data-grid/src/components/GridScrollbarFillerCell.tsx +++ b/packages/x-data-grid/src/components/GridScrollbarFillerCell.tsx @@ -23,9 +23,6 @@ const Style = styled('div')({ position: 'sticky', right: 0, }, - [`&:not(.${classes.header}):not(.${classes.pinnedRight})`]: { - transform: 'translate3d(var(--DataGrid-offsetLeft), 0, 0)', - }, }); function GridScrollbarFillerCell({ diff --git a/packages/x-data-grid/src/components/containers/GridRootStyles.ts b/packages/x-data-grid/src/components/containers/GridRootStyles.ts index efa21da4ebae6..b5f81dbc7b7c2 100644 --- a/packages/x-data-grid/src/components/containers/GridRootStyles.ts +++ b/packages/x-data-grid/src/components/containers/GridRootStyles.ts @@ -572,8 +572,10 @@ export const GridRootStyles = styled('div', { }, }, }, - [`& .${c.cell}:not(.${c['cell--pinnedLeft']}):not(.${c['cell--pinnedRight']})`]: { - transform: 'translate3d(var(--DataGrid-offsetLeft), 0, 0)', + [`& .${c.cellOffsetLeft}`]: { + flex: '0 0 auto', + display: 'inline-block', + width: 'var(--DataGrid-offsetLeft)', }, [`& .${c.columnHeaderDraggableContainer}`]: { display: 'flex', diff --git a/packages/x-data-grid/src/constants/gridClasses.ts b/packages/x-data-grid/src/constants/gridClasses.ts index a54196c2cf97b..f83194a44d634 100644 --- a/packages/x-data-grid/src/constants/gridClasses.ts +++ b/packages/x-data-grid/src/constants/gridClasses.ts @@ -112,6 +112,11 @@ export interface GridClasses { * Styles applied to the skeleton cell element. */ cellSkeleton: string; + /** + * @ignore - do not document. + * Styles applied to the left offset cell element. + */ + cellOffsetLeft: string; /** * Styles applied to the selection checkbox element. */ @@ -634,6 +639,7 @@ export const gridClasses = generateUtilityClasses('MuiDataGrid', [ 'cellCheckbox', 'cellEmpty', 'cellSkeleton', + 'cellOffsetLeft', 'checkboxInput', 'columnHeader--alignCenter', 'columnHeader--alignLeft', diff --git a/packages/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx b/packages/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx index f84849ac0f60a..7718470f4155b 100644 --- a/packages/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx +++ b/packages/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx @@ -65,7 +65,6 @@ export const useGridVirtualScroller = () => { const gridRootRef = apiRef.current.rootElementRef; const mainRef = apiRef.current.mainElementRef; const scrollerRef = apiRef.current.virtualScrollerRef; - const renderZoneRef = React.useRef(null); const scrollbarVerticalRef = React.useRef(null); const scrollbarHorizontalRef = React.useRef(null); const contentHeight = dimensions.contentSize.height; @@ -343,6 +342,16 @@ export const useGridVirtualScroller = () => { for (let i = 0; i < renderedRows.length; i += 1) { const { id, model } = renderedRows[i]; + + const rowIndexInPage = (currentPage?.range?.firstRowIndex || 0) + firstRowToRender + i; + let index = rowIndexOffset + rowIndexInPage; + if (isRowWithFocusedCellNotInRange && cellFocus?.id === id) { + index = indexOfRowWithFocusedCell; + isRowWithFocusedCellRendered = true; + } else if (isRowWithFocusedCellRendered) { + index -= 1; + } + const isRowNotVisible = isRowWithFocusedCellNotInRange && cellFocus!.id === id; const baseRowHeight = !apiRef.current.rowHasAutoHeight(id) @@ -358,7 +367,7 @@ export const useGridVirtualScroller = () => { let isFirstVisible = false; if (params.position === undefined) { - isFirstVisible = i === 0; + isFirstVisible = rowIndexInPage === 0; } let isLastVisible = false; @@ -394,14 +403,6 @@ export const useGridVirtualScroller = () => { tabbableCell = cellParams.cellMode === 'view' ? cellTabIndex.field : null; } - let index = rowIndexOffset + (currentPage?.range?.firstRowIndex || 0) + firstRowToRender + i; - if (isRowWithFocusedCellNotInRange && cellFocus?.id === id) { - index = indexOfRowWithFocusedCell; - isRowWithFocusedCellRendered = true; - } else if (isRowWithFocusedCellRendered) { - index -= 1; - } - rows.push( { style: contentSize, role: 'presentation', }), - getRenderZoneProps: () => ({ ref: renderZoneRef, role: 'rowgroup' }), + getRenderZoneProps: () => ({ role: 'rowgroup' }), getScrollbarVerticalProps: () => ({ ref: scrollbarVerticalRef, role: 'presentation' }), getScrollbarHorizontalProps: () => ({ ref: scrollbarHorizontalRef, role: 'presentation' }), };