Skip to content

Commit

Permalink
[DataGrid] Refactor: row virtualization & rendering (mui#12247)
Browse files Browse the repository at this point in the history
  • Loading branch information
romgrk authored and thomasmoon committed Sep 6, 2024
1 parent 7891662 commit 414bc3a
Show file tree
Hide file tree
Showing 18 changed files with 230 additions and 295 deletions.
15 changes: 14 additions & 1 deletion packages/x-data-grid-pro/src/components/GridPinnedRows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getDataGridUtilityClass, gridClasses, useGridSelector } from '@mui/x-da
import {
GridPinnedRowsProps,
gridPinnedRowsSelector,
gridRenderContextSelector,
useGridPrivateApiContext,
} from '@mui/x-data-grid/internals';

Expand All @@ -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 (
Expand Down
117 changes: 65 additions & 52 deletions packages/x-data-grid/src/components/GridRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -33,6 +33,7 @@ import { PinnedPosition } from './cell/GridCell';
import { GridScrollbarFillerCell as ScrollbarFiller } from './GridScrollbarFillerCell';

export interface GridRowProps extends React.HTMLAttributes<HTMLDivElement> {
row: GridRowModel;
rowId: GridRowId;
selected: boolean;
/**
Expand All @@ -41,28 +42,25 @@ export interface GridRowProps extends React.HTMLAttributes<HTMLDivElement> {
*/
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<HTMLDivElement>;
onDoubleClick?: React.MouseEventHandler<HTMLDivElement>;
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
Expand Down Expand Up @@ -121,15 +119,14 @@ const GridRow = React.forwardRef<HTMLDivElement, GridRowProps>(function GridRow(
rowHeight,
className,
visibleColumns,
renderedColumns,
pinnedColumns,
offsets,
offsetTop,
offsetLeft,
dimensions,
firstColumnToRender,
lastColumnToRender,
renderContext,
focusedColumnIndex,
isFirstVisible,
isLastVisible,
focusedCellColumnIndexNotInRange,
isNotVisible,
focusedCell,
tabbableCell,
Expand All @@ -154,6 +151,16 @@ const GridRow = React.forwardRef<HTMLDivElement, GridRowProps>(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 = {
Expand Down Expand Up @@ -354,10 +361,13 @@ const GridRow = React.forwardRef<HTMLDivElement, GridRowProps>(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
Expand All @@ -373,13 +383,12 @@ const GridRow = React.forwardRef<HTMLDivElement, GridRowProps>(function GridRow(
scrollbarWidth;
break;
case PinnedPosition.NONE:
case PinnedPosition.VIRTUAL:
pinnedOffset = 0;
break;
}

if (rowNode?.type === 'skeletonRow') {
const { width } = cellColSpanInfo.cellProps;

return (
<slots.skeletonCell
key={column.field}
Expand All @@ -391,8 +400,6 @@ const GridRow = React.forwardRef<HTMLDivElement, GridRowProps>(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
Expand All @@ -405,13 +412,7 @@ const GridRow = React.forwardRef<HTMLDivElement, GridRowProps>(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 (
<slots.cell
Expand Down Expand Up @@ -468,21 +469,33 @@ const GridRow = React.forwardRef<HTMLDivElement, GridRowProps>(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
Expand Down Expand Up @@ -517,7 +530,7 @@ const GridRow = React.forwardRef<HTMLDivElement, GridRowProps>(function GridRow(
<div
role="presentation"
className={gridClasses.cellOffsetLeft}
style={{ width: offsets.left }}
style={{ width: offsetLeft }}
/>
{cells}
{emptyCellWidth > 0 && <EmptyCell width={emptyCellWidth} />}
Expand Down Expand Up @@ -568,33 +581,33 @@ 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.
*/
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,
Expand Down
3 changes: 2 additions & 1 deletion packages/x-data-grid/src/components/cell/GridCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export enum PinnedPosition {
NONE,
LEFT,
RIGHT,
VIRTUAL,
}

export type GridCellProps = {
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const useUtilityClasses = () => {

const Element = styled('div')({
position: 'sticky',
zIndex: 2,
zIndex: 4,
bottom: 'calc(var(--DataGrid-hasScrollX) * var(--DataGrid-scrollbarSize))',
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const useUtilityClasses = () => {

const Element = styled('div')({
position: 'sticky',
zIndex: 2,
zIndex: 4,
top: 0,
'&::after': {
content: '" "',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -39,15 +40,19 @@ 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 (
<VirtualScrollerRenderZoneRoot
ref={ref}
className={clsx(classes.root, className)}
ownerState={rootProps}
style={{
transform: `translate3d(0, ${offsets.top}px, 0)`,
transform: `translate3d(0, ${offsetTop}px, 0)`,
}}
{...other}
/>
Expand Down
Loading

0 comments on commit 414bc3a

Please sign in to comment.