Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[DataGrid] Add disableVirtualization to disable virtualization completely #2326

Merged
merged 11 commits into from
Aug 31, 2021
1 change: 1 addition & 0 deletions docs/pages/api-docs/data-grid/data-grid-pro.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ The name <code>MuiDataGridPro</code> can be used when providing [default props](
| <span class="prop-name">disableMultipleColumnsSorting</span> | <span class="prop-type">boolean</span> | false | If `true`, sorting with multiple columns is disabled. |
| <span class="prop-name">disableMultipleSelection</span> | <span class="prop-type">boolean</span> | false | If `true`, multiple selection using the CTRL or CMD key is disabled. |
| <span class="prop-name">disableSelectionOnClick</span> | <span class="prop-type">boolean</span> | false | If `true`, the selection on click on a row or cell is disabled. |
| <span class="prop-name">disableVirtualization</span> | <span class="prop-type">boolean</span> | false | If `true`, the virtualization is disabled. |
| <span class="prop-name">error</span> | <span class="prop-type">any</span> | | An error that will turn the grid into its error state and display the error component. |
| <span class="prop-name">editMode</span> | <span class="prop-type">GridEditMode</span> | 'cell' | Controls whether to use the cell or row editing. |
| <span class="prop-name">editRowsModel</span> | <span class="prop-type">GridEditRowsModel</span> | undefined | Set the edit rows model of the grid. |
Expand Down
1 change: 1 addition & 0 deletions docs/pages/api-docs/data-grid/data-grid.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ The name <code>MuiDataGrid</code> can be used when providing [default props](/cu
| <span class="prop-name">disableDensitySelector</span> | <span class="prop-type">boolean</span> | false | If `true`, the density selector is disabled. |
| <span class="prop-name">disableExtendRowFullWidth</span> | <span class="prop-type">boolean</span> | false | If `true`, rows will not be extended to fill the full width of the grid container. |
| <span class="prop-name">disableSelectionOnClick</span> | <span class="prop-type">boolean</span> | false | If `true`, the selection on click on a row or cell is disabled. |
| <span class="prop-name">disableVirtualization</span> | <span class="prop-type">boolean</span> | false | If `true`, the virtualization is disabled. |
| <span class="prop-name">error</span> | <span class="prop-type">any</span> | | An error that will turn the grid into its error state and display the error component. |
| <span class="prop-name">editMode</span> | <span class="prop-type">GridEditMode</span> | 'cell' | Controls whether to use the cell or row editing. |
| <span class="prop-name">editRowsModel</span> | <span class="prop-type">GridEditRowsModel</span> | undefined | Set the edit rows model of the grid. |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "GridVirtualizationApi",
"description": "The virtualization API interface that is available in the grid apiRef.",
"name": "GridScrollApi",
"description": "The scroll API interface that is available in the grid apiRef.",
"properties": [
{
"name": "getScrollPosition",
Expand Down
2 changes: 1 addition & 1 deletion docs/scripts/buildApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ function run(argv: { outputDirectory?: string }) {
'GridFilterApi',
'GridCsvExportApi',
'GridExportCsvOptions',
'GridVirtualizationApi',
'GridScrollApi',
'GridEditRowApi',
'GridEvents',
];
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ title: Data Grid - Virtualization
DOM virtualization is the feature that allows the grid to handle an unlimited\* number of rows and columns.
This is a built-in feature of the rendering engine and greatly improves rendering performance.

_unlimited\*: Browsers set a limit on the number of pixels a scroll container can host: 17.5 million pixels on Firefox, 33.5 million pixels on Chrome, Edge, and Safari. A [reproduction](https://codesandbox.io/s/beautiful-silence-1yifo?file=/src/App.js)._
_\*unlimited: Browsers set a limit on the number of pixels a scroll container can host: 17.5 million pixels on Firefox, 33.5 million pixels on Chrome, Edge, and Safari. A [reproduction](https://codesandbox.io/s/beautiful-silence-1yifo?file=/src/App.js)._

## Row virtualization [<span class="pro"></span>](https://material-ui.com/store/items/material-ui-pro/)

Expand All @@ -33,9 +33,16 @@ By default, 2 columns are rendered outside of the viewport. You can change this

You can disable column virtualization by setting the column buffer to a higher number than the number of rendered columns, e.g. with `columnBuffer={columns.length}` or `columnBuffer={Number.MAX_SAFE_INTEGER}`.

## apiRef [<span class="pro"></span>](https://material-ui.com/store/items/material-ui-pro/)
## Disable virtualization

{{"demo": "pages/components/data-grid/virtualization/VirtualizationApiNoSnap.js", "bg": "inline", "hideToolbar": true}}
Copy link
Member

Choose a reason for hiding this comment

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

Why did you remove this section?

Copy link
Member Author

Choose a reason for hiding this comment

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

One of the changes is to move the scroll methods to a separated hook. Since this interface is empty now I removed it. Maybe we should add a page for scrolling with a demo: #1103 (comment). In this new page we could document the API available.

The virtualization can be disabled completely using the `disableVirtualization` prop.
You may want to turn it off to be able to test the grid with a headless browser, like jsdom.

```tsx
<DataGrid {...data} disableVirtualization />
```

**Note**: Disabling the virtualization will increase the size of the DOM and drastically reduce the performance. Use it only for testing purposes or on small datasets.

## API

Expand Down
1 change: 1 addition & 0 deletions packages/grid/_modules_/grid/hooks/features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export * from './rows';
export * from './selection';
export * from './sorting';
export * from './virtualization';
export * from './scroll';

export * from './useGridApiRef';
export * from './columnResize';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useGridScroll';
151 changes: 151 additions & 0 deletions packages/grid/_modules_/grid/hooks/features/scroll/useGridScroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import * as React from 'react';
import { GridCellIndexCoordinates } from '../../../models/gridCell';
import { GridApiRef } from '../../../models/api/gridApiRef';
import { useLogger } from '../../utils/useLogger';
import {
gridColumnsMetaSelector,
visibleGridColumnsSelector,
} from '../columns/gridColumnsSelector';
import { useGridSelector } from '../core/useGridSelector';
import { GridComponentProps } from '../../../GridComponentProps';
import { gridPaginationSelector } from '../pagination/gridPaginationSelector';
import { gridRowCountSelector } from '../rows/gridRowsSelector';
import { gridDensityRowHeightSelector } from '../density/densitySelector';
import { GridScrollParams } from '../../../models/params/gridScrollParams';
import { GridScrollApi } from '../../../models/api/gridScrollApi';
import { scrollStateSelector } from '../virtualization/renderingStateSelector';
import { useGridApiMethod } from '../../root/useGridApiMethod';
import { useNativeEventListener } from '../../root/useNativeEventListener';

// Logic copied from https://www.w3.org/TR/wai-aria-practices/examples/listbox/js/listbox.js
// Similar to https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
function scrollIntoView(dimensions) {
const { clientHeight, scrollTop, offsetHeight, offsetTop } = dimensions;

const elementBottom = offsetTop + offsetHeight;
if (elementBottom - clientHeight > scrollTop) {
return elementBottom - clientHeight;
}
if (offsetTop < scrollTop) {
return offsetTop;
}
return undefined;
}

export const useGridScroll = (
apiRef: GridApiRef,
props: Pick<GridComponentProps, 'pagination'>,
): void => {
const logger = useLogger('useGridScroll');
const colRef = apiRef.current.columnHeadersElementRef!;
const windowRef = apiRef.current.windowRef!;

const rowHeight = useGridSelector(apiRef, gridDensityRowHeightSelector);
const paginationState = useGridSelector(apiRef, gridPaginationSelector);
const totalRowCount = useGridSelector(apiRef, gridRowCountSelector);
const visibleColumns = useGridSelector(apiRef, visibleGridColumnsSelector);
const columnsMeta = useGridSelector(apiRef, gridColumnsMetaSelector);

const scrollToIndexes = React.useCallback<GridScrollApi['scrollToIndexes']>(
(params: Partial<GridCellIndexCoordinates>) => {
if (totalRowCount === 0 || visibleColumns.length === 0) {
return false;
}

logger.debug(`Scrolling to cell at row ${params.rowIndex}, col: ${params.colIndex} `);

const scrollCoordinates: any = {};

if (params.colIndex != null) {
scrollCoordinates.left = scrollIntoView({
clientHeight: windowRef.current!.clientWidth,
scrollTop: windowRef.current!.scrollLeft,
offsetHeight: visibleColumns[params.colIndex].computedWidth,
offsetTop: columnsMeta.positions[params.colIndex],
});
}

if (params.rowIndex != null) {
const elementIndex = !props.pagination
? params.rowIndex
: params.rowIndex - paginationState.page * paginationState.pageSize;

scrollCoordinates.top = scrollIntoView({
clientHeight: windowRef.current!.clientHeight,
scrollTop: windowRef.current!.scrollTop,
offsetHeight: rowHeight,
offsetTop: rowHeight * elementIndex,
});
}

if (
typeof scrollCoordinates.left !== undefined ||
typeof scrollCoordinates.top !== undefined
) {
apiRef.current.scroll(scrollCoordinates);
return true;
}

return false;
},
[
totalRowCount,
visibleColumns,
logger,
apiRef,
props.pagination,
paginationState.page,
paginationState.pageSize,
windowRef,
columnsMeta.positions,
rowHeight,
],
);

const scroll = React.useCallback<GridScrollApi['scroll']>(
(params: Partial<GridScrollParams>) => {
if (windowRef.current && params.left != null && colRef.current) {
colRef.current.scrollLeft = params.left;
windowRef.current.scrollLeft = params.left;
logger.debug(`Scrolling left: ${params.left}`);
}
if (windowRef.current && params.top != null) {
windowRef.current.scrollTop = params.top;
logger.debug(`Scrolling top: ${params.top}`);
}
logger.debug(`Scrolling, updating container, and viewport`);
},
[windowRef, colRef, logger],
);

const getScrollPosition = React.useCallback<GridScrollApi['getScrollPosition']>(
() => scrollStateSelector(apiRef.current.getState()),
[apiRef],
);

const scrollApi: GridScrollApi = {
scroll,
scrollToIndexes,
getScrollPosition,
};
useGridApiMethod(apiRef, scrollApi, 'GridScrollApi');

const preventScroll = React.useCallback((event: any) => {
event.target.scrollLeft = 0;
event.target.scrollTop = 0;
}, []);

useNativeEventListener(
apiRef,
() => apiRef.current?.renderingZoneRef?.current?.parentElement,
'scroll',
preventScroll,
);

useNativeEventListener(
apiRef,
() => apiRef.current?.columnHeadersContainerElementRef?.current,
'scroll',
preventScroll,
);
};
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './useGridVirtualRows';
export * from './useGridVirtualization';
export * from './renderingState';
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import * as React from 'react';
import { GridComponentProps } from '../../../GridComponentProps';
import { GridApiRef } from '../../../models/api/gridApiRef';
import { useNativeEventListener } from '../../root/useNativeEventListener';
import { useGridScrollFn } from '../../utils/useGridScrollFn';
import { visibleGridColumnsSelector } from '../columns/gridColumnsSelector';
import { useGridSelector } from '../core';
import { useGridState } from '../core/useGridState';
import { gridPaginationSelector } from '../pagination/gridPaginationSelector';

export const useGridNoVirtualization = (
apiRef: GridApiRef,
props: Pick<GridComponentProps, 'disableVirtualization' | 'pagination' | 'paginationMode'>,
): void => {
const windowRef = apiRef.current.windowRef;
const columnsHeaderRef = apiRef.current.columnHeadersElementRef;
const renderingZoneRef = apiRef.current.renderingZoneRef;
const [gridState, setGridState, forceUpdate] = useGridState(apiRef);
const [scrollTo] = useGridScrollFn(renderingZoneRef!, columnsHeaderRef!);
const paginationState = useGridSelector(apiRef, gridPaginationSelector);
const visibleColumns = useGridSelector(apiRef, visibleGridColumnsSelector);

const syncState = React.useCallback(() => {
if (!gridState.containerSizes || !windowRef?.current) {
return;
}

let firstRowIdx = 0;
const { page, pageSize } = paginationState;
if (props.pagination && props.paginationMode === 'client') {
firstRowIdx = pageSize * page;
}
const lastRowIdx = firstRowIdx + gridState.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();
}, [
gridState.containerSizes,
paginationState,
props.pagination,
props.paginationMode,
setGridState,
forceUpdate,
visibleColumns.length,
windowRef,
]);

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]);

useNativeEventListener(apiRef, windowRef!, 'scroll', handleScroll, { passive: true });
};
Loading