diff --git a/docs/data/data-grid/column-pinning/ColumnPinningDynamicRowHeight.js b/docs/data/data-grid/column-pinning/ColumnPinningDynamicRowHeight.js new file mode 100644 index 000000000000..433301b5a5c7 --- /dev/null +++ b/docs/data/data-grid/column-pinning/ColumnPinningDynamicRowHeight.js @@ -0,0 +1,172 @@ +import * as React from 'react'; +import DeleteIcon from '@mui/icons-material/Delete'; +import EditIcon from '@mui/icons-material/Edit'; +import PrintIcon from '@mui/icons-material/Print'; +import Stack from '@mui/material/Stack'; +import Button from '@mui/material/Button'; +import { DataGridPro, useGridApiRef } from '@mui/x-data-grid-pro'; +import { + randomCreatedDate, + randomTraderName, + randomEmail, + randomUpdatedDate, +} from '@mui/x-data-grid-generator'; + +export default function ColumnPinningDynamicRowHeight() { + const apiRef = useGridApiRef(); + const [showEditDelete, setShowEditDelete] = React.useState(true); + + const columns = React.useMemo( + () => [ + { field: 'name', headerName: 'Name', width: 160, editable: true }, + { field: 'email', headerName: 'Email', width: 200, editable: true }, + { field: 'age', headerName: 'Age', type: 'number', editable: true }, + { + field: 'dateCreated', + headerName: 'Date Created', + type: 'date', + width: 180, + editable: true, + }, + { + field: 'lastLogin', + headerName: 'Last Login', + type: 'dateTime', + width: 220, + editable: true, + }, + { + field: 'actions', + headerName: 'Actions', + width: 100, + renderCell: () => ( + + {showEditDelete && ( + + + + + )} + + + + ), + }, + ], + [showEditDelete], + ); + + const handleToggleClick = React.useCallback(() => { + setShowEditDelete((prevShowEditDelete) => !prevShowEditDelete); + }, []); + + React.useLayoutEffect(() => { + apiRef.current.resetRowHeights(); + }, [apiRef, showEditDelete]); + + return ( +
+ +
+ 'auto'} + initialState={{ pinnedColumns: { left: ['name'], right: ['actions'] } }} + /> +
+
+ ); +} + +const rows = [ + { + id: 1, + name: randomTraderName(), + email: randomEmail(), + age: 25, + dateCreated: randomCreatedDate(), + lastLogin: randomUpdatedDate(), + }, + { + id: 2, + name: randomTraderName(), + email: randomEmail(), + age: 36, + dateCreated: randomCreatedDate(), + lastLogin: randomUpdatedDate(), + }, + { + id: 3, + name: randomTraderName(), + email: randomEmail(), + age: 19, + dateCreated: randomCreatedDate(), + lastLogin: randomUpdatedDate(), + }, + { + id: 4, + name: randomTraderName(), + email: randomEmail(), + age: 28, + dateCreated: randomCreatedDate(), + lastLogin: randomUpdatedDate(), + }, + { + id: 5, + name: randomTraderName(), + email: randomEmail(), + age: 23, + dateCreated: randomCreatedDate(), + lastLogin: randomUpdatedDate(), + }, + { + id: 6, + name: randomTraderName(), + email: randomEmail(), + age: 27, + dateCreated: randomCreatedDate(), + lastLogin: randomUpdatedDate(), + }, + { + id: 7, + name: randomTraderName(), + email: randomEmail(), + age: 18, + dateCreated: randomCreatedDate(), + lastLogin: randomUpdatedDate(), + }, + { + id: 8, + name: randomTraderName(), + email: randomEmail(), + age: 31, + dateCreated: randomCreatedDate(), + lastLogin: randomUpdatedDate(), + }, + { + id: 9, + name: randomTraderName(), + email: randomEmail(), + age: 24, + dateCreated: randomCreatedDate(), + lastLogin: randomUpdatedDate(), + }, + { + id: 10, + name: randomTraderName(), + email: randomEmail(), + age: 35, + dateCreated: randomCreatedDate(), + lastLogin: randomUpdatedDate(), + }, +]; diff --git a/docs/data/data-grid/column-pinning/ColumnPinningDynamicRowHeight.tsx b/docs/data/data-grid/column-pinning/ColumnPinningDynamicRowHeight.tsx new file mode 100644 index 000000000000..b3f8cafbcdb0 --- /dev/null +++ b/docs/data/data-grid/column-pinning/ColumnPinningDynamicRowHeight.tsx @@ -0,0 +1,176 @@ +import * as React from 'react'; +import DeleteIcon from '@mui/icons-material/Delete'; +import EditIcon from '@mui/icons-material/Edit'; +import PrintIcon from '@mui/icons-material/Print'; +import Stack from '@mui/material/Stack'; +import Button from '@mui/material/Button'; +import { + DataGridPro, + GridColumns, + GridRowsProp, + useGridApiRef, +} from '@mui/x-data-grid-pro'; +import { + randomCreatedDate, + randomTraderName, + randomEmail, + randomUpdatedDate, +} from '@mui/x-data-grid-generator'; + +export default function ColumnPinningDynamicRowHeight() { + const apiRef = useGridApiRef(); + const [showEditDelete, setShowEditDelete] = React.useState(true); + + const columns: GridColumns = React.useMemo( + () => [ + { field: 'name', headerName: 'Name', width: 160, editable: true }, + { field: 'email', headerName: 'Email', width: 200, editable: true }, + { field: 'age', headerName: 'Age', type: 'number', editable: true }, + { + field: 'dateCreated', + headerName: 'Date Created', + type: 'date', + width: 180, + editable: true, + }, + { + field: 'lastLogin', + headerName: 'Last Login', + type: 'dateTime', + width: 220, + editable: true, + }, + { + field: 'actions', + headerName: 'Actions', + width: 100, + renderCell: () => ( + + {showEditDelete && ( + + + + + )} + + + ), + }, + ], + [showEditDelete], + ); + + const handleToggleClick = React.useCallback(() => { + setShowEditDelete((prevShowEditDelete) => !prevShowEditDelete); + }, []); + + React.useLayoutEffect(() => { + apiRef.current.resetRowHeights(); + }, [apiRef, showEditDelete]); + + return ( +
+ +
+ 'auto'} + initialState={{ pinnedColumns: { left: ['name'], right: ['actions'] } }} + /> +
+
+ ); +} + +const rows: GridRowsProp = [ + { + id: 1, + name: randomTraderName(), + email: randomEmail(), + age: 25, + dateCreated: randomCreatedDate(), + lastLogin: randomUpdatedDate(), + }, + { + id: 2, + name: randomTraderName(), + email: randomEmail(), + age: 36, + dateCreated: randomCreatedDate(), + lastLogin: randomUpdatedDate(), + }, + { + id: 3, + name: randomTraderName(), + email: randomEmail(), + age: 19, + dateCreated: randomCreatedDate(), + lastLogin: randomUpdatedDate(), + }, + { + id: 4, + name: randomTraderName(), + email: randomEmail(), + age: 28, + dateCreated: randomCreatedDate(), + lastLogin: randomUpdatedDate(), + }, + { + id: 5, + name: randomTraderName(), + email: randomEmail(), + age: 23, + dateCreated: randomCreatedDate(), + lastLogin: randomUpdatedDate(), + }, + { + id: 6, + name: randomTraderName(), + email: randomEmail(), + age: 27, + dateCreated: randomCreatedDate(), + lastLogin: randomUpdatedDate(), + }, + { + id: 7, + name: randomTraderName(), + email: randomEmail(), + age: 18, + dateCreated: randomCreatedDate(), + lastLogin: randomUpdatedDate(), + }, + { + id: 8, + name: randomTraderName(), + email: randomEmail(), + age: 31, + dateCreated: randomCreatedDate(), + lastLogin: randomUpdatedDate(), + }, + { + id: 9, + name: randomTraderName(), + email: randomEmail(), + age: 24, + dateCreated: randomCreatedDate(), + lastLogin: randomUpdatedDate(), + }, + { + id: 10, + name: randomTraderName(), + email: randomEmail(), + age: 35, + dateCreated: randomCreatedDate(), + lastLogin: randomUpdatedDate(), + }, +]; diff --git a/docs/data/data-grid/column-pinning/ColumnPinningDynamicRowHeight.tsx.preview b/docs/data/data-grid/column-pinning/ColumnPinningDynamicRowHeight.tsx.preview new file mode 100644 index 000000000000..8cab3f590970 --- /dev/null +++ b/docs/data/data-grid/column-pinning/ColumnPinningDynamicRowHeight.tsx.preview @@ -0,0 +1,12 @@ + +
+ 'auto'} + initialState={{ pinnedColumns: { left: ['name'], right: ['actions'] } }} + /> +
\ No newline at end of file diff --git a/docs/data/data-grid/column-pinning/column-pinning.md b/docs/data/data-grid/column-pinning/column-pinning.md index adfbe360021e..1f9931265b04 100644 --- a/docs/data/data-grid/column-pinning/column-pinning.md +++ b/docs/data/data-grid/column-pinning/column-pinning.md @@ -77,6 +77,16 @@ To pin the checkbox column added when using `checkboxSelection`, add `GRID_CHECK {{"demo": "ColumnPinningWithCheckboxSelection.js", "disableAd": true, "bg": "inline"}} +## Usage with dynamic row height + +You can have both pinned columns and [dynamic row height](/x/react-data-grid/row-height/#dynamic-row-height) enabled at the same time. +However, if the rows change their content after the initial calculation, you may need to trigger a manual recalculation to avoid incorrect measurements. +You can do this by calling `apiRef.current.resetRowHeights()` every time that the content changes. + +The demo below contains an example of both features enabled: + +{{"demo": "ColumnPinningDynamicRowHeight.js", "disableAd": true, "bg": "inline"}} + ## apiRef {{"demo": "ColumnPinningApiNoSnap.js", "bg": "inline", "hideToolbar": true}} diff --git a/docs/pages/x/api/data-grid/grid-api.md b/docs/pages/x/api/data-grid/grid-api.md index 046966d80cee..c53c0a5b3984 100644 --- a/docs/pages/x/api/data-grid/grid-api.md +++ b/docs/pages/x/api/data-grid/grid-api.md @@ -71,6 +71,7 @@ import { GridApi } from '@mui/x-data-grid-pro'; | pinColumn [](/x/introduction/licensing/#pro-plan) | (field: string, side: GridPinnedPosition) => void | Pins a column to the left or right side of the grid. | | publishEvent | GridEventPublisher | Emits an event. | | removeRowGroupingCriteria [](https://mui.com/store/items/material-ui-premium/) | (groupingCriteriaField: string) => void | Remove the field from the row grouping model. | +| resetRowHeights | () => void | Forces the recalculation of the heights of all rows. | | resize | () => void | Triggers a resize of the component and recalculation of width and height. | | restoreState | (stateToRestore: InitialState) => void | Inject the given values into the state of the DataGrid. | | scroll | (params: Partial<GridScrollParams>) => void | Triggers the viewport to scroll to the given positions (in pixels). | diff --git a/packages/grid/x-data-grid-pro/src/components/DataGridProVirtualScroller.tsx b/packages/grid/x-data-grid-pro/src/components/DataGridProVirtualScroller.tsx index a691b282005f..7acc3f9041be 100644 --- a/packages/grid/x-data-grid-pro/src/components/DataGridProVirtualScroller.tsx +++ b/packages/grid/x-data-grid-pro/src/components/DataGridProVirtualScroller.tsx @@ -331,19 +331,21 @@ const DataGridProVirtualScroller = React.forwardRef< const detailPanels = getDetailPanels(); - const topPinnedRows = getRows({ renderContext, rows: topPinnedRowsData }); + const topPinnedRows = getRows({ renderContext, rows: topPinnedRowsData, position: 'center' }); const pinnedRowsHeight = calculatePinnedRowsHeight(apiRef); const mainRows = getRows({ renderContext, rowIndexOffset: topPinnedRowsData.length, + position: 'center', }); const bottomPinnedRows = getRows({ renderContext, rows: bottomPinnedRowsData, rowIndexOffset: topPinnedRowsData.length + (mainRows ? mainRows.length : 0), + position: 'center', }); const contentProps = getContentProps(); @@ -373,8 +375,8 @@ const DataGridProVirtualScroller = React.forwardRef< minFirstColumn: leftRenderContext.firstColumnIndex, maxLastColumn: leftRenderContext.lastColumnIndex, availableSpace: 0, - ignoreAutoHeight: true, rows: topPinnedRowsData, + position: 'left', })} )} @@ -394,9 +396,9 @@ const DataGridProVirtualScroller = React.forwardRef< renderContext: rightRenderContext, minFirstColumn: rightRenderContext.firstColumnIndex, maxLastColumn: rightRenderContext.lastColumnIndex, - ignoreAutoHeight: true, availableSpace: 0, rows: topPinnedRowsData, + position: 'right', })} )} @@ -415,8 +417,8 @@ const DataGridProVirtualScroller = React.forwardRef< minFirstColumn: leftRenderContext.firstColumnIndex, maxLastColumn: leftRenderContext.lastColumnIndex, availableSpace: 0, - ignoreAutoHeight: true, rowIndexOffset: topPinnedRowsData.length, + position: 'left', })} )} @@ -435,8 +437,8 @@ const DataGridProVirtualScroller = React.forwardRef< minFirstColumn: rightRenderContext.firstColumnIndex, maxLastColumn: rightRenderContext.lastColumnIndex, availableSpace: 0, - ignoreAutoHeight: true, rowIndexOffset: topPinnedRowsData.length, + position: 'right', })} )} @@ -463,9 +465,9 @@ const DataGridProVirtualScroller = React.forwardRef< minFirstColumn: leftRenderContext.firstColumnIndex, maxLastColumn: leftRenderContext.lastColumnIndex, availableSpace: 0, - ignoreAutoHeight: true, rows: bottomPinnedRowsData, rowIndexOffset: topPinnedRowsData.length + (mainRows ? mainRows.length : 0), + position: 'left', })} )} @@ -486,9 +488,9 @@ const DataGridProVirtualScroller = React.forwardRef< minFirstColumn: rightRenderContext.firstColumnIndex, maxLastColumn: rightRenderContext.lastColumnIndex, availableSpace: 0, - ignoreAutoHeight: true, rows: bottomPinnedRowsData, rowIndexOffset: topPinnedRowsData.length + (mainRows ? mainRows.length : 0), + position: 'right', })} )} diff --git a/packages/grid/x-data-grid-pro/src/tests/columnPinning.DataGridPro.test.tsx b/packages/grid/x-data-grid-pro/src/tests/columnPinning.DataGridPro.test.tsx index 3c9e7e5aa6bb..589bb4958b68 100644 --- a/packages/grid/x-data-grid-pro/src/tests/columnPinning.DataGridPro.test.tsx +++ b/packages/grid/x-data-grid-pro/src/tests/columnPinning.DataGridPro.test.tsx @@ -20,6 +20,7 @@ import { } from '@mui/monorepo/test/utils'; import { getCell, getColumnHeaderCell, getColumnHeadersTextContent } from 'test/utils/helperFn'; import { useData } from 'storybook/src/hooks/useData'; +import { getData } from 'storybook/src/data/data-service'; // TODO Move to utils // Fix https://github.com/mui/mui-x/pull/2085/files/058f56ac3c729b2142a9a28b79b5b13535cdb819#diff-db85480a519a5286d7341e9b8957844762cf04cdacd946331ebaaaff287482ec @@ -50,6 +51,39 @@ describe(' - Column pinning', () => { ); }; + function ResizeObserverMock( + callback: (entries: { borderBoxSize: [{ blockSize: number }] }[]) => void, + ) { + let timeout: NodeJS.Timeout; + + return { + observe: (element: HTMLElement) => { + // Simulates the async behavior of the native ResizeObserver + timeout = setTimeout(() => { + callback([{ borderBoxSize: [{ blockSize: element.clientHeight }] }]); + }); + }, + disconnect: () => { + clearTimeout(timeout); + }, + }; + } + + const originalResizeObserver = window.ResizeObserver; + + beforeEach(() => { + const { userAgent } = window.navigator; + + if (userAgent.includes('Chrome') && !userAgent.includes('Headless')) { + // Only use the mock in non-headless Chrome + window.ResizeObserver = ResizeObserverMock as any; + } + }); + + afterEach(() => { + window.ResizeObserver = originalResizeObserver; + }); + it('should scroll when the next cell to focus is covered by the left pinned columns', function test() { if (isJSDOM) { // Need layouting @@ -229,6 +263,97 @@ describe(' - Column pinning', () => { expect(getColumnHeadersTextContent()).to.deep.equal(['id', '', 'Currency Pair']); }); + describe('dynamic row height', () => { + it('should work with dynamic row height', async function test() { + if (isJSDOM) { + // Need layouting + this.skip(); + } + + const Test = ({ bioHeight }: { bioHeight: number }) => { + const data = React.useMemo(() => getData(1, 2), []); + + const columns = [ + ...data.columns, + { field: 'bio', renderCell: () =>
}, + ]; + + return ( + 'auto'} + initialState={{ pinnedColumns: { left: ['id'], right: ['bio'] } }} + /> + ); + }; + + render(); + await act(() => Promise.resolve()); + clock.runToLast(); + const leftRow = document.querySelector(`.${gridClasses['pinnedColumns--left']} [role="row"]`); + expect(leftRow).toHaveInlineStyle({ maxHeight: 'none', minHeight: '101px' }); + const centerRow = document.querySelector( + `.${gridClasses.virtualScrollerRenderZone} [role="row"]`, + ); + expect(centerRow).toHaveInlineStyle({ maxHeight: 'none', minHeight: '101px' }); + const rightRow = document.querySelector( + `.${gridClasses['pinnedColumns--right']} [role="row"]`, + ); + expect(rightRow).toHaveInlineStyle({ maxHeight: 'none', minHeight: '101px' }); + }); + + it('should react to content height changes', async function test() { + if (isJSDOM) { + // Need layouting + this.skip(); + } + + const Test = ({ bioHeight }: { bioHeight: number }) => { + const data = React.useMemo(() => getData(1, 2), []); + + const columns = [ + ...data.columns, + { field: 'bio', renderCell: () =>
}, + ]; + + return ( + 'auto'} + initialState={{ pinnedColumns: { left: ['id'], right: ['bio'] } }} + /> + ); + }; + + const { setProps } = render(); + await act(() => Promise.resolve()); + clock.runToLast(); + const centerRow = document.querySelector( + `.${gridClasses.virtualScrollerRenderZone} [role="row"]`, + ); + expect(centerRow).toHaveInlineStyle({ maxHeight: 'none', minHeight: '101px' }); + + setProps({ bioHeight: 200 }); + await act(() => Promise.resolve()); + clock.runToLast(); + expect(centerRow).toHaveInlineStyle({ maxHeight: 'none', minHeight: '201px' }); + + setProps({ bioHeight: 100 }); + await act(() => Promise.resolve()); + clock.runToLast(); + // If the new height is smaller than the current one, it won't be reflected unless + // apiRef.current.resetRowHeights() is called + expect(centerRow).toHaveInlineStyle({ maxHeight: 'none', minHeight: '201px' }); + + act(() => apiRef.current.resetRowHeights()); + await act(() => Promise.resolve()); + clock.runToLast(); + expect(centerRow).toHaveInlineStyle({ maxHeight: 'none', minHeight: '101px' }); + }); + }); + describe('prop: onPinnedColumnsChange', () => { it('should call when a column is pinned', () => { const handlePinnedColumnsChange = spy(); diff --git a/packages/grid/x-data-grid/src/components/GridRow.tsx b/packages/grid/x-data-grid/src/components/GridRow.tsx index 4dc39966e32c..2a3d2fbf9202 100644 --- a/packages/grid/x-data-grid/src/components/GridRow.tsx +++ b/packages/grid/x-data-grid/src/components/GridRow.tsx @@ -48,6 +48,7 @@ export interface GridRowProps { cellFocus: GridCellIdentifier | null; cellTabIndex: GridCellIdentifier | null; editRowsState: GridEditRowsModel; + position: 'left' | 'center' | 'right'; row?: GridRowModel; isLastVisible?: boolean; onClick?: React.MouseEventHandler; @@ -97,6 +98,7 @@ function GridRow(props: React.HTMLAttributes & GridRowProps) { row, index, style: styleProp, + position, rowHeight, className, visibleColumns, @@ -143,9 +145,9 @@ function GridRow(props: React.HTMLAttributes & GridRowProps) { React.useLayoutEffect(() => { if (rowHeight === 'auto' && ref.current && typeof ResizeObserver === 'undefined') { // Fallback for IE - apiRef.current.unstable_storeRowHeightMeasurement(rowId, ref.current.clientHeight); + apiRef.current.unstable_storeRowHeightMeasurement(rowId, ref.current.clientHeight, position); } - }, [apiRef, rowHeight, rowId]); + }, [apiRef, rowHeight, rowId, position]); React.useLayoutEffect(() => { if (currentPage.range) { @@ -173,13 +175,13 @@ function GridRow(props: React.HTMLAttributes & GridRowProps) { entry.borderBoxSize && entry.borderBoxSize.length > 0 ? entry.borderBoxSize[0].blockSize : entry.contentRect.height; - apiRef.current.unstable_storeRowHeightMeasurement(rowId, height); + apiRef.current.unstable_storeRowHeightMeasurement(rowId, height, position); }); resizeObserver.observe(rootElement); return () => resizeObserver.disconnect(); - }, [apiRef, currentPage.range, index, rowHeight, rowId]); + }, [apiRef, currentPage.range, index, rowHeight, rowId, position]); const publish = React.useCallback( ( @@ -365,14 +367,34 @@ function GridRow(props: React.HTMLAttributes & GridRowProps) { ], ); + const sizes = apiRef.current.unstable_getRowInternalSizes(rowId); + + let minHeight = rowHeight; + if (minHeight === 'auto' && sizes) { + let numberOfBaseSizes = 0; + const maximumSize = Object.entries(sizes).reduce((acc, [key, size]) => { + const isBaseHeight = /^base[A-Z]/.test(key); + if (!isBaseHeight) { + return acc; + } + numberOfBaseSizes += 1; + if (size > acc) { + return size; + } + return acc; + }, 0); + + if (maximumSize > 0 && numberOfBaseSizes > 1) { + minHeight = maximumSize; + } + } + const style = { ...styleProp, maxHeight: rowHeight === 'auto' ? 'none' : rowHeight, // max-height doesn't support "auto" - minHeight: rowHeight, + minHeight, }; - const sizes = apiRef.current.unstable_getRowInternalSizes(rowId); - if (sizes?.spacingTop) { const property = rootProps.rowSpacingType === 'border' ? 'borderTopWidth' : 'marginTop'; style[property] = sizes.spacingTop; @@ -484,6 +506,7 @@ GridRow.propTypes = { index: PropTypes.number.isRequired, isLastVisible: PropTypes.bool, lastColumnToRender: PropTypes.number.isRequired, + position: PropTypes.oneOf(['center', 'left', 'right']).isRequired, renderedColumns: PropTypes.arrayOf(PropTypes.object).isRequired, row: PropTypes.object, rowHeight: PropTypes.oneOfType([PropTypes.oneOf(['auto']), PropTypes.number]).isRequired, diff --git a/packages/grid/x-data-grid/src/hooks/features/rows/useGridRowsMeta.ts b/packages/grid/x-data-grid/src/hooks/features/rows/useGridRowsMeta.ts index 3174e6ed9cbf..d848f746098b 100644 --- a/packages/grid/x-data-grid/src/hooks/features/rows/useGridRowsMeta.ts +++ b/packages/grid/x-data-grid/src/hooks/features/rows/useGridRowsMeta.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { debounce } from '@mui/material/utils'; +import { debounce, capitalize } from '@mui/material/utils'; import { GridApiCommunity } from '../../../models/api/gridApiCommunity'; import { GridRowsMetaApi } from '../../../models/api/gridRowsMetaApi'; import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; @@ -69,7 +69,7 @@ export const useGridRowsMeta = ( const calculateRowProcessedSizes = (row: GridRowEntry) => { if (!rowsHeightLookup.current[row.id]) { rowsHeightLookup.current[row.id] = { - sizes: { base: rowHeightFromDensity }, + sizes: { baseCenter: rowHeightFromDensity }, isResized: false, autoHeight: false, needsFirstMeasurement: true, // Assume all rows will need to be measured by default @@ -78,7 +78,7 @@ export const useGridRowsMeta = ( const { isResized, needsFirstMeasurement, sizes } = rowsHeightLookup.current[row.id]; let baseRowHeight = rowHeightFromDensity; - const existingBaseRowHeight = sizes.base; + const existingBaseRowHeight = sizes.baseCenter; if (isResized) { // Do not recalculate resized row height and use the value from the lookup @@ -110,8 +110,21 @@ export const useGridRowsMeta = ( rowsHeightLookup.current[row.id].needsFirstMeasurement = false; } + const existingBaseSizes = Object.entries(sizes).reduce>( + (acc, [key, size]) => { + if (/^base[A-Z]/.test(key)) { + acc[key] = size; + } + return acc; + }, + {}, + ); + // We use an object to make simple to check if a height is already added or not - const initialHeights: Record = { base: baseRowHeight }; + const initialHeights: Record = { + ...existingBaseSizes, + baseCenter: baseRowHeight, + }; if (getRowSpacing) { const indexRelativeToCurrentPage = apiRef.current.getRowIndexRelativeToVisibleRows(row.id); @@ -142,10 +155,19 @@ export const useGridRowsMeta = ( const currentPageTotalHeight = currentPage.rows.reduce((acc, row) => { positions.push(acc); + let maximumBaseSize = 0; + let otherSizes = 0; + const processedSizes = calculateRowProcessedSizes(row); + Object.entries(processedSizes).forEach(([size, value]) => { + if (/^base[A-Z]/.test(size)) { + maximumBaseSize = value > maximumBaseSize ? value : maximumBaseSize; + } else { + otherSizes += value; + } + }); - const finalRowHeight = Object.values(processedSizes).reduce((acc2, value) => acc2 + value, 0); - return acc + finalRowHeight; + return acc + maximumBaseSize + otherSizes; }, 0); pinnedRows?.top?.forEach((row) => { @@ -185,7 +207,7 @@ export const useGridRowsMeta = ( const getRowHeight = React.useCallback( (rowId) => { const height = rowsHeightLookup.current[rowId]; - return height ? height.sizes.base : rowHeightFromDensity; + return height ? height.sizes.baseCenter : rowHeightFromDensity; }, [rowHeightFromDensity], ); @@ -195,7 +217,7 @@ export const useGridRowsMeta = ( const setRowHeight = React.useCallback( (id: GridRowId, height: number) => { - rowsHeightLookup.current[id].sizes.base = height; + rowsHeightLookup.current[id].sizes.baseCenter = height; rowsHeightLookup.current[id].isResized = true; rowsHeightLookup.current[id].needsFirstMeasurement = false; hydrateRowsMeta(); @@ -211,16 +233,17 @@ export const useGridRowsMeta = ( const storeMeasuredRowHeight = React.useCallback< GridRowsMetaApi['unstable_storeRowHeightMeasurement'] >( - (id, height) => { + (id, height, position) => { if (!rowsHeightLookup.current[id] || !rowsHeightLookup.current[id].autoHeight) { return; } // Only trigger hydration if the value is different, otherwise we trigger a loop - const needsHydration = rowsHeightLookup.current[id].sizes.base !== height; + const needsHydration = + rowsHeightLookup.current[id].sizes[`base${capitalize(position)}`] !== height; rowsHeightLookup.current[id].needsFirstMeasurement = false; - rowsHeightLookup.current[id].sizes.base = height; + rowsHeightLookup.current[id].sizes[`base${capitalize(position)}`] = height; if (needsHydration) { debouncedHydrateRowsMeta(); @@ -247,6 +270,11 @@ export const useGridRowsMeta = ( } }, []); + const resetRowHeights = React.useCallback(() => { + rowsHeightLookup.current = {}; + hydrateRowsMeta(); + }, [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(() => { @@ -263,6 +291,7 @@ export const useGridRowsMeta = ( unstable_getRowInternalSizes: getRowInternalSizes, unstable_setRowHeight: setRowHeight, unstable_storeRowHeightMeasurement: storeMeasuredRowHeight, + resetRowHeights, }; useGridApiMethod(apiRef, rowsMetaApi, 'GridRowsMetaApi'); diff --git a/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx b/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx index a93119b8fcff..4b720107f2cc 100644 --- a/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx +++ b/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx @@ -377,10 +377,10 @@ export const useGridVirtualScroller = (props: UseGridVirtualScrollerProps) => { const getRows = ( params: { renderContext: GridRenderContext | null; + position?: string; minFirstColumn?: number; maxLastColumn?: number; availableSpace?: number | null; - ignoreAutoHeight?: boolean; rows?: GridRowEntry[]; rowIndexOffset?: number; } = { renderContext }, @@ -390,8 +390,8 @@ export const useGridVirtualScroller = (props: UseGridVirtualScrollerProps) => { minFirstColumn = renderZoneMinColumnIndex, maxLastColumn = renderZoneMaxColumnIndex, availableSpace = containerWidth, - ignoreAutoHeight, rowIndexOffset = 0, + position = 'center', } = params; if (!nextRenderContext || availableSpace == null) { @@ -461,10 +461,9 @@ export const useGridVirtualScroller = (props: UseGridVirtualScrollerProps) => { for (let i = 0; i < renderedRows.length; i += 1) { const { id, model } = renderedRows[i]; const lastVisibleRowIndex = firstRowToRender + i === currentPage.rows.length - 1; - const baseRowHeight = - !apiRef.current.unstable_rowHasAutoHeight(id) || ignoreAutoHeight - ? apiRef.current.unstable_getRowHeight(id) - : 'auto'; + const baseRowHeight = !apiRef.current.unstable_rowHasAutoHeight(id) + ? apiRef.current.unstable_getRowHeight(id) + : 'auto'; let isSelected: boolean; if (selectedRowsLookup[id] == null) { @@ -490,6 +489,7 @@ export const useGridVirtualScroller = (props: UseGridVirtualScrollerProps) => { index={rowIndexOffset + (currentPage?.range?.firstRowIndex || 0) + firstRowToRender + i} containerWidth={availableSpace} isLastVisible={lastVisibleRowIndex} + position={position} {...(typeof getRowProps === 'function' ? getRowProps(id, model) : {})} {...rootProps.componentsProps?.row} />, diff --git a/packages/grid/x-data-grid/src/models/api/gridRowsMetaApi.ts b/packages/grid/x-data-grid/src/models/api/gridRowsMetaApi.ts index 6917d286b626..8adf0b0236c8 100644 --- a/packages/grid/x-data-grid/src/models/api/gridRowsMetaApi.ts +++ b/packages/grid/x-data-grid/src/models/api/gridRowsMetaApi.ts @@ -29,9 +29,14 @@ export interface GridRowsMetaApi { * Stores the row height measurement and triggers an hydration, if needed. * @param {GridRowId} id The id of the row. * @param {number} height The new height. + * @param {string} position The position to it the row belongs to. * @ignore - do not document. */ - unstable_storeRowHeightMeasurement: (id: GridRowId, height: number) => void; + unstable_storeRowHeightMeasurement: ( + id: GridRowId, + height: number, + position: 'left' | 'center' | 'right', + ) => void; /** * Determines if the height of a row is "auto". * @ignore - do not document. @@ -49,4 +54,8 @@ export interface GridRowsMetaApi { * @ignore - do not document. */ unstable_setLastMeasuredRowIndex: (index: number) => void; + /** + * Forces the recalculation of the heights of all rows. + */ + resetRowHeights: () => void; }