From 0bb5ec12c7ac1402ac8a0bb1894428533928b640 Mon Sep 17 00:00:00 2001 From: Rom Grk Date: Wed, 28 Feb 2024 17:23:44 -0500 Subject: [PATCH] [DataGrid] Refactor: row virtualization & rendering (#12247) --- .../src/components/GridPinnedRows.tsx | 15 +- .../x-data-grid/src/components/GridRow.tsx | 117 +++---- .../src/components/cell/GridCell.tsx | 3 +- .../components/containers/GridRootStyles.ts | 2 +- .../virtualization/GridBottomContainer.tsx | 2 +- .../virtualization/GridTopContainer.tsx | 2 +- .../GridVirtualScrollerRenderZone.tsx | 11 +- .../columnHeaders/useGridColumnHeaders.tsx | 22 +- .../features/columns/gridColumnsSelector.ts | 10 + .../hooks/features/columns/useGridColumns.tsx | 2 + .../hooks/features/rows/useGridRowsMeta.ts | 16 +- .../gridVirtualizationSelectors.ts | 10 - .../virtualization/useGridVirtualScroller.tsx | 290 ++++++------------ .../virtualization/useGridVirtualization.tsx | 7 - packages/x-data-grid/src/utils/utils.ts | 7 + scripts/x-data-grid-premium.exports.json | 3 +- scripts/x-data-grid-pro.exports.json | 3 +- scripts/x-data-grid.exports.json | 3 +- 18 files changed, 230 insertions(+), 295 deletions(-) diff --git a/packages/x-data-grid-pro/src/components/GridPinnedRows.tsx b/packages/x-data-grid-pro/src/components/GridPinnedRows.tsx index 0aaefee5ea85..46ea0b06e976 100644 --- a/packages/x-data-grid-pro/src/components/GridPinnedRows.tsx +++ b/packages/x-data-grid-pro/src/components/GridPinnedRows.tsx @@ -5,6 +5,7 @@ import { getDataGridUtilityClass, gridClasses, useGridSelector } from '@mui/x-da import { GridPinnedRowsProps, gridPinnedRowsSelector, + gridRenderContextSelector, useGridPrivateApiContext, } from '@mui/x-data-grid/internals'; @@ -19,10 +20,22 @@ export function GridPinnedRows({ position, virtualScroller, ...other }: GridPinn const classes = useUtilityClasses(); const apiRef = useGridPrivateApiContext(); + const renderContext = useGridSelector(apiRef, gridRenderContextSelector); const pinnedRowsData = useGridSelector(apiRef, gridPinnedRowsSelector); + const rows = pinnedRowsData[position]; + const pinnedRows = virtualScroller.getRows({ position, - rows: pinnedRowsData[position], + rows, + renderContext: React.useMemo( + () => ({ + firstRowIndex: 0, + lastRowIndex: rows.length, + firstColumnIndex: renderContext.firstColumnIndex, + lastColumnIndex: renderContext.lastColumnIndex, + }), + [rows, renderContext.firstColumnIndex, renderContext.lastColumnIndex], + ), }); return ( diff --git a/packages/x-data-grid/src/components/GridRow.tsx b/packages/x-data-grid/src/components/GridRow.tsx index be92dd0d0d4a..7253f0aa4070 100644 --- a/packages/x-data-grid/src/components/GridRow.tsx +++ b/packages/x-data-grid/src/components/GridRow.tsx @@ -15,6 +15,7 @@ import { useGridRootProps } from '../hooks/utils/useGridRootProps'; import type { DataGridProcessedProps } from '../models/props/DataGridProps'; import type { GridPinnedColumns } from '../hooks/features/columns'; import type { GridStateColDef } from '../models/colDef/gridColDef'; +import type { GridRenderContext } from '../models/params/gridScrollParams'; import { gridColumnPositionsSelector } from '../hooks/features/columns/gridColumnsSelector'; import { useGridSelector, objectShallowCompare } from '../hooks/utils/useGridSelector'; import { GridRowClassNameParams } from '../models/params/gridRowParams'; @@ -23,7 +24,6 @@ import { findParentElementFromClassName, isEventTargetInPortal } from '../utils/ import { GRID_CHECKBOX_SELECTION_COL_DEF } from '../colDef/gridCheckboxSelectionColDef'; import { GRID_ACTIONS_COLUMN_TYPE } from '../colDef/gridActionsColDef'; import { GRID_DETAIL_PANEL_TOGGLE_FIELD } from '../constants/gridDetailPanelToggleField'; -import type { GridVirtualizationState } from '../hooks/features/virtualization'; import type { GridDimensions } from '../hooks/features/dimensions'; import { gridSortModelSelector } from '../hooks/features/sorting/gridSortingSelector'; import { gridRowMaximumTreeDepthSelector } from '../hooks/features/rows/gridRowsSelector'; @@ -33,6 +33,7 @@ import { PinnedPosition } from './cell/GridCell'; import { GridScrollbarFillerCell as ScrollbarFiller } from './GridScrollbarFillerCell'; export interface GridRowProps extends React.HTMLAttributes { + row: GridRowModel; rowId: GridRowId; selected: boolean; /** @@ -41,28 +42,25 @@ export interface GridRowProps extends React.HTMLAttributes { */ index: number; rowHeight: number | 'auto'; - offsets: GridVirtualizationState['offsets']; + offsetTop: number | undefined; + offsetLeft: number; dimensions: GridDimensions; - firstColumnToRender: number; - lastColumnToRender: number; + renderContext: GridRenderContext; visibleColumns: GridStateColDef[]; - renderedColumns: GridStateColDef[]; pinnedColumns: GridPinnedColumns; /** * Determines which cell has focus. * If `null`, no cell in this row has focus. */ - focusedCell: string | null; + focusedColumnIndex: number | undefined; /** * Determines which cell should be tabbable by having tabIndex=0. * If `null`, no cell in this row is in the tab sequence. */ tabbableCell: string | null; - row?: GridRowModel; isFirstVisible: boolean; isLastVisible: boolean; - focusedCellColumnIndexNotInRange?: number; - isNotVisible?: boolean; + isNotVisible: boolean; onClick?: React.MouseEventHandler; onDoubleClick?: React.MouseEventHandler; onMouseEnter?: React.MouseEventHandler; @@ -121,15 +119,14 @@ const GridRow = React.forwardRef(function GridRow( rowHeight, className, visibleColumns, - renderedColumns, pinnedColumns, - offsets, + offsetTop, + offsetLeft, dimensions, - firstColumnToRender, - lastColumnToRender, + renderContext, + focusedColumnIndex, isFirstVisible, isLastVisible, - focusedCellColumnIndexNotInRange, isNotVisible, focusedCell, tabbableCell, @@ -154,6 +151,16 @@ const GridRow = React.forwardRef(function GridRow( const rowNode = apiRef.current.getRowNode(rowId); const scrollbarWidth = dimensions.hasScrollY ? dimensions.scrollbarSize : 0; + const hasFocusCell = focusedColumnIndex !== undefined; + const hasVirtualFocusCellLeft = + hasFocusCell && + focusedColumnIndex >= pinnedColumns.left.length && + focusedColumnIndex < renderContext.firstColumnIndex; + const hasVirtualFocusCellRight = + hasFocusCell && + focusedColumnIndex < visibleColumns.length - pinnedColumns.right.length && + focusedColumnIndex >= renderContext.lastColumnIndex; + const ariaRowIndex = index + headerGroupingMaxDepth + 2; // 1 for the header row and 1 as it's 1-based const ownerState = { @@ -354,10 +361,13 @@ const GridRow = React.forwardRef(function GridRow( indexRelativeToAllColumns, ); - if (!cellColSpanInfo || cellColSpanInfo.spannedByColSpan) { + if (cellColSpanInfo?.spannedByColSpan) { return null; } + const width = cellColSpanInfo?.cellProps.width ?? column.computedWidth; + const colSpan = cellColSpanInfo?.cellProps.colSpan ?? 1; + let pinnedOffset: number; // FIXME: Why is the switch check exhaustiveness not validated with typescript-eslint? // eslint-disable-next-line default-case @@ -373,13 +383,12 @@ const GridRow = React.forwardRef(function GridRow( scrollbarWidth; break; case PinnedPosition.NONE: + case PinnedPosition.VIRTUAL: pinnedOffset = 0; break; } if (rowNode?.type === 'skeletonRow') { - const { width } = cellColSpanInfo.cellProps; - return ( (function GridRow( ); } - const { colSpan, width } = cellColSpanInfo.cellProps; - const editCellState = editRowsState[rowId]?.[column.field] ?? null; // when the cell is a reorder cell we are not allowing to reorder the col @@ -405,13 +412,7 @@ const GridRow = React.forwardRef(function GridRow( const disableDragEvents = !(canReorderColumn || (isReorderCell && canReorderRow)); - let cellIsNotVisible = false; - if ( - focusedCellColumnIndexNotInRange !== undefined && - visibleColumns[focusedCellColumnIndexNotInRange].field === column.field - ) { - cellIsNotVisible = true; - } + const cellIsNotVisible = pinnedPosition === PinnedPosition.VIRTUAL; return ( (function GridRow( visibleColumns.length - pinnedColumns.left.length - pinnedColumns.right.length; const cells = [] as React.ReactNode[]; - for (let i = 0; i < renderedColumns.length; i += 1) { - const column = renderedColumns[i]; - - let indexRelativeToAllColumns = firstColumnToRender + i; - if (focusedCellColumnIndexNotInRange !== undefined && focusedCell) { - if (visibleColumns[focusedCellColumnIndexNotInRange].field === column.field) { - indexRelativeToAllColumns = focusedCellColumnIndexNotInRange; - } else { - indexRelativeToAllColumns -= 1; - } - } - - const indexInSection = indexRelativeToAllColumns - pinnedColumns.left.length; + if (hasVirtualFocusCellLeft) { + cells.push( + getCell( + visibleColumns[focusedColumnIndex], + focusedColumnIndex - pinnedColumns.left.length, + focusedColumnIndex, + middleColumnsLength, + PinnedPosition.VIRTUAL, + ), + ); + } + for (let i = renderContext.firstColumnIndex; i < renderContext.lastColumnIndex; i += 1) { + const column = visibleColumns[i]; + const indexInSection = i - pinnedColumns.left.length; - cells.push(getCell(column, indexInSection, indexRelativeToAllColumns, middleColumnsLength)); + cells.push(getCell(column, indexInSection, i, middleColumnsLength)); + } + if (hasVirtualFocusCellRight) { + cells.push( + getCell( + visibleColumns[focusedColumnIndex], + focusedColumnIndex - pinnedColumns.left.length, + focusedColumnIndex, + middleColumnsLength, + PinnedPosition.VIRTUAL, + ), + ); } const eventHandlers = row @@ -517,7 +530,7 @@ const GridRow = React.forwardRef(function GridRow(
{cells} {emptyCellWidth > 0 && } @@ -568,13 +581,11 @@ GridRow.propTypes = { width: PropTypes.number.isRequired, }).isRequired, }).isRequired, - firstColumnToRender: PropTypes.number.isRequired, /** * Determines which cell has focus. * If `null`, no cell in this row has focus. */ - focusedCell: PropTypes.string, - focusedCellColumnIndexNotInRange: PropTypes.number, + focusedColumnIndex: PropTypes.number, /** * Index of the row in the whole sorted and filtered dataset. * If some rows above have expanded children, this index also take those children into account. @@ -582,19 +593,21 @@ GridRow.propTypes = { index: PropTypes.number.isRequired, isFirstVisible: PropTypes.bool.isRequired, isLastVisible: PropTypes.bool.isRequired, - isNotVisible: PropTypes.bool, - lastColumnToRender: PropTypes.number.isRequired, - offsets: PropTypes.shape({ - left: PropTypes.number.isRequired, - top: PropTypes.number.isRequired, - }).isRequired, + isNotVisible: PropTypes.bool.isRequired, + offsetLeft: PropTypes.number.isRequired, + offsetTop: PropTypes.number, onClick: PropTypes.func, onDoubleClick: PropTypes.func, onMouseEnter: PropTypes.func, onMouseLeave: PropTypes.func, pinnedColumns: PropTypes.object.isRequired, - renderedColumns: PropTypes.arrayOf(PropTypes.object).isRequired, - row: PropTypes.object, + renderContext: PropTypes.shape({ + firstColumnIndex: PropTypes.number.isRequired, + firstRowIndex: PropTypes.number.isRequired, + lastColumnIndex: PropTypes.number.isRequired, + lastRowIndex: PropTypes.number.isRequired, + }).isRequired, + row: PropTypes.object.isRequired, rowHeight: PropTypes.oneOfType([PropTypes.oneOf(['auto']), PropTypes.number]).isRequired, rowId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, selected: PropTypes.bool.isRequired, diff --git a/packages/x-data-grid/src/components/cell/GridCell.tsx b/packages/x-data-grid/src/components/cell/GridCell.tsx index 8487b686ab59..0e2531316a15 100644 --- a/packages/x-data-grid/src/components/cell/GridCell.tsx +++ b/packages/x-data-grid/src/components/cell/GridCell.tsx @@ -37,6 +37,7 @@ export enum PinnedPosition { NONE, LEFT, RIGHT, + VIRTUAL, } export type GridCellProps = { @@ -494,7 +495,7 @@ GridCell.propTypes = { onMouseDown: PropTypes.func, onMouseUp: PropTypes.func, pinnedOffset: PropTypes.number.isRequired, - pinnedPosition: PropTypes.oneOf([0, 1, 2]).isRequired, + pinnedPosition: PropTypes.oneOf([0, 1, 2, 3]).isRequired, rowId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, sectionIndex: PropTypes.number.isRequired, sectionLength: PropTypes.number.isRequired, diff --git a/packages/x-data-grid/src/components/containers/GridRootStyles.ts b/packages/x-data-grid/src/components/containers/GridRootStyles.ts index f423f2e97b71..fa76ca44813a 100644 --- a/packages/x-data-grid/src/components/containers/GridRootStyles.ts +++ b/packages/x-data-grid/src/components/containers/GridRootStyles.ts @@ -349,7 +349,7 @@ export const GridRootStyles = styled('div', { [`& .${c.columnSeparator}`]: { visibility: 'hidden', position: 'absolute', - zIndex: 100, + zIndex: 3, display: 'flex', flexDirection: 'column', justifyContent: 'center', diff --git a/packages/x-data-grid/src/components/virtualization/GridBottomContainer.tsx b/packages/x-data-grid/src/components/virtualization/GridBottomContainer.tsx index 47dbd64aa731..fa5b24d25ed0 100644 --- a/packages/x-data-grid/src/components/virtualization/GridBottomContainer.tsx +++ b/packages/x-data-grid/src/components/virtualization/GridBottomContainer.tsx @@ -13,7 +13,7 @@ const useUtilityClasses = () => { const Element = styled('div')({ position: 'sticky', - zIndex: 2, + zIndex: 4, bottom: 'calc(var(--DataGrid-hasScrollX) * var(--DataGrid-scrollbarSize))', }); diff --git a/packages/x-data-grid/src/components/virtualization/GridTopContainer.tsx b/packages/x-data-grid/src/components/virtualization/GridTopContainer.tsx index 8300de59e50a..03873c05f560 100644 --- a/packages/x-data-grid/src/components/virtualization/GridTopContainer.tsx +++ b/packages/x-data-grid/src/components/virtualization/GridTopContainer.tsx @@ -13,7 +13,7 @@ const useUtilityClasses = () => { const Element = styled('div')({ position: 'sticky', - zIndex: 2, + zIndex: 4, top: 0, '&::after': { content: '" "', diff --git a/packages/x-data-grid/src/components/virtualization/GridVirtualScrollerRenderZone.tsx b/packages/x-data-grid/src/components/virtualization/GridVirtualScrollerRenderZone.tsx index f15d7f5691db..240238494e0b 100644 --- a/packages/x-data-grid/src/components/virtualization/GridVirtualScrollerRenderZone.tsx +++ b/packages/x-data-grid/src/components/virtualization/GridVirtualScrollerRenderZone.tsx @@ -4,7 +4,8 @@ import { styled, SxProps, Theme } from '@mui/system'; import { unstable_composeClasses as composeClasses } from '@mui/utils'; import { useGridApiContext } from '../../hooks/utils/useGridApiContext'; import { useGridSelector } from '../../hooks/utils/useGridSelector'; -import { gridOffsetsSelector } from '../../hooks/features/virtualization'; +import { gridRowsMetaSelector } from '../../hooks/features/rows'; +import { gridRenderContextSelector } from '../../hooks/features/virtualization'; import { useGridRootProps } from '../../hooks/utils/useGridRootProps'; import { getDataGridUtilityClass } from '../../constants/gridClasses'; import { DataGridProcessedProps } from '../../models/props/DataGridProps'; @@ -39,7 +40,11 @@ const GridVirtualScrollerRenderZone = React.forwardRef< const apiRef = useGridApiContext(); const rootProps = useGridRootProps(); const classes = useUtilityClasses(rootProps); - const offsets = useGridSelector(apiRef, gridOffsetsSelector); + const offsetTop = useGridSelector(apiRef, () => { + const renderContext = gridRenderContextSelector(apiRef); + const rowsMeta = gridRowsMetaSelector(apiRef.current.state); + return rowsMeta.positions[renderContext.firstRowIndex] ?? 0; + }); return ( diff --git a/packages/x-data-grid/src/hooks/features/columnHeaders/useGridColumnHeaders.tsx b/packages/x-data-grid/src/hooks/features/columnHeaders/useGridColumnHeaders.tsx index 45240a94ae71..9b0275319ffe 100644 --- a/packages/x-data-grid/src/hooks/features/columnHeaders/useGridColumnHeaders.tsx +++ b/packages/x-data-grid/src/hooks/features/columnHeaders/useGridColumnHeaders.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { unstable_useForkRef as useForkRef } from '@mui/utils'; -import { styled } from '@mui/material/styles'; +import { styled, useTheme } from '@mui/material/styles'; import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; import { useGridSelector } from '../../utils'; import { useGridRootProps } from '../../utils/useGridRootProps'; @@ -11,10 +11,10 @@ import { GridEventListener } from '../../../models/events'; import { GridColumnHeaderItem } from '../../../components/columnHeaders/GridColumnHeaderItem'; import { gridDimensionsSelector } from '../dimensions'; import { - gridOffsetsSelector, gridRenderContextColumnsSelector, gridVirtualizationColumnEnabledSelector, } from '../virtualization'; +import { computeOffsetLeft } from '../virtualization/useGridVirtualScroller'; import { GridColumnGroupHeader } from '../../../components/columnHeaders/GridColumnGroupHeader'; import { GridColumnGroup } from '../../../models/gridColumnGrouping'; import { GridStateColDef } from '../../../models/colDef/gridColDef'; @@ -25,6 +25,7 @@ import { GridColumnMenuState } from '../columnMenu'; import { GridPinnedColumnPosition, GridColumnVisibilityModel, + gridColumnPositionsSelector, gridVisiblePinnedColumnDefinitionsSelector, } from '../columns'; import { GridGroupingStructure } from '../columnGrouping/gridColumnGroupsInterfaces'; @@ -104,15 +105,22 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => { const [resizeCol, setResizeCol] = React.useState(''); const apiRef = useGridPrivateApiContext(); + const theme = useTheme(); const rootProps = useGridRootProps(); const hasVirtualization = useGridSelector(apiRef, gridVirtualizationColumnEnabledSelector); const innerRef = React.useRef(null); const handleInnerRef = useForkRef(innerRefProp, innerRef); const dimensions = useGridSelector(apiRef, gridDimensionsSelector); - const offsets = useGridSelector(apiRef, gridOffsetsSelector); + const columnPositions = useGridSelector(apiRef, gridColumnPositionsSelector); const renderContext = useGridSelector(apiRef, gridRenderContextColumnsSelector); - const visiblePinnedColumns = useGridSelector(apiRef, gridVisiblePinnedColumnDefinitionsSelector); + const pinnedColumns = useGridSelector(apiRef, gridVisiblePinnedColumnDefinitionsSelector); + const offsetLeft = computeOffsetLeft( + columnPositions, + renderContext, + theme.direction, + pinnedColumns.left.length, + ); React.useEffect(() => { apiRef.current.columnHeadersContainerElementRef!.current!.scrollLeft = 0; @@ -170,10 +178,10 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => { const isNotPinned = params?.position === undefined; const hasScrollbarFiller = - (visiblePinnedColumns.right.length > 0 && isPinnedRight) || - (visiblePinnedColumns.right.length === 0 && isNotPinned); + (pinnedColumns.right.length > 0 && isPinnedRight) || + (pinnedColumns.right.length === 0 && isNotPinned); - const leftOffsetWidth = offsets.left - leftOverflow; + const leftOffsetWidth = offsetLeft - leftOverflow; return ( diff --git a/packages/x-data-grid/src/hooks/features/columns/gridColumnsSelector.ts b/packages/x-data-grid/src/hooks/features/columns/gridColumnsSelector.ts index cb957b2120f5..c06920307eb8 100644 --- a/packages/x-data-grid/src/hooks/features/columns/gridColumnsSelector.ts +++ b/packages/x-data-grid/src/hooks/features/columns/gridColumnsSelector.ts @@ -192,3 +192,13 @@ export const gridFilterableColumnLookupSelector = createSelectorMemoized( return acc; }, {}), ); + +/** + * Checks if some column has a colSpan field. + * @category Columns + * @ignore - Do not document + */ +export const gridHasColSpanSelector = createSelectorMemoized( + gridColumnDefinitionsSelector, + (columns) => columns.some((column) => column.colSpan !== undefined), +); diff --git a/packages/x-data-grid/src/hooks/features/columns/useGridColumns.tsx b/packages/x-data-grid/src/hooks/features/columns/useGridColumns.tsx index 6da78afff714..8b8702126ed9 100644 --- a/packages/x-data-grid/src/hooks/features/columns/useGridColumns.tsx +++ b/packages/x-data-grid/src/hooks/features/columns/useGridColumns.tsx @@ -96,6 +96,7 @@ export function useGridColumns( apiRef.current.setState(mergeColumnsState(columnsState)); apiRef.current.publishEvent('columnsChange', columnsState.orderedFields); + apiRef.current.updateRenderContext?.(); apiRef.current.forceUpdate(); }, [logger, apiRef], @@ -152,6 +153,7 @@ export function useGridColumns( keepOnlyColumnsToUpsert: false, }), })); + apiRef.current.updateRenderContext?.(); apiRef.current.forceUpdate(); } }, diff --git a/packages/x-data-grid/src/hooks/features/rows/useGridRowsMeta.ts b/packages/x-data-grid/src/hooks/features/rows/useGridRowsMeta.ts index 6833a22afc3d..e4d57828bcb4 100644 --- a/packages/x-data-grid/src/hooks/features/rows/useGridRowsMeta.ts +++ b/packages/x-data-grid/src/hooks/features/rows/useGridRowsMeta.ts @@ -147,14 +147,7 @@ export const useGridRowsMeta = ( rowsHeightLookup.current[row.id].needsFirstMeasurement = false; } - const initialHeights = {} as Record; - /* eslint-disable-next-line no-restricted-syntax */ - for (const key in sizes) { - if (/^base[A-Z]/.test(key)) { - initialHeights[key] = sizes[key]; - } - } - initialHeights.baseCenter = baseRowHeight; + const initialHeights = { baseCenter: baseRowHeight } as Record; if (getRowSpacing) { const indexRelativeToCurrentPage = apiRef.current.getRowIndexRelativeToVisibleRows(row.id); @@ -185,21 +178,18 @@ export const useGridRowsMeta = ( const currentPageTotalHeight = currentPage.rows.reduce((acc, row) => { positions.push(acc); - let maximumBaseSize = 0; let otherSizes = 0; const processedSizes = calculateRowProcessedSizes(row); /* eslint-disable-next-line no-restricted-syntax, guard-for-in */ for (const key in processedSizes) { const value = processedSizes[key]; - if (/^base[A-Z]/.test(key)) { - maximumBaseSize = value > maximumBaseSize ? value : maximumBaseSize; - } else { + if (key !== 'baseCenter') { otherSizes += value; } } - return acc + maximumBaseSize + otherSizes; + return acc + processedSizes.baseCenter + otherSizes; }, 0); pinnedRows?.top?.forEach((row) => { diff --git a/packages/x-data-grid/src/hooks/features/virtualization/gridVirtualizationSelectors.ts b/packages/x-data-grid/src/hooks/features/virtualization/gridVirtualizationSelectors.ts index 186b5e179335..2b70baf64ac9 100644 --- a/packages/x-data-grid/src/hooks/features/virtualization/gridVirtualizationSelectors.ts +++ b/packages/x-data-grid/src/hooks/features/virtualization/gridVirtualizationSelectors.ts @@ -36,16 +36,6 @@ export const gridRenderContextSelector = createSelector( (state) => state.renderContext, ); -/** - * Get the offsets - * @category Virtualization - * @ignore - do not document. - */ -export const gridOffsetsSelector = createSelector( - gridVirtualizationSelector, - (state) => state.offsets, -); - /** * Get the render context, with only columns filled in. * This is cached, so it can be used to only re-render when the column interval changes. 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 7ec6e4263240..1d533ee81bd0 100644 --- a/packages/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx +++ b/packages/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx @@ -5,34 +5,32 @@ import { unstable_useEventCallback as useEventCallback, } from '@mui/utils'; import { useTheme, Theme } from '@mui/material/styles'; -import { defaultMemoize } from 'reselect'; import { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; import { useGridPrivateApiContext } from '../../utils/useGridPrivateApiContext'; import { useGridRootProps } from '../../utils/useGridRootProps'; import { useGridSelector } from '../../utils/useGridSelector'; -import { useLazyRef } from '../../utils/useLazyRef'; import { useResizeObserver } from '../../utils/useResizeObserver'; import { useRunOnce } from '../../utils/useRunOnce'; import { gridVisibleColumnDefinitionsSelector, gridVisiblePinnedColumnDefinitionsSelector, gridColumnPositionsSelector, + gridHasColSpanSelector, } from '../columns/gridColumnsSelector'; import { gridDimensionsSelector } from '../dimensions/gridDimensionsSelectors'; import { gridPinnedRowsSelector } from '../rows/gridRowsSelector'; import { GridPinnedRowsPosition } from '../rows/gridRowsInterfaces'; import { gridFocusCellSelector, gridTabIndexCellSelector } from '../focus/gridFocusStateSelector'; import { useGridVisibleRows, getVisibleRows } from '../../utils/useGridVisibleRows'; -import { clamp } from '../../../utils/utils'; +import { useGridApiEventHandler } from '../../utils'; +import { clamp, range } from '../../../utils/utils'; import { GridRenderContext, GridRowEntry, GridRowId } from '../../../models'; import { selectedIdsLookupSelector } from '../rowSelection/gridRowSelectionSelector'; import { gridRowsMetaSelector } from '../rows/gridRowsMetaSelector'; -import { GridStateColDef } from '../../../models/colDef/gridColDef'; import { getFirstNonSpannedColumnToRender } from '../columns/gridColumnsUtils'; import { getMinimalContentHeight } from '../rows/gridRowsUtils'; import { GridRowProps } from '../../../components/GridRow'; import { - gridOffsetsSelector, gridRenderContextSelector, gridVirtualizationEnabledSelector, gridVirtualizationColumnEnabledSelector, @@ -69,31 +67,27 @@ export const useGridVirtualScroller = () => { const scrollbarHorizontalRef = React.useRef(null); const contentHeight = dimensions.contentSize.height; const columnsTotalWidth = dimensions.columnsTotalWidth; + const hasColSpan = useGridSelector(apiRef, gridHasColSpanSelector); useResizeObserver(mainRef, () => apiRef.current.resize()); const previousContext = React.useRef(EMPTY_RENDER_CONTEXT); const previousRowContext = React.useRef(EMPTY_RENDER_CONTEXT); - const offsets = useGridSelector(apiRef, gridOffsetsSelector); const renderContext = useGridSelector(apiRef, gridRenderContextSelector); const scrollPosition = React.useRef({ top: 0, left: 0 }).current; const prevTotalWidth = React.useRef(columnsTotalWidth); - const getRenderedColumns = useLazyRef(createGetRenderedColumns).current; - - const indexOfRowWithFocusedCell = React.useMemo(() => { - if (cellFocus !== null) { - return currentPage.rows.findIndex((row) => row.id === cellFocus.id); - } - return -1; - }, [cellFocus, currentPage.rows]); - - const indexOfColumnWithFocusedCell = React.useMemo(() => { - if (cellFocus !== null) { - return visibleColumns.findIndex((column) => column.field === cellFocus.field); - } - return -1; - }, [cellFocus, visibleColumns]); + const focusedCell = { + rowIndex: React.useMemo( + () => (cellFocus ? currentPage.rows.findIndex((row) => row.id === cellFocus.id) : -1), + [cellFocus, currentPage.rows], + ), + columnIndex: React.useMemo( + () => + cellFocus ? visibleColumns.findIndex((column) => column.field === cellFocus.field) : -1, + [cellFocus, visibleColumns], + ), + }; const updateRenderContext = React.useCallback( (nextRenderContext: GridRenderContext, rawRenderContext: GridRenderContext) => { @@ -107,20 +101,12 @@ export const useGridVirtualScroller = () => { nextRenderContext.firstRowIndex !== previousRowContext.current.firstRowIndex || nextRenderContext.lastRowIndex !== previousRowContext.current.lastRowIndex; - const nextOffsets = computeOffsets( - apiRef, - nextRenderContext, - theme.direction, - pinnedColumns.left.length, - ); - apiRef.current.setState((state) => { return { ...state, virtualization: { ...state.virtualization, renderContext: nextRenderContext, - offsets: nextOffsets, }, }; }); @@ -136,13 +122,7 @@ export const useGridVirtualScroller = () => { previousContext.current = rawRenderContext; prevTotalWidth.current = dimensions.columnsTotalWidth; }, - [ - apiRef, - pinnedColumns.left.length, - theme.direction, - dimensions.isReady, - dimensions.columnsTotalWidth, - ], + [apiRef, dimensions.isReady, dimensions.columnsTotalWidth], ); const triggerUpdateRenderContext = () => { @@ -227,21 +207,26 @@ export const useGridVirtualScroller = () => { apiRef.current.publishEvent('virtualScrollerTouchMove', {}, event); }); - const minFirstColumn = pinnedColumns.left.length; - const maxLastColumn = visibleColumns.length - pinnedColumns.right.length; - const getRows = ( params: { rows?: GridRowEntry[]; position?: GridPinnedRowsPosition; + renderContext?: GridRenderContext; } = {}, ) => { + if (!params.rows && !currentPage.range) { + return []; + } + + const columnPositions = gridColumnPositionsSelector(apiRef); + const currentRenderContext = params.renderContext ?? renderContext; + const isLastSection = (!hasBottomPinnedRows && params.position === undefined) || (hasBottomPinnedRows && params.position === 'bottom'); const isPinnedSection = params.position !== undefined; - let rowIndexOffset; + let rowIndexOffset: number; // FIXME: Why is the switch check exhaustiveness not validated with typescript-eslint? // eslint-disable-next-line default-case switch (params.position) { @@ -256,99 +241,65 @@ export const useGridVirtualScroller = () => { break; } - const firstRowToRender = renderContext.firstRowIndex; - const lastRowToRender = renderContext.lastRowIndex; - const firstColumnToRender = renderContext.firstColumnIndex; - const lastColumnToRender = renderContext.lastColumnIndex; + const rowModels = params.rows ?? currentPage.rows; - if (!params.rows && !currentPage.range) { - return []; - } - - const renderedRows = params.rows ?? currentPage.rows.slice(firstRowToRender, lastRowToRender); + const firstRowToRender = currentRenderContext.firstRowIndex; + const lastRowToRender = Math.min(currentRenderContext.lastRowIndex, rowModels.length); - // If the selected row is not within the current range of rows being displayed, - // we need to render it at either the top or bottom of the rows, - // depending on whether it is above or below the range. - let isRowWithFocusedCellNotInRange = false; - if ( - !isPinnedSection && - indexOfRowWithFocusedCell > -1 && - (firstRowToRender > indexOfRowWithFocusedCell || lastRowToRender < indexOfRowWithFocusedCell) - ) { - isRowWithFocusedCellNotInRange = true; + const rowIndexes = params.rows + ? range(0, params.rows.length) + : range(firstRowToRender, lastRowToRender); - const rowWithFocusedCell = currentPage.rows[indexOfRowWithFocusedCell]; - - if (indexOfRowWithFocusedCell > firstRowToRender) { - renderedRows.push(rowWithFocusedCell); - } else { - renderedRows.unshift(rowWithFocusedCell); + let virtualRowIndex = -1; + if (!isPinnedSection && focusedCell.rowIndex !== -1) { + if (focusedCell.rowIndex < firstRowToRender) { + virtualRowIndex = focusedCell.rowIndex; + rowIndexes.unshift(virtualRowIndex); + } + if (focusedCell.rowIndex >= lastRowToRender) { + virtualRowIndex = focusedCell.rowIndex; + rowIndexes.push(virtualRowIndex); } } - let isColumnWihFocusedCellNotInRange = false; - if ( - !isPinnedSection && - (firstColumnToRender > indexOfColumnWithFocusedCell || - lastColumnToRender < indexOfColumnWithFocusedCell) - ) { - isColumnWihFocusedCellNotInRange = true; - } + const rows: React.ReactNode[] = []; + const rowProps = rootProps.slotProps?.row; - const { focusedCellColumnIndexNotInRange, renderedColumns } = getRenderedColumns( - visibleColumns, - firstColumnToRender, - lastColumnToRender, - minFirstColumn, - maxLastColumn, - isColumnWihFocusedCellNotInRange ? indexOfColumnWithFocusedCell : -1, - ); + rowIndexes.forEach((rowIndexInPage) => { + const { id, model } = rowModels[rowIndexInPage]; - renderedRows.forEach((row) => { - apiRef.current.calculateColSpan({ - rowId: row.id, - minFirstColumn, - maxLastColumn, - columns: visibleColumns, - }); + // NOTE: This is an expensive feature, the colSpan code could be optimized. + if (hasColSpan) { + const minFirstColumn = pinnedColumns.left.length; + const maxLastColumn = visibleColumns.length - pinnedColumns.right.length; - if (pinnedColumns.left.length > 0) { apiRef.current.calculateColSpan({ - rowId: row.id, - minFirstColumn: 0, - maxLastColumn: pinnedColumns.left.length, + rowId: id, + minFirstColumn, + maxLastColumn, columns: visibleColumns, }); - } - if (pinnedColumns.right.length > 0) { - apiRef.current.calculateColSpan({ - rowId: row.id, - minFirstColumn: visibleColumns.length - pinnedColumns.right.length, - maxLastColumn: visibleColumns.length, - columns: visibleColumns, - }); - } - }); + if (pinnedColumns.left.length > 0) { + apiRef.current.calculateColSpan({ + rowId: id, + minFirstColumn: 0, + maxLastColumn: pinnedColumns.left.length, + columns: visibleColumns, + }); + } - const rows: React.ReactNode[] = []; - const rowProps = rootProps.slotProps?.row; - let isRowWithFocusedCellRendered = false; - - 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; + if (pinnedColumns.right.length > 0) { + apiRef.current.calculateColSpan({ + rowId: id, + minFirstColumn: visibleColumns.length - pinnedColumns.right.length, + maxLastColumn: visibleColumns.length, + columns: visibleColumns, + }); + } } - const isRowNotVisible = isRowWithFocusedCellNotInRange && cellFocus!.id === id; + const hasFocus = cellFocus?.id === id; const baseRowHeight = !apiRef.current.rowHasAutoHeight(id) ? apiRef.current.unstable_getRowHeight(id) @@ -370,28 +321,18 @@ export const useGridVirtualScroller = () => { if (isLastSection) { if (!isPinnedSection) { const lastIndex = currentPage.rows.length - 1; - const isLastVisibleRowIndex = isRowWithFocusedCellNotInRange - ? firstRowToRender + i === lastIndex + 1 - : firstRowToRender + i === lastIndex; + const isLastVisibleRowIndex = rowIndexInPage === lastIndex; if (isLastVisibleRowIndex) { isLastVisible = true; } } else { - isLastVisible = i === renderedRows.length - 1; + isLastVisible = rowIndexInPage === rowModels.length - 1; } } - const focusedCell = cellFocus !== null && cellFocus.id === id ? cellFocus.field : null; - - const columnWithFocusedCellNotInRange = - focusedCellColumnIndexNotInRange !== undefined && - visibleColumns[focusedCellColumnIndexNotInRange]; - - const renderedColumnsWithFocusedCell = - columnWithFocusedCellNotInRange && focusedCell - ? [columnWithFocusedCellNotInRange, ...renderedColumns] - : renderedColumns; + const isVirtualRow = rowIndexInPage === virtualRowIndex; + const isNotVisible = isVirtualRow; let tabbableCell: GridRowProps['tabbableCell'] = null; if (cellTabIndex !== null && cellTabIndex.id === id) { @@ -399,27 +340,34 @@ export const useGridVirtualScroller = () => { tabbableCell = cellParams.cellMode === 'view' ? cellTabIndex.field : null; } + const offsetLeft = computeOffsetLeft( + columnPositions, + currentRenderContext, + theme.direction, + pinnedColumns.left.length, + ); + + const rowIndex = (currentPage?.range?.firstRowIndex || 0) + rowIndexOffset + rowIndexInPage; + rows.push( , ); @@ -428,7 +376,7 @@ export const useGridVirtualScroller = () => { if (panel) { rows.push(panel); } - } + }); return rows; }; @@ -507,6 +455,10 @@ export const useGridVirtualScroller = () => { updateRenderContext: forceUpdateRenderContext, }); + useGridApiEventHandler(apiRef, 'columnsChange', forceUpdateRenderContext); + useGridApiEventHandler(apiRef, 'filteredRowsSet', forceUpdateRenderContext); + useGridApiEventHandler(apiRef, 'rowExpansionChange', forceUpdateRenderContext); + return { renderContext, setPanels, @@ -533,48 +485,6 @@ export const useGridVirtualScroller = () => { }; }; -function createGetRenderedColumns() { - return defaultMemoize( - ( - columns: GridStateColDef[], - firstColumnToRender: number, - lastColumnToRender: number, - minFirstColumn: number, - maxLastColumn: number, - indexOfColumnWithFocusedCell: number, - ) => { - // If the selected column is not within the current range of columns being displayed, - // we need to render it at either the left or right of the columns, - // depending on whether it is above or below the range. - let focusedCellColumnIndexNotInRange; - - const renderedColumns = columns.slice(firstColumnToRender, lastColumnToRender); - - if (indexOfColumnWithFocusedCell > -1) { - // check if it is not on the left pinned column. - if ( - firstColumnToRender > indexOfColumnWithFocusedCell && - indexOfColumnWithFocusedCell >= minFirstColumn - ) { - focusedCellColumnIndexNotInRange = indexOfColumnWithFocusedCell; - } - // check if it is not on the right pinned column. - else if ( - lastColumnToRender < indexOfColumnWithFocusedCell && - indexOfColumnWithFocusedCell < maxLastColumn - ) { - focusedCellColumnIndexNotInRange = indexOfColumnWithFocusedCell; - } - } - - return { - focusedCellColumnIndexNotInRange, - renderedColumns, - }; - }, - ); -} - type ScrollPosition = { top: number; left: number }; type RenderContextInputs = { enabled: boolean; @@ -871,20 +781,16 @@ export function areRenderContextsEqual(context1: GridRenderContext, context2: Gr ); } -function computeOffsets( - apiRef: React.MutableRefObject, +export function computeOffsetLeft( + columnPositions: number[], renderContext: GridRenderContext, direction: Theme['direction'], pinnedLeftLength: number, ) { const factor = direction === 'ltr' ? 1 : -1; - const rowPositions = gridRowsMetaSelector(apiRef.current.state).positions; - const columnPositions = gridColumnPositionsSelector(apiRef); - - const top = rowPositions[renderContext.firstRowIndex] ?? 0; const left = factor * (columnPositions[renderContext.firstColumnIndex] ?? 0) - (columnPositions[pinnedLeftLength] ?? 0); - return { top, left }; + return left; } diff --git a/packages/x-data-grid/src/hooks/features/virtualization/useGridVirtualization.tsx b/packages/x-data-grid/src/hooks/features/virtualization/useGridVirtualization.tsx index f59725c74531..febc90553c82 100644 --- a/packages/x-data-grid/src/hooks/features/virtualization/useGridVirtualization.tsx +++ b/packages/x-data-grid/src/hooks/features/virtualization/useGridVirtualization.tsx @@ -11,12 +11,6 @@ export type GridVirtualizationState = { enabled: boolean; enabledForColumns: boolean; renderContext: GridRenderContext; - offsets: { top: number; left: number }; -}; - -export const EMPTY_OFFSETS = { - top: 0, - left: 0, }; export const EMPTY_RENDER_CONTEXT = { @@ -31,7 +25,6 @@ export const virtualizationStateInitializer: GridStateInitializer = ( enabled: !props.disableVirtualization, enabledForColumns: true, renderContext: EMPTY_RENDER_CONTEXT, - offsets: EMPTY_OFFSETS, }; return { diff --git a/packages/x-data-grid/src/utils/utils.ts b/packages/x-data-grid/src/utils/utils.ts index 4b0a08bab678..cc682162e59f 100644 --- a/packages/x-data-grid/src/utils/utils.ts +++ b/packages/x-data-grid/src/utils/utils.ts @@ -35,6 +35,13 @@ export function escapeRegExp(value: string): string { export const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value)); +/** + * Create an array containing the range [from, to[ + */ +export function range(from: number, to: number) { + return Array.from({ length: to - from }).map((_, i) => from + i); +} + /** * Based on `fast-deep-equal` * diff --git a/scripts/x-data-grid-premium.exports.json b/scripts/x-data-grid-premium.exports.json index 6cd89e11c590..41fbd341ce7e 100644 --- a/scripts/x-data-grid-premium.exports.json +++ b/scripts/x-data-grid-premium.exports.json @@ -31,7 +31,6 @@ { "name": "DEFAULT_GRID_COL_TYPE_KEY", "kind": "Variable" }, { "name": "DetailPanelsPropsOverrides", "kind": "Interface" }, { "name": "ElementSize", "kind": "Interface" }, - { "name": "EMPTY_OFFSETS", "kind": "Variable" }, { "name": "EMPTY_PINNED_COLUMN_FIELDS", "kind": "Variable" }, { "name": "EMPTY_RENDER_CONTEXT", "kind": "Variable" }, { "name": "FilterColumnsArgs", "kind": "Interface" }, @@ -363,6 +362,7 @@ { "name": "GridGroupingValueGetter", "kind": "TypeAlias" }, { "name": "GridGroupNode", "kind": "TypeAlias" }, { "name": "GridGroupWorkIcon", "kind": "Variable" }, + { "name": "gridHasColSpanSelector", "kind": "Variable" }, { "name": "GridHeader", "kind": "Function" }, { "name": "GridHeaderCheckbox", "kind": "Variable" }, { "name": "GridHeaderFilterCell", "kind": "Variable" }, @@ -395,7 +395,6 @@ { "name": "GridMultiSelectionApi", "kind": "Interface" }, { "name": "GridNoRowsOverlay", "kind": "Variable" }, { "name": "gridNumberComparator", "kind": "Variable" }, - { "name": "gridOffsetsSelector", "kind": "Variable" }, { "name": "GridOverlay", "kind": "Variable" }, { "name": "GridOverlayProps", "kind": "TypeAlias" }, { "name": "GridOverlays", "kind": "Function" }, diff --git a/scripts/x-data-grid-pro.exports.json b/scripts/x-data-grid-pro.exports.json index f6c92b33e008..4b31200d35d6 100644 --- a/scripts/x-data-grid-pro.exports.json +++ b/scripts/x-data-grid-pro.exports.json @@ -30,7 +30,6 @@ { "name": "DEFAULT_GRID_COL_TYPE_KEY", "kind": "Variable" }, { "name": "DetailPanelsPropsOverrides", "kind": "Interface" }, { "name": "ElementSize", "kind": "Interface" }, - { "name": "EMPTY_OFFSETS", "kind": "Variable" }, { "name": "EMPTY_PINNED_COLUMN_FIELDS", "kind": "Variable" }, { "name": "EMPTY_RENDER_CONTEXT", "kind": "Variable" }, { "name": "FilterColumnsArgs", "kind": "Interface" }, @@ -327,6 +326,7 @@ { "name": "GridGroupingColDefOverride", "kind": "Interface" }, { "name": "GridGroupingColDefOverrideParams", "kind": "Interface" }, { "name": "GridGroupNode", "kind": "TypeAlias" }, + { "name": "gridHasColSpanSelector", "kind": "Variable" }, { "name": "GridHeader", "kind": "Function" }, { "name": "GridHeaderCheckbox", "kind": "Variable" }, { "name": "GridHeaderFilterCell", "kind": "Variable" }, @@ -359,7 +359,6 @@ { "name": "GridMultiSelectionApi", "kind": "Interface" }, { "name": "GridNoRowsOverlay", "kind": "Variable" }, { "name": "gridNumberComparator", "kind": "Variable" }, - { "name": "gridOffsetsSelector", "kind": "Variable" }, { "name": "GridOverlay", "kind": "Variable" }, { "name": "GridOverlayProps", "kind": "TypeAlias" }, { "name": "GridOverlays", "kind": "Function" }, diff --git a/scripts/x-data-grid.exports.json b/scripts/x-data-grid.exports.json index c33834114833..8ecdfb322227 100644 --- a/scripts/x-data-grid.exports.json +++ b/scripts/x-data-grid.exports.json @@ -27,7 +27,6 @@ { "name": "DEFAULT_GRID_COL_TYPE_KEY", "kind": "Variable" }, { "name": "DetailPanelsPropsOverrides", "kind": "Interface" }, { "name": "ElementSize", "kind": "Interface" }, - { "name": "EMPTY_OFFSETS", "kind": "Variable" }, { "name": "EMPTY_PINNED_COLUMN_FIELDS", "kind": "Variable" }, { "name": "EMPTY_RENDER_CONTEXT", "kind": "Variable" }, { "name": "FilterColumnsArgs", "kind": "Interface" }, @@ -297,6 +296,7 @@ { "name": "GridGenericColumnMenuProps", "kind": "Interface" }, { "name": "GridGetRowsToExportParams", "kind": "Interface" }, { "name": "GridGroupNode", "kind": "TypeAlias" }, + { "name": "gridHasColSpanSelector", "kind": "Variable" }, { "name": "GridHeader", "kind": "Function" }, { "name": "GridHeaderCheckbox", "kind": "Variable" }, { "name": "GridHeaderFilterEventLookup", "kind": "Interface" }, @@ -325,7 +325,6 @@ { "name": "GridMultiSelectionApi", "kind": "Interface" }, { "name": "GridNoRowsOverlay", "kind": "Variable" }, { "name": "gridNumberComparator", "kind": "Variable" }, - { "name": "gridOffsetsSelector", "kind": "Variable" }, { "name": "GridOverlay", "kind": "Variable" }, { "name": "GridOverlayProps", "kind": "TypeAlias" }, { "name": "GridOverlays", "kind": "Function" },