diff --git a/docs/pages/api-docs/data-grid/data-grid.md b/docs/pages/api-docs/data-grid/data-grid.md
index 1926f22659ca6..93630d4f604a6 100644
--- a/docs/pages/api-docs/data-grid/data-grid.md
+++ b/docs/pages/api-docs/data-grid/data-grid.md
@@ -44,6 +44,7 @@ import { DataGrid } from '@material-ui/data-grid';
| hideFooterRowCount | boolean | false | If `true`, the row count in the footer is hidden. |
| hideFooterSelectedRowCount | boolean | false | If `true`, the selected row count in the footer is hidden. |
| icons | IconsOptions | | Set of icons used in the grid. |
+| infniteLoadingMode | GridFeatureMode | 'client' | Infnite loading can be processed on the server or client-side. Set it to 'client' if you would like to handle the infnite loading on the client-side. Set it to 'server' if you would like to handle the infnite loading on the server-side. |
| isCellEditable | (params: GridCellParams) => boolean | | Callback fired when a cell is rendered, returns true if the cell is editable. |
| isRowSelectable | (params: GridRowParams) => boolean | | Determines if a row can be selected. |
| loading | boolean | false | If `true`, a loading overlay is displayed. |
diff --git a/docs/pages/api-docs/data-grid/grid-api.md b/docs/pages/api-docs/data-grid/grid-api.md
index ea39eac9082cd..efa1fba889ced 100644
--- a/docs/pages/api-docs/data-grid/grid-api.md
+++ b/docs/pages/api-docs/data-grid/grid-api.md
@@ -57,6 +57,7 @@ import { GridApi } from '@material-ui/x-grid';
| hideColumnMenu | () => void | Hides the column menu that is open. |
| hideFilterPanel | () => void | Hides the filter panel. |
| hidePreferences | () => void | Hides the preferences panel. |
+| insertRows | (params: GridInsertRowParams) => void | Inserts a new subset of Rows. |
| isCellEditable | (params: GridCellParams) => boolean | Controls if a cell is editable. |
| isColumnVisibleInWindow | (colIndex: number) => boolean | Checks if a column at the index given by `colIndex` is currently visible in the viewport. |
| publishEvent | (name: string, ...args: any[]) => void | Emits an event. |
diff --git a/docs/pages/api-docs/data-grid/x-grid.md b/docs/pages/api-docs/data-grid/x-grid.md
index 86773eeba8518..cf03d3d6bc241 100644
--- a/docs/pages/api-docs/data-grid/x-grid.md
+++ b/docs/pages/api-docs/data-grid/x-grid.md
@@ -48,6 +48,7 @@ import { XGrid } from '@material-ui/x-grid';
| hideFooterRowCount | boolean | false | If `true`, the row count in the footer is hidden. |
| hideFooterSelectedRowCount | boolean | false | If `true`, the selected row count in the footer is hidden. |
| icons | IconsOptions | | Set of icons used in the grid. |
+| infniteLoadingMode | GridFeatureMode | 'client' | Infnite loading can be processed on the server or client-side. Set it to 'client' if you would like to handle the infnite loading on the client-side. Set it to 'server' if you would like to handle the infnite loading on the server-side. |
| isCellEditable | (params: GridCellParams) => boolean | | Callback fired when a cell is rendered, returns true if the cell is editable. |
| isRowSelectable | (params: GridRowParams) => boolean | | Determines if a row can be selected. |
| loading | boolean | false | If `true`, a loading overlay is displayed.. |
diff --git a/docs/src/pages/components/data-grid/events/events.json b/docs/src/pages/components/data-grid/events/events.json
index cc88648d1a25c..58f73e20a9900 100644
--- a/docs/src/pages/components/data-grid/events/events.json
+++ b/docs/src/pages/components/data-grid/events/events.json
@@ -181,5 +181,13 @@
{
"name": "columnVisibilityChange",
"description": "Fired when a column visibility changes. Called with a GridColumnVisibilityChangeParams object."
+ },
+ {
+ "name": "virtualPageChange",
+ "description": "Fired when the virtual page changes. Called with a GridVirtualPageChangeParams object."
+ },
+ {
+ "name": "fetchRows",
+ "description": "Fired when the virtual page changes. Called with a GridFetchRowsParams object."
}
]
diff --git a/docs/src/pages/components/data-grid/rows/ServerInfiniteLoadingGrid.js b/docs/src/pages/components/data-grid/rows/ServerInfiniteLoadingGrid.js
new file mode 100644
index 0000000000000..bf5ab5c14af2d
--- /dev/null
+++ b/docs/src/pages/components/data-grid/rows/ServerInfiniteLoadingGrid.js
@@ -0,0 +1,55 @@
+import * as React from 'react';
+import { XGrid } from '@material-ui/x-grid';
+import {
+ useDemoData,
+ getRealData,
+ getCommodityColumns,
+} from '@material-ui/x-grid-data-generator';
+
+async function sleep(duration) {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve();
+ }, duration);
+ });
+}
+
+const loadServerRows = async (newRowLength) => {
+ const newData = await getRealData(newRowLength, getCommodityColumns());
+ // Simulate network throttle
+ await sleep(Math.random() * 100 + 100);
+
+ return newData.rows;
+};
+
+export default function InfiniteLoadingGrid() {
+ const { data } = useDemoData({
+ dataSet: 'Commodity',
+ rowLength: 10,
+ maxColumns: 6,
+ });
+
+ const handleFetchRows = async (params) => {
+ const newRowsBatch = await loadServerRows(params.viewportPageSize);
+
+ params.api.current.insertRows({
+ startIndex: params.startIndex,
+ pageSize: params.viewportPageSize,
+ newRows: newRowsBatch,
+ });
+ };
+
+ return (
+
+
+
+ );
+}
diff --git a/docs/src/pages/components/data-grid/rows/ServerInfiniteLoadingGrid.tsx b/docs/src/pages/components/data-grid/rows/ServerInfiniteLoadingGrid.tsx
new file mode 100644
index 0000000000000..425c0dcf93b92
--- /dev/null
+++ b/docs/src/pages/components/data-grid/rows/ServerInfiniteLoadingGrid.tsx
@@ -0,0 +1,57 @@
+import * as React from 'react';
+import { GridFetchRowsParams, GridRowData, XGrid } from '@material-ui/x-grid';
+import {
+ useDemoData,
+ getRealData,
+ getCommodityColumns,
+} from '@material-ui/x-grid-data-generator';
+
+async function sleep(duration) {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve();
+ }, duration);
+ });
+}
+
+const loadServerRows = async (newRowLength: number): Promise => {
+ const newData = await getRealData(newRowLength, getCommodityColumns());
+ // Simulate network throttle
+ await sleep(Math.random() * 100 + 100);
+
+ return newData.rows;
+};
+
+export default function InfiniteLoadingGrid() {
+ const { data } = useDemoData({
+ dataSet: 'Commodity',
+ rowLength: 10,
+ maxColumns: 6,
+ });
+
+ const handleFetchRows = async (params: GridFetchRowsParams) => {
+ const newRowsBatch: GridRowData[] = await loadServerRows(
+ params.viewportPageSize,
+ );
+
+ params.api.current.insertRows({
+ startIndex: params.startIndex,
+ pageSize: params.viewportPageSize,
+ newRows: newRowsBatch,
+ });
+ };
+
+ return (
+
+
+
+ );
+}
diff --git a/docs/src/pages/components/data-grid/rows/rows.md b/docs/src/pages/components/data-grid/rows/rows.md
index d0cfd7521c888..5c63e275e5f16 100644
--- a/docs/src/pages/components/data-grid/rows/rows.md
+++ b/docs/src/pages/components/data-grid/rows/rows.md
@@ -35,6 +35,19 @@ In addition, the area in which the callback provided to the `onRowsScrollEnd` is
{{"demo": "pages/components/data-grid/rows/InfiniteLoadingGrid.js", "bg": "inline", "disableAd": true}}
+### Unstable Server-side infinite loading [](https://material-ui.com/store/items/material-ui-pro/)
+
+By default, infinite loading works on the client-side.
+To switch it to server-side, set `infniteLoadingMode="server"`.
+Then the `rowCount` needs to be set and the number of initially loaded rows needs to be less than the `rowCount` value.
+In addition, you need to handle the `onFetchRows` callback to fetch the rows for the corresponding index.
+Finally, you need to use the `apiRef.current.insertRows()` to tell the DataGrid where to insert the newly fetched rows.
+
+**Note**: in order for the filtering and sorting to work you need to set their modes to `server`.
+You can find out more information about how to do that on the [server-side filter page](/components/data-grid/filtering/#server-side-filter) and on the [server-side sorting page](/components/data-grid/sorting/#server-side-sorting).
+
+{{"demo": "pages/components/data-grid/rows/ServerInfiniteLoadingGrid.js", "bg": "inline", "disableAd": true}}
+
### apiRef [](https://material-ui.com/store/items/material-ui-pro/)
The second way to update rows is to use the apiRef.
diff --git a/packages/grid/_modules_/grid/components/GridViewport.tsx b/packages/grid/_modules_/grid/components/GridViewport.tsx
index 0ed7f2e839a60..cf2ce0b8760df 100644
--- a/packages/grid/_modules_/grid/components/GridViewport.tsx
+++ b/packages/grid/_modules_/grid/components/GridViewport.tsx
@@ -17,6 +17,7 @@ import { GridEmptyCell } from './cell/GridEmptyCell';
import { GridRenderingZone } from './GridRenderingZone';
import { GridRow } from './GridRow';
import { GridRowCells } from './cell/GridRowCells';
+import { GridSkeletonRowCells } from './cell/GridSkeletonRowCells';
import { GridStickyContainer } from './GridStickyContainer';
import {
gridContainerSizesSelector,
@@ -52,6 +53,7 @@ export const GridViewport: ViewportType = React.forwardRef(
renderState.renderContext.firstRowIdx,
renderState.renderContext.lastRowIdx!,
);
+
return renderedRows.map(([id, row], idx) => (
(
rowIndex={renderState.renderContext!.firstRowIdx! + idx}
>
-
+ {id.toString().indexOf('null-') === 0 ? (
+
+ ) : (
+
+ )}
));
diff --git a/packages/grid/_modules_/grid/components/cell/GridSkeletonCell.tsx b/packages/grid/_modules_/grid/components/cell/GridSkeletonCell.tsx
new file mode 100644
index 0000000000000..8b9af01d50197
--- /dev/null
+++ b/packages/grid/_modules_/grid/components/cell/GridSkeletonCell.tsx
@@ -0,0 +1,43 @@
+import * as React from 'react';
+import clsx from 'clsx';
+import Skeleton from '@material-ui/lab/Skeleton';
+import { GRID_SKELETON_CELL_CSS_CLASS } from '../../constants/cssClassesConstants';
+
+export interface GridSkeletonCellProps {
+ colIndex: number;
+ height: number;
+ rowIndex: number;
+ showRightBorder?: boolean;
+ width: number;
+}
+
+export const GridSkeletonCell = React.memo(function GridSkeletonCell(props: GridSkeletonCellProps) {
+ const { colIndex, height, rowIndex, showRightBorder, width } = props;
+
+ const cellRef = React.useRef(null);
+ const cssClasses = clsx(GRID_SKELETON_CELL_CSS_CLASS, {
+ 'MuiDataGrid-withBorder': showRightBorder,
+ });
+
+ const style = {
+ minWidth: width,
+ maxWidth: width,
+ lineHeight: `${height - 1}px`,
+ minHeight: height,
+ maxHeight: height,
+ };
+
+ return (
+
+
+
+ );
+});
diff --git a/packages/grid/_modules_/grid/components/cell/GridSkeletonRowCells.tsx b/packages/grid/_modules_/grid/components/cell/GridSkeletonRowCells.tsx
new file mode 100644
index 0000000000000..847a196662a19
--- /dev/null
+++ b/packages/grid/_modules_/grid/components/cell/GridSkeletonRowCells.tsx
@@ -0,0 +1,61 @@
+import * as React from 'react';
+import { GridColumns } from '../../models/index';
+import { GridApiContext } from '../GridApiContext';
+import { gridDensityRowHeightSelector } from '../../hooks/features/density/densitySelector';
+import { useGridSelector } from '../../hooks/features/core/useGridSelector';
+import { GridSkeletonCell } from './GridSkeletonCell';
+
+interface SkeletonRowCellsProps {
+ columns: GridColumns;
+ extendRowFullWidth: boolean;
+ firstColIdx: number;
+ hasScrollX: boolean;
+ hasScrollY: boolean;
+ lastColIdx: number;
+ rowIndex: number;
+ showCellRightBorder: boolean;
+}
+
+export const GridSkeletonRowCells = React.memo(function GridSkeletonRowCells(
+ props: SkeletonRowCellsProps,
+) {
+ const {
+ columns,
+ firstColIdx,
+ hasScrollX,
+ hasScrollY,
+ lastColIdx,
+ rowIndex,
+ showCellRightBorder,
+ } = props;
+ const apiRef = React.useContext(GridApiContext);
+ const rowHeight = useGridSelector(apiRef, gridDensityRowHeightSelector);
+
+ const skeletonCellsProps = columns.slice(firstColIdx, lastColIdx + 1).map((column, colIdx) => {
+ const colIndex = firstColIdx + colIdx;
+ const isLastColumn = colIndex === columns.length - 1;
+ const removeLastBorderRight = isLastColumn && hasScrollX && !hasScrollY;
+ const showRightBorder = !isLastColumn
+ ? showCellRightBorder
+ : !removeLastBorderRight && !props.extendRowFullWidth;
+
+ const skeletonCellProps = {
+ field: column.field,
+ width: column.width!,
+ height: rowHeight,
+ showRightBorder,
+ rowIndex,
+ colIndex,
+ };
+
+ return skeletonCellProps;
+ });
+
+ return (
+
+ {skeletonCellsProps.map((skeletonCellProps) => (
+
+ ))}
+
+ );
+});
diff --git a/packages/grid/_modules_/grid/components/cell/index.ts b/packages/grid/_modules_/grid/components/cell/index.ts
index 8d5f70eb20a5c..0b4303bb6c6ad 100644
--- a/packages/grid/_modules_/grid/components/cell/index.ts
+++ b/packages/grid/_modules_/grid/components/cell/index.ts
@@ -2,3 +2,5 @@ export * from './GridCell';
export * from './GridEditInputCell';
export * from './GridEmptyCell';
export * from './GridRowCells';
+export * from './GridSkeletonRowCells';
+export * from './GridSkeletonCell';
diff --git a/packages/grid/_modules_/grid/components/containers/GridRootStyles.ts b/packages/grid/_modules_/grid/components/containers/GridRootStyles.ts
index 784380e846376..25ff3adb53410 100644
--- a/packages/grid/_modules_/grid/components/containers/GridRootStyles.ts
+++ b/packages/grid/_modules_/grid/components/containers/GridRootStyles.ts
@@ -84,7 +84,7 @@ export const useStyles = makeStyles(
alignItems: 'center',
overflow: 'hidden',
},
- '& .MuiDataGrid-columnHeader, & .MuiDataGrid-cell': {
+ '& .MuiDataGrid-columnHeader, & .MuiDataGrid-cell, & .MuiDataGrid-skeletonCell': {
WebkitTapHighlightColor: 'transparent',
lineHeight: null,
padding: '0 10px',
@@ -244,7 +244,7 @@ export const useStyles = makeStyles(
},
},
},
- '& .MuiDataGrid-cell': {
+ '& .MuiDataGrid-cell, & .MuiDataGrid-skeletonCell': {
display: 'block',
overflow: 'hidden',
textOverflow: 'ellipsis',
diff --git a/packages/grid/_modules_/grid/constants/cssClassesConstants.ts b/packages/grid/_modules_/grid/constants/cssClassesConstants.ts
index f79b283f2f869..3bccf44586bb1 100644
--- a/packages/grid/_modules_/grid/constants/cssClassesConstants.ts
+++ b/packages/grid/_modules_/grid/constants/cssClassesConstants.ts
@@ -12,3 +12,4 @@ export const GRID_COLUMN_HEADER_SEPARATOR_RESIZABLE_CSS_CLASS = `${GRID_CSS_CLAS
export const GRID_COLUMN_HEADER_TITLE_CSS_CLASS = `${GRID_CSS_CLASS_PREFIX}-columnHeaderTitleContainer`;
export const GRID_COLUMN_HEADER_DROP_ZONE_CSS_CLASS = `${GRID_CSS_CLASS_PREFIX}-columnHeaderDropZone`;
export const GRID_COLUMN_HEADER_DRAGGING_CSS_CLASS = `${GRID_CSS_CLASS_PREFIX}-${GRID_COLUMN_HEADER_CSS_CLASS_SUFFIX}--dragging`;
+export const GRID_SKELETON_CELL_CSS_CLASS = `${GRID_CSS_CLASS_PREFIX}-skeletonCell`;
diff --git a/packages/grid/_modules_/grid/constants/eventsConstants.ts b/packages/grid/_modules_/grid/constants/eventsConstants.ts
index 53d00deadcec4..73301c9fe0502 100644
--- a/packages/grid/_modules_/grid/constants/eventsConstants.ts
+++ b/packages/grid/_modules_/grid/constants/eventsConstants.ts
@@ -452,3 +452,15 @@ export const GRID_STATE_CHANGE = 'stateChange';
* @event
*/
export const GRID_COLUMN_VISIBILITY_CHANGE = 'columnVisibilityChange';
+
+/**
+ * Fired when the virtual page changes. Called with a [[GridVirtualPageChangeParams]] object.
+ * @event
+ */
+export const GRID_VIRTUAL_PAGE_CHANGE = 'virtualPageChange';
+
+/**
+ * Fired when the virtual page changes. Called with a [[GridFetchRowsParams]] object.
+ * @event
+ */
+export const GRID_FETCH_ROWS = 'fetchRows';
diff --git a/packages/grid/_modules_/grid/hooks/features/export/serializers/csvSerializer.ts b/packages/grid/_modules_/grid/hooks/features/export/serializers/csvSerializer.ts
index 9738ff87b849e..1f3614d65c838 100644
--- a/packages/grid/_modules_/grid/hooks/features/export/serializers/csvSerializer.ts
+++ b/packages/grid/_modules_/grid/hooks/features/export/serializers/csvSerializer.ts
@@ -43,7 +43,7 @@ interface BuildCSVOptions {
export function buildCSV(options: BuildCSVOptions): string {
const { columns, rows, selectedRows, getCellParams, delimiterCharacter } = options;
- let rowIds = [...rows.keys()];
+ let rowIds = [...rows.keys()].filter((id) => id.toString().indexOf('null-') !== 0);
const selectedRowIds = Object.keys(selectedRows);
if (selectedRowIds.length) {
diff --git a/packages/grid/_modules_/grid/hooks/features/filter/useGridFilter.ts b/packages/grid/_modules_/grid/hooks/features/filter/useGridFilter.ts
index b4e5fc4bfa224..b5f81b32af50b 100644
--- a/packages/grid/_modules_/grid/hooks/features/filter/useGridFilter.ts
+++ b/packages/grid/_modules_/grid/hooks/features/filter/useGridFilter.ts
@@ -102,6 +102,10 @@ export const useGridFilter = (
const rows = sortedGridRowsSelector(state);
rows.forEach((row: GridRowModel, id: GridRowId) => {
+ if (id.toString().indexOf('null-') === 0) {
+ return;
+ }
+
const params = apiRef.current.getCellParams(id, newFilterItem.columnField!);
const isShown = applyFilterOnRow(params);
diff --git a/packages/grid/_modules_/grid/hooks/features/infiniteLoader/useGridInfiniteLoader.ts b/packages/grid/_modules_/grid/hooks/features/infiniteLoader/useGridInfiniteLoader.ts
index 6e1d217f46b13..1e948c1688e8b 100644
--- a/packages/grid/_modules_/grid/hooks/features/infiniteLoader/useGridInfiniteLoader.ts
+++ b/packages/grid/_modules_/grid/hooks/features/infiniteLoader/useGridInfiniteLoader.ts
@@ -2,20 +2,44 @@ import * as React from 'react';
import { optionsSelector } from '../../utils/optionsSelector';
import { GridApiRef } from '../../../models/api/gridApiRef';
import { useGridSelector } from '../core/useGridSelector';
-import { GRID_ROWS_SCROLL, GRID_ROWS_SCROLL_END } from '../../../constants/eventsConstants';
+import {
+ GRID_FETCH_ROWS,
+ GRID_FILTER_MODEL_CHANGE,
+ GRID_ROWS_SCROLL,
+ GRID_ROWS_SCROLL_END,
+ GRID_SORT_MODEL_CHANGE,
+ GRID_VIRTUAL_PAGE_CHANGE,
+} from '../../../constants/eventsConstants';
import { gridContainerSizesSelector } from '../../root/gridContainerSizesSelector';
import { useGridApiEventHandler, useGridApiOptionHandler } from '../../root/useGridApiEventHandler';
import { GridRowScrollEndParams } from '../../../models/params/gridRowScrollEndParams';
import { visibleGridColumnsSelector } from '../columns/gridColumnsSelector';
+import { GridVirtualPageChangeParams } from '../../../models/params/gridVirtualPageChangeParams';
+import { GridSortModelParams } from '../../../models/params/gridSortModelParams';
+import { GridFilterModelParams } from '../../../models/params/gridFilterModelParams';
+import { useLogger } from '../../utils/useLogger';
+import { renderStateSelector } from '../virtualization/renderingStateSelector';
+import { unorderedGridRowIdsSelector } from '../rows/gridRowsSelector';
+import { gridSortModelSelector } from '../sorting/gridSortingSelector';
+import { filterGridStateSelector } from '../filter/gridFilterSelector';
+import { GridFetchRowsParams } from '../../../models/params/gridFetchRowsParams';
+import { GridRowId } from '../../../models/gridRows';
+import { GridFeatureModeConstant } from '../../../models/gridFeatureMode';
export const useGridInfiniteLoader = (apiRef: GridApiRef): void => {
+ const logger = useLogger('useGridInfiniteLoader');
const options = useGridSelector(apiRef, optionsSelector);
const containerSizes = useGridSelector(apiRef, gridContainerSizesSelector);
const visibleColumns = useGridSelector(apiRef, visibleGridColumnsSelector);
+ const renderState = useGridSelector(apiRef, renderStateSelector);
+ const allRows = useGridSelector(apiRef, unorderedGridRowIdsSelector);
+ const sortModel = useGridSelector(apiRef, gridSortModelSelector);
+ const filterState = useGridSelector(apiRef, filterGridStateSelector);
const isInScrollBottomArea = React.useRef(false);
const handleGridScroll = React.useCallback(() => {
- if (!containerSizes) {
+ logger.debug('Checking if scroll position reached bottom');
+ if (!containerSizes || options.infniteLoadingMode !== GridFeatureModeConstant.client) {
return;
}
@@ -29,19 +53,106 @@ export const useGridInfiniteLoader = (apiRef: GridApiRef): void => {
if (
scrollPositionBottom >= containerSizes.dataContainerSizes.height &&
- !isInScrollBottomArea.current
+ !isInScrollBottomArea.current &&
+ !options.rowCount
) {
- const rowScrollEndParam: GridRowScrollEndParams = {
+ const rowScrollEndParams: GridRowScrollEndParams = {
api: apiRef,
visibleColumns,
viewportPageSize: containerSizes.viewportPageSize,
virtualRowsCount: containerSizes.virtualRowsCount,
};
- apiRef.current.publishEvent(GRID_ROWS_SCROLL_END, rowScrollEndParam);
+ apiRef.current.publishEvent(GRID_ROWS_SCROLL_END, rowScrollEndParams);
isInScrollBottomArea.current = true;
}
- }, [options, containerSizes, apiRef, visibleColumns]);
+ }, [logger, options, containerSizes, apiRef, visibleColumns]);
+
+ const handleGridVirtualPageChange = React.useCallback(
+ (params: GridVirtualPageChangeParams) => {
+ logger.debug('Virtual page changed');
+ if (!containerSizes || options.infniteLoadingMode !== GridFeatureModeConstant.server) {
+ return;
+ }
+
+ let nextPage: number = params.nextPage;
+ if (params.currentPage < params.nextPage && params.nextPage !== containerSizes.lastPage) {
+ nextPage += 1;
+ }
+
+ const newRowsBatchStartIndex = nextPage * containerSizes.viewportPageSize;
+ const remainingIndexes =
+ allRows.length - (newRowsBatchStartIndex + containerSizes.viewportPageSize);
+ let newRowsBatchLength = containerSizes.viewportPageSize;
+
+ if (remainingIndexes < containerSizes.renderingZonePageSize) {
+ newRowsBatchLength = containerSizes.renderingZonePageSize;
+ }
+
+ const toBeLoadedRange: Array = [...allRows].splice(
+ newRowsBatchStartIndex,
+ newRowsBatchLength,
+ );
+
+ if (!toBeLoadedRange.includes(null)) {
+ return;
+ }
+
+ const fetchRowsParams: GridFetchRowsParams = {
+ startIndex: newRowsBatchStartIndex,
+ viewportPageSize: newRowsBatchLength,
+ sortModel,
+ filterModel: filterState,
+ api: apiRef,
+ };
+ apiRef.current.publishEvent(GRID_FETCH_ROWS, fetchRowsParams);
+ },
+ [logger, options, allRows, sortModel, containerSizes, filterState, apiRef],
+ );
+
+ const handleGridSortModelChange = React.useCallback(
+ (params: GridSortModelParams) => {
+ logger.debug('Sort model changed');
+ if (!containerSizes || options.infniteLoadingMode !== GridFeatureModeConstant.server) {
+ return;
+ }
+
+ const newRowsBatchStartIndex = renderState.virtualPage * containerSizes.viewportPageSize;
+ const fetchRowsParams: GridFetchRowsParams = {
+ startIndex: newRowsBatchStartIndex,
+ viewportPageSize: containerSizes.viewportPageSize,
+ sortModel: params.sortModel,
+ filterModel: filterState,
+ api: apiRef,
+ };
+ apiRef.current.publishEvent(GRID_FETCH_ROWS, fetchRowsParams);
+ },
+ [logger, options, renderState, containerSizes, filterState, apiRef],
+ );
+
+ const handleGridFilterModelChange = React.useCallback(
+ (params: GridFilterModelParams) => {
+ logger.debug('Filter model changed');
+ if (!containerSizes || options.infniteLoadingMode !== GridFeatureModeConstant.server) {
+ return;
+ }
+
+ const newRowsBatchStartIndex = renderState.virtualPage * containerSizes.viewportPageSize;
+ const fetchRowsParams: GridFetchRowsParams = {
+ startIndex: newRowsBatchStartIndex,
+ viewportPageSize: containerSizes.viewportPageSize,
+ sortModel,
+ filterModel: params.filterModel,
+ api: apiRef,
+ };
+ apiRef.current.publishEvent(GRID_FETCH_ROWS, fetchRowsParams);
+ },
+ [logger, options, containerSizes, sortModel, renderState, apiRef],
+ );
useGridApiEventHandler(apiRef, GRID_ROWS_SCROLL, handleGridScroll);
+ useGridApiEventHandler(apiRef, GRID_VIRTUAL_PAGE_CHANGE, handleGridVirtualPageChange);
+ useGridApiEventHandler(apiRef, GRID_SORT_MODEL_CHANGE, handleGridSortModelChange);
+ useGridApiEventHandler(apiRef, GRID_FILTER_MODEL_CHANGE, handleGridFilterModelChange);
useGridApiOptionHandler(apiRef, GRID_ROWS_SCROLL_END, options.onRowsScrollEnd);
+ useGridApiOptionHandler(apiRef, GRID_FETCH_ROWS, options.onFetchRows);
};
diff --git a/packages/grid/_modules_/grid/hooks/features/rows/gridRowsState.ts b/packages/grid/_modules_/grid/hooks/features/rows/gridRowsState.ts
index 2509273373dbc..3d041836bdd4c 100644
--- a/packages/grid/_modules_/grid/hooks/features/rows/gridRowsState.ts
+++ b/packages/grid/_modules_/grid/hooks/features/rows/gridRowsState.ts
@@ -6,8 +6,8 @@ export interface InternalGridRowsState {
totalRowCount: number;
}
-export const getInitialGridRowState: () => InternalGridRowsState = () => ({
+export const getInitialGridRowState: (rowCount?: number) => InternalGridRowsState = (rowCount) => ({
idRowsLookup: {},
- allRows: [],
+ allRows: rowCount !== undefined ? new Array(rowCount).fill(null) : [],
totalRowCount: 0,
});
diff --git a/packages/grid/_modules_/grid/hooks/features/rows/useGridRows.ts b/packages/grid/_modules_/grid/hooks/features/rows/useGridRows.ts
index a9501c1a7c0df..a41a1f4ef6033 100644
--- a/packages/grid/_modules_/grid/hooks/features/rows/useGridRows.ts
+++ b/packages/grid/_modules_/grid/hooks/features/rows/useGridRows.ts
@@ -7,6 +7,7 @@ import {
import { GridComponentProps } from '../../../GridComponentProps';
import { GridApiRef } from '../../../models/api/gridApiRef';
import { GridRowApi } from '../../../models/api/gridRowApi';
+import { GridFeatureMode, GridFeatureModeConstant } from '../../../models/gridFeatureMode';
import {
checkGridRowIdIsValid,
GridRowModel,
@@ -15,9 +16,12 @@ import {
GridRowsProp,
GridRowIdGetter,
GridRowData,
+ GridInsertRowParams,
} from '../../../models/gridRows';
import { useGridApiMethod } from '../../root/useGridApiMethod';
+import { optionsSelector } from '../../utils/optionsSelector';
import { useLogger } from '../../utils/useLogger';
+import { useGridSelector } from '../core/useGridSelector';
import { useGridState } from '../core/useGridState';
import { getInitialGridRowState, InternalGridRowsState } from './gridRowsState';
@@ -35,15 +39,21 @@ export function convertGridRowsPropToState(
rows: GridRowsProp,
totalRowCount?: number,
rowIdGetter?: GridRowIdGetter,
+ infniteLoadingMode?: GridFeatureMode,
): InternalGridRowsState {
+ const numberOfRows = totalRowCount && totalRowCount > rows.length ? totalRowCount : rows.length;
+ const initialRowState =
+ infniteLoadingMode === GridFeatureModeConstant.server
+ ? getInitialGridRowState(numberOfRows)
+ : getInitialGridRowState();
const state: InternalGridRowsState = {
- ...getInitialGridRowState(),
- totalRowCount: totalRowCount && totalRowCount > rows.length ? totalRowCount : rows.length,
+ ...initialRowState,
+ totalRowCount: numberOfRows,
};
- rows.forEach((rowData) => {
+ rows.forEach((rowData, index) => {
const id = getGridRowId(rowData, rowIdGetter);
- state.allRows.push(id);
+ state.allRows[index] = id;
state.idRowsLookup[id] = rowData;
});
@@ -55,6 +65,7 @@ export const useGridRows = (
{ rows, getRowId }: Pick,
): void => {
const logger = useLogger('useGridRows');
+ const { infniteLoadingMode } = useGridSelector(apiRef, optionsSelector);
const [gridState, setGridState, updateComponent] = useGridState(apiRef);
const updateTimeout = React.useRef();
@@ -86,10 +97,11 @@ export const useGridRows = (
rows,
state.options.rowCount,
getRowId,
+ infniteLoadingMode,
);
return { ...state, rows: internalRowsState.current };
});
- }, [getRowId, rows, setGridState]);
+ }, [getRowId, rows, setGridState, infniteLoadingMode]);
const getRowIndexFromId = React.useCallback(
(id: GridRowId): number => {
@@ -114,6 +126,48 @@ export const useGridRows = (
[apiRef],
);
+ const insertRows = React.useCallback(
+ ({ startIndex, pageSize, newRows, rowCount }: GridInsertRowParams) => {
+ logger.debug(`Insert rows from index:${startIndex}`);
+
+ const newRowsToState = convertGridRowsPropToState(
+ newRows,
+ newRows.length,
+ getRowId,
+ infniteLoadingMode,
+ );
+
+ setGridState((state) => {
+ // rowCount can be 0
+ const allRows =
+ rowCount !== undefined ? new Array(rowCount).fill(null) : state.rows.allRows;
+ const allRowsUpdated = allRows.map((row, index) => {
+ if (index >= startIndex && index < startIndex + pageSize) {
+ return newRowsToState.allRows[index - startIndex];
+ }
+ return row;
+ });
+
+ const idRowsLookupUpdated =
+ rowCount !== undefined
+ ? newRowsToState.idRowsLookup
+ : { ...state.rows.idRowsLookup, ...newRowsToState.idRowsLookup };
+
+ internalRowsState.current = {
+ allRows: allRowsUpdated,
+ idRowsLookup: idRowsLookupUpdated,
+ totalRowCount: rowCount !== undefined ? rowCount : state.rows.totalRowCount,
+ };
+ return { ...state, rows: internalRowsState.current };
+ });
+
+ forceUpdate();
+ apiRef.current.updateViewport();
+ apiRef.current.applySorting();
+ },
+ [logger, apiRef, getRowId, setGridState, forceUpdate, infniteLoadingMode],
+ );
+
const setRows = React.useCallback(
(allNewRows: GridRowModel[]) => {
logger.debug(`updating all rows, new length ${allNewRows.length}`);
@@ -227,6 +281,7 @@ export const useGridRows = (
getAllRowIds,
setRows,
updateRows,
+ insertRows,
};
useGridApiMethod(apiRef, rowApi, 'GridRowApi');
};
diff --git a/packages/grid/_modules_/grid/hooks/features/sorting/gridSortingSelector.ts b/packages/grid/_modules_/grid/hooks/features/sorting/gridSortingSelector.ts
index a9f44a9b9e35b..ddf15ba6ebc04 100644
--- a/packages/grid/_modules_/grid/hooks/features/sorting/gridSortingSelector.ts
+++ b/packages/grid/_modules_/grid/hooks/features/sorting/gridSortingSelector.ts
@@ -33,7 +33,9 @@ export const sortedGridRowsSelector = createSelector<
(sortedIds: GridRowId[], idRowsLookup: GridRowsLookup) => {
const map = new Map();
sortedIds.forEach((id) => {
- map.set(id, idRowsLookup[id]);
+ // This was taken from the useId in the core
+ const normalizeId = id !== null ? id : `null-${Math.round(Math.random() * 1e5)}`;
+ map.set(normalizeId, idRowsLookup[id]);
});
return map;
},
diff --git a/packages/grid/_modules_/grid/hooks/features/sorting/useGridSorting.ts b/packages/grid/_modules_/grid/hooks/features/sorting/useGridSorting.ts
index 6971fda635de3..e270d8fa22245 100644
--- a/packages/grid/_modules_/grid/hooks/features/sorting/useGridSorting.ts
+++ b/packages/grid/_modules_/grid/hooks/features/sorting/useGridSorting.ts
@@ -172,6 +172,7 @@ export const useGridSorting = (apiRef: GridApiRef, { rows }: { rows: GridRowsPro
if (sortModel.length > 0) {
const comparatorList = buildComparatorList(sortModel);
logger.debug('Sorting rows with ', sortModel);
+
sorted = rowIds
.map((id) => {
return comparatorList.map((colComparator) => {
diff --git a/packages/grid/_modules_/grid/hooks/features/virtualization/useGridVirtualRows.ts b/packages/grid/_modules_/grid/hooks/features/virtualization/useGridVirtualRows.ts
index b30e7972a8dbd..b9ec68e43c8cd 100644
--- a/packages/grid/_modules_/grid/hooks/features/virtualization/useGridVirtualRows.ts
+++ b/packages/grid/_modules_/grid/hooks/features/virtualization/useGridVirtualRows.ts
@@ -1,5 +1,9 @@
import * as React from 'react';
-import { GRID_SCROLL, GRID_ROWS_SCROLL } from '../../../constants/eventsConstants';
+import {
+ GRID_SCROLL,
+ GRID_ROWS_SCROLL,
+ GRID_VIRTUAL_PAGE_CHANGE,
+} from '../../../constants/eventsConstants';
import { GridApiRef } from '../../../models/api/gridApiRef';
import { GridVirtualizationApi } from '../../../models/api/gridVirtualizationApi';
import { GridCellIndexCoordinates } from '../../../models/gridCell';
@@ -147,6 +151,12 @@ export const useGridVirtualRows = (apiRef: GridApiRef): void => {
if (containerProps.isVirtualized && page !== nextPage) {
setRenderingState({ virtualPage: nextPage });
logger.debug(`Changing page from ${page} to ${nextPage}`);
+
+ apiRef.current.publishEvent(GRID_VIRTUAL_PAGE_CHANGE, {
+ currentPage: page,
+ nextPage,
+ api: apiRef,
+ });
requireRerender = true;
} else {
if (!containerProps.isVirtualized && page > 0) {
diff --git a/packages/grid/_modules_/grid/models/api/gridRowApi.ts b/packages/grid/_modules_/grid/models/api/gridRowApi.ts
index 4315ef9512cdd..52870cb8c1e0e 100644
--- a/packages/grid/_modules_/grid/models/api/gridRowApi.ts
+++ b/packages/grid/_modules_/grid/models/api/gridRowApi.ts
@@ -1,4 +1,4 @@
-import { GridRowModel, GridRowId, GridRowModelUpdate } from '../gridRows';
+import { GridRowModel, GridRowId, GridRowModelUpdate, GridInsertRowParams } from '../gridRows';
/**
* The Row API interface that is available in the grid `apiRef`.
@@ -47,4 +47,9 @@ export interface GridRowApi {
* @returns {GridRowModel} The row data.
*/
getRow: (id: GridRowId) => GridRowModel;
+ /**
+ * Inserts a new subset of Rows.
+ * @param {GridInsertRowParams} params The new rows.
+ */
+ insertRows: (params: GridInsertRowParams) => void;
}
diff --git a/packages/grid/_modules_/grid/models/gridOptions.tsx b/packages/grid/_modules_/grid/models/gridOptions.tsx
index 616786fa0ad7b..17d7161fbe11a 100644
--- a/packages/grid/_modules_/grid/models/gridOptions.tsx
+++ b/packages/grid/_modules_/grid/models/gridOptions.tsx
@@ -29,6 +29,7 @@ import { GridColumnOrderChangeParams } from './params/gridColumnOrderChangeParam
import { GridResizeParams } from './params/gridResizeParams';
import { GridColumnResizeParams } from './params/gridColumnResizeParams';
import { GridColumnVisibilityChangeParams } from './params/gridColumnVisibilityChangeParams';
+import { GridFetchRowsParams } from './params/gridFetchRowsParams';
import { GridClasses } from './gridClasses';
export type MuiEvent = (E | React.SyntheticEvent) & {
@@ -180,6 +181,12 @@ export interface GridOptions {
* @default false
*/
hideFooterSelectedRowCount?: boolean;
+ /**
+ * Infnite loading can be processed on the server or client-side.
+ * Set it to 'client' if you would like to handle the infnite loading on the client-side.
+ * Set it to 'server' if you would like to handle the infnite loading on the server-side.
+ */
+ infniteLoadingMode?: GridFeatureMode;
/**
* Callback fired when a cell is rendered, returns true if the cell is editable.
*/
@@ -351,6 +358,10 @@ export interface GridOptions {
* @param param With all properties from [[GridFilterModelParams]].
*/
onFilterModelChange?: (params: GridFilterModelParams) => void;
+ /**
+ * Callback fired when rowCount is set and virtual pages change.
+ */
+ onFetchRows?: (params: GridFetchRowsParams) => void;
/**
* Callback fired when the current page has changed.
* @param param With all properties from [[GridPageChangeParams]].
@@ -509,6 +520,7 @@ export const DEFAULT_GRID_OPTIONS: GridOptions = {
density: GridDensityTypes.Standard,
filterMode: GridFeatureModeConstant.client,
headerHeight: 56,
+ infniteLoadingMode: GridFeatureModeConstant.client,
localeText: GRID_DEFAULT_LOCALE_TEXT,
pageSize: 100,
paginationMode: GridFeatureModeConstant.client,
diff --git a/packages/grid/_modules_/grid/models/gridRows.ts b/packages/grid/_modules_/grid/models/gridRows.ts
index 03c91268c4fb1..419aae7ab271f 100644
--- a/packages/grid/_modules_/grid/models/gridRows.ts
+++ b/packages/grid/_modules_/grid/models/gridRows.ts
@@ -46,3 +46,25 @@ export function checkGridRowIdIsValid(
return true;
}
+
+/**
+ * The type of api.current.insertRows params.
+ */
+export interface GridInsertRowParams {
+ /**
+ * The start index from which rows needs to be loaded.
+ */
+ startIndex: number;
+ /**
+ * The page size.
+ */
+ pageSize: number;
+ /**
+ * Rows to be inserted.
+ */
+ newRows: GridRowModel[];
+ /**
+ * The updated row count.
+ */
+ rowCount?: number;
+}
diff --git a/packages/grid/_modules_/grid/models/params/gridFetchRowsParams.ts b/packages/grid/_modules_/grid/models/params/gridFetchRowsParams.ts
new file mode 100644
index 0000000000000..c8b1a22eb2a7f
--- /dev/null
+++ b/packages/grid/_modules_/grid/models/params/gridFetchRowsParams.ts
@@ -0,0 +1,28 @@
+import { GridFilterModel } from '../../hooks/features/filter/gridFilterModelState';
+import { GridSortModel } from '../gridSortModel';
+
+/**
+ * Object passed as parameter to the [[onFetchRows]] option.
+ */
+export interface GridFetchRowsParams {
+ /**
+ * The start index from which rows needs to be loaded.
+ */
+ startIndex: number;
+ /**
+ * The viewport page size.
+ */
+ viewportPageSize: number;
+ /**
+ * The sort model used to sort the grid.
+ */
+ sortModel: GridSortModel;
+ /**
+ * The filter model.
+ */
+ filterModel: GridFilterModel;
+ /**
+ * GridApiRef that let you manipulate the grid.
+ */
+ api: any;
+}
diff --git a/packages/grid/_modules_/grid/models/params/gridVirtualPageChangeParams.ts b/packages/grid/_modules_/grid/models/params/gridVirtualPageChangeParams.ts
new file mode 100644
index 0000000000000..52bd9f23d0489
--- /dev/null
+++ b/packages/grid/_modules_/grid/models/params/gridVirtualPageChangeParams.ts
@@ -0,0 +1,17 @@
+/**
+ * Object passed as parameter of the virtual page change event.
+ */
+export interface GridVirtualPageChangeParams {
+ /**
+ * The current page.
+ */
+ currentPage: number;
+ /**
+ * The next page.
+ */
+ nextPage: number;
+ /**
+ * Api that let you manipulate the grid.
+ */
+ api: any;
+}
diff --git a/packages/grid/_modules_/grid/models/params/index.ts b/packages/grid/_modules_/grid/models/params/index.ts
index 1a46417fabdd4..6fd7d543e6592 100644
--- a/packages/grid/_modules_/grid/models/params/index.ts
+++ b/packages/grid/_modules_/grid/models/params/index.ts
@@ -1,5 +1,6 @@
export * from './gridCellParams';
export * from './gridEditCellParams';
+export * from './gridFetchRowsParams';
export * from './gridColumnHeaderParams';
export * from './gridColumnOrderChangeParams';
export * from './gridColumnResizeParams';
diff --git a/packages/grid/x-grid/src/tests/rows.XGrid.test.tsx b/packages/grid/x-grid/src/tests/rows.XGrid.test.tsx
index 02d981294cdf8..e357ea1116591 100644
--- a/packages/grid/x-grid/src/tests/rows.XGrid.test.tsx
+++ b/packages/grid/x-grid/src/tests/rows.XGrid.test.tsx
@@ -1,12 +1,17 @@
import * as React from 'react';
-import { createClientRenderStrictMode } from 'test/utils';
-import { useFakeTimers } from 'sinon';
+import {
+ createClientRenderStrictMode,
+ // @ts-expect-error need to migrate helpers to TypeScript
+ fireEvent,
+} from 'test/utils';
+import { useFakeTimers, spy } from 'sinon';
import { expect } from 'chai';
-import { getCell, getColumnValues } from 'test/utils/helperFn';
+import { getCell, getColumnHeaderCell, getColumnValues } from 'test/utils/helperFn';
import {
GridApiRef,
GridComponentProps,
GridRowData,
+ GRID_SKELETON_CELL_CSS_CLASS,
useGridApiRef,
XGrid,
XGridProps,
@@ -448,4 +453,131 @@ describe(' - Rows', () => {
});
});
});
+
+ describe('Infinite loader', () => {
+ before(function beforeHook() {
+ if (isJSDOM) {
+ // Need layouting
+ this.skip();
+ }
+ });
+
+ let apiRef: GridApiRef;
+ const TestCaseInfiniteLoading = (
+ props: Partial & { nbRows?: number; nbCols?: number; height?: number },
+ ) => {
+ apiRef = useGridApiRef();
+ const data = useData(props.nbRows || 100, props.nbCols || 10);
+
+ return (
+
+
+
+ );
+ };
+
+ it('should call onRowsScrollEnd when scroll reaches the bottom of the grid', () => {
+ const handleRowsScrollEnd = spy();
+ render();
+
+ const gridWindow = document.querySelector('.MuiDataGrid-window')!;
+ gridWindow.scrollTop = 10e6; // scroll to the bottom
+ gridWindow.dispatchEvent(new Event('scroll'));
+
+ expect(handleRowsScrollEnd.callCount).to.equal(1);
+ });
+
+ it('should call onFetchRows when scroll reaches the bottom of the grid', () => {
+ const handleFetchRows = spy();
+ render(
+ ,
+ );
+
+ const gridWindow = document.querySelector('.MuiDataGrid-window')!;
+ gridWindow.scrollTop = 10e6; // scroll to the bottom
+ gridWindow.dispatchEvent(new Event('scroll'));
+
+ expect(handleFetchRows.callCount).to.equal(1);
+ });
+
+ it('should call onFetchRows when sorting is applied', () => {
+ const handleFetchRows = spy();
+ render(
+ ,
+ );
+
+ fireEvent.click(getColumnHeaderCell(0));
+ expect(handleFetchRows.callCount).to.equal(1);
+ });
+
+ it('should render skeleton cell if rowCount is bigger than the number of rows', () => {
+ render(
+ ,
+ );
+
+ const gridWindow = document.querySelector('.MuiDataGrid-window')!;
+ gridWindow.scrollTop = 10e6; // scroll to the bottom
+ gridWindow.dispatchEvent(new Event('scroll'));
+
+ const lastCell = document.querySelector('[role="row"]:last-child [role="cell"]:first-child')!;
+
+ expect(lastCell.classList.contains(GRID_SKELETON_CELL_CSS_CLASS)).to.equal(true);
+ });
+
+ it('should update allRows accordingly when apiRef.current.insertRows is called', () => {
+ render(
+ ,
+ );
+
+ const pageSize = 3;
+ const startIndex = 7;
+ const endIndex = startIndex + pageSize;
+ const newRows: GridRowData[] = [
+ { id: 'new-1', currencyPair: '' },
+ { id: 'new-2', currencyPair: '' },
+ { id: 'new-3', currencyPair: '' },
+ ];
+
+ const initialAllRows = apiRef!.current!.getState().rows!.allRows;
+ expect(initialAllRows.slice(startIndex, endIndex)).to.deep.equal([null, null, null]);
+
+ apiRef!.current!.insertRows({ startIndex, pageSize, newRows });
+
+ const updatedAllRows = apiRef!.current!.getState().rows!.allRows;
+ expect(updatedAllRows.slice(startIndex, endIndex)).to.deep.equal(['new-1', 'new-2', 'new-3']);
+ });
+ });
});
diff --git a/packages/storybook/src/stories/grid-rows.stories.tsx b/packages/storybook/src/stories/grid-rows.stories.tsx
index 629339f96e720..1052e6968da8e 100644
--- a/packages/storybook/src/stories/grid-rows.stories.tsx
+++ b/packages/storybook/src/stories/grid-rows.stories.tsx
@@ -20,8 +20,9 @@ import {
GridEditCellPropsParams,
GridEditRowModelParams,
GRID_CELL_EDIT_ENTER,
+ GridFetchRowsParams,
} from '@material-ui/x-grid';
-import { useDemoData } from '@material-ui/x-grid-data-generator';
+import { getCommodityColumns, getRealData, useDemoData } from '@material-ui/x-grid-data-generator';
import { action } from '@storybook/addon-actions';
import { randomInt } from '../data/random-generator';
@@ -930,6 +931,53 @@ export function DeferRendering() {
return ;
}
+async function sleep(duration) {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve();
+ }, duration);
+ });
+}
+
+const loadServerRows = async (newRowLength: number): Promise => {
+ const newData = await getRealData(newRowLength, getCommodityColumns());
+ // Simulate network throttle
+ await sleep(Math.random() * 100 + 100);
+
+ return newData.rows;
+};
+
+export function ServerSideInfiniteLoading() {
+ const { data } = useDemoData({
+ dataSet: 'Commodity',
+ rowLength: 20,
+ maxColumns: 20,
+ });
+
+ const handleFetchRows = async (params: GridFetchRowsParams) => {
+ const newRowsBatch: GridRowData[] = await loadServerRows(params.viewportPageSize);
+
+ params.api.current.insertRows({
+ startIndex: params.startIndex,
+ pageSize: params.viewportPageSize,
+ newRows: newRowsBatch,
+ });
+ };
+
+ return (
+
+
+
+ );
+}
+
export const ZeroHeightGrid = () => (