From 54cde58f1f351978b0a32714e70b8c21ddd0c99c Mon Sep 17 00:00:00 2001 From: Rom Grk Date: Wed, 16 Oct 2024 19:28:14 -0400 Subject: [PATCH] [DataGrid] Fix scroll jumping (#14929) --- .../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 +- .../src/DataGridPremium/DataGridPremium.tsx | 1 + .../src/DataGridPro/DataGridPro.tsx | 1 + .../detailPanel/useGridDetailPanel.ts | 8 +- .../src/tests/rows.DataGridPro.test.tsx | 71 ---- .../x-data-grid/src/DataGrid/DataGrid.tsx | 1 + .../x-data-grid/src/components/GridRow.tsx | 60 +--- .../pipeProcessing/gridPipeProcessingApi.ts | 3 +- .../features/rows/gridRowsMetaInterfaces.ts | 20 ++ .../src/hooks/features/rows/gridRowsUtils.ts | 5 + .../hooks/features/rows/useGridRowsMeta.ts | 310 +++++++++--------- .../src/hooks/utils/useGridApiMethod.ts | 3 +- .../src/models/api/gridRowsMetaApi.ts | 29 +- .../x-data-grid/src/models/gridApiCaches.ts | 2 + .../src/models/props/DataGridProps.ts | 3 +- .../x-data-grid/src/utils/ResizeObserver.ts | 14 + 18 files changed, 249 insertions(+), 300 deletions(-) create mode 100644 packages/x-data-grid/src/hooks/features/rows/gridRowsMetaInterfaces.ts create mode 100644 packages/x-data-grid/src/utils/ResizeObserver.ts 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 1667cca1371e..b9c22994df5f 100644 --- a/docs/pages/x/api/data-grid/data-grid-premium.json +++ b/docs/pages/x/api/data-grid/data-grid-premium.json @@ -588,7 +588,11 @@ "rowGroupingModel": { "type": { "name": "arrayOf", "description": "Array<string>" } }, "rowHeight": { "type": { "name": "number" }, "default": "52" }, "rowModesModel": { "type": { "name": "object" } }, - "rowPositionsDebounceMs": { "type": { "name": "number" }, "default": "166" }, + "rowPositionsDebounceMs": { + "type": { "name": "number" }, + "default": "166", + "deprecated": true + }, "rowReordering": { "type": { "name": "bool" }, "default": "false" }, "rows": { "type": { "name": "arrayOf", "description": "Array<object>" }, 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 cc714abf8fba..f0e2cc6f48c4 100644 --- a/docs/pages/x/api/data-grid/data-grid-pro.json +++ b/docs/pages/x/api/data-grid/data-grid-pro.json @@ -523,7 +523,11 @@ "rowCount": { "type": { "name": "number" } }, "rowHeight": { "type": { "name": "number" }, "default": "52" }, "rowModesModel": { "type": { "name": "object" } }, - "rowPositionsDebounceMs": { "type": { "name": "number" }, "default": "166" }, + "rowPositionsDebounceMs": { + "type": { "name": "number" }, + "default": "166", + "deprecated": true + }, "rowReordering": { "type": { "name": "bool" }, "default": "false" }, "rows": { "type": { "name": "arrayOf", "description": "Array<object>" }, diff --git a/docs/pages/x/api/data-grid/data-grid.json b/docs/pages/x/api/data-grid/data-grid.json index 3fd158be8aa0..25e56f8402bc 100644 --- a/docs/pages/x/api/data-grid/data-grid.json +++ b/docs/pages/x/api/data-grid/data-grid.json @@ -439,7 +439,11 @@ "rowCount": { "type": { "name": "number" } }, "rowHeight": { "type": { "name": "number" }, "default": "52" }, "rowModesModel": { "type": { "name": "object" } }, - "rowPositionsDebounceMs": { "type": { "name": "number" }, "default": "166" }, + "rowPositionsDebounceMs": { + "type": { "name": "number" }, + "default": "166", + "deprecated": true + }, "rows": { "type": { "name": "arrayOf", "description": "Array<object>" }, "default": "[]" diff --git a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx index 89e77912b74c..86be3303b736 100644 --- a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx +++ b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx @@ -967,6 +967,7 @@ DataGridPremiumRaw.propTypes = { * Setting it to a lower value could be useful when using dynamic row height, * but might reduce performance when displaying a large number of rows. * @default 166 + * @deprecated */ rowPositionsDebounceMs: PropTypes.number, /** diff --git a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx index 4590c79ccfdf..8cea3a676d0d 100644 --- a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx +++ b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx @@ -873,6 +873,7 @@ DataGridProRaw.propTypes = { * Setting it to a lower value could be useful when using dynamic row height, * but might reduce performance when displaying a large number of rows. * @default 166 + * @deprecated */ rowPositionsDebounceMs: PropTypes.number, /** diff --git a/packages/x-data-grid-pro/src/hooks/features/detailPanel/useGridDetailPanel.ts b/packages/x-data-grid-pro/src/hooks/features/detailPanel/useGridDetailPanel.ts index f618ffe20a7f..2efc3fbe9250 100644 --- a/packages/x-data-grid-pro/src/hooks/features/detailPanel/useGridDetailPanel.ts +++ b/packages/x-data-grid-pro/src/hooks/features/detailPanel/useGridDetailPanel.ts @@ -313,7 +313,13 @@ export const useGridDetailPanel = ( const isFirstRender = React.useRef(true); if (isFirstRender.current) { - isFirstRender.current = false; updateCachesIfNeeded(); } + React.useEffect(() => { + if (!isFirstRender.current) { + updateCachesIfNeeded(); + apiRef.current.hydrateRowsMeta(); + } + isFirstRender.current = false; + }, [apiRef, updateCachesIfNeeded]); }; 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 3acb545b4ed5..ba199d341d21 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 @@ -887,77 +887,6 @@ describe(' - Rows', () => { }); }); - describe('apiRef: setRowHeight', () => { - const ROW_HEIGHT = 52; - - before(function beforeHook() { - if (isJSDOM) { - // Need layouting - this.skip(); - } - }); - - beforeEach(() => { - baselineProps = { - rows: [ - { - id: 0, - brand: 'Nike', - }, - { - id: 1, - brand: 'Adidas', - }, - { - id: 2, - brand: 'Puma', - }, - ], - columns: [{ field: 'brand', headerName: 'Brand' }], - }; - }); - - let apiRef: React.MutableRefObject; - - function TestCase(props: Partial) { - apiRef = useGridApiRef(); - return ( -
- -
- ); - } - - it('should change row height', () => { - const resizedRowId = 1; - render(); - - expect(getRow(1).clientHeight).to.equal(ROW_HEIGHT); - - act(() => apiRef.current.unstable_setRowHeight(resizedRowId, 100)); - expect(getRow(resizedRowId).clientHeight).to.equal(100); - }); - - it('should preserve changed row height after sorting', () => { - const resizedRowId = 0; - const getRowHeight = spy(); - render(); - - const row = getRow(resizedRowId); - expect(row.clientHeight).to.equal(ROW_HEIGHT); - - getRowHeight.resetHistory(); - act(() => apiRef.current.unstable_setRowHeight(resizedRowId, 100)); - expect(row.clientHeight).to.equal(100); - - // sort - fireEvent.click(getColumnHeaderCell(resizedRowId)); - - expect(row.clientHeight).to.equal(100); - expect(getRowHeight.neverCalledWithMatch({ id: resizedRowId })).to.equal(true); - }); - }); - describe('prop: rowCount', () => { function TestCase(props: DataGridProProps) { return ( diff --git a/packages/x-data-grid/src/DataGrid/DataGrid.tsx b/packages/x-data-grid/src/DataGrid/DataGrid.tsx index 72a2bb43cb78..ccda733ed0de 100644 --- a/packages/x-data-grid/src/DataGrid/DataGrid.tsx +++ b/packages/x-data-grid/src/DataGrid/DataGrid.tsx @@ -739,6 +739,7 @@ DataGridRaw.propTypes = { * Setting it to a lower value could be useful when using dynamic row height, * but might reduce performance when displaying a large number of rows. * @default 166 + * @deprecated */ rowPositionsDebounceMs: PropTypes.number, /** diff --git a/packages/x-data-grid/src/components/GridRow.tsx b/packages/x-data-grid/src/components/GridRow.tsx index 03c8b20797fc..f326364029d1 100644 --- a/packages/x-data-grid/src/components/GridRow.tsx +++ b/packages/x-data-grid/src/components/GridRow.tsx @@ -6,7 +6,6 @@ import { fastMemo } from '@mui/x-internals/fastMemo'; import { GridRowEventLookup } from '../models/events'; import { GridRowId, GridRowModel } from '../models/gridRows'; import { GridEditModes, GridRowModes, GridCellModes } from '../models/gridEditRowModel'; -import { useGridApiContext } from '../hooks/utils/useGridApiContext'; import { gridClasses } from '../constants/gridClasses'; import { composeGridClasses } from '../utils/composeGridClasses'; import { useGridRootProps } from '../hooks/utils/useGridRootProps'; @@ -29,6 +28,7 @@ import { PinnedPosition, gridPinnedColumnPositionLookup } from './cell/GridCell' import { GridScrollbarFillerCell as ScrollbarFiller } from './GridScrollbarFillerCell'; import { getPinnedCellOffset } from '../internals/utils/getPinnedCellOffset'; import { useGridConfiguration } from '../hooks/utils/useGridConfiguration'; +import { useGridPrivateApiContext } from '../hooks/utils/useGridPrivateApiContext'; export interface GridRowProps extends React.HTMLAttributes { row: GridRowModel; @@ -111,7 +111,7 @@ const GridRow = React.forwardRef(function GridRow( onMouseOver, ...other } = props; - const apiRef = useGridApiContext(); + const apiRef = useGridPrivateApiContext(); const configuration = useGridConfiguration(); const ref = React.useRef(null); const rootProps = useGridRootProps(); @@ -153,37 +153,19 @@ const GridRow = React.forwardRef(function GridRow( React.useLayoutEffect(() => { if (currentPage.range) { - // The index prop is relative to the rows from all pages. As example, the index prop of the - // first row is 5 if `paginationModel.pageSize=5` and `paginationModel.page=1`. However, the index used by the virtualization - // doesn't care about pagination and considers the rows from the current page only, so the - // first row always has index=0. We need to subtract the index of the first row to make it - // compatible with the index used by the virtualization. const rowIndex = apiRef.current.getRowIndexRelativeToVisibleRows(rowId); - // pinned rows are not part of the visible rows - if (rowIndex != null) { + // Pinned rows are not part of the visible rows + if (rowIndex !== undefined) { apiRef.current.unstable_setLastMeasuredRowIndex(rowIndex); } } - const rootElement = ref.current; - const hasFixedHeight = rowHeight !== 'auto'; - if (!rootElement || hasFixedHeight || typeof ResizeObserver === 'undefined') { - return undefined; + if (ref.current && rowHeight === 'auto') { + return apiRef.current.observeRowHeight(ref.current, rowId); } - const resizeObserver = new ResizeObserver((entries) => { - const [entry] = entries; - const height = - entry.borderBoxSize && entry.borderBoxSize.length > 0 - ? entry.borderBoxSize[0].blockSize - : entry.contentRect.height; - apiRef.current.unstable_storeRowHeightMeasurement(rowId, height); - }); - - resizeObserver.observe(rootElement); - - return () => resizeObserver.disconnect(); - }, [apiRef, currentPage.range, index, rowHeight, rowId]); + return undefined; + }, [apiRef, currentPage.range, rowHeight, rowId]); const publish = React.useCallback( ( @@ -254,22 +236,12 @@ const GridRow = React.forwardRef(function GridRow( const rowReordering = (rootProps as any).rowReordering as boolean; - const sizes = useGridSelector( + const heightEntry = useGridSelector( apiRef, - () => ({ ...apiRef.current.unstable_getRowInternalSizes(rowId) }), + () => ({ ...apiRef.current.getRowHeightEntry(rowId) }), objectShallowCompare, ); - let minHeight = rowHeight; - if (minHeight === 'auto' && sizes) { - const numberOfBaseSizes = 1; - const maximumSize = sizes.baseCenter ?? 0; - - if (maximumSize > 0 && numberOfBaseSizes > 1) { - minHeight = maximumSize; - } - } - const style = React.useMemo(() => { if (isNotVisible) { return { @@ -282,28 +254,28 @@ const GridRow = React.forwardRef(function GridRow( const rowStyle = { ...styleProp, maxHeight: rowHeight === 'auto' ? 'none' : rowHeight, // max-height doesn't support "auto" - minHeight, + minHeight: rowHeight, '--height': typeof rowHeight === 'number' ? `${rowHeight}px` : rowHeight, }; - if (sizes?.spacingTop) { + if (heightEntry.spacingTop) { const property = rootProps.rowSpacingType === 'border' ? 'borderTopWidth' : 'marginTop'; - rowStyle[property] = sizes.spacingTop; + rowStyle[property] = heightEntry.spacingTop; } - if (sizes?.spacingBottom) { + if (heightEntry.spacingBottom) { const property = rootProps.rowSpacingType === 'border' ? 'borderBottomWidth' : 'marginBottom'; let propertyValue = rowStyle[property]; // avoid overriding existing value if (typeof propertyValue !== 'number') { propertyValue = parseInt(propertyValue || '0', 10); } - propertyValue += sizes.spacingBottom; + propertyValue += heightEntry.spacingBottom; rowStyle[property] = propertyValue; } return rowStyle; - }, [isNotVisible, rowHeight, styleProp, minHeight, sizes, rootProps.rowSpacingType]); + }, [isNotVisible, rowHeight, styleProp, heightEntry, rootProps.rowSpacingType]); const rowClassNames = apiRef.current.unstable_applyPipeProcessors('rowClassName', [], rowId); const ariaAttributes = rowNode ? getRowAriaAttributes(rowNode, index) : undefined; diff --git a/packages/x-data-grid/src/hooks/core/pipeProcessing/gridPipeProcessingApi.ts b/packages/x-data-grid/src/hooks/core/pipeProcessing/gridPipeProcessingApi.ts index 5e065f1a572a..7cd516a68c06 100644 --- a/packages/x-data-grid/src/hooks/core/pipeProcessing/gridPipeProcessingApi.ts +++ b/packages/x-data-grid/src/hooks/core/pipeProcessing/gridPipeProcessingApi.ts @@ -20,6 +20,7 @@ import { import { GridRowEntry, GridRowId } from '../../../models/gridRows'; import { GridHydrateRowsValue } from '../../features/rows/gridRowsInterfaces'; import { GridPreferencePanelsValue } from '../../features/preferencesPanel'; +import { HeightEntry } from '../../features/rows/gridRowsMetaInterfaces'; export type GridPipeProcessorGroup = keyof GridPipeProcessingLookup; @@ -41,7 +42,7 @@ export interface GridPipeProcessingLookup { value: GridRestoreStatePreProcessingValue; context: GridRestoreStatePreProcessingContext; }; - rowHeight: { value: Record; context: GridRowEntry }; + rowHeight: { value: HeightEntry; context: GridRowEntry }; scrollToIndexes: { value: Partial; context: Partial; diff --git a/packages/x-data-grid/src/hooks/features/rows/gridRowsMetaInterfaces.ts b/packages/x-data-grid/src/hooks/features/rows/gridRowsMetaInterfaces.ts new file mode 100644 index 000000000000..c9fee83fd2ad --- /dev/null +++ b/packages/x-data-grid/src/hooks/features/rows/gridRowsMetaInterfaces.ts @@ -0,0 +1,20 @@ +import { GridRowId } from '../../../models/gridRows'; + +export type HeightEntry = { + content: number; + spacingTop: number; + spacingBottom: number; + detail: number; + + autoHeight: boolean; + needsFirstMeasurement: boolean; +}; + +export type HeightCache = Map; + +export interface GridRowsMetaInternalCache { + /** + * Map of height cache entries. + */ + heights: HeightCache; +} diff --git a/packages/x-data-grid/src/hooks/features/rows/gridRowsUtils.ts b/packages/x-data-grid/src/hooks/features/rows/gridRowsUtils.ts index e7b217aaa9a3..5630a73d551d 100644 --- a/packages/x-data-grid/src/hooks/features/rows/gridRowsUtils.ts +++ b/packages/x-data-grid/src/hooks/features/rows/gridRowsUtils.ts @@ -464,3 +464,8 @@ export const rowHeightWarning = [ `MUI X: The \`rowHeight\` prop should be a number greater than 0.`, `The default value will be used instead.`, ].join('\n'); + +export const getRowHeightWarning = [ + `MUI X: The \`getRowHeight\` prop should return a number greater than 0 or 'auto'.`, + `The default value will be used instead.`, +].join('\n'); 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 bb49e04a9ae6..501b8690a710 100644 --- a/packages/x-data-grid/src/hooks/features/rows/useGridRowsMeta.ts +++ b/packages/x-data-grid/src/hooks/features/rows/useGridRowsMeta.ts @@ -1,11 +1,13 @@ import * as React from 'react'; -import { unstable_debounce as debounce } from '@mui/utils'; +import useLazyRef from '@mui/utils/useLazyRef'; +import { ResizeObserver } from '../../../utils/ResizeObserver'; import { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; import { GridRowsMetaApi, GridRowsMetaPrivateApi } from '../../../models/api/gridRowsMetaApi'; import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; import { useGridVisibleRows } from '../../utils/useGridVisibleRows'; +import { eslintUseValue } from '../../../utils/utils'; import { useGridApiMethod } from '../../utils/useGridApiMethod'; -import { GridRowEntry, GridRowId } from '../../../models/gridRows'; +import { GridRowEntry } from '../../../models/gridRows'; import { useGridSelector } from '../../utils/useGridSelector'; import { gridDensityFactorSelector } from '../density/densitySelector'; import { gridFilterModelSelector } from '../filter/gridFilterSelector'; @@ -15,22 +17,24 @@ import { GridStateInitializer } from '../../utils/useGridInitializeState'; import { useGridRegisterPipeApplier } from '../../core/pipeProcessing'; import { gridPinnedRowsSelector } from './gridRowsSelector'; import { gridDimensionsSelector } from '../dimensions/gridDimensionsSelectors'; -import { getValidRowHeight } from './gridRowsUtils'; +import { getValidRowHeight, getRowHeightWarning } from './gridRowsUtils'; +import type { HeightEntry } from './gridRowsMetaInterfaces'; -// TODO: I think the row heights can now be encoded as a single `size` instead of `sizes.baseXxxx` +/* eslint-disable no-underscore-dangle */ -export const rowsMetaStateInitializer: GridStateInitializer = (state) => ({ - ...state, - rowsMeta: { - currentPageTotalHeight: 0, - positions: [], - }, -}); +export const rowsMetaStateInitializer: GridStateInitializer = (state, props, apiRef) => { + apiRef.current.caches.rowsMeta = { + heights: new Map(), + }; -const getRowHeightWarning = [ - `MUI X: The \`getRowHeight\` prop should return a number greater than 0 or 'auto'.`, - `The default value will be used instead.`, -].join('\n'); + return { + ...state, + rowsMeta: { + currentPageTotalHeight: 0, + positions: [], + }, + }; +}; /** * @requires useGridPageSize (method) @@ -50,18 +54,12 @@ export const useGridRowsMeta = ( >, ): void => { const { getRowHeight: getRowHeightProp, getRowSpacing, getEstimatedRowHeight } = props; - const rowsHeightLookup = React.useRef<{ - [key: GridRowId]: { - isResized: boolean; - sizes: Record; - autoHeight: boolean; // Determines if the row has dynamic height - needsFirstMeasurement: boolean; // Determines if the row was never measured. If true, use the estimated height as row height. - }; - }>(Object.create(null)); - - // Inspired by https://github.com/bvaughn/react-virtualized/blob/master/source/Grid/utils/CellSizeAndPositionManager.js + const heightCache = apiRef.current.caches.rowsMeta.heights; + const lastMeasuredRowIndex = React.useRef(-1); const hasRowWithAutoHeight = React.useRef(false); + const isHeightMetaValid = React.useRef(false); + const densityFactor = useGridSelector(apiRef, gridDensityFactorSelector); const filterModel = useGridSelector(apiRef, gridFilterModelSelector); const paginationState = useGridSelector(apiRef, gridPaginationSelector); @@ -73,55 +71,57 @@ export const useGridRowsMeta = ( () => gridDimensionsSelector(apiRef.current.state).rowHeight, ); - const hydrateRowsMeta = React.useCallback(() => { - hasRowWithAutoHeight.current = false; + const getRowHeightEntry: GridRowsMetaPrivateApi['getRowHeightEntry'] = (rowId) => { + let entry = heightCache.get(rowId); + if (entry === undefined) { + entry = { + content: rowHeight, + spacingTop: 0, + spacingBottom: 0, + detail: 0, + autoHeight: false, + needsFirstMeasurement: true, + }; + heightCache.set(rowId, entry); + } + return entry; + }; - const calculateRowProcessedSizes = (row: GridRowEntry) => { - if (!rowsHeightLookup.current[row.id]) { - rowsHeightLookup.current[row.id] = { - sizes: { baseCenter: rowHeight }, - isResized: false, - autoHeight: false, - needsFirstMeasurement: true, // Assume all rows will need to be measured by default - }; - } + const processHeightEntry = React.useCallback( + (row: GridRowEntry) => { + // HACK: rowHeight trails behind the most up-to-date value just enough to + // mess the initial rowsMeta hydration :/ + const baseRowHeight = gridDimensionsSelector(apiRef.current.state).rowHeight; + eslintUseValue(rowHeight); - const { isResized, needsFirstMeasurement, sizes } = rowsHeightLookup.current[row.id]; - let baseRowHeight = typeof rowHeight === 'number' && rowHeight > 0 ? rowHeight : 52; - const existingBaseRowHeight = sizes.baseCenter; + const entry = apiRef.current.getRowHeightEntry(row.id); - if (isResized) { - // Do not recalculate resized row height and use the value from the lookup - baseRowHeight = existingBaseRowHeight; - } else if (getRowHeightProp) { + if (!getRowHeightProp) { + entry.content = baseRowHeight; + entry.needsFirstMeasurement = false; + } else { const rowHeightFromUser = getRowHeightProp({ ...row, densityFactor }); if (rowHeightFromUser === 'auto') { - if (needsFirstMeasurement) { + if (entry.needsFirstMeasurement) { const estimatedRowHeight = getEstimatedRowHeight ? getEstimatedRowHeight({ ...row, densityFactor }) - : rowHeight; + : baseRowHeight; // If the row was not measured yet use the estimated row height - baseRowHeight = estimatedRowHeight ?? rowHeight; - } else { - baseRowHeight = existingBaseRowHeight; + entry.content = estimatedRowHeight ?? baseRowHeight; } hasRowWithAutoHeight.current = true; - rowsHeightLookup.current[row.id].autoHeight = true; + entry.autoHeight = true; } else { // Default back to base rowHeight if getRowHeight returns invalid value. - baseRowHeight = getValidRowHeight(rowHeightFromUser, rowHeight, getRowHeightWarning); - rowsHeightLookup.current[row.id].needsFirstMeasurement = false; - rowsHeightLookup.current[row.id].autoHeight = false; + entry.content = getValidRowHeight(rowHeightFromUser, baseRowHeight, getRowHeightWarning); + entry.needsFirstMeasurement = false; + entry.autoHeight = false; } - } else { - rowsHeightLookup.current[row.id].needsFirstMeasurement = false; } - const initialHeights = { baseCenter: baseRowHeight } as Record; - if (getRowSpacing) { const indexRelativeToCurrentPage = apiRef.current.getRowIndexRelativeToVisibleRows(row.id); @@ -132,46 +132,48 @@ export const useGridRowsMeta = ( indexRelativeToCurrentPage, }); - initialHeights.spacingTop = spacing.top ?? 0; - initialHeights.spacingBottom = spacing.bottom ?? 0; + entry.spacingTop = spacing.top ?? 0; + entry.spacingBottom = spacing.bottom ?? 0; + } else { + entry.spacingTop = 0; + entry.spacingBottom = 0; } - const processedSizes = apiRef.current.unstable_applyPipeProcessors( - 'rowHeight', - initialHeights, - row, - ) as Record; + apiRef.current.unstable_applyPipeProcessors('rowHeight', entry, row) as HeightEntry; - rowsHeightLookup.current[row.id].sizes = processedSizes; + return entry; + }, + [ + apiRef, + currentPage.rows.length, + getRowHeightProp, + getEstimatedRowHeight, + rowHeight, + getRowSpacing, + densityFactor, + ], + ); - return processedSizes; - }; + const hydrateRowsMeta = React.useCallback(() => { + hasRowWithAutoHeight.current = false; + + pinnedRows.top.forEach(processHeightEntry); + pinnedRows.bottom.forEach(processHeightEntry); const positions: number[] = []; const currentPageTotalHeight = currentPage.rows.reduce((acc, row) => { positions.push(acc); - let otherSizes = 0; + const entry = processHeightEntry(row); + const total = entry.content + entry.spacingTop + entry.spacingBottom + entry.detail; - const processedSizes = calculateRowProcessedSizes(row); - /* eslint-disable-next-line guard-for-in */ - for (const key in processedSizes) { - const value = processedSizes[key]; - if (key !== 'baseCenter') { - otherSizes += value; - } - } - - return acc + processedSizes.baseCenter + otherSizes; + return acc + total; }, 0); - pinnedRows?.top?.forEach((row) => { - calculateRowProcessedSizes(row); - }); - - pinnedRows?.bottom?.forEach((row) => { - calculateRowProcessedSizes(row); - }); + if (!hasRowWithAutoHeight.current) { + // No row has height=auto, so all rows are already measured + lastMeasuredRowIndex.current = Infinity; + } apiRef.current.setState((state) => { return { @@ -183,113 +185,93 @@ export const useGridRowsMeta = ( }; }); - if (!hasRowWithAutoHeight.current) { - // No row has height=auto, so all rows are already measured - lastMeasuredRowIndex.current = Infinity; - } + isHeightMetaValid.current = true; + }, [apiRef, pinnedRows, currentPage.rows, processHeightEntry]); - apiRef.current.forceUpdate(); - }, [ - apiRef, - currentPage.rows, - rowHeight, - getRowHeightProp, - getRowSpacing, - getEstimatedRowHeight, - pinnedRows, - densityFactor, - ]); - - const getRowHeight = React.useCallback( - (rowId) => { - const height = rowsHeightLookup.current[rowId]; - return height ? height.sizes.baseCenter : rowHeight; - }, - [rowHeight], - ); - - const getRowInternalSizes = (rowId: GridRowId): Record | undefined => - rowsHeightLookup.current[rowId]?.sizes; - - const setRowHeight = React.useCallback( - (id: GridRowId, height: number) => { - rowsHeightLookup.current[id].sizes.baseCenter = height; - rowsHeightLookup.current[id].isResized = true; - rowsHeightLookup.current[id].needsFirstMeasurement = false; - hydrateRowsMeta(); - }, - [hydrateRowsMeta], - ); - - const debouncedHydrateRowsMeta = React.useMemo( - () => debounce(hydrateRowsMeta, props.rowPositionsDebounceMs), - [hydrateRowsMeta, props.rowPositionsDebounceMs], - ); + const getRowHeight: GridRowsMetaApi['unstable_getRowHeight'] = (rowId) => { + return heightCache.get(rowId)?.content ?? rowHeight; + }; - const storeMeasuredRowHeight = React.useCallback< - GridRowsMetaApi['unstable_storeRowHeightMeasurement'] - >( - (id, height) => { - if (!rowsHeightLookup.current[id] || !rowsHeightLookup.current[id].autoHeight) { - return; - } + const storeRowHeightMeasurement: GridRowsMetaApi['unstable_storeRowHeightMeasurement'] = ( + id, + height, + ) => { + const entry = apiRef.current.getRowHeightEntry(id); - // Only trigger hydration if the value is different, otherwise we trigger a loop - const needsHydration = rowsHeightLookup.current[id].sizes.baseCenter !== height; + const didChange = entry.content !== height; - rowsHeightLookup.current[id].needsFirstMeasurement = false; - rowsHeightLookup.current[id].sizes.baseCenter = height; + entry.needsFirstMeasurement = false; + entry.content = height; - if (needsHydration) { - debouncedHydrateRowsMeta(); - } - }, - [debouncedHydrateRowsMeta], - ); + isHeightMetaValid.current &&= !didChange; + }; - const rowHasAutoHeight = React.useCallback((id) => { - return rowsHeightLookup.current[id]?.autoHeight || false; - }, []); + const rowHasAutoHeight: GridRowsMetaPrivateApi['rowHasAutoHeight'] = (id) => { + return heightCache.get(id)?.autoHeight ?? false; + }; - const getLastMeasuredRowIndex = React.useCallback< - GridRowsMetaPrivateApi['getLastMeasuredRowIndex'] - >(() => { + const getLastMeasuredRowIndex: GridRowsMetaPrivateApi['getLastMeasuredRowIndex'] = () => { return lastMeasuredRowIndex.current; - }, []); + }; - const setLastMeasuredRowIndex = React.useCallback< - GridRowsMetaApi['unstable_setLastMeasuredRowIndex'] - >((index) => { + const setLastMeasuredRowIndex: GridRowsMetaApi['unstable_setLastMeasuredRowIndex'] = (index) => { if (hasRowWithAutoHeight.current && index > lastMeasuredRowIndex.current) { lastMeasuredRowIndex.current = index; } - }, []); + }; - const resetRowHeights = React.useCallback(() => { - rowsHeightLookup.current = {}; + const resetRowHeights: GridRowsMetaApi['resetRowHeights'] = () => { + heightCache.clear(); hydrateRowsMeta(); - }, [hydrateRowsMeta]); + }; + + const resizeObserver = useLazyRef( + () => + new ResizeObserver((entries) => { + for (let i = 0; i < entries.length; i += 1) { + const entry = entries[i]; + const height = + entry.borderBoxSize && entry.borderBoxSize.length > 0 + ? entry.borderBoxSize[0].blockSize + : entry.contentRect.height; + const rowId = (entry.target as any).__mui_id; + apiRef.current.unstable_storeRowHeightMeasurement(rowId, height); + } + if (!isHeightMetaValid.current) { + apiRef.current.requestPipeProcessorsApplication('rowHeight'); + } + }), + ).current; + + const observeRowHeight: GridRowsMetaPrivateApi['observeRowHeight'] = (element, rowId) => { + (element as any).__mui_id = rowId; + + resizeObserver.observe(element); + + return () => resizeObserver.unobserve(element); + }; + + useGridRegisterPipeApplier(apiRef, 'rowHeight', hydrateRowsMeta); // The effect is used to build the rows meta data - currentPageTotalHeight and positions. // Because of variable row height this is needed for the virtualization React.useEffect(() => { hydrateRowsMeta(); - }, [rowHeight, filterModel, paginationState, sortModel, hydrateRowsMeta]); - - useGridRegisterPipeApplier(apiRef, 'rowHeight', hydrateRowsMeta); + }, [filterModel, paginationState, sortModel, hydrateRowsMeta]); const rowsMetaApi: GridRowsMetaApi = { - unstable_setLastMeasuredRowIndex: setLastMeasuredRowIndex, unstable_getRowHeight: getRowHeight, - unstable_getRowInternalSizes: getRowInternalSizes, - unstable_setRowHeight: setRowHeight, - unstable_storeRowHeightMeasurement: storeMeasuredRowHeight, + unstable_setLastMeasuredRowIndex: setLastMeasuredRowIndex, + unstable_storeRowHeightMeasurement: storeRowHeightMeasurement, resetRowHeights, }; const rowsMetaPrivateApi: GridRowsMetaPrivateApi = { - getLastMeasuredRowIndex, + hydrateRowsMeta, + observeRowHeight, rowHasAutoHeight, + getRowHeightEntry, + getLastMeasuredRowIndex, }; useGridApiMethod(apiRef, rowsMetaApi, 'public'); diff --git a/packages/x-data-grid/src/hooks/utils/useGridApiMethod.ts b/packages/x-data-grid/src/hooks/utils/useGridApiMethod.ts index fb7935e9d913..bbaaa2b3eb6c 100644 --- a/packages/x-data-grid/src/hooks/utils/useGridApiMethod.ts +++ b/packages/x-data-grid/src/hooks/utils/useGridApiMethod.ts @@ -1,4 +1,5 @@ import * as React from 'react'; +import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; import { GridPrivateApiCommon } from '../../models/api/gridApiCommon'; type GetPublicApiType = PrivateApi extends { getPublicApi: () => infer PublicApi } @@ -14,7 +15,7 @@ export function useGridApiMethod< >(privateApiRef: React.MutableRefObject, apiMethods: T, visibility: V) { const isFirstRender = React.useRef(true); - React.useEffect(() => { + useEnhancedEffect(() => { isFirstRender.current = false; privateApiRef.current.register(visibility, apiMethods); }, [privateApiRef, visibility, apiMethods]); diff --git a/packages/x-data-grid/src/models/api/gridRowsMetaApi.ts b/packages/x-data-grid/src/models/api/gridRowsMetaApi.ts index 77e744b599aa..9dba011f4175 100644 --- a/packages/x-data-grid/src/models/api/gridRowsMetaApi.ts +++ b/packages/x-data-grid/src/models/api/gridRowsMetaApi.ts @@ -1,4 +1,5 @@ import { GridRowId } from '../gridRows'; +import { HeightEntry } from '../../hooks/features/rows/gridRowsMetaInterfaces'; /** * The Row Meta API interface that is available in the grid `apiRef`. @@ -11,20 +12,6 @@ export interface GridRowsMetaApi { * @ignore - do not document. */ unstable_getRowHeight: (id: GridRowId) => number; - /** - * Gets all sizes that compose the total height that the given row takes. - * @param {GridRowId} id The id of the row. - * @returns {Record} The object containing the sizes. - * @ignore - do not document. - */ - unstable_getRowInternalSizes: (id: GridRowId) => Record | undefined; - /** - * Updates the base height of a row. - * @param {GridRowId} id The id of the row. - * @param {number} height The new height. - * @ignore - do not document. - */ - unstable_setRowHeight: (id: GridRowId, height: number) => void; /** * Stores the row height measurement and triggers an hydration, if needed. * @param {GridRowId} id The id of the row. @@ -46,6 +33,14 @@ export interface GridRowsMetaApi { } export interface GridRowsMetaPrivateApi { + hydrateRowsMeta: () => void; + /** + * Observe row for 'auto' height changes. + * @param {Element} element The row element to observe. + * @param {GridRowId} rowId The id of the row. + * @returns {ReturnType} A dispose callback + */ + observeRowHeight: (element: Element, rowId: GridRowId) => ReturnType; /** * Determines if the height of a row is "auto". * @param {GridRowId} id The id of the row. @@ -58,4 +53,10 @@ export interface GridRowsMetaPrivateApi { * @returns {number} The index of the last measured row. */ getLastMeasuredRowIndex: () => number; + /** + * Get the height entry from the cache or create one. + * @param {GridRowId} id The id of the row. + * @returns {HeightEntry} The height cache entry + */ + getRowHeightEntry: (id: GridRowId) => HeightEntry; } diff --git a/packages/x-data-grid/src/models/gridApiCaches.ts b/packages/x-data-grid/src/models/gridApiCaches.ts index aa2e8932b91d..5b3652b69224 100644 --- a/packages/x-data-grid/src/models/gridApiCaches.ts +++ b/packages/x-data-grid/src/models/gridApiCaches.ts @@ -1,5 +1,7 @@ import { GridRowsInternalCache } from '../hooks/features/rows/gridRowsInterfaces'; +import { GridRowsMetaInternalCache } from '../hooks/features/rows/gridRowsMetaInterfaces'; export interface GridApiCaches { rows: GridRowsInternalCache; + rowsMeta: GridRowsMetaInternalCache; } diff --git a/packages/x-data-grid/src/models/props/DataGridProps.ts b/packages/x-data-grid/src/models/props/DataGridProps.ts index 866b882977f6..e831f19f6eeb 100644 --- a/packages/x-data-grid/src/models/props/DataGridProps.ts +++ b/packages/x-data-grid/src/models/props/DataGridProps.ts @@ -377,8 +377,9 @@ export interface DataGridPropsWithDefaultValues