From ba15dcd37129f2dd66f486462589558e5c9cbf72 Mon Sep 17 00:00:00 2001 From: Matheus Wichman Date: Tue, 19 Oct 2021 16:25:00 -0300 Subject: [PATCH] [DataGrid] New virtualization implementation (#2673) --- .../pages/api-docs/data-grid/data-grid-pro.md | 6 - docs/pages/api-docs/data-grid/data-grid.md | 5 - docs/scripts/generateProptypes.ts | 1 + .../components/data-grid/events/events.json | 6 +- .../grid/_modules_/grid/GridComponentProps.ts | 12 - .../grid/components/GridRenderingZone.tsx | 59 --- .../_modules_/grid/components/GridRow.tsx | 92 ++-- .../grid/components/GridScrollArea.tsx | 7 +- .../grid/components/GridStickyContainer.tsx | 60 --- .../grid/components/GridViewport.tsx | 107 ----- .../grid/components/GridVirtualScroller.tsx | 344 ++++++++++++++ .../grid/components/base/GridBody.tsx | 75 +++- .../grid/components/cell/GridCell.tsx | 6 +- .../grid/components/cell/GridEmptyCell.tsx | 59 --- .../_modules_/grid/components/cell/index.ts | 1 - .../columnHeaders/GridColumnHeaderItem.tsx | 60 +-- .../columnHeaders/GridColumnHeaders.tsx | 179 ++++++-- .../GridColumnHeadersItemCollection.tsx | 2 +- .../containers/GridDataContainer.tsx | 40 -- .../components/containers/GridOverlay.tsx | 11 +- .../components/containers/GridRootStyles.ts | 30 +- .../grid/components/containers/GridWindow.tsx | 93 ---- .../grid/components/containers/index.ts | 2 - .../grid/_modules_/grid/components/index.ts | 3 - .../grid/constants/eventsConstants.ts | 4 - packages/grid/_modules_/grid/gridClasses.ts | 29 +- .../columnReorder/useGridColumnReorder.tsx | 3 +- .../features/export/useGridPrintExport.tsx | 14 +- .../infiniteLoader/useGridInfiniteLoader.ts | 50 +-- .../hooks/features/scroll/useGridScroll.ts | 18 +- .../virtualization/useGridNoVirtualization.ts | 121 ----- .../virtualization/useGridVirtualization.ts | 418 ------------------ .../grid/_modules_/grid/models/api/gridApi.ts | 2 - .../api/gridDisableVirtualizationApi.ts | 5 + .../grid/models/api/gridVirtualizationApi.ts | 26 -- .../grid/_modules_/grid/models/api/index.ts | 1 - .../_modules_/grid/models/gridOptions.tsx | 24 +- .../params/gridViewportRowsChangeParams.ts | 13 - .../_modules_/grid/models/params/index.ts | 1 - packages/grid/data-grid/src/DataGrid.tsx | 19 +- packages/grid/data-grid/src/DataGridProps.ts | 1 - .../src/tests/keyboard.DataGrid.test.tsx | 6 +- .../src/tests/layout.DataGrid.test.tsx | 12 +- .../src/tests/pagination.DataGrid.test.tsx | 4 +- .../data-grid/src/useDataGridComponent.tsx | 4 - .../grid/data-grid/src/useDataGridProps.ts | 1 - packages/grid/x-grid/src/DataGridPro.tsx | 26 +- .../tests/columnHeaders.DataGridPro.test.tsx | 4 +- .../src/tests/events.DataGridPro.test.tsx | 43 +- .../src/tests/rows.DataGridPro.test.tsx | 200 +++++---- .../x-grid/src/useDataGridProComponent.tsx | 4 - test/e2e/index.test.ts | 6 +- 52 files changed, 884 insertions(+), 1435 deletions(-) delete mode 100644 packages/grid/_modules_/grid/components/GridRenderingZone.tsx delete mode 100644 packages/grid/_modules_/grid/components/GridStickyContainer.tsx delete mode 100644 packages/grid/_modules_/grid/components/GridViewport.tsx create mode 100644 packages/grid/_modules_/grid/components/GridVirtualScroller.tsx delete mode 100644 packages/grid/_modules_/grid/components/cell/GridEmptyCell.tsx delete mode 100644 packages/grid/_modules_/grid/components/containers/GridDataContainer.tsx delete mode 100644 packages/grid/_modules_/grid/components/containers/GridWindow.tsx delete mode 100644 packages/grid/_modules_/grid/hooks/features/virtualization/useGridNoVirtualization.ts delete mode 100644 packages/grid/_modules_/grid/hooks/features/virtualization/useGridVirtualization.ts delete mode 100644 packages/grid/_modules_/grid/models/api/gridVirtualizationApi.ts delete mode 100644 packages/grid/_modules_/grid/models/params/gridViewportRowsChangeParams.ts diff --git a/docs/pages/api-docs/data-grid/data-grid-pro.md b/docs/pages/api-docs/data-grid/data-grid-pro.md index 1fce187f4944..5ee6cc69f017 100644 --- a/docs/pages/api-docs/data-grid/data-grid-pro.md +++ b/docs/pages/api-docs/data-grid/data-grid-pro.md @@ -98,7 +98,6 @@ The name MuiDataGrid can be used when providing [default props](/cu | onRowsScrollEnd | (params: GridRowScrollEndParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void | | Callback fired when scrolling to the bottom of the grid viewport. | | onSelectionModelChange | (model: GridSelectionModel) => void | | Callback fired when the selection state of one or multiple rows changes. | | onSortModelChange | (model: GridSortModel) => void | | Callback fired when the sort model changes before a column is sorted. | -| onViewportRowsChange | (params: GridViewportRowsChangeParams) => void | | Callback fired when the rows in the viewport change. | | page | number | 0 | The zero-based index of the current page. | | pageSize | number | 100 | Set the number of rows in one page. | | pagination | boolean | false | If `true`, pagination is enabled. | @@ -186,7 +185,6 @@ You can use the [slots API](/components/data-grid/components/#overriding-compone | columnSeparator--resizable | .MuiDataGrid-columnSeparator--resizable | Styles applied to the column header separator if the column is resizable. | | columnSeparator--resizing | .MuiDataGrid-columnSeparator--resizing | Styles applied to the column header separator if the column is being resized. | | columnSeparator | .MuiDataGrid-columnSeparator | Styles applied to the column header separator element. | -| dataContainer | .MuiDataGrid-dataContainer | Styles applied to the data container element. | | editBooleanCell | .MuiDataGrid-editBooleanCell | Styles applied to root of the boolean edit component. | | editInputCell | .MuiDataGrid-editInputCell | Styles applied to the root of the input component. | | filterIcon | .MuiDataGrid-filterIcon | Styles applied to the filter icon element. | @@ -198,7 +196,6 @@ You can use the [slots API](/components/data-grid/components/#overriding-compone | menuIconButton | .MuiDataGrid-menuIconButton | Styles applied to the menu icon button element. | | menuOpen | .MuiDataGrid-menuOpen | Styles applied to the menu icon element if the menu is open. | | overlay | .MuiDataGrid-overlay | Styles applied to the overlay element. | -| renderingZone | .MuiDataGrid-renderingZone | Styles applied to the rendering zone element. | | root | .MuiDataGrid-root | Styles applied to the root element. | | row--editable | .MuiDataGrid-row--editable | Styles applied to the row element if the row is editable. | | row--editing | .MuiDataGrid-row--editing | Styles applied to the row element if the row is in edit mode. | @@ -210,9 +207,6 @@ You can use the [slots API](/components/data-grid/components/#overriding-compone | selectedRowCount | .MuiDataGrid-selectedRowCount | Styles applied to the footer selected row count element. | | sortIcon | .MuiDataGrid-sortIcon | Styles applied to the sort icon element. | | toolbarContainer | .MuiDataGrid-toolbarContainer | Styles applied to the toolbar container element. | -| viewport | .MuiDataGrid-viewport | Styles applied to the viewport element. | -| window | .MuiDataGrid-window | Styles applied to the window element. | -| windowContainer | .MuiDataGrid-windowContainer | Styles applied to the window container element. | | withBorder | .MuiDataGrid-withBorder | Styles applied to both the cell and the column header if `showColumnRightBorder={true}`. | You can override the style of the component thanks to one of these customization points: diff --git a/docs/pages/api-docs/data-grid/data-grid.md b/docs/pages/api-docs/data-grid/data-grid.md index ea77c6a198cb..608541681434 100644 --- a/docs/pages/api-docs/data-grid/data-grid.md +++ b/docs/pages/api-docs/data-grid/data-grid.md @@ -174,7 +174,6 @@ You can use the [slots API](/components/data-grid/components/#overriding-compone | columnSeparator--resizable | .MuiDataGrid-columnSeparator--resizable | Styles applied to the column header separator if the column is resizable. | | columnSeparator--resizing | .MuiDataGrid-columnSeparator--resizing | Styles applied to the column header separator if the column is being resized. | | columnSeparator | .MuiDataGrid-columnSeparator | Styles applied to the column header separator element. | -| dataContainer | .MuiDataGrid-dataContainer | Styles applied to the data container element. | | editBooleanCell | .MuiDataGrid-editBooleanCell | Styles applied to root of the boolean edit component. | | editInputCell | .MuiDataGrid-editInputCell | Styles applied to the root of the input component. | | filterIcon | .MuiDataGrid-filterIcon | Styles applied to the filter icon element. | @@ -186,7 +185,6 @@ You can use the [slots API](/components/data-grid/components/#overriding-compone | menuIconButton | .MuiDataGrid-menuIconButton | Styles applied to the menu icon button element. | | menuOpen | .MuiDataGrid-menuOpen | Styles applied to the menu icon element if the menu is open. | | overlay | .MuiDataGrid-overlay | Styles applied to the overlay element. | -| renderingZone | .MuiDataGrid-renderingZone | Styles applied to the rendering zone element. | | root | .MuiDataGrid-root | Styles applied to the root element. | | row--editable | .MuiDataGrid-row--editable | Styles applied to the row element if the row is editable. | | row--editing | .MuiDataGrid-row--editing | Styles applied to the row element if the row is in edit mode. | @@ -198,9 +196,6 @@ You can use the [slots API](/components/data-grid/components/#overriding-compone | selectedRowCount | .MuiDataGrid-selectedRowCount | Styles applied to the footer selected row count element. | | sortIcon | .MuiDataGrid-sortIcon | Styles applied to the sort icon element. | | toolbarContainer | .MuiDataGrid-toolbarContainer | Styles applied to the toolbar container element. | -| viewport | .MuiDataGrid-viewport | Styles applied to the viewport element. | -| window | .MuiDataGrid-window | Styles applied to the window element. | -| windowContainer | .MuiDataGrid-windowContainer | Styles applied to the window container element. | | withBorder | .MuiDataGrid-withBorder | Styles applied to both the cell and the column header if `showColumnRightBorder={true}`. | You can override the style of the component thanks to one of these customization points: diff --git a/docs/scripts/generateProptypes.ts b/docs/scripts/generateProptypes.ts index 6458b73d7774..f8b149eee8b6 100644 --- a/docs/scripts/generateProptypes.ts +++ b/docs/scripts/generateProptypes.ts @@ -26,6 +26,7 @@ async function generateProptypes(program: ttp.ts.Program, sourceFile: string) { 'renderedColumns', 'scrollBarState', 'renderState', + 'visibleColumns', 'cellFocus', 'cellTabIndex', 'csvOptions', diff --git a/docs/src/pages/components/data-grid/events/events.json b/docs/src/pages/components/data-grid/events/events.json index 45a4adc4d394..18f2075a12aa 100644 --- a/docs/src/pages/components/data-grid/events/events.json +++ b/docs/src/pages/components/data-grid/events/events.json @@ -147,9 +147,5 @@ "name": "stateChange", "description": "Fired when the state of the grid is updated. Called with a GridState object." }, - { "name": "unmount", "description": "Fired when the grid is unmounted." }, - { - "name": "viewportRowsChange", - "description": "Fired when the rows in the viewport is changed. Called with a GridViewportRowsChange object." - } + { "name": "unmount", "description": "Fired when the grid is unmounted." } ] diff --git a/packages/grid/_modules_/grid/GridComponentProps.ts b/packages/grid/_modules_/grid/GridComponentProps.ts index 055bea4c5df0..81184293edbb 100644 --- a/packages/grid/_modules_/grid/GridComponentProps.ts +++ b/packages/grid/_modules_/grid/GridComponentProps.ts @@ -27,7 +27,6 @@ import { GridRowParams } from './models/params/gridRowParams'; import { GridColumnOrderChangeParams } from './models/params/gridColumnOrderChangeParams'; import { GridColumnResizeParams } from './models/params/gridColumnResizeParams'; import { GridColumnVisibilityChangeParams } from './models/params/gridColumnVisibilityChangeParams'; -import { GridViewportRowsChangeParams } from './models/params/gridViewportRowsChangeParams'; import { GridSlotsComponentsProps } from './models/gridSlotsComponentsProps'; import { GridClasses } from './gridClasses'; import { GridCallbackDetails } from './models/api/gridCallbackDetails'; @@ -375,17 +374,6 @@ interface GridComponentOtherProps { * @internal */ onStateChange?: (state: GridState, event: MuiEvent<{}>, details: GridCallbackDetails) => void; - /** - * Callback fired when the rows in the viewport change. - * @param {GridViewportRowsChangeParams} params The viewport params. - * @param {MuiEvent<{}>} event The event object. - * @param {GridCallbackDetails} details Additional details for this callback. - */ - onViewportRowsChange?: ( - params: GridViewportRowsChangeParams, - event: MuiEvent<{}>, - details: GridCallbackDetails, - ) => void; /** * The zero-based index of the current page. * @default 0 diff --git a/packages/grid/_modules_/grid/components/GridRenderingZone.tsx b/packages/grid/_modules_/grid/components/GridRenderingZone.tsx deleted file mode 100644 index 90a98c66a012..000000000000 --- a/packages/grid/_modules_/grid/components/GridRenderingZone.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import * as React from 'react'; -import PropTypes from 'prop-types'; -import { ElementSize } from '../models'; -import { getDataGridUtilityClass } from '../gridClasses'; -import { useGridRootProps } from '../hooks/utils/useGridRootProps'; -import { composeClasses } from '../utils/material-ui-utils'; -import { GridComponentProps } from '../GridComponentProps'; - -type WithChildren = { children?: React.ReactNode }; - -type OwnerState = { classes: GridComponentProps['classes'] }; - -const useUtilityClasses = (ownerState: OwnerState) => { - const { classes } = ownerState; - - const slots = { - root: ['renderingZone'], - }; - - return composeClasses(slots, getDataGridUtilityClass, classes); -}; - -const GridRenderingZone = React.forwardRef( - function GridRenderingZone(props, ref) { - const { height, width, children } = props; - const rootProps = useGridRootProps(); - const ownerState = { classes: rootProps.classes }; - const classes = useUtilityClasses(ownerState); - return ( -
- {children} -
- ); - }, -); - -GridRenderingZone.propTypes = { - // ----------------------------- Warning -------------------------------- - // | These PropTypes are generated from the TypeScript type definitions | - // | To update them edit the TypeScript types and run "yarn proptypes" | - // ---------------------------------------------------------------------- - /** - * The height of a container or HTMLElement. - */ - height: PropTypes.number.isRequired, - /** - * The width of a container or HTMLElement. - */ - width: PropTypes.number.isRequired, -} as any; - -export { GridRenderingZone }; diff --git a/packages/grid/_modules_/grid/components/GridRow.tsx b/packages/grid/_modules_/grid/components/GridRow.tsx index 96f5087f1ebc..86625af02511 100644 --- a/packages/grid/_modules_/grid/components/GridRow.tsx +++ b/packages/grid/_modules_/grid/components/GridRow.tsx @@ -11,31 +11,32 @@ import { composeClasses } from '../utils/material-ui-utils'; import { getDataGridUtilityClass, gridClasses } from '../gridClasses'; import { useGridRootProps } from '../hooks/utils/useGridRootProps'; import { GridComponentProps } from '../GridComponentProps'; +import { GridStateColDef } from '../models/colDef/gridColDef'; import { GridCellIdentifier } from '../hooks/features/focus/gridFocusState'; import { GridScrollBarState } from '../models/gridContainerProps'; -import { GridStateColDef } from '../models/colDef/gridColDef'; -import { GridEmptyCell } from './cell/GridEmptyCell'; -import { GridRenderingState } from '../hooks/features/virtualization/renderingState'; +import { gridColumnsMetaSelector } from '../hooks/features/columns/gridColumnsSelector'; +import { useGridSelector } from '../hooks/utils/useGridSelector'; export interface GridRowProps { - id: GridRowId; + rowId: GridRowId; selected: boolean; index: number; rowHeight: number; + containerWidth: number; row: GridRowData; - renderState: GridRenderingState; firstColumnToRender: number; + lastColumnToRender: number; + visibleColumns: GridStateColDef[]; renderedColumns: GridStateColDef[]; - children: React.ReactNode; cellFocus: GridCellIdentifier | null; cellTabIndex: GridCellIdentifier | null; - editRowsModel: GridEditRowsModel; + editRowsState: GridEditRowsModel; scrollBarState: GridScrollBarState; onClick?: React.MouseEventHandler; onDoubleClick?: React.MouseEventHandler; } -type OwnerState = GridRowProps & { +type OwnerState = Pick & { editable: boolean; editing: boolean; classes?: GridComponentProps['classes']; @@ -51,33 +52,47 @@ const useUtilityClasses = (ownerState: OwnerState) => { return composeClasses(slots, getDataGridUtilityClass, classes); }; -function GridRow(props: GridRowProps) { +const EmptyCell = ({ width, height }) => { + if (!width || !height) { + return null; + } + + const style = { width, height }; + + return
; // TODO change to .MuiDataGrid-emptyCell or .MuiDataGrid-rowFiller +}; + +function GridRow(props: React.HTMLAttributes & GridRowProps) { const { selected, - id, + rowId, row, index, + style: styleProp, rowHeight, + className, + visibleColumns, renderedColumns, + containerWidth, firstColumnToRender, - children, + lastColumnToRender, cellFocus, cellTabIndex, - editRowsModel, + editRowsState, scrollBarState, // to be removed - renderState, // to be removed onClick, onDoubleClick, ...other } = props; - const ariaRowIndex = index + 2; // 1 for the header row and 1 as it's 1 based + const ariaRowIndex = index + 2; // 1 for the header row and 1 as it's 1-based const apiRef = useGridApiContext(); const rootProps = useGridRootProps(); + const columnsMeta = useGridSelector(apiRef, gridColumnsMetaSelector); const ownerState = { - ...props, + selected, classes: rootProps.classes, - editing: apiRef.current.getRowMode(id) === GridRowModes.Edit, + editing: apiRef.current.getRowMode(rowId) === GridRowModes.Edit, editable: rootProps.editMode === GridEditModes.Row, }; @@ -96,27 +111,28 @@ function GridRow(props: GridRowProps) { } // The row might have been deleted - if (!apiRef.current.getRow(id)) { + if (!apiRef.current.getRow(rowId)) { return; } - apiRef.current.publishEvent(eventName, apiRef.current.getRowParams(id), event); + apiRef.current.publishEvent(eventName, apiRef.current.getRowParams(rowId), event); if (propHandler) { propHandler(event); } }, - [apiRef, id], + [apiRef, rowId], ); const style = { maxHeight: rowHeight, minHeight: rowHeight, + ...styleProp, }; const rowClassName = typeof rootProps.getRowClassName === 'function' && - rootProps.getRowClassName(apiRef.current.getRowParams(id)); + rootProps.getRowClassName(apiRef.current.getRowParams(rowId)); const cells: JSX.Element[] = []; @@ -124,14 +140,14 @@ function GridRow(props: GridRowProps) { const column = renderedColumns[i]; const indexRelativeToAllColumns = firstColumnToRender + i; - const isLastColumn = indexRelativeToAllColumns === renderedColumns.length - 1; + const isLastColumn = indexRelativeToAllColumns === visibleColumns.length - 1; const removeLastBorderRight = isLastColumn && scrollBarState.hasScrollX && !scrollBarState.hasScrollY; const showRightBorder = !isLastColumn ? rootProps.showCellRightBorder : !removeLastBorderRight && rootProps.disableExtendRowFullWidth; - const cellParams = apiRef.current.getCellParams(id, column.field); + const cellParams = apiRef.current.getCellParams(rowId, column.field); const classNames: string[] = []; @@ -145,7 +161,7 @@ function GridRow(props: GridRowProps) { ); } - const editCellState = editRowsModel[id] && editRowsModel[id][column.field]; + const editCellState = editRowsState[rowId] ? editRowsState[rowId][column.field] : null; let content: React.ReactNode = null; if (editCellState == null && column.renderCell) { @@ -168,11 +184,12 @@ function GridRow(props: GridRowProps) { classNames.push(rootProps.getCellClassName(cellParams)); } - const hasFocus = cellFocus !== null && cellFocus.id === id && cellFocus.field === column.field; + const hasFocus = + cellFocus !== null && cellFocus.id === rowId && cellFocus.field === column.field; const tabIndex = cellTabIndex !== null && - cellTabIndex.id === id && + cellTabIndex.id === rowId && cellTabIndex.field === column.field && cellParams.cellMode === 'view' ? 0 @@ -180,11 +197,11 @@ function GridRow(props: GridRowProps) { cells.push( - {cells} - + {emptyCellWidth > 0 && }
); } @@ -230,19 +247,18 @@ GridRow.propTypes = { // ---------------------------------------------------------------------- cellFocus: PropTypes.object, cellTabIndex: PropTypes.object, - children: PropTypes.node, - editRowsModel: PropTypes.object.isRequired, + containerWidth: PropTypes.number.isRequired, + editRowsState: PropTypes.object.isRequired, firstColumnToRender: PropTypes.number.isRequired, - id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, index: PropTypes.number.isRequired, - onClick: PropTypes.func, - onDoubleClick: PropTypes.func, + lastColumnToRender: PropTypes.number.isRequired, renderedColumns: PropTypes.arrayOf(PropTypes.object).isRequired, - renderState: PropTypes.object.isRequired, row: PropTypes.object.isRequired, rowHeight: PropTypes.number.isRequired, + rowId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, scrollBarState: PropTypes.object.isRequired, selected: PropTypes.bool.isRequired, + visibleColumns: PropTypes.arrayOf(PropTypes.object).isRequired, } as any; export { GridRow }; diff --git a/packages/grid/_modules_/grid/components/GridScrollArea.tsx b/packages/grid/_modules_/grid/components/GridScrollArea.tsx index 29be255170d0..de3026474c80 100644 --- a/packages/grid/_modules_/grid/components/GridScrollArea.tsx +++ b/packages/grid/_modules_/grid/components/GridScrollArea.tsx @@ -8,6 +8,8 @@ import { getDataGridUtilityClass } from '../gridClasses'; import { composeClasses } from '../utils/material-ui-utils'; import { useGridRootProps } from '../hooks/utils/useGridRootProps'; import { GridComponentProps } from '../GridComponentProps'; +import { gridDensityHeaderHeightSelector } from '../hooks/features/density/densitySelector'; +import { useGridSelector } from '../hooks/utils/useGridSelector'; const CLIFF = 1; const SLOP = 1.5; @@ -24,7 +26,7 @@ const useUtilityClasses = (ownerState: OwnerState) => { const { scrollDirection, classes } = ownerState; const slots = { - root: ['scrollArea', `scrollArea__${scrollDirection}`], + root: ['scrollArea', `scrollArea--${scrollDirection}`], }; return composeClasses(slots, getDataGridUtilityClass, classes); @@ -36,6 +38,7 @@ function GridScrollAreaRaw(props: ScrollAreaProps) { const apiRef = useGridApiContext(); const timeout = React.useRef(); const [dragging, setDragging] = React.useState(false); + const height = useGridSelector(apiRef, gridDensityHeaderHeightSelector); const scrollPosition = React.useRef({ left: 0, top: 0, @@ -90,7 +93,7 @@ function GridScrollAreaRaw(props: ScrollAreaProps) { useGridApiEventHandler(apiRef, GridEvents.columnHeaderDragEnd, toggleDragging); return dragging ? ( -
+
) : null; } diff --git a/packages/grid/_modules_/grid/components/GridStickyContainer.tsx b/packages/grid/_modules_/grid/components/GridStickyContainer.tsx deleted file mode 100644 index 4d3c10d8f0dd..000000000000 --- a/packages/grid/_modules_/grid/components/GridStickyContainer.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from 'react'; -import PropTypes from 'prop-types'; -import { getDataGridUtilityClass } from '../gridClasses'; -import { ElementSize } from '../models'; -import { useGridRootProps } from '../hooks/utils/useGridRootProps'; -import { composeClasses } from '../utils/material-ui-utils'; -import { GridComponentProps } from '../GridComponentProps'; - -interface GridStickyContainerProps extends ElementSize { - children: React.ReactNode; -} - -type OwnerState = { classes: GridComponentProps['classes'] }; - -const useUtilityClasses = (ownerState: OwnerState) => { - const { classes } = ownerState; - - const slots = { - root: ['viewport'], - }; - - return composeClasses(slots, getDataGridUtilityClass, classes); -}; - -function GridStickyContainer(props: GridStickyContainerProps) { - const { height, width, children } = props; - const rootProps = useGridRootProps(); - const ownerState = { classes: rootProps.classes }; - const classes = useUtilityClasses(ownerState); - return ( -
- {children} -
- ); -} - -GridStickyContainer.propTypes = { - // ----------------------------- Warning -------------------------------- - // | These PropTypes are generated from the TypeScript type definitions | - // | To update them edit the TypeScript types and run "yarn proptypes" | - // ---------------------------------------------------------------------- - children: PropTypes.node, - /** - * The height of a container or HTMLElement. - */ - height: PropTypes.number.isRequired, - /** - * The width of a container or HTMLElement. - */ - width: PropTypes.number.isRequired, -} as any; - -export { GridStickyContainer }; diff --git a/packages/grid/_modules_/grid/components/GridViewport.tsx b/packages/grid/_modules_/grid/components/GridViewport.tsx deleted file mode 100644 index 863648e143cd..000000000000 --- a/packages/grid/_modules_/grid/components/GridViewport.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import * as React from 'react'; -import { visibleGridColumnsSelector } from '../hooks/features/columns/gridColumnsSelector'; -import { useGridSelector } from '../hooks/utils/useGridSelector'; -import { gridDensityRowHeightSelector } from '../hooks/features/density/densitySelector'; -import { visibleSortedGridRowsAsArraySelector } from '../hooks/features/filter/gridFilterSelector'; -import { - gridFocusCellSelector, - gridTabIndexCellSelector, -} from '../hooks/features/focus/gridFocusStateSelector'; -import { gridEditRowsStateSelector } from '../hooks/features/editRows/gridEditRowsSelector'; -import { gridSelectionStateSelector } from '../hooks/features/selection/gridSelectionSelector'; -import { gridRenderingSelector } from '../hooks/features/virtualization/renderingStateSelector'; -import { useGridApiContext } from '../hooks/utils/useGridApiContext'; -import { GridDataContainer } from './containers/GridDataContainer'; -import { GridRenderingZone } from './GridRenderingZone'; -import { GridStickyContainer } from './GridStickyContainer'; -import { - gridContainerSizesSelector, - gridViewportSizesSelector, - gridScrollBarSizeSelector, -} from '../hooks/features/container/gridContainerSizesSelector'; -import { useGridRootProps } from '../hooks/utils/useGridRootProps'; - -type ViewportType = React.ForwardRefExoticComponent>; - -export const GridViewport: ViewportType = React.forwardRef( - function GridViewport(props, renderingZoneRef) { - const apiRef = useGridApiContext(); - const rootProps = useGridRootProps(); - const containerSizes = useGridSelector(apiRef, gridContainerSizesSelector); - const viewportSizes = useGridSelector(apiRef, gridViewportSizesSelector); - const scrollBarState = useGridSelector(apiRef, gridScrollBarSizeSelector); - const visibleColumns = useGridSelector(apiRef, visibleGridColumnsSelector); - const renderState = useGridSelector(apiRef, gridRenderingSelector); - const cellFocus = useGridSelector(apiRef, gridFocusCellSelector); - const cellTabIndex = useGridSelector(apiRef, gridTabIndexCellSelector); - const selection = useGridSelector(apiRef, gridSelectionStateSelector); - const visibleSortedRowsAsArray = useGridSelector(apiRef, visibleSortedGridRowsAsArraySelector); - const rowHeight = useGridSelector(apiRef, gridDensityRowHeightSelector); - const editRowsState = useGridSelector(apiRef, gridEditRowsStateSelector); - - const filteredSelection = React.useMemo( - () => - typeof rootProps.isRowSelectable === 'function' - ? selection.filter((id) => rootProps.isRowSelectable!(apiRef.current.getRowParams(id))) - : selection, - [apiRef, rootProps.isRowSelectable, selection], - ); - - const selectionLookup = React.useMemo( - () => - filteredSelection.reduce((lookup, rowId) => { - lookup[rowId] = rowId; - return lookup; - }, {}), - [filteredSelection], - ); - - const getRowsElements = () => { - if (renderState.renderContext == null) { - return null; - } - - const renderedRows = visibleSortedRowsAsArray.slice( - renderState.renderContext.firstRowIdx, - renderState.renderContext.lastRowIdx!, - ); - - const renderedColumns = visibleColumns.slice( - renderState.renderContext.firstColIdx!, - renderState.renderContext.lastColIdx! + 1, - ); - - return renderedRows.map(([id, row], idx) => ( - - )); - }; - - return ( - - - - {getRowsElements()} - - - - ); - }, -); diff --git a/packages/grid/_modules_/grid/components/GridVirtualScroller.tsx b/packages/grid/_modules_/grid/components/GridVirtualScroller.tsx new file mode 100644 index 000000000000..035ead18b895 --- /dev/null +++ b/packages/grid/_modules_/grid/components/GridVirtualScroller.tsx @@ -0,0 +1,344 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { useForkRef } from '@mui/material/utils'; +import { styled } from '@mui/material/styles'; +import { composeClasses } from '../utils/material-ui-utils'; +import { useGridRootProps } from '../hooks/utils/useGridRootProps'; +import { useGridApiContext } from '../hooks/utils/useGridApiContext'; +import { useGridSelector } from '../hooks/utils/useGridSelector'; +import { gridScrollBarSizeSelector } from '../hooks/features/container/gridContainerSizesSelector'; +import { + visibleGridColumnsSelector, + gridColumnsMetaSelector, +} from '../hooks/features/columns/gridColumnsSelector'; +import { + gridFocusCellSelector, + gridTabIndexCellSelector, +} from '../hooks/features/focus/gridFocusStateSelector'; +import { visibleSortedGridRowsAsArraySelector } from '../hooks/features/filter/gridFilterSelector'; +import { gridDensityRowHeightSelector } from '../hooks/features/density/densitySelector'; +import { gridEditRowsStateSelector } from '../hooks/features/editRows/gridEditRowsSelector'; +import { GridEvents } from '../constants/eventsConstants'; +import { gridPaginationSelector } from '../hooks/features/pagination/gridPaginationSelector'; +import { useGridApiEventHandler } from '../hooks/utils/useGridApiEventHandler'; +import { getDataGridUtilityClass } from '../gridClasses'; +import { GridComponentProps } from '../GridComponentProps'; +import { GridRowId } from '../models/gridRows'; + +type OwnerState = { classes: GridComponentProps['classes'] }; + +const useUtilityClasses = (ownerState: OwnerState) => { + const { classes } = ownerState; + + const slots = { + root: ['virtualScroller'], + content: ['virtualScrollerContent'], + renderZone: ['virtualScrollerRenderZone'], + }; + + return composeClasses(slots, getDataGridUtilityClass, classes); +}; + +const VirtualScrollerRoot = styled('div', { + name: 'MuiDataGrid', + slot: 'VirtualScroller', +})({ + overflow: 'auto', + '@media print': { + overflow: 'hidden', + }, +}); + +const VirtualScrollerContent = styled('div', { + name: 'MuiDataGrid', + slot: 'Content', +})({ + position: 'relative', + overflow: 'hidden', +}); + +const VirtualScrollerRenderZone = styled('div', { + name: 'MuiDataGrid', + slot: 'RenderingZone', +})({ + position: 'absolute', +}); + +// Uses binary search to avoid looping through all possible positions +export function getIndexFromScroll( + offset: number, + positions: number[], + sliceStart = 0, + sliceEnd = positions.length, +): number { + if (positions.length <= 0) { + return -1; + } + + if (sliceStart >= sliceEnd) { + return sliceStart; + } + + const pivot = sliceStart + Math.floor((sliceEnd - sliceStart) / 2); + const itemOffset = positions[pivot]; + return offset <= itemOffset + ? getIndexFromScroll(offset, positions, sliceStart, pivot) + : getIndexFromScroll(offset, positions, pivot + 1, sliceEnd); +} + +export interface RenderContext { + firstRowIndex: number; + lastRowIndex: number; + firstColumnIndex: number; + lastColumnIndex: number; +} + +interface GridVirtualScrollerProps extends React.HTMLAttributes { + selectionLookup: Record; + disableVirtualization?: boolean; +} + +const GridVirtualScroller = React.forwardRef( + function GridVirtualScroller(props, ref) { + const { className, selectionLookup, disableVirtualization, ...other } = props; + const apiRef = useGridApiContext(); + const rootProps = useGridRootProps(); + const visibleColumns = useGridSelector(apiRef, visibleGridColumnsSelector); + const columnsMeta = useGridSelector(apiRef, gridColumnsMetaSelector); + const visibleSortedRowsAsArray = useGridSelector(apiRef, visibleSortedGridRowsAsArraySelector); + const rowHeight = useGridSelector(apiRef, gridDensityRowHeightSelector); + const cellFocus = useGridSelector(apiRef, gridFocusCellSelector); + const cellTabIndex = useGridSelector(apiRef, gridTabIndexCellSelector); + const editRowsState = useGridSelector(apiRef, gridEditRowsStateSelector); + const scrollBarState = useGridSelector(apiRef, gridScrollBarSizeSelector); + const paginationState = useGridSelector(apiRef, gridPaginationSelector); + const renderZoneRef = React.useRef(null); + const rootRef = React.useRef(null); + const handleRef = useForkRef(ref, rootRef); + const [renderContext, setRenderContext] = React.useState(null); + const prevRenderContext = React.useRef(renderContext); + const scrollPosition = React.useRef({ top: 0, left: 0 }); + const [containerWidth, setContainerWidth] = React.useState(null); + const ownerState = { classes: rootProps.classes }; + const classes = useUtilityClasses(ownerState); + const prevTotalWidth = React.useRef(columnsMeta.totalWidth); + + const rowsInCurrentPage = React.useMemo(() => { + if (rootProps.pagination && rootProps.paginationMode === 'client') { + const start = paginationState.pageSize * paginationState.page; + return visibleSortedRowsAsArray.slice(start, start + paginationState.pageSize); + } + return visibleSortedRowsAsArray; + }, [paginationState, rootProps.pagination, rootProps.paginationMode, visibleSortedRowsAsArray]); + + const computeRenderContext = React.useCallback(() => { + if (disableVirtualization) { + return { + firstRowIndex: 0, + lastRowIndex: rowsInCurrentPage.length, + firstColumnIndex: 0, + lastColumnIndex: visibleColumns.length, + }; + } + + const { top, left } = scrollPosition.current!; + + const numberOfRowsToRender = rootProps.autoHeight + ? rowsInCurrentPage.length + : Math.floor(rootRef.current!.clientHeight / rowHeight); + + const firstRowIndex = Math.floor(top / rowHeight); + const lastRowIndex = firstRowIndex + numberOfRowsToRender; + + const { positions } = gridColumnsMetaSelector(apiRef.current.state); // To avoid infinite loop + const firstColumnIndex = getIndexFromScroll(left, positions); + const lastColumnIndex = getIndexFromScroll(left + containerWidth!, positions); + + return { + firstRowIndex, + lastRowIndex, + firstColumnIndex, + lastColumnIndex, + }; + }, [ + apiRef, + containerWidth, + rootProps.autoHeight, + disableVirtualization, + rowHeight, + rowsInCurrentPage.length, + visibleColumns.length, + ]); + + React.useEffect(() => { + if (disableVirtualization) { + renderZoneRef.current!.style.transform = `translate3d(0px, 0px, 0px)`; + } else { + // TODO a scroll reset should not be necessary + rootRef.current!.scrollLeft = 0; + rootRef.current!.scrollTop = 0; + } + + setContainerWidth(rootRef.current!.clientWidth); + }, [disableVirtualization]); + + React.useEffect(() => { + if (containerWidth == null) { + return; + } + + const initialRenderContext = computeRenderContext(); + prevRenderContext.current = initialRenderContext; + setRenderContext(initialRenderContext); + + const { top, left } = scrollPosition.current!; + const params = { top, left, renderContext: initialRenderContext }; + apiRef.current.publishEvent(GridEvents.rowsScroll, params); + apiRef.current.publishEvent('teste', params); + }, [apiRef, computeRenderContext, containerWidth]); + + const handleResize = React.useCallback(() => { + if (rootRef.current) { + setContainerWidth(rootRef.current.clientWidth); + } + }, []); + + useGridApiEventHandler(apiRef, GridEvents.resize, handleResize); + + const handleScroll = (event: React.UIEvent) => { + const { scrollTop, scrollLeft } = event.currentTarget; + scrollPosition.current.top = scrollTop; + scrollPosition.current.left = scrollLeft; + + // On iOS and macOS, negative offsets are possible when swiping past the start + if (scrollLeft < 0 || scrollTop < 0 || !prevRenderContext.current) { + return; + } + + // When virtualization is disabled, the context never changes during scroll + const nextRenderContext = disableVirtualization + ? prevRenderContext.current + : computeRenderContext(); + + const rowsScrolledSincePreviousRender = Math.abs( + nextRenderContext.firstRowIndex - prevRenderContext.current.firstRowIndex, + ); + + const columnsScrolledSincePreviousRender = Math.abs( + nextRenderContext.firstColumnIndex - prevRenderContext.current.firstColumnIndex, + ); + + const shouldSetState = + rowsScrolledSincePreviousRender >= rootProps.rowThreshold || + columnsScrolledSincePreviousRender >= rootProps.columnThreshold || + prevTotalWidth.current !== columnsMeta.totalWidth; + + // TODO rename event to a wider name, it's not only fired for row scrolling + // TODO create a interface to type correctly the params + apiRef.current.publishEvent(GridEvents.rowsScroll, { + top: scrollTop, + left: scrollLeft, + renderContext: shouldSetState ? nextRenderContext : prevRenderContext.current, + }); + + if (shouldSetState) { + setRenderContext(nextRenderContext); + prevRenderContext.current = nextRenderContext; + prevTotalWidth.current = columnsMeta.totalWidth; + + const top = Math.max(nextRenderContext.firstRowIndex - rootProps.rowBuffer, 0) * rowHeight; + const firstColumnToRender = Math.max( + nextRenderContext.firstColumnIndex - rootProps.columnBuffer, + 0, + ); + const left = columnsMeta.positions[firstColumnToRender]; + renderZoneRef.current!.style.transform = `translate3d(${left}px, ${top}px, 0px)`; + } + }; + + const getRows = () => { + if (!renderContext || containerWidth == null) { + return null; + } + + const rowBuffer = !disableVirtualization ? rootProps.rowBuffer : 0; + const columnBuffer = !disableVirtualization ? rootProps.columnBuffer : 0; + + const firstRowToRender = Math.max(renderContext.firstRowIndex - rowBuffer, 0); + const lastRowToRender = Math.min( + renderContext.lastRowIndex! + rowBuffer, + rowsInCurrentPage.length, + ); + + const firstColumnToRender = Math.max(renderContext.firstColumnIndex - columnBuffer, 0); + const lastColumnToRender = Math.min( + renderContext.lastColumnIndex + columnBuffer, + visibleColumns.length, + ); + + const renderedRows = rowsInCurrentPage.slice(firstRowToRender, lastRowToRender); + const renderedColumns = visibleColumns.slice(firstColumnToRender, lastColumnToRender); + const startIndex = paginationState.pageSize * paginationState.page; + + const rows: JSX.Element[] = []; + + for (let i = 0; i < renderedRows.length; i += 1) { + const [id, row] = renderedRows[i]; + + rows.push( + , + ); + } + + return rows; + }; + + const needsHorizontalScrollbar = containerWidth && columnsMeta.totalWidth > containerWidth; + + const contentSize = { + width: needsHorizontalScrollbar ? columnsMeta.totalWidth : 'auto', + // In cases where the columns exceed the available width, + // the horizontal scrollbar should be shown even when there're no rows. + // Keeping 1px as minimum height ensures that the scrollbar will visible if necessary. + height: Math.max(rowsInCurrentPage.length * rowHeight, 1), + }; + + if (rootProps.autoHeight && rowsInCurrentPage.length === 0) { + contentSize.height = 2 * rowHeight; // Give room to show the overlay when there no rows. + } + + return ( + + + + {getRows()} + + + + ); + }, +); + +export { GridVirtualScroller }; diff --git a/packages/grid/_modules_/grid/components/base/GridBody.tsx b/packages/grid/_modules_/grid/components/base/GridBody.tsx index 490c1c95be9b..25ac90039edb 100644 --- a/packages/grid/_modules_/grid/components/base/GridBody.tsx +++ b/packages/grid/_modules_/grid/components/base/GridBody.tsx @@ -6,11 +6,14 @@ import { ElementSize } from '../../models/elementSize'; import { GridColumnsHeader } from '../columnHeaders/GridColumnHeaders'; import { GridColumnsContainer } from '../containers/GridColumnsContainer'; import { GridMainContainer } from '../containers/GridMainContainer'; -import { GridWindow } from '../containers/GridWindow'; import { GridAutoSizer } from '../GridAutoSizer'; -import { GridViewport } from '../GridViewport'; import { GridOverlays } from './GridOverlays'; import { useGridRootProps } from '../../hooks/utils/useGridRootProps'; +import { GridVirtualScroller } from '../GridVirtualScroller'; +import { useGridSelector } from '../../hooks/utils/useGridSelector'; +import { gridSelectionStateSelector } from '../../hooks/features/selection/gridSelectionSelector'; +import { gridDensityHeaderHeightSelector } from '../../hooks/features/density/densitySelector'; +import { GridScrollArea } from '../GridScrollArea'; interface GridBodyProps { children?: React.ReactNode; @@ -20,6 +23,28 @@ function GridBody(props: GridBodyProps) { const { children } = props; const apiRef = useGridApiContext(); const rootProps = useGridRootProps(); + const selection = useGridSelector(apiRef, gridSelectionStateSelector); + const headerHeight = useGridSelector(apiRef, gridDensityHeaderHeightSelector); + const [isVirtualizationDisabled, setIsVirtualizationDisabled] = React.useState( + rootProps.disableVirtualization, + ); + + const disableVirtualization = React.useCallback(() => { + setIsVirtualizationDisabled(true); + }, []); + + const enableVirtualization = React.useCallback(() => { + setIsVirtualizationDisabled(false); + }, []); + + // The `useGridStateInit` hook can't be used here, because it only installs the + // method if it doesn't exist yet. Once installed, it's never updated again. + // This break the methods above, since their closure comes from the first time + // they were installed. Which means that calling `setIsVirtualizationDisabled` + // will trigger a re-render, but it won't update the state. That can be solved + // by migrating the virtualization status to the global state. + apiRef.current.UNSTABLE_disableVirtualization = disableVirtualization; + apiRef.current.UNSTABLE_enableVirtualization = enableVirtualization; const columnsHeaderRef = React.useRef(null); const columnsContainerRef = React.useRef(null); @@ -28,30 +53,62 @@ function GridBody(props: GridBodyProps) { apiRef.current.columnHeadersContainerElementRef = columnsContainerRef; apiRef.current.columnHeadersElementRef = columnsHeaderRef; - apiRef.current.windowRef = windowRef; - apiRef.current.renderingZoneRef = renderingZoneRef; + apiRef.current.windowRef = windowRef; // TODO rename, it's not attached to the window anymore + apiRef.current.renderingZoneRef = renderingZoneRef; // TODO remove, nobody should have access to internal parts of the virtualization const handleResize = React.useCallback( (size: ElementSize) => apiRef.current.publishEvent(GridEvents.resize, size), [apiRef], ); + const filteredSelection = React.useMemo( + () => + typeof rootProps.isRowSelectable === 'function' + ? selection.filter((id) => rootProps.isRowSelectable!(apiRef.current.getRowParams(id))) + : selection, + [apiRef, rootProps.isRowSelectable, selection], + ); + + const selectionLookup = React.useMemo( + () => + filteredSelection.reduce((lookup, rowId) => { + lookup[rowId] = rowId; + return lookup; + }, {}), + [filteredSelection], + ); + return ( + + - {(size: any) => ( - - - - )} + {(size: { height?: number; width: number }) => { + const style = { + width: size.width, + // If `autoHeight` is on, there will be no height value. + // In this case, let the container to grow whatever it needs. + height: size.height ? size.height - headerHeight : 'auto', + marginTop: headerHeight, + }; + + return ( + + ); + }} {children} diff --git a/packages/grid/_modules_/grid/components/cell/GridCell.tsx b/packages/grid/_modules_/grid/components/cell/GridCell.tsx index 549587b19fb6..7a5d3c0f4047 100644 --- a/packages/grid/_modules_/grid/components/cell/GridCell.tsx +++ b/packages/grid/_modules_/grid/components/cell/GridCell.tsx @@ -76,7 +76,7 @@ const useUtilityClasses = (ownerState: OwnerState) => { return composeClasses(slots, getDataGridUtilityClass, classes); }; -function GridCellRaw(props: GridCellProps) { +function GridCell(props: GridCellProps) { const { align, children, @@ -205,9 +205,7 @@ function GridCellRaw(props: GridCellProps) { ); } -const GridCell = React.memo(GridCellRaw); - -GridCellRaw.propTypes = { +GridCell.propTypes = { // ----------------------------- Warning -------------------------------- // | These PropTypes are generated from the TypeScript type definitions | // | To update them edit the TypeScript types and run "yarn proptypes" | diff --git a/packages/grid/_modules_/grid/components/cell/GridEmptyCell.tsx b/packages/grid/_modules_/grid/components/cell/GridEmptyCell.tsx deleted file mode 100644 index 4b2def0c5bbd..000000000000 --- a/packages/grid/_modules_/grid/components/cell/GridEmptyCell.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import * as React from 'react'; -import PropTypes from 'prop-types'; -import { getDataGridUtilityClass } from '../../gridClasses'; -import { GridComponentProps } from '../../GridComponentProps'; -import { useGridRootProps } from '../../hooks/utils/useGridRootProps'; -import { composeClasses } from '../../utils/material-ui-utils'; - -export interface GridEmptyCellProps { - width?: number; - height?: number; -} - -type OwnerState = { classes: GridComponentProps['classes'] }; - -const useUtilityClasses = (ownerState: OwnerState) => { - const { classes } = ownerState; - - const slots = { - root: ['cell'], - }; - - return composeClasses(slots, getDataGridUtilityClass, classes); -}; - -function GridEmptyCellRaw({ width, height }: GridEmptyCellProps) { - const rootProps = useGridRootProps(); - const ownerState = { classes: rootProps.classes }; - const classes = useUtilityClasses(ownerState); - - if (!width || !height) { - return null; - } - - return ( -
- ); -} - -GridEmptyCellRaw.propTypes = { - // ----------------------------- Warning -------------------------------- - // | These PropTypes are generated from the TypeScript type definitions | - // | To update them edit the TypeScript types and run "yarn proptypes" | - // ---------------------------------------------------------------------- - height: PropTypes.number, - width: PropTypes.number, -} as any; - -const GridEmptyCell = React.memo(GridEmptyCellRaw); - -export { GridEmptyCell }; diff --git a/packages/grid/_modules_/grid/components/cell/index.ts b/packages/grid/_modules_/grid/components/cell/index.ts index 1c4a12936071..7c89b3415d93 100644 --- a/packages/grid/_modules_/grid/components/cell/index.ts +++ b/packages/grid/_modules_/grid/components/cell/index.ts @@ -1,6 +1,5 @@ export * from './GridCell'; export * from './GridEditInputCell'; export * from './GridEditSingleSelectCell'; -export * from './GridEmptyCell'; export * from './GridActionsCell'; export * from './GridActionsCellItem'; diff --git a/packages/grid/_modules_/grid/components/columnHeaders/GridColumnHeaderItem.tsx b/packages/grid/_modules_/grid/components/columnHeaders/GridColumnHeaderItem.tsx index 9d61a0eb850a..1d3620734bf4 100644 --- a/packages/grid/_modules_/grid/components/columnHeaders/GridColumnHeaderItem.tsx +++ b/packages/grid/_modules_/grid/components/columnHeaders/GridColumnHeaderItem.tsx @@ -96,7 +96,7 @@ function GridColumnHeaderItem(props: GridColumnHeaderItemProps) { } const publish = React.useCallback( - (eventName: string) => (event: React.MouseEvent | React.DragEvent) => + (eventName: string) => (event: React.SyntheticEvent) => apiRef.current.publishEvent( eventName, apiRef.current.getColumnHeaderParams(column.field), @@ -105,37 +105,24 @@ function GridColumnHeaderItem(props: GridColumnHeaderItemProps) { [apiRef, column.field], ); - const mouseEventsHandlers = React.useMemo( - () => ({ - onClick: publish(GridEvents.columnHeaderClick), - onDoubleClick: publish(GridEvents.columnHeaderDoubleClick), - onMouseOver: publish(GridEvents.columnHeaderOver), - onMouseOut: publish(GridEvents.columnHeaderOut), - onMouseEnter: publish(GridEvents.columnHeaderEnter), - onMouseLeave: publish(GridEvents.columnHeaderLeave), - onKeyDown: publish(GridEvents.columnHeaderKeyDown), - onFocus: publish(GridEvents.columnHeaderFocus), - onBlur: publish(GridEvents.columnHeaderBlur), - }), - [publish], - ); - - const draggableEventHandlers = React.useMemo( - () => ({ - onDragStart: publish(GridEvents.columnHeaderDragStart), - onDragEnter: publish(GridEvents.columnHeaderDragEnter), - onDragOver: publish(GridEvents.columnHeaderDragOver), - onDragEnd: publish(GridEvents.columnHeaderDragEnd), - }), - [publish], - ); + const mouseEventsHandlers = { + onClick: publish(GridEvents.columnHeaderClick), + onDoubleClick: publish(GridEvents.columnHeaderDoubleClick), + onMouseOver: publish(GridEvents.columnHeaderOver), // TODO remove as it's not used + onMouseOut: publish(GridEvents.columnHeaderOut), // TODO remove as it's not used + onMouseEnter: publish(GridEvents.columnHeaderEnter), // TODO remove as it's not used + onMouseLeave: publish(GridEvents.columnHeaderLeave), // TODO remove as it's not used + onKeyDown: publish(GridEvents.columnHeaderKeyDown), + onFocus: publish(GridEvents.columnHeaderFocus), + onBlur: publish(GridEvents.columnHeaderBlur), + }; - const resizeEventHandlers = React.useMemo( - () => ({ - onMouseDown: publish(GridEvents.columnSeparatorMouseDown), - }), - [publish], - ); + const draggableEventHandlers = { + onDragStart: publish(GridEvents.columnHeaderDragStart), + onDragEnter: publish(GridEvents.columnHeaderDragEnter), + onDragOver: publish(GridEvents.columnHeaderDragOver), + onDragEnd: publish(GridEvents.columnHeaderDragEnd), + }; const removeLastBorderRight = isLastColumn && hasScrollX && !hasScrollY; const showRightBorder = !isLastColumn @@ -152,11 +139,9 @@ function GridColumnHeaderItem(props: GridColumnHeaderItemProps) { const width = column.computedWidth; - let ariaSort: any; + let ariaSort: 'ascending' | 'descending' | undefined; if (sortDirection != null) { - ariaSort = { - 'aria-sort': sortDirection === 'asc' ? 'ascending' : 'descending', - }; + ariaSort = sortDirection === 'asc' ? 'ascending' : 'descending'; } const columnMenuIconButton = !rootProps.disableColumnMenu && !column.disableColumnMenu && ( @@ -199,7 +184,6 @@ function GridColumnHeaderItem(props: GridColumnHeaderItemProps) {
state.scrollBar; +import { RenderContext } from '../GridVirtualScroller'; +import { GridColumnHeaderItem } from './GridColumnHeaderItem'; +import { filterGridColumnLookupSelector } from '../../hooks/features/filter/gridFilterSelector'; +import { gridColumnMenuSelector } from '../../hooks/features/columnMenu/columnMenuSelector'; +import { gridSortColumnLookupSelector } from '../../hooks/features/sorting/gridSortingSelector'; +import { + gridTabIndexColumnHeaderSelector, + gridTabIndexCellSelector, + gridFocusColumnHeaderSelector, +} from '../../hooks/features/focus/gridFocusStateSelector'; type OwnerState = { classes?: GridComponentProps['classes']; @@ -35,7 +42,7 @@ const useUtilityClasses = (ownerState: OwnerState) => { return composeClasses(slots, getDataGridUtilityClass, classes); }; -export const GridColumnsHeader = React.forwardRef(function GridColumnsHeader( +export const GridColumnsHeader = React.forwardRef(function GridColumnsHeader( props, ref, ) { @@ -43,22 +50,77 @@ export const GridColumnsHeader = React.forwardRef(function G const [resizeCol, setResizeCol] = React.useState(''); const apiRef = useGridApiContext(); - const columns = useGridSelector(apiRef, visibleGridColumnsSelector); - const containerSizes = useGridSelector(apiRef, gridContainerSizesSelector); + const visibleColumns = useGridSelector(apiRef, visibleGridColumnsSelector); + const columnsMeta = useGridSelector(apiRef, gridColumnsMetaSelector); + const scrollBarState = useGridSelector(apiRef, gridScrollBarSizeSelector); + const tabIndexState = useGridSelector(apiRef, gridTabIndexColumnHeaderSelector); + const cellTabIndexState = useGridSelector(apiRef, gridTabIndexCellSelector); + const columnHeaderFocus = useGridSelector(apiRef, gridFocusColumnHeaderSelector); const headerHeight = useGridSelector(apiRef, gridDensityHeaderHeightSelector); - const renderCtx = useGridSelector(apiRef, gridRenderingSelector).renderContext; - const { hasScrollX } = useGridSelector(apiRef, gridScrollbarStateSelector); + const filterColumnLookup = useGridSelector(apiRef, filterGridColumnLookupSelector); + const sortColumnLookup = useGridSelector(apiRef, gridSortColumnLookupSelector); + const columnMenuState = useGridSelector(apiRef, gridColumnMenuSelector); const rootProps = useGridRootProps(); + const wrapperRef = React.useRef(null); + const handleRef = useForkRef(ref, wrapperRef); + const [renderContext, setRenderContext] = React.useState(null); + const prevRenderContext = React.useRef(renderContext); + const prevScrollLeft = React.useRef(0); const ownerState = { dragCol, classes: rootProps.classes }; const classes = useUtilityClasses(ownerState); - const renderedCols = React.useMemo(() => { - if (renderCtx == null) { + const renderedColumns = React.useMemo(() => { + if (renderContext == null) { return []; } - return columns.slice(renderCtx.firstColIdx, renderCtx.lastColIdx! + 1); - }, [columns, renderCtx]); + + const firstColumnToRender = Math.max( + renderContext.firstColumnIndex! - rootProps.columnBuffer, + 0, + ); + + const lastColumnToRender = Math.min( + renderContext.lastColumnIndex! + rootProps.columnBuffer, + visibleColumns.length, + ); + + return visibleColumns.slice(firstColumnToRender, lastColumnToRender); + }, [renderContext, rootProps.columnBuffer, visibleColumns]); + + React.useEffect(() => { + apiRef.current.columnHeadersContainerElementRef!.current!.scrollLeft = 0; + }, [apiRef]); + + const handleScroll = React.useCallback( + ({ left, renderContext: nextRenderContext }) => { + if (!wrapperRef.current) { + return; + } + + // Ignore vertical scroll. + // Excepts the first event which sets the previous render context. + if (prevScrollLeft.current === left && prevRenderContext.current) { + return; + } + prevScrollLeft.current = left; + + if (nextRenderContext !== prevRenderContext.current || !prevRenderContext.current) { + setRenderContext(nextRenderContext); + prevRenderContext.current = nextRenderContext; + } + + const firstColumnToRender = Math.max( + nextRenderContext!.firstColumnIndex - rootProps.columnBuffer, + 0, + ); + + const offset = + firstColumnToRender > 0 ? left % columnsMeta.positions[firstColumnToRender] : left; + wrapperRef!.current!.style.transform = `translate3d(${-offset}px, 0px, 0px)`; + }, + [columnsMeta.positions, rootProps.columnBuffer], + ); const handleColumnResizeStart = React.useCallback( (params: { field: string }) => setResizeCol(params.field), @@ -76,25 +138,68 @@ export const GridColumnsHeader = React.forwardRef(function G useGridApiEventHandler(apiRef, GridEvents.columnHeaderDragStart, handleColumnReorderStart); useGridApiEventHandler(apiRef, GridEvents.columnHeaderDragEnd, handleColumnReorderStop); + useGridApiEventHandler(apiRef, GridEvents.rowsScroll, handleScroll); + + const getColumns = () => { + if (!renderContext) { + return null; + } + + const columns: JSX.Element[] = []; + + const firstColumnToRender = Math.max( + renderContext!.firstColumnIndex! - rootProps.columnBuffer, + 0, + ); + + for (let i = 0; i < renderedColumns.length; i += 1) { + const column = renderedColumns[i]; + + const columnIndex = firstColumnToRender + i; + const isFirstColumn = columnIndex === 0; + const hasTabbableElement = !(tabIndexState === null && cellTabIndexState === null); + const tabIndex = + (tabIndexState !== null && tabIndexState.field === column.field) || + (isFirstColumn && !hasTabbableElement) + ? 0 + : -1; + const hasFocus = columnHeaderFocus !== null && columnHeaderFocus.field === column.field; + const open = columnMenuState.open && columnMenuState.field === column.field; + + columns.push( + , + ); + } + + return columns; + }; + return ( - - -
- - - -
- -
+
+ {getColumns()} +
); }); diff --git a/packages/grid/_modules_/grid/components/columnHeaders/GridColumnHeadersItemCollection.tsx b/packages/grid/_modules_/grid/components/columnHeaders/GridColumnHeadersItemCollection.tsx index 2c2a5a78d12c..b646ca63026b 100644 --- a/packages/grid/_modules_/grid/components/columnHeaders/GridColumnHeadersItemCollection.tsx +++ b/packages/grid/_modules_/grid/components/columnHeaders/GridColumnHeadersItemCollection.tsx @@ -59,7 +59,7 @@ function GridColumnHeadersItemCollection(props: GridColumnHeadersItemCollectionP return ( ; - -type OwnerState = { classes: GridComponentProps['classes'] }; - -const useUtilityClasses = (ownerState: OwnerState) => { - const { classes } = ownerState; - - const slots = { - root: ['dataContainer'], - }; - - return composeClasses(slots, getDataGridUtilityClass, classes); -}; - -export function GridDataContainer(props: GridDataContainerProps) { - const { className, ...other } = props; - const apiRef = useGridApiContext(); - const dataContainerSizes = useGridSelector(apiRef, gridDataContainerSizesSelector); - const rootProps = useGridRootProps(); - const ownerState = { classes: rootProps.classes }; - const classes = useUtilityClasses(ownerState); - - const style: any = { - // TODO remove min - minWidth: dataContainerSizes?.width, - minHeight: dataContainerSizes?.height, - }; - - return
; -} diff --git a/packages/grid/_modules_/grid/components/containers/GridOverlay.tsx b/packages/grid/_modules_/grid/components/containers/GridOverlay.tsx index 4ff8d73afe46..3659673c9495 100644 --- a/packages/grid/_modules_/grid/components/containers/GridOverlay.tsx +++ b/packages/grid/_modules_/grid/components/containers/GridOverlay.tsx @@ -33,11 +33,20 @@ export const GridOverlay = React.forwardRef(fu const classes = useUtilityClasses(ownerState); const headerHeight = useGridSelector(apiRef, gridDensityHeaderHeightSelector); + const windowRef = apiRef.current.windowRef?.current; + const verticalScrollbarSize = windowRef ? windowRef.offsetWidth - windowRef.clientWidth : 0; + const horizontalScrollbarSize = windowRef ? windowRef.offsetHeight - windowRef.clientHeight : 0; + return (
); diff --git a/packages/grid/_modules_/grid/components/containers/GridRootStyles.ts b/packages/grid/_modules_/grid/components/containers/GridRootStyles.ts index 444e14b2565e..8e530afe2573 100644 --- a/packages/grid/_modules_/grid/components/containers/GridRootStyles.ts +++ b/packages/grid/_modules_/grid/components/containers/GridRootStyles.ts @@ -23,11 +23,6 @@ export const useStyles = makeStyles( height: '100%', display: 'flex', flexDirection: 'column', - '@media print': { - '& *::-webkit-scrollbar': { - display: 'none', - }, - }, [`&.${gridClasses.autoHeight}`]: { height: 'auto', }, @@ -59,7 +54,7 @@ export const useStyles = makeStyles( right: 0, overflow: 'hidden', display: 'flex', - flexDirection: 'column', + alignItems: 'center', borderBottom: `1px solid ${borderColor}`, }, [`& .${gridClasses.scrollArea}`]: { @@ -77,9 +72,7 @@ export const useStyles = makeStyles( }, [`& .${gridClasses.columnHeaderWrapper}`]: { display: 'flex', - width: '100%', alignItems: 'center', - overflow: 'hidden', }, [`& .${gridClasses.columnHeader}, & .${gridClasses.cell}`]: { WebkitTapHighlightColor: 'transparent', @@ -212,27 +205,6 @@ export const useStyles = makeStyles( [`& .${gridClasses.columnHeaderWrapper}.scroll .${gridClasses.columnHeader}:last-child`]: { borderRight: 'none', }, - [`& .${gridClasses.dataContainer}`]: { - position: 'relative', - flexGrow: 1, - display: 'flex', - flexDirection: 'column', - }, - [`& .${gridClasses.window}`]: { - position: 'absolute', - bottom: 0, - left: 0, - right: 0, - overflowX: 'auto', - }, - [`& .${gridClasses.viewport}`]: { - position: 'sticky', - top: 0, - left: 0, - display: 'flex', - flexDirection: 'column', - overflow: 'hidden', - }, [`& .${gridClasses.row}`]: { display: 'flex', width: 'fit-content', diff --git a/packages/grid/_modules_/grid/components/containers/GridWindow.tsx b/packages/grid/_modules_/grid/components/containers/GridWindow.tsx deleted file mode 100644 index 9ce2f101e281..000000000000 --- a/packages/grid/_modules_/grid/components/containers/GridWindow.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import * as React from 'react'; -import PropTypes from 'prop-types'; -import clsx from 'clsx'; -import { useGridSelector } from '../../hooks/utils/useGridSelector'; -import { - gridDensityHeaderHeightSelector, - gridDensityRowHeightSelector, -} from '../../hooks/features/density/densitySelector'; -import { gridDataContainerHeightSelector } from '../../hooks/features/container/gridContainerSizesSelector'; -import { useGridApiContext } from '../../hooks/utils/useGridApiContext'; -import { getDataGridUtilityClass } from '../../gridClasses'; -import { useGridRootProps } from '../../hooks/utils/useGridRootProps'; -import { composeClasses } from '../../utils/material-ui-utils'; -import { GridComponentProps } from '../../GridComponentProps'; - -export interface GridWindowProps extends React.HTMLAttributes { - size: { width: number; height: number }; -} - -type OwnerState = GridWindowProps & { - classes?: GridComponentProps['classes']; -}; - -const useUtilityClasses = (ownerState: OwnerState) => { - const { classes } = ownerState; - - const slots = { - root: ['windowContainer'], - window: ['window'], - }; - - return composeClasses(slots, getDataGridUtilityClass, classes); -}; - -const GridWindow = React.forwardRef(function GridWindow( - props, - ref, -) { - const { className, size, ...other } = props; - const apiRef = useGridApiContext(); - const rootProps = useGridRootProps(); - const headerHeight = useGridSelector(apiRef, gridDensityHeaderHeightSelector); - const rowHeight = useGridSelector(apiRef, gridDensityRowHeightSelector); - const dataContainerHeight = useGridSelector(apiRef, gridDataContainerHeightSelector); - const ownerProps = { ...props, classes: rootProps.classes }; - const classes = useUtilityClasses(ownerProps); - - React.useEffect(() => { - // refs are run before effect. Waiting for an effect guarantees that - // windowRef is resolved first. - // Once windowRef is resolved, we can update the size of the container. - apiRef.current.resize(); - }, [apiRef]); - - const containerHeight = React.useMemo(() => { - if (!rootProps.autoHeight) { - return size.height; - } - // If we have no rows, we give the size of 2 rows to display the no rows overlay - const dataHeight = dataContainerHeight < rowHeight ? rowHeight * 2 : dataContainerHeight; - return headerHeight + dataHeight; - }, [rootProps.autoHeight, dataContainerHeight, headerHeight, rowHeight, size.height]); - - return ( -
-
-
- ); -}); - -GridWindow.propTypes = { - // ----------------------------- Warning -------------------------------- - // | These PropTypes are generated from the TypeScript type definitions | - // | To update them edit the TypeScript types and run "yarn proptypes" | - // ---------------------------------------------------------------------- - size: PropTypes /* @typescript-to-proptypes-ignore */.shape({ - height: PropTypes.number, - width: PropTypes.number, - }).isRequired, -} as any; - -export { GridWindow }; diff --git a/packages/grid/_modules_/grid/components/containers/index.ts b/packages/grid/_modules_/grid/components/containers/index.ts index b0224a114230..a59bff5ee776 100644 --- a/packages/grid/_modules_/grid/components/containers/index.ts +++ b/packages/grid/_modules_/grid/components/containers/index.ts @@ -1,7 +1,5 @@ export * from './GridRoot'; export * from './GridColumnsContainer'; -export * from './GridDataContainer'; export * from './GridFooterContainer'; export * from './GridOverlay'; -export * from './GridWindow'; export * from './GridToolbarContainer'; diff --git a/packages/grid/_modules_/grid/components/index.ts b/packages/grid/_modules_/grid/components/index.ts index a9c4ef6fc38b..44acbafd2741 100644 --- a/packages/grid/_modules_/grid/components/index.ts +++ b/packages/grid/_modules_/grid/components/index.ts @@ -15,10 +15,7 @@ export * from './GridHeader'; export * from './GridLoadingOverlay'; export * from './GridNoRowsOverlay'; export * from './GridPagination'; -export * from './GridRenderingZone'; export * from './GridRowCount'; export * from './GridRow'; export * from './GridSelectedRowCount'; -export * from './GridStickyContainer'; -export * from './GridViewport'; export * from './GridScrollArea'; diff --git a/packages/grid/_modules_/grid/constants/eventsConstants.ts b/packages/grid/_modules_/grid/constants/eventsConstants.ts index a36d10646650..61a8e2824648 100644 --- a/packages/grid/_modules_/grid/constants/eventsConstants.ts +++ b/packages/grid/_modules_/grid/constants/eventsConstants.ts @@ -292,8 +292,4 @@ export enum GridEvents { * Fired when a column visibility changes. Called with a [[GridColumnVisibilityChangeParams]] object. */ columnVisibilityChange = 'columnVisibilityChange', - /** - * Fired when the rows in the viewport is changed. Called with a [[GridViewportRowsChange]] object. - */ - viewportRowsChange = 'viewportRowsChange', } diff --git a/packages/grid/_modules_/grid/gridClasses.ts b/packages/grid/_modules_/grid/gridClasses.ts index 1ead0f27ad88..c5811c076f57 100644 --- a/packages/grid/_modules_/grid/gridClasses.ts +++ b/packages/grid/_modules_/grid/gridClasses.ts @@ -121,10 +121,6 @@ export interface GridClasses { * Styles applied to the column header separator element. */ columnSeparator: string; - /** - * Styles applied to the data container element. - */ - dataContainer: string; /** * Styles applied to root of the boolean edit component. */ @@ -170,9 +166,17 @@ export interface GridClasses { */ overlay: string; /** - * Styles applied to the rendering zone element. + * Styles applied to the virtualization container. */ - renderingZone: string; + virtualScroller; + /** + * Styles applied to the virtualization content. + */ + virtualScrollerContent; + /** + * Styles applied to the virtualization render zone. + */ + virtualScrollerRenderZone; /** * Styles applied to the root element. */ @@ -225,10 +229,6 @@ export interface GridClasses { * Styles applied to the window element. */ window: string; - /** - * Styles applied to the window container element. - */ - windowContainer: string; /** * Styles applied to both the cell and the column header if `showColumnRightBorder={true}`. */ @@ -273,7 +273,6 @@ export const gridClasses = generateUtilityClasses('MuiDataGrid', [ 'columnSeparator--resizable', 'columnSeparator--resizing', 'columnSeparator', - 'dataContainer', 'editBooleanCell', 'editInputCell', 'filterIcon', @@ -285,8 +284,6 @@ export const gridClasses = generateUtilityClasses('MuiDataGrid', [ 'menuIconButton', 'menuOpen', 'overlay', - 'renderingZone', - 'root', 'root', 'row--editable', 'row--editing', @@ -298,8 +295,8 @@ export const gridClasses = generateUtilityClasses('MuiDataGrid', [ 'selectedRowCount', 'sortIcon', 'toolbarContainer', - 'viewport', - 'window', - 'windowContainer', + 'virtualScroller', + 'virtualScrollerContent', + 'virtualScrollerRenderZone', 'withBorder', ]); diff --git a/packages/grid/_modules_/grid/hooks/features/columnReorder/useGridColumnReorder.tsx b/packages/grid/_modules_/grid/hooks/features/columnReorder/useGridColumnReorder.tsx index 70aac517ba9d..7017386957e5 100644 --- a/packages/grid/_modules_/grid/hooks/features/columnReorder/useGridColumnReorder.tsx +++ b/packages/grid/_modules_/grid/hooks/features/columnReorder/useGridColumnReorder.tsx @@ -184,7 +184,8 @@ export const useGridColumnReorder = ( // Check if the column was dropped outside the grid. if (event.dataTransfer.dropEffect === 'none') { - apiRef.current.setColumnIndex(params.field, originColumnIndex.current!); + // Accessing params.field may contain the wrong field as header elements are reused + apiRef.current.setColumnIndex(dragColField, originColumnIndex.current!); originColumnIndex.current = null; } diff --git a/packages/grid/_modules_/grid/hooks/features/export/useGridPrintExport.tsx b/packages/grid/_modules_/grid/hooks/features/export/useGridPrintExport.tsx index f27b5cfe8be7..356067cf0960 100644 --- a/packages/grid/_modules_/grid/hooks/features/export/useGridPrintExport.tsx +++ b/packages/grid/_modules_/grid/hooks/features/export/useGridPrintExport.tsx @@ -119,11 +119,17 @@ export const useGridPrintExport = ( const gridRootElement = apiRef.current.rootElementRef!.current; const gridClone = gridRootElement!.cloneNode(true) as HTMLElement; const gridCloneViewport: HTMLElement | null = gridClone.querySelector( - `.${gridClasses.viewport}`, + `.${gridClasses.virtualScroller}`, ); // Expand the viewport window to prevent clipping - gridCloneViewport!.style.minWidth = '100%'; - gridCloneViewport!.style.maxWidth = '100%'; + gridCloneViewport!.style.height = 'auto'; + gridCloneViewport!.style.width = 'auto'; + gridCloneViewport!.parentElement!.style.width = 'auto'; + gridCloneViewport!.parentElement!.style.height = 'auto'; + + const columnsContainer = gridClone.querySelector(`.${gridClasses.columnsContainer}`); + const columnHeaders = columnsContainer!.firstChild! as HTMLElement; + columnHeaders.style.width = '100%'; let gridToolbarElementHeight = gridRootElement!.querySelector(`.${gridClasses.toolbarContainer}`)?.clientHeight || 0; @@ -221,6 +227,8 @@ export const useGridPrintExport = ( ...previousGridState.current, })); + apiRef.current.UNSTABLE_enableVirtualization(); + // Revert columns to their original state if (previousHiddenColumns.current.length) { apiRef.current.updateColumns( diff --git a/packages/grid/_modules_/grid/hooks/features/infiniteLoader/useGridInfiniteLoader.ts b/packages/grid/_modules_/grid/hooks/features/infiniteLoader/useGridInfiniteLoader.ts index 978f7ed6c35b..a65cc9dbdd37 100644 --- a/packages/grid/_modules_/grid/hooks/features/infiniteLoader/useGridInfiniteLoader.ts +++ b/packages/grid/_modules_/grid/hooks/features/infiniteLoader/useGridInfiniteLoader.ts @@ -10,8 +10,6 @@ import { import { GridRowScrollEndParams } from '../../../models/params/gridRowScrollEndParams'; import { visibleGridColumnsSelector } from '../columns/gridColumnsSelector'; import { GridComponentProps } from '../../../GridComponentProps'; -import { gridRenderingSelector } from '../virtualization/renderingStateSelector'; -import { GridViewportRowsChangeParams } from '../../../models/params/gridViewportRowsChangeParams'; import { GridScrollParams } from '../../../models/params/gridScrollParams'; /** @@ -24,20 +22,12 @@ import { GridScrollParams } from '../../../models/params/gridScrollParams'; */ export const useGridInfiniteLoader = ( apiRef: GridApiRef, - props: Pick< - GridComponentProps, - 'onRowsScrollEnd' | 'onViewportRowsChange' | 'scrollEndThreshold' - >, + props: Pick, ): void => { const containerSizes = useGridSelector(apiRef, gridContainerSizesSelector); const visibleColumns = useGridSelector(apiRef, visibleGridColumnsSelector); - const renderState = useGridSelector(apiRef, gridRenderingSelector); const isInScrollBottomArea = React.useRef(false); - const previousRenderContext = React.useRef(null); const handleRowsScrollEnd = React.useCallback( (scrollPosition: GridScrollParams) => { @@ -68,39 +58,13 @@ export const useGridInfiniteLoader = ( [apiRef, props.scrollEndThreshold, visibleColumns, containerSizes], ); - const handleGridScroll = React.useCallback(() => { - const scrollPosition = apiRef.current.getScrollPosition(); - - handleRowsScrollEnd(scrollPosition); - }, [apiRef, handleRowsScrollEnd]); - - // TODO: Check if onViewportRowsChange works as expected once virtualization is reworked - React.useEffect(() => { - const renderContext = renderState.renderContext!; - - if (!renderContext) { - return; - } - - if ( - !previousRenderContext.current || - renderContext.firstRowIdx !== previousRenderContext.current.firstRowIndex || - renderContext.lastRowIdx !== previousRenderContext.current.lastRowIndex - ) { - const viewportRowsChangeParams: GridViewportRowsChangeParams = { - firstRowIndex: renderContext.firstRowIdx!, - lastRowIndex: renderContext.lastRowIdx!, - }; - apiRef.current.publishEvent(GridEvents.viewportRowsChange, viewportRowsChangeParams); - } - - previousRenderContext.current = { - firstRowIndex: renderContext.firstRowIdx!, - lastRowIndex: renderContext.lastRowIdx!, - }; - }, [apiRef, props.onViewportRowsChange, renderState]); + const handleGridScroll = React.useCallback( + ({ left, top }) => { + handleRowsScrollEnd({ left, top }); + }, + [handleRowsScrollEnd], + ); useGridApiEventHandler(apiRef, GridEvents.rowsScroll, handleGridScroll); useGridApiOptionHandler(apiRef, GridEvents.rowsScrollEnd, props.onRowsScrollEnd); - useGridApiOptionHandler(apiRef, GridEvents.viewportRowsChange, props.onViewportRowsChange); }; diff --git a/packages/grid/_modules_/grid/hooks/features/scroll/useGridScroll.ts b/packages/grid/_modules_/grid/hooks/features/scroll/useGridScroll.ts index 540adff7f373..b904596c23fe 100644 --- a/packages/grid/_modules_/grid/hooks/features/scroll/useGridScroll.ts +++ b/packages/grid/_modules_/grid/hooks/features/scroll/useGridScroll.ts @@ -13,7 +13,6 @@ import { gridRowCountSelector } from '../rows/gridRowsSelector'; import { gridDensityRowHeightSelector } from '../density/densitySelector'; import { GridScrollParams } from '../../../models/params/gridScrollParams'; import { GridScrollApi } from '../../../models/api/gridScrollApi'; -import { gridScrollSelector } from '../virtualization/renderingStateSelector'; import { useGridApiMethod } from '../../utils/useGridApiMethod'; import { useGridNativeEventListener } from '../../utils/useGridNativeEventListener'; @@ -125,10 +124,12 @@ export const useGridScroll = ( [windowRef, colRef, logger], ); - const getScrollPosition = React.useCallback( - () => gridScrollSelector(apiRef.current.state), - [apiRef], - ); + const getScrollPosition = React.useCallback(() => { + if (!windowRef?.current) { + return { top: 0, left: 0 }; + } + return { top: windowRef.current.scrollTop, left: windowRef.current.scrollLeft }; + }, [windowRef]); const scrollApi: GridScrollApi = { scroll, @@ -148,11 +149,4 @@ export const useGridScroll = ( 'scroll', preventScroll, ); - - useGridNativeEventListener( - apiRef, - () => apiRef.current?.columnHeadersContainerElementRef?.current, - 'scroll', - preventScroll, - ); }; diff --git a/packages/grid/_modules_/grid/hooks/features/virtualization/useGridNoVirtualization.ts b/packages/grid/_modules_/grid/hooks/features/virtualization/useGridNoVirtualization.ts deleted file mode 100644 index d0659d59ce9e..000000000000 --- a/packages/grid/_modules_/grid/hooks/features/virtualization/useGridNoVirtualization.ts +++ /dev/null @@ -1,121 +0,0 @@ -import * as React from 'react'; -import { GridComponentProps } from '../../../GridComponentProps'; -import { GridApiRef } from '../../../models/api/gridApiRef'; -import { useGridNativeEventListener } from '../../utils/useGridNativeEventListener'; -import { useGridScrollFn } from '../../utils/useGridScrollFn'; -import { visibleGridColumnsSelector } from '../columns/gridColumnsSelector'; -import { useGridSelector, useGridState } from '../../utils'; -import { gridPaginationSelector } from '../pagination/gridPaginationSelector'; -import { gridContainerSizesSelector } from '../container/gridContainerSizesSelector'; -import { visibleGridRowCountSelector } from '../filter/gridFilterSelector'; -import { GridDisableVirtualizationApi } from '../../../models/api/gridDisableVirtualizationApi'; -import { useGridApiMethod } from '../../utils/useGridApiMethod'; - -/** - * @requires useGridPage (state) - * @requires useGridPageSize (state) - * @requires useGridColumns (state) - * @requires useGridFilter (state) - * @requires useGridContainerProps (state) - */ -export const useGridNoVirtualization = ( - apiRef: GridApiRef, - props: Pick< - GridComponentProps, - 'disableVirtualization' | 'pagination' | 'paginationMode' | 'rowHeight' - >, -): void => { - const windowRef = apiRef.current.windowRef; - const columnsHeaderRef = apiRef.current.columnHeadersElementRef; - const renderingZoneRef = apiRef.current.renderingZoneRef; - const [, setGridState, forceUpdate] = useGridState(apiRef); - const [scrollTo] = useGridScrollFn(apiRef, renderingZoneRef!, columnsHeaderRef!); - const paginationState = useGridSelector(apiRef, gridPaginationSelector); - const visibleColumns = useGridSelector(apiRef, visibleGridColumnsSelector); - const visibleRowCount = useGridSelector(apiRef, visibleGridRowCountSelector); - const containerSizes = useGridSelector(apiRef, gridContainerSizesSelector); - - const syncState = React.useCallback(() => { - if (!containerSizes || !windowRef?.current) { - return; - } - - let firstRowIdx = 0; - const { page, pageSize } = paginationState; - if (props.pagination && props.paginationMode === 'client') { - firstRowIdx = pageSize * page; - } - const lastRowIdx = firstRowIdx + containerSizes.virtualRowsCount; - const lastColIdx = visibleColumns.length > 0 ? visibleColumns.length - 1 : 0; - const renderContext = { firstRowIdx, lastRowIdx, firstColIdx: 0, lastColIdx }; - - const scrollParams = { - top: windowRef.current!.scrollTop, - left: windowRef.current!.scrollLeft, - }; - - setGridState((state) => ({ - ...state, - rendering: { - ...state.rendering, - virtualPage: 0, - renderContext, - realScroll: scrollParams, - renderingZoneScroll: scrollParams, - }, - })); - forceUpdate(); - }, [ - containerSizes, - paginationState, - props.pagination, - props.paginationMode, - setGridState, - forceUpdate, - visibleColumns.length, - windowRef, - ]); - - const disableVirtualization = React.useCallback((): void => { - setGridState((state) => ({ - ...state, - containerSizes: { - ...state.containerSizes!, - renderingZone: { - ...state.containerSizes!.renderingZone, - height: visibleRowCount * props.rowHeight!, - }, - renderingZonePageSize: visibleRowCount, - }, - viewportSizes: { - ...state.viewportSizes, - height: visibleRowCount * props.rowHeight!, - }, - })); - syncState(); - }, [visibleRowCount, props.rowHeight, setGridState, syncState]); - - React.useEffect(() => { - if (!props.disableVirtualization) { - return; - } - syncState(); - }, [props.disableVirtualization, syncState]); - - const handleScroll = React.useCallback(() => { - if (!props.disableVirtualization || !windowRef?.current) { - return; - } - const { scrollLeft, scrollTop } = windowRef.current; - scrollTo({ top: scrollTop, left: scrollLeft }); - syncState(); - }, [props.disableVirtualization, scrollTo, windowRef, syncState]); - - useGridNativeEventListener(apiRef, windowRef!, 'scroll', handleScroll, { passive: true }); - - const disableVirtualizationApi: GridDisableVirtualizationApi = { - UNSTABLE_disableVirtualization: disableVirtualization, - }; - - useGridApiMethod(apiRef, disableVirtualizationApi, 'GridDisableVirtualization'); -}; diff --git a/packages/grid/_modules_/grid/hooks/features/virtualization/useGridVirtualization.ts b/packages/grid/_modules_/grid/hooks/features/virtualization/useGridVirtualization.ts deleted file mode 100644 index be753f4434ca..000000000000 --- a/packages/grid/_modules_/grid/hooks/features/virtualization/useGridVirtualization.ts +++ /dev/null @@ -1,418 +0,0 @@ -import * as React from 'react'; -import { GridEvents } from '../../../constants/eventsConstants'; -import { GridApiRef } from '../../../models/api/gridApiRef'; -import { GridVirtualizationApi } from '../../../models/api/gridVirtualizationApi'; -import { - GridRenderContextProps, - GridRenderRowProps, - GridRenderColumnsProps, -} from '../../../models/gridRenderContextProps'; -import { GridContainerProps } from '../../../models/gridContainerProps'; -import { isDeepEqual } from '../../../utils/utils'; -import { useEnhancedEffect } from '../../../utils/material-ui-utils'; -import { - gridColumnsMetaSelector, - visibleGridColumnsSelector, -} from '../columns/gridColumnsSelector'; -import { useGridSelector } from '../../utils/useGridSelector'; -import { useGridState } from '../../utils/useGridState'; -import { gridPaginationSelector } from '../pagination/gridPaginationSelector'; -import { gridRowCountSelector } from '../rows/gridRowsSelector'; -import { useGridApiMethod } from '../../utils/useGridApiMethod'; -import { useGridNativeEventListener } from '../../utils/useGridNativeEventListener'; -import { useGridLogger } from '../../utils/useGridLogger'; -import { useGridScrollFn } from '../../utils/useGridScrollFn'; -import { GridRenderingState } from './renderingState'; -import { GridComponentProps } from '../../../GridComponentProps'; -import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; -import { useGridStateInit } from '../../utils/useGridStateInit'; - -// Uses binary search to avoid looping through all possible positions -function getIdxFromScroll( - offset: number, - positions: number[], - sliceStart = 0, - sliceEnd = positions.length, -): number { - if (positions.length <= 0) { - return -1; - } - - if (sliceStart >= sliceEnd) { - return sliceStart; - } - - const pivot = sliceStart + Math.floor((sliceEnd - sliceStart) / 2); - const itemOffset = positions[pivot]; - return offset <= itemOffset - ? getIdxFromScroll(offset, positions, sliceStart, pivot) - : getIdxFromScroll(offset, positions, pivot + 1, sliceEnd); -} - -/** - * @requires useGridColumns (state) - * @requires useGridPage (state) - * @requires useGridPageSize (state) - * @requires useGridRows (state) - */ -export const useGridVirtualization = ( - apiRef: GridApiRef, - props: Pick< - GridComponentProps, - | 'pagination' - | 'paginationMode' - | 'columnBuffer' - | 'disableExtendRowFullWidth' - | 'disableVirtualization' - >, -): void => { - const logger = useGridLogger(apiRef, 'useGridVirtualization'); - - useGridStateInit(apiRef, (state) => ({ - ...state, - rendering: { - realScroll: { left: 0, top: 0 }, - renderContext: null, - renderingZoneScroll: { left: 0, top: 0 }, - virtualPage: 0, - virtualRowsCount: 0, - }, - })); - - const colRef = apiRef.current.columnHeadersElementRef!; - const windowRef = apiRef.current.windowRef!; - const renderingZoneRef = apiRef.current.renderingZoneRef!; - - const [gridState, setGridState, forceUpdate] = useGridState(apiRef); - const paginationState = useGridSelector(apiRef, gridPaginationSelector); - const totalRowCount = useGridSelector(apiRef, gridRowCountSelector); - const visibleColumns = useGridSelector(apiRef, visibleGridColumnsSelector); - const columnsMeta = useGridSelector(apiRef, gridColumnsMetaSelector); - const renderedColRef = React.useRef(null); - const containerPropsRef = React.useRef(null); - const lastScrollLeftRef = React.useRef(0); - - const [scrollTo] = useGridScrollFn(apiRef, renderingZoneRef, colRef); - - const setRenderingState = React.useCallback( - (newState: Partial) => { - let stateChanged = false; - setGridState((state) => { - const currentRenderingState = { ...state.rendering, ...newState }; - if (!isDeepEqual(state.rendering, currentRenderingState)) { - stateChanged = true; - return { ...state, rendering: currentRenderingState }; - } - return state; - }); - return stateChanged; - }, - [setGridState], - ); - - const getRenderRowProps = React.useCallback( - (page: number) => { - if (apiRef.current.state.containerSizes == null) { - return null; - } - let minRowIdx = 0; - if (props.pagination && props.paginationMode === 'client') { - minRowIdx = paginationState.pageSize * paginationState.page; - } - - const firstRowIdx = page * apiRef.current.state.containerSizes.viewportPageSize + minRowIdx; - let lastRowIdx = firstRowIdx + apiRef.current.state.containerSizes.renderingZonePageSize; - const maxIndex = apiRef.current.state.containerSizes.virtualRowsCount + minRowIdx; - if (lastRowIdx > maxIndex) { - lastRowIdx = maxIndex; - } - - const rowProps: GridRenderRowProps = { page, firstRowIdx, lastRowIdx }; - return rowProps; - }, - [ - apiRef, - props.pagination, - paginationState.pageSize, - props.paginationMode, - paginationState.page, - ], - ); - - const getRenderingState = React.useCallback((): Partial | null => { - if (apiRef.current.state.containerSizes == null) { - return null; - } - - const newRenderCtx: Partial = { - ...renderedColRef.current, - ...getRenderRowProps(apiRef.current.state.rendering.virtualPage), - paginationCurrentPage: paginationState.page, - pageSize: paginationState.pageSize, - }; - return newRenderCtx; - }, [renderedColRef, getRenderRowProps, apiRef, paginationState.page, paginationState.pageSize]); - - const reRender = React.useCallback(() => { - const renderingState = getRenderingState(); - const hasChanged = setRenderingState({ renderContext: renderingState }); - if (hasChanged) { - logger.debug('reRender: trigger rendering'); - forceUpdate(); - } - }, [getRenderingState, logger, forceUpdate, setRenderingState]); - - const getColumnIdxFromScroll = React.useCallback( - (left: number) => getIdxFromScroll(left, columnsMeta.positions), - [columnsMeta.positions], - ); - - const getColumnFromScroll = React.useCallback( - (left: number) => { - if (!visibleColumns.length) { - return null; - } - return visibleColumns[getColumnIdxFromScroll(left)]; - }, - [getColumnIdxFromScroll, visibleColumns], - ); - - const updateRenderedCols = React.useCallback( - (containerProps: GridContainerProps | null, scrollLeft: number) => { - if (!containerProps) { - return false; - } - - containerPropsRef.current = containerProps; - const windowWidth = containerProps.windowSizes.width; - - lastScrollLeftRef.current = scrollLeft; - logger.debug( - `GridColumns from ${getColumnFromScroll(scrollLeft)?.field} to ${ - getColumnFromScroll(scrollLeft + windowWidth)?.field - }`, - ); - const firstDisplayedIdx = getColumnIdxFromScroll(scrollLeft); - const lastDisplayedIdx = getColumnIdxFromScroll(scrollLeft + windowWidth); - const prevFirstColIdx = renderedColRef?.current?.firstColIdx || 0; - const prevLastColIdx = renderedColRef?.current?.lastColIdx || 0; - const columnBuffer = props.columnBuffer; - const tolerance = columnBuffer > 1 ? columnBuffer - 1 : columnBuffer; // Math.floor(columnBuffer / 2); - const diffFirst = Math.abs(firstDisplayedIdx - tolerance - prevFirstColIdx); - const diffLast = Math.abs(lastDisplayedIdx + tolerance - prevLastColIdx); - logger.debug(`Column buffer: ${columnBuffer}, tolerance: ${tolerance}`); - logger.debug(`Previous values => first: ${prevFirstColIdx}, last: ${prevLastColIdx}`); - logger.debug( - `Current displayed values => first: ${firstDisplayedIdx}, last: ${lastDisplayedIdx}`, - ); - logger.debug(`Difference with first: ${diffFirst} and last: ${diffLast} `); - - const lastVisibleColIdx = visibleColumns.length > 0 ? visibleColumns.length - 1 : 0; - const firstColIdx = - firstDisplayedIdx - columnBuffer >= 0 ? firstDisplayedIdx - columnBuffer : 0; - const newRenderedColState = { - leftEmptyWidth: columnsMeta.positions[firstColIdx], - rightEmptyWidth: 0, - firstColIdx, - lastColIdx: - lastDisplayedIdx + columnBuffer >= lastVisibleColIdx - ? lastVisibleColIdx - : lastDisplayedIdx + columnBuffer, - }; - - if (apiRef.current.state.scrollBar.hasScrollX) { - newRenderedColState.rightEmptyWidth = - columnsMeta.totalWidth - - columnsMeta.positions[newRenderedColState.lastColIdx] - - visibleColumns[newRenderedColState.lastColIdx].computedWidth; - } else if (!props.disableExtendRowFullWidth) { - newRenderedColState.rightEmptyWidth = - apiRef.current.state.viewportSizes.width - columnsMeta.totalWidth; - } - - if (!isDeepEqual(newRenderedColState, renderedColRef.current)) { - renderedColRef.current = newRenderedColState; - logger.debug('New columns state to render', newRenderedColState); - return true; - } - logger.debug(`No rendering needed on columns`); - return false; - }, - [ - apiRef, - columnsMeta.positions, - columnsMeta.totalWidth, - getColumnFromScroll, - getColumnIdxFromScroll, - logger, - props.columnBuffer, - props.disableExtendRowFullWidth, - visibleColumns, - ], - ); - - const updateViewport = React.useCallback( - (forceReRender = false) => { - if (props.disableVirtualization) { - return; - } - - const lastState = apiRef.current.state; - const containerProps = lastState.containerSizes; - if (!windowRef || !windowRef.current || !containerProps) { - return; - } - const scrollBar = lastState.scrollBar; - - const { scrollLeft, scrollTop } = windowRef.current; - logger.debug(`Handling scroll Left: ${scrollLeft} Top: ${scrollTop}`); - - let requireRerender = updateRenderedCols(containerProps, scrollLeft); - - const rzScrollLeft = scrollLeft; - const maxScrollHeight = lastState.containerSizes!.renderingZoneScrollHeight; - - const page = lastState.rendering.virtualPage; - const nextPage = maxScrollHeight > 0 ? Math.floor(scrollTop / maxScrollHeight) : 0; - const rzScrollTop = scrollTop % maxScrollHeight; - - const scrollParams = { - left: scrollBar.hasScrollX ? rzScrollLeft : 0, - top: containerProps.isVirtualized ? rzScrollTop : scrollTop, - }; - - if (containerProps.isVirtualized && page !== nextPage) { - setRenderingState({ virtualPage: nextPage }); - logger.debug(`Changing page from ${page} to ${nextPage}`); - requireRerender = true; - } else { - if (!containerProps.isVirtualized && page > 0) { - logger.debug(`Virtualization disabled, setting virtualPage to 0`); - setRenderingState({ virtualPage: 0 }); - } - - scrollTo(scrollParams); - } - setRenderingState({ - renderingZoneScroll: scrollParams, - realScroll: { - left: windowRef.current.scrollLeft, - top: windowRef.current.scrollTop, - }, - }); - apiRef.current.publishEvent(GridEvents.rowsScroll, scrollParams); - - const pageChanged = - lastState.rendering.renderContext && - lastState.rendering.renderContext.paginationCurrentPage !== paginationState.page; - if (forceReRender || requireRerender || pageChanged) { - reRender(); - } - }, - [ - apiRef, - logger, - paginationState.page, - reRender, - scrollTo, - setRenderingState, - updateRenderedCols, - windowRef, - props.disableVirtualization, - ], - ); - - const resetScroll = React.useCallback(() => { - scrollTo({ left: 0, top: 0 }); - setRenderingState({ virtualPage: 0 }); - - if (windowRef && windowRef.current) { - windowRef.current.scrollTop = 0; - windowRef.current.scrollLeft = 0; - } - setRenderingState({ renderingZoneScroll: { left: 0, top: 0 } }); - }, [scrollTo, setRenderingState, windowRef]); - - const handleScroll = React.useCallback(() => { - if (props.disableVirtualization) { - return; - } - - // On iOS the inertia scrolling allows to return negative values. - if (windowRef.current!.scrollLeft < 0 || windowRef.current!.scrollTop < 0) { - return; - } - - if (apiRef.current.updateViewport) { - apiRef.current.updateViewport(); - } - }, [props.disableVirtualization, windowRef, apiRef]); - - const getContainerPropsState = React.useCallback( - () => gridState.containerSizes, - [gridState.containerSizes], - ); - - const getRenderContextState = React.useCallback(() => { - return gridState.rendering.renderContext || undefined; - }, [gridState.rendering.renderContext]); - - useEnhancedEffect(() => { - if (props.disableVirtualization) { - return; - } - - if (renderingZoneRef && renderingZoneRef.current) { - logger.debug('applying scrollTop ', gridState.rendering.renderingZoneScroll.top); - scrollTo(gridState.rendering.renderingZoneScroll); - } - }); - - const virtualApi: Partial = { - getContainerPropsState, - getRenderContextState, - updateViewport, - }; - useGridApiMethod(apiRef, virtualApi, 'GridVirtualizationApi'); - - React.useEffect(() => { - if ( - gridState.rendering.renderContext?.paginationCurrentPage !== paginationState.page && - apiRef.current.updateViewport - ) { - logger.debug(`State paginationState.page changed to ${paginationState.page}. `); - apiRef.current.updateViewport(true); - resetScroll(); - } - }, [ - apiRef, - paginationState.page, - gridState.rendering.renderContext?.paginationCurrentPage, - logger, - resetScroll, - ]); - - React.useEffect(() => { - if (apiRef.current.updateViewport) { - logger.debug(`totalRowCount has changed to ${totalRowCount}, updating viewport.`); - apiRef.current.updateViewport(true); - } - }, [ - logger, - totalRowCount, - gridState.viewportSizes, - gridState.scrollBar, - gridState.containerSizes, - apiRef, - ]); - - useGridNativeEventListener(apiRef, windowRef, 'scroll', handleScroll, { passive: true }); - - const resetRenderedColState = React.useCallback(() => { - logger.debug('Clearing previous renderedColRef'); - renderedColRef.current = null; - }, [logger, renderedColRef]); - - useGridApiEventHandler(apiRef, GridEvents.columnsChange, resetRenderedColState); - useGridApiEventHandler(apiRef, GridEvents.debouncedResize, resetRenderedColState); -}; diff --git a/packages/grid/_modules_/grid/models/api/gridApi.ts b/packages/grid/_modules_/grid/models/api/gridApi.ts index c1708ff7a6bc..b91c4ceaf723 100644 --- a/packages/grid/_modules_/grid/models/api/gridApi.ts +++ b/packages/grid/_modules_/grid/models/api/gridApi.ts @@ -20,7 +20,6 @@ import { GridRowApi } from './gridRowApi'; import { GridSelectionApi } from './gridSelectionApi'; import { GridSortApi } from './gridSortApi'; import { GridStateApi } from './gridStateApi'; -import { GridVirtualizationApi } from './gridVirtualizationApi'; import { GridLoggerApi } from './gridLoggerApi'; import { GridScrollApi } from './gridScrollApi'; import type { GridColumnsPreProcessingApi } from '../../hooks/core/columnsPreProcessing'; @@ -43,7 +42,6 @@ export interface GridApi GridColumnApi, GridSelectionApi, GridSortApi, - GridVirtualizationApi, GridPageApi, GridPageSizeApi, GridCsvExportApi, diff --git a/packages/grid/_modules_/grid/models/api/gridDisableVirtualizationApi.ts b/packages/grid/_modules_/grid/models/api/gridDisableVirtualizationApi.ts index 29228a89ec4d..6884ad4caf89 100644 --- a/packages/grid/_modules_/grid/models/api/gridDisableVirtualizationApi.ts +++ b/packages/grid/_modules_/grid/models/api/gridDisableVirtualizationApi.ts @@ -7,4 +7,9 @@ export interface GridDisableVirtualizationApi { * @ignore - do not document. Remove before releasing v5 stable version. */ UNSTABLE_disableVirtualization: () => void; + /** + * Enables grid's virtualization. + * @ignore - do not document. Remove before releasing v5 stable version. + */ + UNSTABLE_enableVirtualization: () => void; } diff --git a/packages/grid/_modules_/grid/models/api/gridVirtualizationApi.ts b/packages/grid/_modules_/grid/models/api/gridVirtualizationApi.ts deleted file mode 100644 index 27b5771f9886..000000000000 --- a/packages/grid/_modules_/grid/models/api/gridVirtualizationApi.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { GridContainerProps } from '../gridContainerProps'; -import { GridRenderContextProps } from '../gridRenderContextProps'; - -/** - * The virtualization API interface that is available in the grid [[apiRef]]. - */ -export interface GridVirtualizationApi { - /** - * Get the current containerProps. - * @returns {GridContainerProps | null} The container properties. - * @ignore - do not document. - */ - getContainerPropsState: () => GridContainerProps | null; - /** - * Get the current renderContext. - * @returns {Partial | undefined} The render context. - * @ignore - do not document. - */ - getRenderContextState: () => Partial | undefined; - /** - * Refreshes the viewport cells according to the scroll positions - * @param {boolean} forceRender If `true`, forces a rerender. By default, it is `false`. - * @ignore - do not document. - */ - updateViewport: (forceRender?: boolean) => void; -} diff --git a/packages/grid/_modules_/grid/models/api/index.ts b/packages/grid/_modules_/grid/models/api/index.ts index 246b8208e718..2e5529f8f178 100644 --- a/packages/grid/_modules_/grid/models/api/index.ts +++ b/packages/grid/_modules_/grid/models/api/index.ts @@ -12,7 +12,6 @@ export * from './gridRowApi'; export * from './gridSelectionApi'; export * from './gridSortApi'; export * from './gridStateApi'; -export * from './gridVirtualizationApi'; export * from './gridLocaleTextApi'; export * from './gridCsvExportApi'; export * from './gridFocusApi'; diff --git a/packages/grid/_modules_/grid/models/gridOptions.tsx b/packages/grid/_modules_/grid/models/gridOptions.tsx index 2711e3cbfa3a..cda35d3f4d97 100644 --- a/packages/grid/_modules_/grid/models/gridOptions.tsx +++ b/packages/grid/_modules_/grid/models/gridOptions.tsx @@ -54,10 +54,25 @@ export interface GridSimpleOptions { */ checkboxSelectionVisibleOnly: boolean; /** - * Number of columns rendered outside the grid viewport. - * @default 2 + * Number of extra columns to be rendered before/after the visible slice. + * @default 3 */ columnBuffer: number; + /** + * Number of extra rows to be rendered before/after the visible slice. + * @default 3 + */ + rowBuffer: number; + /** + * Number of rows from the `rowBuffer` that can be visible before a new slice is rendered. + * @default 3 + */ + rowThreshold: number; + /** + * Number of rows from the `columnBuffer` that can be visible before a new slice is rendered. + * @default 3 + */ + columnThreshold: number; /** * Set the density of the grid. * @default "standard" @@ -234,7 +249,10 @@ export const GRID_DEFAULT_SIMPLE_OPTIONS: GridSimpleOptions = { autoPageSize: false, checkboxSelection: false, checkboxSelectionVisibleOnly: false, - columnBuffer: 2, + columnBuffer: 3, + rowBuffer: 3, + columnThreshold: 3, + rowThreshold: 3, density: GridDensityTypes.Standard, disableExtendRowFullWidth: false, disableColumnFilter: false, diff --git a/packages/grid/_modules_/grid/models/params/gridViewportRowsChangeParams.ts b/packages/grid/_modules_/grid/models/params/gridViewportRowsChangeParams.ts deleted file mode 100644 index 3ff5723d1f17..000000000000 --- a/packages/grid/_modules_/grid/models/params/gridViewportRowsChangeParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Object passed as parameter of the virtual rows change event. - */ -export interface GridViewportRowsChangeParams { - /** - * The index of the first row in the viewport. - */ - firstRowIndex: number; - /** - * The index of the last row in the viewport. - */ - lastRowIndex: number; -} diff --git a/packages/grid/_modules_/grid/models/params/index.ts b/packages/grid/_modules_/grid/models/params/index.ts index 9a6441f45e0b..19cde29037b7 100644 --- a/packages/grid/_modules_/grid/models/params/index.ts +++ b/packages/grid/_modules_/grid/models/params/index.ts @@ -9,6 +9,5 @@ export * from './gridRowScrollEndParams'; export * from './gridScrollParams'; export * from './gridSortModelParams'; export * from './gridStateChangeParams'; -export * from './gridViewportRowsChangeParams'; export * from './gridRowSelectionCheckboxParams'; export * from './gridHeaderSelectionCheckboxParams'; diff --git a/packages/grid/data-grid/src/DataGrid.tsx b/packages/grid/data-grid/src/DataGrid.tsx index 1564c01ed8ab..440c4e5dda16 100644 --- a/packages/grid/data-grid/src/DataGrid.tsx +++ b/packages/grid/data-grid/src/DataGrid.tsx @@ -74,8 +74,8 @@ DataGridRaw.propTypes = { */ className: PropTypes.string, /** - * Number of columns rendered outside the grid viewport. - * @default 2 + * Number of extra columns to be rendered before/after the visible slice. + * @default 3 */ columnBuffer: PropTypes.number, /** @@ -94,6 +94,11 @@ DataGridRaw.propTypes = { } return null; }), + /** + * Number of rows from the `columnBuffer` that can be visible before a new slice is rendered. + * @default 3 + */ + columnThreshold: PropTypes.number, /** * Extend native column types with your new column types. */ @@ -530,6 +535,11 @@ DataGridRaw.propTypes = { * @default "client" */ paginationMode: PropTypes.oneOf(['client', 'server']), + /** + * Number of extra rows to be rendered before/after the visible slice. + * @default 3 + */ + rowBuffer: PropTypes.number, /** * Set the total number of rows, if it is different than the length of the value `rows` prop. */ @@ -548,6 +558,11 @@ DataGridRaw.propTypes = { * @default [25, 50, 100] */ rowsPerPageOptions: PropTypes.arrayOf(PropTypes.number), + /** + * Number of rows from the `rowBuffer` that can be visible before a new slice is rendered. + * @default 3 + */ + rowThreshold: PropTypes.number, /** * Override the height/width of the grid inner scrollbar. */ diff --git a/packages/grid/data-grid/src/DataGridProps.ts b/packages/grid/data-grid/src/DataGridProps.ts index dac80ef5b90e..cd35bad7b69e 100644 --- a/packages/grid/data-grid/src/DataGridProps.ts +++ b/packages/grid/data-grid/src/DataGridProps.ts @@ -18,7 +18,6 @@ export type DataGridProps = Omit< | 'hideFooterRowCount' | 'options' | 'onRowsScrollEnd' - | 'onViewportRowsChange' | 'scrollEndThreshold' | 'signature' > & { diff --git a/packages/grid/data-grid/src/tests/keyboard.DataGrid.test.tsx b/packages/grid/data-grid/src/tests/keyboard.DataGrid.test.tsx index 827f4d923e05..d9414cbd27b4 100644 --- a/packages/grid/data-grid/src/tests/keyboard.DataGrid.test.tsx +++ b/packages/grid/data-grid/src/tests/keyboard.DataGrid.test.tsx @@ -258,10 +258,10 @@ describe(' - Keyboard', () => { } render(); getColumnHeaderCell(0).focus(); - const gridWindow = document.querySelector('.MuiDataGrid-window')! as HTMLElement; - expect(gridWindow.scrollLeft).to.equal(0); + const virtualScroller = document.querySelector('.MuiDataGrid-virtualScroller')! as HTMLElement; + expect(virtualScroller.scrollLeft).to.equal(0); fireEvent.keyDown(document.activeElement!, { key: 'ArrowRight' }); - expect(gridWindow.scrollLeft).not.to.equal(0); + expect(virtualScroller.scrollLeft).not.to.equal(0); }); it('Shift + Space should select a row', () => { diff --git a/packages/grid/data-grid/src/tests/layout.DataGrid.test.tsx b/packages/grid/data-grid/src/tests/layout.DataGrid.test.tsx index bf5459556b09..6b1dfc0ba825 100644 --- a/packages/grid/data-grid/src/tests/layout.DataGrid.test.tsx +++ b/packages/grid/data-grid/src/tests/layout.DataGrid.test.tsx @@ -605,8 +605,8 @@ describe(' - Layout & Warnings', () => { />
, ); - const gridWindow = document.querySelector('.MuiDataGrid-window'); - const scrollBarSize = gridWindow!.scrollHeight - gridWindow!.clientHeight; + const virtualScroller = document.querySelector('.MuiDataGrid-virtualScroller'); + const scrollBarSize = virtualScroller!.offsetHeight - virtualScroller!.clientHeight; expect(scrollBarSize).not.to.equal(0); expect(document.querySelector('.MuiDataGrid-main')!.clientHeight).to.equal( scrollBarSize + headerHeight + rowHeight * baselineProps.rows.length, @@ -625,9 +625,9 @@ describe(' - Layout & Warnings', () => { ); }; render(); - const gridWindow = document.querySelector('.MuiDataGrid-window'); + const virtualScroller = document.querySelector('.MuiDataGrid-virtualScroller'); // It should not have a horizontal scrollbar - expect(gridWindow!.scrollWidth - gridWindow!.clientWidth).to.equal(0); + expect(virtualScroller!.scrollWidth - virtualScroller!.clientWidth).to.equal(0); }); it('should have a horizontal scrollbar when there are more columns to show and no rows', function test() { @@ -640,8 +640,8 @@ describe(' - Layout & Warnings', () => {
, ); - const gridWindow = document.querySelector('.MuiDataGrid-window'); - expect(gridWindow!.scrollWidth - gridWindow!.clientWidth).not.to.equal(0); + const virtualScroller = document.querySelector('.MuiDataGrid-virtualScroller'); + expect(virtualScroller!.scrollWidth - virtualScroller!.clientWidth).not.to.equal(0); }); }); diff --git a/packages/grid/data-grid/src/tests/pagination.DataGrid.test.tsx b/packages/grid/data-grid/src/tests/pagination.DataGrid.test.tsx index 080c2a9780bf..48901f1a704c 100644 --- a/packages/grid/data-grid/src/tests/pagination.DataGrid.test.tsx +++ b/packages/grid/data-grid/src/tests/pagination.DataGrid.test.tsx @@ -474,7 +474,7 @@ describe(' - Pagination', () => { (heightAfter - headerHeight - footerHeight) / rowHeight, ); - let rows = document.querySelectorAll('.MuiDataGrid-viewport [role="row"]'); + let rows = document.querySelectorAll('.MuiDataGrid-virtualScrollerRenderZone [role="row"]'); expect(rows.length).to.equal(expectedViewportRowsLengthBefore); setProps({ height: heightAfter }); @@ -485,7 +485,7 @@ describe(' - Pagination', () => { ), ); - rows = document.querySelectorAll('.MuiDataGrid-viewport [role="row"]'); + rows = document.querySelectorAll('.MuiDataGrid-virtualScrollerRenderZone [role="row"]'); expect(rows.length).to.equal(expectedViewportRowsLengthAfter); expect(onPageSizeChange.lastCall.args[0]).to.equal(expectedViewportRowsLengthAfter); diff --git a/packages/grid/data-grid/src/useDataGridComponent.tsx b/packages/grid/data-grid/src/useDataGridComponent.tsx index bd6f650ae7f0..e49f99a294a9 100644 --- a/packages/grid/data-grid/src/useDataGridComponent.tsx +++ b/packages/grid/data-grid/src/useDataGridComponent.tsx @@ -20,8 +20,6 @@ import { useGridRows } from '../../_modules_/grid/hooks/features/rows/useGridRow import { useGridParamsApi } from '../../_modules_/grid/hooks/features/rows/useGridParamsApi'; import { useGridSelection } from '../../_modules_/grid/hooks/features/selection/useGridSelection'; import { useGridSorting } from '../../_modules_/grid/hooks/features/sorting/useGridSorting'; -import { useGridVirtualization } from '../../_modules_/grid/hooks/features/virtualization/useGridVirtualization'; -import { useGridNoVirtualization } from '../../_modules_/grid/hooks/features/virtualization/useGridNoVirtualization'; import { useGridScroll } from '../../_modules_/grid/hooks/features/scroll/useGridScroll'; import { useGridEvents } from '../../_modules_/grid/hooks/features/events/useGridEvents'; import { useGridContainerProps } from '../../_modules_/grid/hooks/features/container/useGridContainerProps'; @@ -44,8 +42,6 @@ export const useDataGridComponent = (apiRef: GridApiRef, props: GridComponentPro useGridPage(apiRef, props); useGridContainerProps(apiRef, props); useGridScroll(apiRef, props); - useGridNoVirtualization(apiRef, props); - useGridVirtualization(apiRef, props); useGridColumnMenu(apiRef); useGridKeyboard(apiRef); useGridKeyboardNavigation(apiRef, props); diff --git a/packages/grid/data-grid/src/useDataGridProps.ts b/packages/grid/data-grid/src/useDataGridProps.ts index 8a77fd618c50..b3252c1a92ba 100644 --- a/packages/grid/data-grid/src/useDataGridProps.ts +++ b/packages/grid/data-grid/src/useDataGridProps.ts @@ -20,7 +20,6 @@ const FORCED_PROPS: { [key in ForcedPropsKey]-?: GridInputComponentProps[key] } hideFooterRowCount: false, pagination: true, onRowsScrollEnd: undefined, - onViewportRowsChange: undefined, checkboxSelectionVisibleOnly: false, scrollEndThreshold: undefined, signature: 'DataGrid', diff --git a/packages/grid/x-grid/src/DataGridPro.tsx b/packages/grid/x-grid/src/DataGridPro.tsx index 2b798505d4a9..88207e109c90 100644 --- a/packages/grid/x-grid/src/DataGridPro.tsx +++ b/packages/grid/x-grid/src/DataGridPro.tsx @@ -109,14 +109,19 @@ DataGridProRaw.propTypes = { */ className: PropTypes.string, /** - * Number of columns rendered outside the grid viewport. - * @default 2 + * Number of extra columns to be rendered before/after the visible slice. + * @default 3 */ columnBuffer: PropTypes.number, /** * Set of columns of type [[GridColumns]]. */ columns: PropTypes.arrayOf(PropTypes.object).isRequired, + /** + * Number of rows from the `columnBuffer` that can be visible before a new slice is rendered. + * @default 3 + */ + columnThreshold: PropTypes.number, /** * Extend native column types with your new column types. */ @@ -550,13 +555,6 @@ DataGridProRaw.propTypes = { * @internal */ onStateChange: PropTypes.func, - /** - * Callback fired when the rows in the viewport change. - * @param {GridViewportRowsChangeParams} params The viewport params. - * @param {MuiEvent<{}>} event The event object. - * @param {GridCallbackDetails} details Additional details for this callback. - */ - onViewportRowsChange: PropTypes.func, /** * The zero-based index of the current page. * @default 0 @@ -579,6 +577,11 @@ DataGridProRaw.propTypes = { * @default "client" */ paginationMode: PropTypes.oneOf(['client', 'server']), + /** + * Number of extra rows to be rendered before/after the visible slice. + * @default 3 + */ + rowBuffer: PropTypes.number, /** * Set the total number of rows, if it is different than the length of the value `rows` prop. */ @@ -597,6 +600,11 @@ DataGridProRaw.propTypes = { * @default [25, 50, 100] */ rowsPerPageOptions: PropTypes.arrayOf(PropTypes.number), + /** + * Number of rows from the `rowBuffer` that can be visible before a new slice is rendered. + * @default 3 + */ + rowThreshold: PropTypes.number, /** * Override the height/width of the grid inner scrollbar. */ diff --git a/packages/grid/x-grid/src/tests/columnHeaders.DataGridPro.test.tsx b/packages/grid/x-grid/src/tests/columnHeaders.DataGridPro.test.tsx index 84cb0766a622..6fb1a54fcb7c 100644 --- a/packages/grid/x-grid/src/tests/columnHeaders.DataGridPro.test.tsx +++ b/packages/grid/x-grid/src/tests/columnHeaders.DataGridPro.test.tsx @@ -51,8 +51,8 @@ describe(' - Column Headers', () => { const menuIconButton = columnCell.querySelector('button[aria-label="Menu"]'); fireEvent.click(menuIconButton); await waitFor(() => expect(screen.queryByRole('menu')).not.to.equal(null)); - const gridWindow = document.querySelector('.MuiDataGrid-window')!; - gridWindow.dispatchEvent(new Event('scroll')); + const virtualScroller = document.querySelector('.MuiDataGrid-virtualScroller')!; + virtualScroller.dispatchEvent(new Event('scroll')); await waitFor(() => expect(screen.queryByRole('menu')).to.equal(null)); }); diff --git a/packages/grid/x-grid/src/tests/events.DataGridPro.test.tsx b/packages/grid/x-grid/src/tests/events.DataGridPro.test.tsx index 62aba2759cf0..6fea837a74a2 100644 --- a/packages/grid/x-grid/src/tests/events.DataGridPro.test.tsx +++ b/packages/grid/x-grid/src/tests/events.DataGridPro.test.tsx @@ -5,8 +5,6 @@ import { fireEvent, // @ts-ignore screen, - // @ts-expect-error need to migrate helpers to TypeScript - waitFor, } from 'test/utils'; import { expect } from 'chai'; import { @@ -24,7 +22,6 @@ import { } from '@mui/x-data-grid-pro'; import { getCell, getColumnHeaderCell, getRow } from 'test/utils/helperFn'; import { spy } from 'sinon'; -import { useData } from 'packages/storybook/src/hooks/useData'; describe(' - Events Params', () => { // TODO v5: replace with createClientRender @@ -77,16 +74,6 @@ describe(' - Events Params', () => { ); }; - const TestVirtualization = (props) => { - const { width, height, ...other } = props; - const data = useData(50, 5); - return ( -
- -
- ); - }; - describe('columnHeaderParams', () => { it('should include the correct params', () => { let eventArgs: { params: GridColumnHeaderParams; event: React.MouseEvent } | null = null; @@ -278,7 +265,7 @@ describe(' - Events Params', () => { const handleRowsScrollEnd = spy(); render(); - apiRef.current.publishEvent(GridEvents.rowsScroll); + apiRef.current.publishEvent(GridEvents.rowsScroll, { left: 0, top: 3 * 52 }); expect(handleRowsScrollEnd.callCount).to.equal(1); }); @@ -323,35 +310,13 @@ describe(' - Events Params', () => { />
, ); - const gridWindow = container.querySelector('.MuiDataGrid-window'); + const virtualScroller = container.querySelector('.MuiDataGrid-virtualScroller'); // arbitrary number to make sure that the bottom of the grid window is reached. - gridWindow.scrollTop = 12345; - gridWindow.dispatchEvent(new Event('scroll')); + virtualScroller.scrollTop = 12345; + virtualScroller.dispatchEvent(new Event('scroll')); expect(handleRowsScrollEnd.callCount).to.equal(1); }); - it('call onViewportRowsChange when the viewport rows change', async () => { - const handleViewportRowsChange = spy(); - // TODO: Set the dimensions of the grid once the Windows test issues are resolved. - const { container } = render( - , - ); - - await waitFor(() => { - expect(handleViewportRowsChange.lastCall.args[0].firstRowIndex).to.equal(0); - expect(handleViewportRowsChange.lastCall.args[0].lastRowIndex).to.equal(6); // should be pageSize + 1 - }); - const gridWindow = container.querySelector('.MuiDataGrid-window'); - // scroll 6 rows so that the renderContext is updated. To be changed to a scroll of 1 row. - // TODO: set RowHeight directly. Currently 52 is used because the test fails under Windows. - gridWindow.scrollTop = 52 * 6; - gridWindow.dispatchEvent(new Event('scroll')); - await waitFor(() => { - expect(handleViewportRowsChange.lastCall.args[0].firstRowIndex).to.equal(6); // should be 1 - expect(handleViewportRowsChange.lastCall.args[0].lastRowIndex).to.equal(12); // should be pageSize + 1 - }); - }); - it('should publish GridEvents.unmount when unmounting the Grid', () => { const onUnmount = spy(); diff --git a/packages/grid/x-grid/src/tests/rows.DataGridPro.test.tsx b/packages/grid/x-grid/src/tests/rows.DataGridPro.test.tsx index dc398913343e..40f1b05db9db 100644 --- a/packages/grid/x-grid/src/tests/rows.DataGridPro.test.tsx +++ b/packages/grid/x-grid/src/tests/rows.DataGridPro.test.tsx @@ -6,7 +6,7 @@ import { } from 'test/utils'; import { useFakeTimers, spy } from 'sinon'; import { expect } from 'chai'; -import { getCell, getColumnValues } from 'test/utils/helperFn'; +import { getCell, getRow, getColumnValues } from 'test/utils/helperFn'; import { GridApiRef, GridComponentProps, @@ -431,19 +431,36 @@ describe(' - Rows', () => { }); it('should render last row when scrolling to the bottom', () => { - render(); - const totalHeight = apiRef.current.state.containerSizes?.totalSizes.height!; + const rowHeight = 50; + const rowBuffer = 4; + const nbRows = 996; + const height = 600; + render( + , + ); - const gridWindow = document.querySelector('.MuiDataGrid-window')!; - const renderingZone = document.querySelector('.MuiDataGrid-renderingZone')! as HTMLElement; - gridWindow.scrollTop = 10e6; // scroll to the bottom - gridWindow.dispatchEvent(new Event('scroll')); + const virtualScroller = document.querySelector('.MuiDataGrid-virtualScroller')!; + const renderingZone = document.querySelector( + '.MuiDataGrid-virtualScrollerRenderZone', + )! as HTMLElement; + virtualScroller.scrollTop = 10e6; // scroll to the bottom + virtualScroller.dispatchEvent(new Event('scroll')); const lastCell = document.querySelector('[role="row"]:last-child [role="cell"]:first-child')!; expect(lastCell).to.have.text('995'); - expect(renderingZone.children.length).to.equal(16); - expect(renderingZone.style.transform).to.equal('translate3d(0px, -312px, 0px)'); - expect(gridWindow.scrollHeight).to.equal(totalHeight); + expect(renderingZone.children.length).to.equal(Math.floor(height / rowHeight) + rowBuffer); + const distanceToFirstRow = (nbRows - renderingZone.children.length) * rowHeight; + expect(renderingZone.style.transform).to.equal( + `translate3d(0px, ${distanceToFirstRow}px, 0px)`, + ); + expect(virtualScroller.scrollHeight).to.equal(nbRows * rowHeight); }); it('Rows should not be virtualized when the grid is in pagination autoPageSize', () => { @@ -460,47 +477,74 @@ describe(' - Rows', () => { expect(isVirtualized).to.equal(false); }); - it('should set the virtual page to 0 when resetting rows to a non virtualized length', () => { - const { setProps } = render(); - - const gridWindow = document.querySelector('.MuiDataGrid-window')!; - gridWindow.scrollTop = 10e6; // scroll to the bottom - gridWindow.dispatchEvent(new Event('scroll')); - - let lastCell = document.querySelector('[role="row"]:last-child [role="cell"]:first-child')!; - expect(lastCell).to.have.text('995'); - - let virtualPage = apiRef.current.state.rendering!.virtualPage; - expect(virtualPage).to.equal(98); - - setProps({ nbRows: 9 }); - - lastCell = document.querySelector('[role="row"]:last-child [role="cell"]:first-child')!; - expect(lastCell).to.have.text('8'); - - const renderingZone = document.querySelector('.MuiDataGrid-renderingZone')! as HTMLElement; - expect(renderingZone.children.length).to.equal(9); - - virtualPage = apiRef.current.state.rendering!.virtualPage; - expect(virtualPage).to.equal(0); + it('should render extra columns when the columnBuffer prop is present', () => { + const border = 1; + const width = 300 + border * 2; + const columnBuffer = 2; + const columnWidth = 100; + render(); + const firstRow = getRow(0); + expect(firstRow.children).to.have.length(Math.floor(width / columnWidth) + columnBuffer); + const virtualScroller = document.querySelector('.MuiDataGrid-virtualScroller')!; + virtualScroller.scrollLeft = 301; + virtualScroller.dispatchEvent(new Event('scroll')); + expect(firstRow.children).to.have.length( + columnBuffer + Math.floor(width / columnWidth) + columnBuffer, + ); + }); - const isVirtualized = apiRef.current.state.containerSizes!.isVirtualized; - expect(isVirtualized).to.equal(false); + it('should render new rows when scrolling past the rowThreshold value', () => { + const rowThreshold = 3; + const rowHeight = 50; + render( + , + ); + const virtualScroller = document.querySelector('.MuiDataGrid-virtualScroller')!; + const renderingZone = document.querySelector('.MuiDataGrid-virtualScrollerRenderZone')!; + const firstRow = renderingZone.firstChild; + expect(firstRow).to.have.attr('data-rowindex', '0'); + virtualScroller.scrollTop = rowThreshold * rowHeight; + virtualScroller.dispatchEvent(new Event('scroll')); + expect(firstRow).to.have.attr('data-rowindex', '3'); + }); + + it('should render new columns when scrolling past the columnThreshold value', () => { + const columnThreshold = 3; + const columnWidth = 100; + render( + , + ); + const virtualScroller = document.querySelector('.MuiDataGrid-virtualScroller')!; + const renderingZone = document.querySelector('.MuiDataGrid-virtualScrollerRenderZone')!; + const firstRow = renderingZone.querySelector('[role="row"]:first-child')!; + const firstColumn = firstRow.firstChild!; + expect(firstColumn).to.have.attr('data-colindex', '0'); + virtualScroller.scrollLeft = columnThreshold * columnWidth; + virtualScroller.dispatchEvent(new Event('scroll')); + expect(firstColumn).to.have.attr('data-colindex', '3'); }); describe('Pagination', () => { it('should render only the pageSize', () => { - render(); - const gridWindow = document.querySelector('.MuiDataGrid-window')!; - gridWindow.scrollTop = 10e6; // scroll to the bottom - gridWindow.dispatchEvent(new Event('scroll')); + const rowHeight = 50; + const nbRows = 32; + render( + , + ); + const virtualScroller = document.querySelector('.MuiDataGrid-virtualScroller')!; + virtualScroller.scrollTop = 10e6; // scroll to the bottom + virtualScroller.dispatchEvent(new Event('scroll')); const lastCell = document.querySelector( '[role="row"]:last-child [role="cell"]:first-child', )!; expect(lastCell).to.have.text('31'); - const totalHeight = apiRef.current.state.containerSizes?.totalSizes.height!; - expect(gridWindow.scrollHeight).to.equal(totalHeight); + expect(virtualScroller.scrollHeight).to.equal(nbRows * rowHeight); }); it('should not virtualized the last page if smaller than viewport', () => { @@ -513,28 +557,26 @@ describe(' - Rows', () => { height={500} />, ); - const gridWindow = document.querySelector('.MuiDataGrid-window')!; - gridWindow.scrollTop = 10e6; // scroll to the bottom - gridWindow.dispatchEvent(new Event('scroll')); + const virtualScroller = document.querySelector('.MuiDataGrid-virtualScroller')!; + virtualScroller.scrollTop = 10e6; // scroll to the bottom + virtualScroller.dispatchEvent(new Event('scroll')); const lastCell = document.querySelector( '[role="row"]:last-child [role="cell"]:first-child', )!; expect(lastCell).to.have.text('99'); - expect(gridWindow.scrollTop).to.equal(0); - expect(gridWindow.scrollHeight).to.equal(gridWindow.clientHeight); - - const isVirtualized = apiRef.current.state.containerSizes!.isVirtualized; - expect(isVirtualized).to.equal(false); - const virtualRowsCount = apiRef.current.state.containerSizes!.virtualRowsCount; - expect(virtualRowsCount).to.equal(4); + expect(virtualScroller.scrollTop).to.equal(0); + expect(virtualScroller.scrollHeight).to.equal(virtualScroller.clientHeight); + expect( + document.querySelector('.MuiDataGrid-virtualScrollerRenderZone')!.children, + ).to.have.length(4); }); it('should paginate small dataset in auto page-size #1492', () => { render( , ); - const gridWindow = document.querySelector('.MuiDataGrid-window')!; + const virtualScroller = document.querySelector('.MuiDataGrid-virtualScroller')!; const lastCell = document.querySelector( '[role="row"]:last-child [role="cell"]:first-child', @@ -543,13 +585,11 @@ describe(' - Rows', () => { const rows = document.querySelectorAll('.MuiDataGrid-row[role="row"]')!; expect(rows.length).to.equal(7); - expect(gridWindow.scrollTop).to.equal(0); - expect(gridWindow.scrollHeight).to.equal(gridWindow.clientHeight); - - const isVirtualized = apiRef.current.state.containerSizes!.isVirtualized; - expect(isVirtualized).to.equal(false); - const virtualRowsCount = apiRef.current.state.containerSizes!.virtualRowsCount; - expect(virtualRowsCount).to.equal(7); + expect(virtualScroller.scrollTop).to.equal(0); + expect(virtualScroller.scrollHeight).to.equal(virtualScroller.clientHeight); + expect( + document.querySelector('.MuiDataGrid-virtualScrollerRenderZone')!.children, + ).to.have.length(7); }); }); @@ -568,9 +608,9 @@ describe(' - Rows', () => { rowHeight={rowHeight} />, ); - const gridWindow = document.querySelector('.MuiDataGrid-window')!; + const virtualScroller = document.querySelector('.MuiDataGrid-virtualScroller')!; apiRef.current.scrollToIndexes({ rowIndex: 4, colIndex: 0 }); - expect(gridWindow.scrollTop).to.equal(rowHeight - offset); + expect(virtualScroller.scrollTop).to.equal(rowHeight - offset); }); it('should scroll correctly when the given index is partially visible at the top', () => { @@ -587,15 +627,15 @@ describe(' - Rows', () => { rowHeight={rowHeight} />, ); - const gridWindow = document.querySelector('.MuiDataGrid-window')!; - gridWindow.scrollTop = offset; - gridWindow.dispatchEvent(new Event('scroll')); // Simulate browser behavior + const virtualScroller = document.querySelector('.MuiDataGrid-virtualScroller')!; + virtualScroller.scrollTop = offset; + virtualScroller.dispatchEvent(new Event('scroll')); // Simulate browser behavior apiRef.current.scrollToIndexes({ rowIndex: 2, colIndex: 0 }); - expect(gridWindow.scrollTop).to.equal(offset); + expect(virtualScroller.scrollTop).to.equal(offset); apiRef.current.scrollToIndexes({ rowIndex: 1, colIndex: 0 }); - expect(gridWindow.scrollTop).to.equal(offset); + expect(virtualScroller.scrollTop).to.equal(offset); apiRef.current.scrollToIndexes({ rowIndex: 0, colIndex: 0 }); - expect(gridWindow.scrollTop).to.equal(0); + expect(virtualScroller.scrollTop).to.equal(0); }); it('should scroll correctly when the given colIndex is partially visible at the right', () => { @@ -610,10 +650,10 @@ describe(' - Rows', () => { { field: 'age', width: columnWidth }, ]; render(); - const gridWindow = document.querySelector('.MuiDataGrid-window')!; - expect(gridWindow.scrollLeft).to.equal(0); + const virtualScroller = document.querySelector('.MuiDataGrid-virtualScroller')!; + expect(virtualScroller.scrollLeft).to.equal(0); apiRef.current.scrollToIndexes({ rowIndex: 0, colIndex: 2 }); - expect(gridWindow.scrollLeft).to.equal(columnWidth * 3 - width); + expect(virtualScroller.scrollLeft).to.equal(columnWidth * 3 - width); }); it('should not scroll when going back', () => { @@ -628,13 +668,13 @@ describe(' - Rows', () => { { field: 'age', width: columnWidth }, ]; render(); - const gridWindow = document.querySelector('.MuiDataGrid-window')!; - expect(gridWindow.scrollLeft).to.equal(0); + const virtualScroller = document.querySelector('.MuiDataGrid-virtualScroller')!; + expect(virtualScroller.scrollLeft).to.equal(0); apiRef.current.scrollToIndexes({ rowIndex: 0, colIndex: 2 }); - gridWindow.dispatchEvent(new Event('scroll')); // Simulate browser behavior - expect(gridWindow.scrollLeft).to.equal(columnWidth * 3 - width); + virtualScroller.dispatchEvent(new Event('scroll')); // Simulate browser behavior + expect(virtualScroller.scrollLeft).to.equal(columnWidth * 3 - width); apiRef.current.scrollToIndexes({ rowIndex: 0, colIndex: 1 }); - expect(gridWindow.scrollLeft).to.equal(columnWidth * 3 - width); + expect(virtualScroller.scrollLeft).to.equal(columnWidth * 3 - width); }); }); }); @@ -670,16 +710,6 @@ describe(' - Rows', () => { apiRef.current.setPage(1); expect(document.querySelectorAll('[role="row"][data-rowindex]')).to.have.length(50); }); - - it('should translate to the correct position on scroll', () => { - render(); - const gridWindow = document.querySelector('.MuiDataGrid-window')!; - const renderingZone = document.querySelector('.MuiDataGrid-renderingZone')! as HTMLElement; - expect(renderingZone.style.transform).to.equal('translate3d(0px, 0px, 0px)'); - gridWindow.scrollTop = 100; - gridWindow.dispatchEvent(new Event('scroll')); - expect(renderingZone.style.transform).to.equal('translate3d(0px, -100px, 0px)'); - }); }); describe('Cell focus', () => { diff --git a/packages/grid/x-grid/src/useDataGridProComponent.tsx b/packages/grid/x-grid/src/useDataGridProComponent.tsx index baa05059e47f..917006aba8b3 100644 --- a/packages/grid/x-grid/src/useDataGridProComponent.tsx +++ b/packages/grid/x-grid/src/useDataGridProComponent.tsx @@ -23,8 +23,6 @@ import { useGridRows } from '../../_modules_/grid/hooks/features/rows/useGridRow import { useGridParamsApi } from '../../_modules_/grid/hooks/features/rows/useGridParamsApi'; import { useGridSelection } from '../../_modules_/grid/hooks/features/selection/useGridSelection'; import { useGridSorting } from '../../_modules_/grid/hooks/features/sorting/useGridSorting'; -import { useGridVirtualization } from '../../_modules_/grid/hooks/features/virtualization/useGridVirtualization'; -import { useGridNoVirtualization } from '../../_modules_/grid/hooks/features/virtualization/useGridNoVirtualization'; import { useGridScroll } from '../../_modules_/grid/hooks/features/scroll/useGridScroll'; import { useGridEvents } from '../../_modules_/grid/hooks/features/events/useGridEvents'; import { useGridContainerProps } from '../../_modules_/grid/hooks/features/container/useGridContainerProps'; @@ -49,8 +47,6 @@ export const useDataGridProComponent = (apiRef: GridApiRef, props: GridComponent useGridPage(apiRef, props); useGridContainerProps(apiRef, props); useGridScroll(apiRef, props); - useGridNoVirtualization(apiRef, props); - useGridVirtualization(apiRef, props); useGridInfiniteLoader(apiRef, props); useGridColumnMenu(apiRef); useGridKeyboard(apiRef); diff --git a/test/e2e/index.test.ts b/test/e2e/index.test.ts index 309cd9518f51..b9eb902238d9 100644 --- a/test/e2e/index.test.ts +++ b/test/e2e/index.test.ts @@ -202,12 +202,14 @@ describe('e2e', () => { document.querySelector('[role="row"][data-rowindex="3"] [role="cell"]')!.scrollIntoView(), ); const scrollTop = await page.evaluate( - () => document.querySelector('.MuiDataGrid-window')!.scrollTop!, + () => document.querySelector('.MuiDataGrid-virtualScroller')!.scrollTop!, ); expect(scrollTop).not.to.equal(0); await page.click('[role="row"][data-rowindex="3"] [role="cell"]'); expect( - await page.evaluate(() => document.querySelector('.MuiDataGrid-window')!.scrollTop!), + await page.evaluate( + () => document.querySelector('.MuiDataGrid-virtualScroller')!.scrollTop!, + ), ).to.equal(scrollTop); }); });