Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DataGrid] Refactor: row virtualization & rendering #12247

Merged
merged 11 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Comment on lines 37 to +40
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've introduced the "virtual" term to refer to out-of-viewport focused cells that need to be rendered to keep their DOM state alive.

}

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
Loading