diff --git a/docs/data/data-grid/overlays/LoadingOverlay.js b/docs/data/data-grid/overlays/LoadingOverlay.js index 0c65d421df94..333a904448f6 100644 --- a/docs/data/data-grid/overlays/LoadingOverlay.js +++ b/docs/data/data-grid/overlays/LoadingOverlay.js @@ -11,7 +11,7 @@ export default function LoadingOverlay() { }); return ( - + ); diff --git a/docs/data/data-grid/overlays/LoadingOverlay.tsx b/docs/data/data-grid/overlays/LoadingOverlay.tsx index 0c65d421df94..333a904448f6 100644 --- a/docs/data/data-grid/overlays/LoadingOverlay.tsx +++ b/docs/data/data-grid/overlays/LoadingOverlay.tsx @@ -11,7 +11,7 @@ export default function LoadingOverlay() { }); return ( - + ); diff --git a/docs/data/data-grid/overlays/LoadingOverlayCustom.js b/docs/data/data-grid/overlays/LoadingOverlayCustom.js index 481a8a5949d2..a936b4296f19 100644 --- a/docs/data/data-grid/overlays/LoadingOverlayCustom.js +++ b/docs/data/data-grid/overlays/LoadingOverlayCustom.js @@ -1,7 +1,63 @@ import * as React from 'react'; import { DataGrid } from '@mui/x-data-grid'; -import LinearProgress from '@mui/material/LinearProgress'; import { useDemoData } from '@mui/x-data-grid-generator'; +import { styled } from '@mui/material/styles'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import CircularProgress from '@mui/material/CircularProgress'; + +const StyledGridOverlay = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + height: '100%', + backgroundColor: + theme.palette.mode === 'light' + ? 'rgba(255, 255, 255, 0.9)' + : 'rgba(18, 18, 18, 0.9)', +})); + +function CircularProgressWithLabel(props) { + return ( + + + + + {`${Math.round(props.value)}%`} + + + + ); +} + +function CustomLoadingOverlay() { + const [progress, setProgress] = React.useState(10); + + React.useEffect(() => { + const timer = setInterval(() => { + setProgress((prevProgress) => (prevProgress >= 100 ? 0 : prevProgress + 10)); + }, 800); + return () => { + clearInterval(timer); + }; + }, []); + + return ( + + + Loading rows… + + ); +} export default function LoadingOverlayCustom() { const { data } = useDemoData({ @@ -14,7 +70,7 @@ export default function LoadingOverlayCustom() {
({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + height: '100%', + backgroundColor: + theme.palette.mode === 'light' + ? 'rgba(255, 255, 255, 0.9)' + : 'rgba(18, 18, 18, 0.9)', +})); + +function CircularProgressWithLabel( + props: CircularProgressProps & { value: number }, +) { + return ( + + + + {`${Math.round(props.value)}%`} + + + ); +} + +function CustomLoadingOverlay() { + const [progress, setProgress] = React.useState(10); + + React.useEffect(() => { + const timer = setInterval(() => { + setProgress((prevProgress) => (prevProgress >= 100 ? 0 : prevProgress + 10)); + }, 800); + return () => { + clearInterval(timer); + }; + }, []); + + return ( + + + Loading rows… + + ); +} export default function LoadingOverlayCustom() { const { data } = useDemoData({ @@ -14,7 +76,7 @@ export default function LoadingOverlayCustom() {
+ + + ); +} diff --git a/docs/data/data-grid/overlays/LoadingOverlayLinearProgress.tsx b/docs/data/data-grid/overlays/LoadingOverlayLinearProgress.tsx new file mode 100644 index 000000000000..5ad0b8a4b3e5 --- /dev/null +++ b/docs/data/data-grid/overlays/LoadingOverlayLinearProgress.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { useDemoData } from '@mui/x-data-grid-generator'; +import { DataGrid } from '@mui/x-data-grid'; + +export default function LoadingOverlayLinearProgress() { + const { data } = useDemoData({ + dataSet: 'Commodity', + rowLength: 100, + maxColumns: 6, + }); + + return ( + + + + ); +} diff --git a/docs/data/data-grid/overlays/LoadingOverlayLinearProgress.tsx.preview b/docs/data/data-grid/overlays/LoadingOverlayLinearProgress.tsx.preview new file mode 100644 index 000000000000..f4e0a9ef7417 --- /dev/null +++ b/docs/data/data-grid/overlays/LoadingOverlayLinearProgress.tsx.preview @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/docs/data/data-grid/overlays/LoadingOverlaySkeleton.js b/docs/data/data-grid/overlays/LoadingOverlaySkeleton.js new file mode 100644 index 000000000000..0e86a253dfcd --- /dev/null +++ b/docs/data/data-grid/overlays/LoadingOverlaySkeleton.js @@ -0,0 +1,32 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { useDemoData } from '@mui/x-data-grid-generator'; +import { DataGridPro } from '@mui/x-data-grid-pro'; + +export default function LoadingOverlaySkeleton() { + const { data } = useDemoData({ + dataSet: 'Commodity', + rowLength: 100, + maxColumns: 9, + }); + + return ( + + + + ); +} diff --git a/docs/data/data-grid/overlays/LoadingOverlaySkeleton.tsx b/docs/data/data-grid/overlays/LoadingOverlaySkeleton.tsx new file mode 100644 index 000000000000..0e86a253dfcd --- /dev/null +++ b/docs/data/data-grid/overlays/LoadingOverlaySkeleton.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { useDemoData } from '@mui/x-data-grid-generator'; +import { DataGridPro } from '@mui/x-data-grid-pro'; + +export default function LoadingOverlaySkeleton() { + const { data } = useDemoData({ + dataSet: 'Commodity', + rowLength: 100, + maxColumns: 9, + }); + + return ( + + + + ); +} diff --git a/docs/data/data-grid/overlays/LoadingOverlaySkeleton.tsx.preview b/docs/data/data-grid/overlays/LoadingOverlaySkeleton.tsx.preview new file mode 100644 index 000000000000..9173142c9f99 --- /dev/null +++ b/docs/data/data-grid/overlays/LoadingOverlaySkeleton.tsx.preview @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/docs/data/data-grid/overlays/overlays.md b/docs/data/data-grid/overlays/overlays.md index c99392620d52..426e8b615b3e 100644 --- a/docs/data/data-grid/overlays/overlays.md +++ b/docs/data/data-grid/overlays/overlays.md @@ -6,11 +6,53 @@ To display a loading overlay and signify that the data grid is in a loading state, set the `loading` prop to `true`. +The data grid supports 3 loading overlay variants out of the box: + +- `circular-progress` (default): a circular loading spinner. +- `linear-progress`: an indeterminate linear progress bar. +- `skeleton`: an animated placeholder of the data grid. + +The type of loading overlay to display can be set via `slotProps.loadingOverlay` for the following two props: + +- `variant`: when `loading` and there are rows in the table. +- `noRowsVariant`: when `loading` and there are not any rows in the table. + +```tsx + +``` + +### Circular progress + +A circular loading spinner, the default loading overlay. + {{"demo": "LoadingOverlay.js", "bg": "inline"}} +### Linear progress + +An indeterminate linear progress bar. + +{{"demo": "LoadingOverlayLinearProgress.js", "bg": "inline"}} + +### Skeleton + +An animated placeholder of the data grid. + +{{"demo": "LoadingOverlaySkeleton.js", "bg": "inline"}} + ### Custom component -If you want to customize the no rows overlay, a component can be passed to the `loadingOverlay` slot. In the following demo, a [LinearProgress](/material-ui/react-progress/#linear) component is rendered in place of the default circular loading spinner. +If you want to customize the no rows overlay, a component can be passed to the `loadingOverlay` slot. + +In the following demo, a labelled determinate [CircularProgress](/material-ui/react-progress/#circular-determinate) component is rendered in place of the default loading overlay, with some additional _Loading rows…_ text. {{"demo": "LoadingOverlayCustom.js", "bg": "inline"}} @@ -22,7 +64,9 @@ The no rows overlay is displayed when the data grid has no rows. ### Custom component -If you want to customize the no rows overlay, a component can be passed to the `noRowsOverlay` slot and rendered in place. In the following demo, an illustration is added on top of the default "No rows" message. +If you want to customize the no rows overlay, a component can be passed to the `noRowsOverlay` slot and rendered in place. + +In the following demo, an illustration is added on top of the default "No rows" message. {{"demo": "NoRowsOverlayCustom.js", "bg": "inline"}} @@ -34,7 +78,9 @@ The no results overlay is displayed when the data grid has no results after filt ### Custom component -If you want to customize the no results overlay, a component can be passed to the `noResults` slot and rendered in place. In the following demo, an illustration is added on top of the default "No results found" message. +If you want to customize the no results overlay, a component can be passed to the `noResults` slot and rendered in place. + +In the following demo, an illustration is added on top of the default "No results found" message. {{"demo": "NoResultsOverlayCustom.js", "bg": "inline"}} diff --git a/docs/pages/x/api/data-grid/data-grid-premium.json b/docs/pages/x/api/data-grid/data-grid-premium.json index 755b38c48e84..bce75b9e5e66 100644 --- a/docs/pages/x/api/data-grid/data-grid-premium.json +++ b/docs/pages/x/api/data-grid/data-grid-premium.json @@ -1764,6 +1764,12 @@ "description": "Styles applied to the row's draggable placeholder element inside the special row reorder cell.", "isGlobal": false }, + { + "key": "rowSkeleton", + "className": "MuiDataGridPremium-rowSkeleton", + "description": "Styles applied to the skeleton row element.", + "isGlobal": false + }, { "key": "scrollArea", "className": "MuiDataGridPremium-scrollArea", diff --git a/docs/pages/x/api/data-grid/data-grid-pro.json b/docs/pages/x/api/data-grid/data-grid-pro.json index 3829a0762d6a..7767957b769d 100644 --- a/docs/pages/x/api/data-grid/data-grid-pro.json +++ b/docs/pages/x/api/data-grid/data-grid-pro.json @@ -1681,6 +1681,12 @@ "description": "Styles applied to the row's draggable placeholder element inside the special row reorder cell.", "isGlobal": false }, + { + "key": "rowSkeleton", + "className": "MuiDataGridPro-rowSkeleton", + "description": "Styles applied to the skeleton row element.", + "isGlobal": false + }, { "key": "scrollArea", "className": "MuiDataGridPro-scrollArea", diff --git a/docs/pages/x/api/data-grid/data-grid.json b/docs/pages/x/api/data-grid/data-grid.json index 4d4ebb3a183c..0691480696ad 100644 --- a/docs/pages/x/api/data-grid/data-grid.json +++ b/docs/pages/x/api/data-grid/data-grid.json @@ -1567,6 +1567,12 @@ "description": "Styles applied to the row's draggable placeholder element inside the special row reorder cell.", "isGlobal": false }, + { + "key": "rowSkeleton", + "className": "MuiDataGrid-rowSkeleton", + "description": "Styles applied to the skeleton row element.", + "isGlobal": false + }, { "key": "scrollArea", "className": "MuiDataGrid-scrollArea", diff --git a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json index bc318d566f86..a8b79b49fb93 100644 --- a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json +++ b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json @@ -1128,6 +1128,10 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the row's draggable placeholder element inside the special row reorder cell" }, + "rowSkeleton": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the skeleton row element" + }, "scrollArea": { "description": "Styles applied to {{nodeName}}.", "nodeName": "both scroll area elements" diff --git a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json index 5812ceb64406..914817291c87 100644 --- a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json +++ b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json @@ -1066,6 +1066,10 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the row's draggable placeholder element inside the special row reorder cell" }, + "rowSkeleton": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the skeleton row element" + }, "scrollArea": { "description": "Styles applied to {{nodeName}}.", "nodeName": "both scroll area elements" diff --git a/docs/translations/api-docs/data-grid/data-grid/data-grid.json b/docs/translations/api-docs/data-grid/data-grid/data-grid.json index 31ec9f91afa4..08fa6f50e6c4 100644 --- a/docs/translations/api-docs/data-grid/data-grid/data-grid.json +++ b/docs/translations/api-docs/data-grid/data-grid/data-grid.json @@ -955,6 +955,10 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the row's draggable placeholder element inside the special row reorder cell" }, + "rowSkeleton": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the skeleton row element" + }, "scrollArea": { "description": "Styles applied to {{nodeName}}.", "nodeName": "both scroll area elements" diff --git a/packages/x-data-grid/src/components/GridLoadingOverlay.tsx b/packages/x-data-grid/src/components/GridLoadingOverlay.tsx index 2bcb4eed7a89..dc68ffccc6d8 100644 --- a/packages/x-data-grid/src/components/GridLoadingOverlay.tsx +++ b/packages/x-data-grid/src/components/GridLoadingOverlay.tsx @@ -1,13 +1,63 @@ import * as React from 'react'; import PropTypes from 'prop-types'; +import LinearProgress from '@mui/material/LinearProgress'; import CircularProgress from '@mui/material/CircularProgress'; import { GridOverlay, GridOverlayProps } from './containers/GridOverlay'; +import { GridSkeletonLoadingOverlay } from './GridSkeletonLoadingOverlay'; +import { useGridApiContext } from '../hooks/utils/useGridApiContext'; +import { gridRowCountSelector, useGridSelector } from '../hooks'; -const GridLoadingOverlay = React.forwardRef( +export type GridLoadingOverlayVariant = 'circular-progress' | 'linear-progress' | 'skeleton'; + +export interface GridLoadingOverlayProps extends GridOverlayProps { + /** + * The variant of the overlay. + * @default 'circular-progress' + */ + variant?: GridLoadingOverlayVariant; + /** + * The variant of the overlay when no rows are displayed. + * @default 'circular-progress' + */ + noRowsVariant?: GridLoadingOverlayVariant; +} + +const LOADING_VARIANTS: Record< + GridLoadingOverlayVariant, + { + component: React.ComponentType; + style: React.CSSProperties; + } +> = { + 'circular-progress': { + component: CircularProgress, + style: {}, + }, + 'linear-progress': { + component: LinearProgress, + style: { display: 'block' }, + }, + skeleton: { + component: GridSkeletonLoadingOverlay, + style: { display: 'block' }, + }, +}; + +const GridLoadingOverlay = React.forwardRef( function GridLoadingOverlay(props, ref) { + const { + variant = 'circular-progress', + noRowsVariant = 'circular-progress', + style, + ...other + } = props; + const apiRef = useGridApiContext(); + const rowsCount = useGridSelector(apiRef, gridRowCountSelector); + const activeVariant = LOADING_VARIANTS[rowsCount === 0 ? noRowsVariant : variant]; + return ( - - + + ); }, @@ -18,11 +68,21 @@ GridLoadingOverlay.propTypes = { // | These PropTypes are generated from the TypeScript type definitions | // | To update them edit the TypeScript types and run "pnpm proptypes" | // ---------------------------------------------------------------------- + /** + * The variant of the overlay when no rows are displayed. + * @default 'circular-progress' + */ + noRowsVariant: PropTypes.oneOf(['circular-progress', 'linear-progress', 'skeleton']), sx: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), PropTypes.func, PropTypes.object, ]), + /** + * The variant of the overlay. + * @default 'circular-progress' + */ + variant: PropTypes.oneOf(['circular-progress', 'linear-progress', 'skeleton']), } as any; export { GridLoadingOverlay }; diff --git a/packages/x-data-grid/src/components/GridRow.tsx b/packages/x-data-grid/src/components/GridRow.tsx index 134786356f65..917deae2ba62 100644 --- a/packages/x-data-grid/src/components/GridRow.tsx +++ b/packages/x-data-grid/src/components/GridRow.tsx @@ -376,10 +376,11 @@ const GridRow = React.forwardRef(function GridRow( return ( ); } diff --git a/packages/x-data-grid/src/components/GridSkeletonLoadingOverlay.tsx b/packages/x-data-grid/src/components/GridSkeletonLoadingOverlay.tsx new file mode 100644 index 000000000000..62643bb382f3 --- /dev/null +++ b/packages/x-data-grid/src/components/GridSkeletonLoadingOverlay.tsx @@ -0,0 +1,265 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { styled } from '@mui/system'; +import useForkRef from '@mui/utils/useForkRef'; +import composeClasses from '@mui/utils/composeClasses'; +import { useGridApiContext } from '../hooks/utils/useGridApiContext'; +import { useGridRootProps } from '../hooks/utils/useGridRootProps'; +import { + GridPinnedColumnPosition, + gridColumnPositionsSelector, + gridColumnsTotalWidthSelector, + gridDimensionsSelector, + gridVisibleColumnDefinitionsSelector, + gridVisiblePinnedColumnDefinitionsSelector, + useGridApiEventHandler, + useGridSelector, +} from '../hooks'; +import { GridEventListener } from '../models'; +import { DataGridProcessedProps } from '../models/props/DataGridProps'; +import { getDataGridUtilityClass, gridClasses } from '../constants/gridClasses'; +import { getPinnedCellOffset } from '../internals/utils/getPinnedCellOffset'; +import { shouldCellShowLeftBorder, shouldCellShowRightBorder } from '../utils/cellBorderUtils'; +import { escapeOperandAttributeSelector } from '../utils/domUtils'; +import { GridScrollbarFillerCell } from './GridScrollbarFillerCell'; + +const SkeletonOverlay = styled('div', { + name: 'MuiDataGrid', + slot: 'SkeletonLoadingOverlay', + overridesResolver: (props, styles) => styles.skeletonLoadingOverlay, +})({ + minWidth: '100%', + width: 'max-content', // prevents overflow: clip; cutting off the x axis + height: '100%', + overflow: 'clip', // y axis is hidden while the x axis is allowed to overflow +}); + +type OwnerState = { classes: DataGridProcessedProps['classes'] }; + +const useUtilityClasses = (ownerState: OwnerState) => { + const { classes } = ownerState; + + const slots = { + root: ['skeletonLoadingOverlay'], + }; + + return composeClasses(slots, getDataGridUtilityClass, classes); +}; + +const getColIndex = (el: HTMLElement) => parseInt(el.getAttribute('data-colindex')!, 10); + +const GridSkeletonLoadingOverlay = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(function GridSkeletonLoadingOverlay(props, forwardedRef) { + const rootProps = useGridRootProps(); + const { slots } = rootProps; + const classes = useUtilityClasses({ classes: rootProps.classes }); + const ref = React.useRef(null); + const handleRef = useForkRef(ref, forwardedRef); + const apiRef = useGridApiContext(); + const dimensions = useGridSelector(apiRef, gridDimensionsSelector); + const viewportHeight = dimensions?.viewportInnerSize.height ?? 0; + const skeletonRowsCount = Math.ceil(viewportHeight / dimensions.rowHeight); + const totalWidth = useGridSelector(apiRef, gridColumnsTotalWidthSelector); + const positions = useGridSelector(apiRef, gridColumnPositionsSelector); + const inViewportCount = React.useMemo( + () => positions.filter((value) => value <= totalWidth).length, + [totalWidth, positions], + ); + const allVisibleColumns = useGridSelector(apiRef, gridVisibleColumnDefinitionsSelector); + const columns = React.useMemo( + () => allVisibleColumns.slice(0, inViewportCount), + [allVisibleColumns, inViewportCount], + ); + const pinnedColumns = useGridSelector(apiRef, gridVisiblePinnedColumnDefinitionsSelector); + + const getPinnedStyle = React.useCallback( + (computedWidth: number, index: number, position: GridPinnedColumnPosition) => { + const pinnedOffset = getPinnedCellOffset( + position, + computedWidth, + index, + positions, + dimensions, + ); + return { [position]: pinnedOffset } as const; + }, + [dimensions, positions], + ); + + const getPinnedPosition = React.useCallback( + (field: string) => { + if (pinnedColumns.left.findIndex((col) => col.field === field) !== -1) { + return GridPinnedColumnPosition.LEFT; + } + if (pinnedColumns.right.findIndex((col) => col.field === field) !== -1) { + return GridPinnedColumnPosition.RIGHT; + } + return undefined; + }, + [pinnedColumns.left, pinnedColumns.right], + ); + + const children = React.useMemo(() => { + const array: React.ReactNode[] = []; + + for (let i = 0; i < skeletonRowsCount; i += 1) { + const rowCells: React.ReactNode[] = []; + + for (let colIndex = 0; colIndex < columns.length; colIndex += 1) { + const column = columns[colIndex]; + const pinnedPosition = getPinnedPosition(column.field); + const isPinnedLeft = pinnedPosition === GridPinnedColumnPosition.LEFT; + const isPinnedRight = pinnedPosition === GridPinnedColumnPosition.RIGHT; + const sectionLength = pinnedPosition + ? pinnedColumns[pinnedPosition].length // pinned section + : columns.length - pinnedColumns.left.length - pinnedColumns.right.length; // middle section + const sectionIndex = pinnedPosition + ? pinnedColumns[pinnedPosition].findIndex((col) => col.field === column.field) // pinned section + : colIndex - pinnedColumns.left.length; // middle section + const pinnedStyle = + pinnedPosition && getPinnedStyle(column.computedWidth, colIndex, pinnedPosition); + const gridHasFiller = dimensions.columnsTotalWidth < dimensions.viewportOuterSize.width; + const showRightBorder = shouldCellShowRightBorder( + pinnedPosition, + sectionIndex, + sectionLength, + rootProps.showCellVerticalBorder, + gridHasFiller, + ); + const showLeftBorder = shouldCellShowLeftBorder(pinnedPosition, sectionIndex); + const isLastColumn = colIndex === columns.length - 1; + const isFirstPinnedRight = isPinnedRight && sectionIndex === 0; + const hasFillerBefore = isFirstPinnedRight && gridHasFiller; + const hasFillerAfter = isLastColumn && !isFirstPinnedRight && gridHasFiller; + const expandedWidth = dimensions.viewportOuterSize.width - dimensions.columnsTotalWidth; + const emptyCellWidth = Math.max(0, expandedWidth); + const emptyCell = ( + + ); + const scrollbarWidth = dimensions.hasScrollY ? dimensions.scrollbarSize : 0; + const hasScrollbarFiller = isLastColumn && scrollbarWidth !== 0; + + if (hasFillerBefore) { + rowCells.push(emptyCell); + } + + rowCells.push( + , + ); + + if (hasFillerAfter) { + rowCells.push(emptyCell); + } + + if (hasScrollbarFiller) { + rowCells.push( 0} />); + } + } + + array.push( +
+ {rowCells} +
, + ); + } + return array; + }, [ + slots, + columns, + pinnedColumns, + skeletonRowsCount, + rootProps.showCellVerticalBorder, + dimensions.columnsTotalWidth, + dimensions.viewportOuterSize.width, + dimensions.rowHeight, + dimensions.hasScrollY, + dimensions.scrollbarSize, + getPinnedPosition, + getPinnedStyle, + ]); + + // Sync the column resize of the overlay columns with the grid + const handleColumnResize: GridEventListener<'columnResize'> = (params) => { + const { colDef, width } = params; + const cells = ref.current?.querySelectorAll( + `[data-field="${escapeOperandAttributeSelector(colDef.field)}"]`, + ); + + if (!cells) { + throw new Error('MUI X: Expected skeleton cells to be defined with `data-field` attribute.'); + } + + const resizedColIndex = columns.findIndex((col) => col.field === colDef.field); + const pinnedPosition = getPinnedPosition(colDef.field); + const isPinnedLeft = pinnedPosition === GridPinnedColumnPosition.LEFT; + const isPinnedRight = pinnedPosition === GridPinnedColumnPosition.RIGHT; + const currentWidth = getComputedStyle(cells[0]).getPropertyValue('--width'); + const delta = parseInt(currentWidth, 10) - width; + + if (cells) { + cells.forEach((element) => { + element.style.setProperty('--width', `${width}px`); + }); + } + + if (isPinnedLeft) { + const pinnedCells = ref.current?.querySelectorAll( + `.${gridClasses['cell--pinnedLeft']}`, + ); + pinnedCells?.forEach((element) => { + const colIndex = getColIndex(element); + if (colIndex > resizedColIndex) { + element.style.left = `${parseInt(getComputedStyle(element).left, 10) - delta}px`; + } + }); + } + + if (isPinnedRight) { + const pinnedCells = ref.current?.querySelectorAll( + `.${gridClasses['cell--pinnedRight']}`, + ); + pinnedCells?.forEach((element) => { + const colIndex = getColIndex(element); + if (colIndex < resizedColIndex) { + element.style.right = `${parseInt(getComputedStyle(element).right, 10) + delta}px`; + } + }); + } + }; + + useGridApiEventHandler(apiRef, 'columnResize', handleColumnResize); + + return ( + + {children} + + ); +}); + +export { GridSkeletonLoadingOverlay }; diff --git a/packages/x-data-grid/src/components/base/GridOverlays.tsx b/packages/x-data-grid/src/components/base/GridOverlays.tsx index 2e214bc219ff..fc64a9c3d759 100644 --- a/packages/x-data-grid/src/components/base/GridOverlays.tsx +++ b/packages/x-data-grid/src/components/base/GridOverlays.tsx @@ -4,40 +4,47 @@ import { styled } from '@mui/system'; import { unstable_composeClasses as composeClasses } from '@mui/utils'; import clsx from 'clsx'; import { useGridSelector } from '../../hooks/utils/useGridSelector'; -import { gridExpandedRowCountSelector } from '../../hooks/features/filter/gridFilterSelector'; -import { - gridRowCountSelector, - gridRowsLoadingSelector, -} from '../../hooks/features/rows/gridRowsSelector'; import { gridDimensionsSelector } from '../../hooks/features/dimensions'; +import { GridOverlayType } from '../../hooks/features/overlays/useGridOverlays'; import { useGridApiContext } from '../../hooks/utils/useGridApiContext'; import { useGridRootProps } from '../../hooks/utils/useGridRootProps'; import { useGridVisibleRows } from '../../hooks/utils/useGridVisibleRows'; import { getMinimalContentHeight } from '../../hooks/features/rows/gridRowsUtils'; import { DataGridProcessedProps } from '../../models/props/DataGridProps'; import { getDataGridUtilityClass } from '../../constants/gridClasses'; +import { GridLoadingOverlayVariant } from '../GridLoadingOverlay'; + +interface GridOverlaysProps { + overlayType: GridOverlayType; + loadingOverlayVariant: GridLoadingOverlayVariant | null; +} const GridOverlayWrapperRoot = styled('div', { name: 'MuiDataGrid', slot: 'OverlayWrapper', - shouldForwardProp: (prop) => prop !== 'overlayType', + shouldForwardProp: (prop) => prop !== 'overlayType' && prop !== 'loadingOverlayVariant', overridesResolver: (props, styles) => styles.overlayWrapper, -})<{ overlayType: 'loadingOverlay' | string }>(({ overlayType }) => ({ - position: 'sticky', // To stay in place while scrolling - top: 'var(--DataGrid-headersTotalHeight)', - left: 0, - width: 0, // To stay above the content instead of shifting it down - height: 0, // To stay above the content instead of shifting it down - zIndex: - overlayType === 'loadingOverlay' - ? 5 // Should be above pinned columns, pinned rows, and detail panel - : 4, // Should be above pinned columns and detail panel -})); +})(({ overlayType, loadingOverlayVariant }) => + // Skeleton overlay should flow with the scroll container and not be sticky + loadingOverlayVariant !== 'skeleton' + ? { + position: 'sticky', // To stay in place while scrolling + top: 'var(--DataGrid-headersTotalHeight)', + left: 0, + width: 0, // To stay above the content instead of shifting it down + height: 0, // To stay above the content instead of shifting it down + zIndex: + overlayType === 'loadingOverlay' + ? 5 // Should be above pinned columns, pinned rows, and detail panel + : 4, // Should be above pinned columns and detail panel + } + : {}, +); const GridOverlayWrapperInner = styled('div', { name: 'MuiDataGrid', slot: 'OverlayWrapperInner', - shouldForwardProp: (prop) => prop !== 'overlayType', + shouldForwardProp: (prop) => prop !== 'overlayType' && prop !== 'loadingOverlayVariant', overridesResolver: (props, styles) => styles.overlayWrapperInner, })({}); @@ -54,7 +61,7 @@ const useUtilityClasses = (ownerState: OwnerState) => { return composeClasses(slots, getDataGridUtilityClass, classes); }; -function GridOverlayWrapper(props: React.PropsWithChildren<{ overlayType: string }>) { +function GridOverlayWrapper(props: React.PropsWithChildren) { const apiRef = useGridApiContext(); const rootProps = useGridRootProps(); const currentPage = useGridVisibleRows(apiRef, rootProps); @@ -72,7 +79,7 @@ function GridOverlayWrapper(props: React.PropsWithChildren<{ overlayType: string const classes = useUtilityClasses({ ...props, classes: rootProps.classes }); return ( - + 0 && visibleRowCount === 0; - - let overlay: React.JSX.Element | null = null; - let overlayType = ''; - - if (showNoRowsOverlay) { - overlay = ; - overlayType = 'noRowsOverlay'; - } - - if (showNoResultsOverlay) { - overlay = ; - overlayType = 'noResultsOverlay'; - } - - if (loading) { - overlay = ; - overlayType = 'loadingOverlay'; - } - - if (overlay === null) { + if (!overlayType) { return null; } - return {overlay}; + const Overlay = rootProps.slots?.[overlayType]; + const overlayProps = rootProps.slotProps?.[overlayType]; + + return ( + + + + ); } diff --git a/packages/x-data-grid/src/components/cell/GridSkeletonCell.tsx b/packages/x-data-grid/src/components/cell/GridSkeletonCell.tsx index fef4188a67f0..28a25539b706 100644 --- a/packages/x-data-grid/src/components/cell/GridSkeletonCell.tsx +++ b/packages/x-data-grid/src/components/cell/GridSkeletonCell.tsx @@ -5,45 +5,101 @@ import { unstable_composeClasses as composeClasses, unstable_capitalize as capitalize, } from '@mui/utils'; +import clsx from 'clsx'; import { fastMemo } from '../../utils/fastMemo'; -import { randomNumberBetween } from '../../utils/utils'; +import { createRandomNumberGenerator } from '../../utils/utils'; import { useGridRootProps } from '../../hooks/utils/useGridRootProps'; import { getDataGridUtilityClass } from '../../constants/gridClasses'; import { DataGridProcessedProps } from '../../models/props/DataGridProps'; +import { GridColType } from '../../models'; -const randomWidth = randomNumberBetween(10000, 20, 80); +const CIRCULAR_CONTENT_SIZE = '1.3em'; -export interface GridSkeletonCellProps { - width: number; - height: number | 'auto'; - field: string; - align: string; +const CONTENT_HEIGHT = '1.2em'; + +const DEFAULT_CONTENT_WIDTH_RANGE = [40, 80] as const; + +const CONTENT_WIDTH_RANGE_BY_TYPE: Partial> = { + number: [40, 60], + string: [40, 80], + date: [40, 60], + dateTime: [60, 80], + singleSelect: [40, 80], +} as const; + +export interface GridSkeletonCellProps extends React.HTMLAttributes { + type?: GridColType; + width?: number | string; + height?: number | 'auto'; + field?: string; + align?: string; + /** + * If `true`, the cell will not display the skeleton but still reserve the cell space. + * @default false + */ + empty?: boolean; } -type OwnerState = Pick & { +type OwnerState = Pick & { classes?: DataGridProcessedProps['classes']; }; const useUtilityClasses = (ownerState: OwnerState) => { - const { align, classes } = ownerState; + const { align, classes, empty } = ownerState; const slots = { - root: ['cell', 'cellSkeleton', `cell--text${capitalize(align)}`, 'withBorderColor'], + root: [ + 'cell', + 'cellSkeleton', + `cell--text${align ? capitalize(align) : 'Left'}`, + empty && 'cellEmpty', + ], }; return composeClasses(slots, getDataGridUtilityClass, classes); }; -function GridSkeletonCell(props: React.HTMLAttributes & GridSkeletonCellProps) { - const { field, align, width, height, ...other } = props; +const randomNumberGenerator = createRandomNumberGenerator(12345); + +function GridSkeletonCell(props: GridSkeletonCellProps) { + const { field, type, align, width, height, empty = false, style, className, ...other } = props; const rootProps = useGridRootProps(); - const ownerState = { classes: rootProps.classes, align }; + const ownerState = { classes: rootProps.classes, align, empty }; const classes = useUtilityClasses(ownerState); - const contentWidth = Math.round(randomWidth()); + + // Memo prevents the non-circular skeleton widths changing to random widths on every render + const skeletonProps = React.useMemo(() => { + const isCircularContent = type === 'boolean' || type === 'actions'; + + if (isCircularContent) { + return { + variant: 'circular', + width: CIRCULAR_CONTENT_SIZE, + height: CIRCULAR_CONTENT_SIZE, + } as const; + } + + // The width of the skeleton is a random number between the min and max values + // The min and max values are determined by the type of the column + const [min, max] = type + ? CONTENT_WIDTH_RANGE_BY_TYPE[type] ?? DEFAULT_CONTENT_WIDTH_RANGE + : DEFAULT_CONTENT_WIDTH_RANGE; + + return { + variant: 'text', + width: `${Math.round(randomNumberGenerator(min, max))}%`, + height: CONTENT_HEIGHT, + } as const; + }, [type]); return ( -
- +
+ {!empty && }
); } @@ -53,10 +109,25 @@ GridSkeletonCell.propTypes = { // | These PropTypes are generated from the TypeScript type definitions | // | To update them edit the TypeScript types and run "pnpm proptypes" | // ---------------------------------------------------------------------- - align: PropTypes.string.isRequired, - field: PropTypes.string.isRequired, - height: PropTypes.oneOfType([PropTypes.oneOf(['auto']), PropTypes.number]).isRequired, - width: PropTypes.number.isRequired, + align: PropTypes.string, + /** + * If `true`, the cell will not display the skeleton but still reserve the cell space. + * @default false + */ + empty: PropTypes.bool, + field: PropTypes.string, + height: PropTypes.oneOfType([PropTypes.oneOf(['auto']), PropTypes.number]), + type: PropTypes.oneOf([ + 'actions', + 'boolean', + 'custom', + 'date', + 'dateTime', + 'number', + 'singleSelect', + 'string', + ]), + width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), } as any; const Memoized = fastMemo(GridSkeletonCell); diff --git a/packages/x-data-grid/src/components/containers/GridRootStyles.ts b/packages/x-data-grid/src/components/containers/GridRootStyles.ts index e1ea183b8bcb..bdfd79712cd3 100644 --- a/packages/x-data-grid/src/components/containers/GridRootStyles.ts +++ b/packages/x-data-grid/src/components/containers/GridRootStyles.ts @@ -148,7 +148,7 @@ export const GridRootStyles = styled('div', { const selectedHoverBackground = t.vars ? `rgba(${t.vars.palette.primary.mainChannel} / calc( - ${t.vars.palette.action.selectedOpacity} + + ${t.vars.palette.action.selectedOpacity} + ${t.vars.palette.action.hoverOpacity} ))` : alpha( @@ -436,6 +436,9 @@ export const GridRootStyles = styled('div', { backgroundColor: 'transparent', }, }, + [`&.${c.rowSkeleton}:hover`]: { + backgroundColor: 'transparent', + }, '&.Mui-selected': selectedStyles, }, [`& .${c['container--top']}, & .${c['container--bottom']}`]: { @@ -645,7 +648,7 @@ export const GridRootStyles = styled('div', { minWidth: 'calc(var(--DataGrid-hasScrollY) * var(--DataGrid-scrollbarSize))', alignSelf: 'stretch', [`&.${c['scrollbarFiller--borderTop']}`]: { - borderTop: '1px solid var(--DataGrid-rowBorderColor)', + borderTop: '1px solid var(--rowBorderColor)', }, [`&.${c['scrollbarFiller--pinnedRight']}`]: { backgroundColor: 'var(--DataGrid-pinnedBackground)', @@ -660,6 +663,13 @@ export const GridRootStyles = styled('div', { [`& .${c['filler--borderTop']}`]: { borderTop: '1px solid var(--DataGrid-rowBorderColor)', }, + + /* Hide grid rows and vertical scrollbar when skeleton overlay is visible */ + [`& .${c['main--hasSkeletonLoadingOverlay']}`]: { + [`& .${c.virtualScrollerContent}, & .${c['scrollbar--vertical']}, & .${c.pinnedRows}`]: { + display: 'none', + }, + }, }; return gridStyle; diff --git a/packages/x-data-grid/src/components/virtualization/GridVirtualScroller.tsx b/packages/x-data-grid/src/components/virtualization/GridVirtualScroller.tsx index aa17ed93be6d..42c2c2bf443a 100644 --- a/packages/x-data-grid/src/components/virtualization/GridVirtualScroller.tsx +++ b/packages/x-data-grid/src/components/virtualization/GridVirtualScroller.tsx @@ -9,7 +9,8 @@ import { getDataGridUtilityClass } from '../../constants/gridClasses'; import { DataGridProcessedProps } from '../../models/props/DataGridProps'; import { GridDimensions, gridDimensionsSelector } from '../../hooks/features/dimensions'; import { useGridVirtualScroller } from '../../hooks/features/virtualization/useGridVirtualScroller'; -import { GridOverlays } from '../base/GridOverlays'; +import { useGridOverlays } from '../../hooks/features/overlays/useGridOverlays'; +import { GridOverlays as Overlays } from '../base/GridOverlays'; import { GridHeaders } from '../GridHeaders'; import { GridMainContainer as Container } from './GridMainContainer'; import { GridTopContainer as TopContainer } from './GridTopContainer'; @@ -18,14 +19,23 @@ import { GridVirtualScrollerContent as Content } from './GridVirtualScrollerCont import { GridVirtualScrollerFiller as SpaceFiller } from './GridVirtualScrollerFiller'; import { GridVirtualScrollerRenderZone as RenderZone } from './GridVirtualScrollerRenderZone'; import { GridVirtualScrollbar as Scrollbar } from './GridVirtualScrollbar'; +import { GridLoadingOverlayVariant } from '../GridLoadingOverlay'; type OwnerState = DataGridProcessedProps; -const useUtilityClasses = (ownerState: OwnerState, dimensions: GridDimensions) => { +const useUtilityClasses = ( + ownerState: OwnerState, + dimensions: GridDimensions, + loadingOverlayVariant: GridLoadingOverlayVariant | null, +) => { const { classes } = ownerState; const slots = { - root: ['main', dimensions.rightPinnedWidth > 0 && 'main--hasPinnedRight'], + root: [ + 'main', + dimensions.rightPinnedWidth > 0 && 'main--hasPinnedRight', + loadingOverlayVariant === 'skeleton' && 'main--hasSkeletonLoadingOverlay', + ], scroller: ['virtualScroller'], }; @@ -61,7 +71,8 @@ function GridVirtualScroller(props: GridVirtualScrollerProps) { const apiRef = useGridApiContext(); const rootProps = useGridRootProps(); const dimensions = useGridSelector(apiRef, gridDimensionsSelector); - const classes = useUtilityClasses(rootProps, dimensions); + const overlaysProps = useGridOverlays(); + const classes = useUtilityClasses(rootProps, dimensions, overlaysProps.loadingOverlayVariant); const virtualScroller = useGridVirtualScroller(); const { @@ -86,7 +97,7 @@ function GridVirtualScroller(props: GridVirtualScrollerProps) { - + diff --git a/packages/x-data-grid/src/constants/gridClasses.ts b/packages/x-data-grid/src/constants/gridClasses.ts index d908705f9162..e18d9ad39c9c 100644 --- a/packages/x-data-grid/src/constants/gridClasses.ts +++ b/packages/x-data-grid/src/constants/gridClasses.ts @@ -367,6 +367,11 @@ export interface GridClasses { * Styles applied to the main container element when it has right pinned columns. */ 'main--hasPinnedRight': string; + /** + * Styles applied to the main container element when it has an active skeleton loading overlay. + * @ignore - do not document. + */ + 'main--hasSkeletonLoadingOverlay': string; /** * Styles applied to the menu element. */ @@ -488,6 +493,10 @@ export interface GridClasses { * Styles applied to the root element of the row reorder cell when dragging is allowed */ 'rowReorderCell--draggable': string; + /** + * Styles applied to the skeleton row element. + */ + rowSkeleton: string; /** * Styles applied to both scroll area elements. */ @@ -706,6 +715,7 @@ export const gridClasses = generateUtilityClasses('MuiDataGrid', [ 'iconSeparator', 'main', 'main--hasPinnedRight', + 'main--hasSkeletonLoadingOverlay', 'menu', 'menuIcon', 'menuIconButton', @@ -732,6 +742,7 @@ export const gridClasses = generateUtilityClasses('MuiDataGrid', [ 'rowReorderCellContainer', 'rowReorderCell', 'rowReorderCell--draggable', + 'rowSkeleton', 'scrollArea--left', 'scrollArea--right', 'scrollArea', diff --git a/packages/x-data-grid/src/hooks/features/overlays/useGridOverlays.ts b/packages/x-data-grid/src/hooks/features/overlays/useGridOverlays.ts new file mode 100644 index 000000000000..f3f9f3998423 --- /dev/null +++ b/packages/x-data-grid/src/hooks/features/overlays/useGridOverlays.ts @@ -0,0 +1,47 @@ +import { useGridSelector } from '../../utils'; +import { useGridApiContext } from '../../utils/useGridApiContext'; +import { useGridRootProps } from '../../utils/useGridRootProps'; +import { gridExpandedRowCountSelector } from '../filter'; +import { gridRowCountSelector, gridRowsLoadingSelector } from '../rows'; +import { GridLoadingOverlayVariant } from '../../../components/GridLoadingOverlay'; +import { GridSlotsComponent } from '../../../models/gridSlotsComponent'; + +export type GridOverlayType = + | keyof Pick + | null; + +/** + * Uses the grid state to determine which overlay to display. + * Returns the active overlay type and the active loading overlay variant. + */ +export const useGridOverlays = () => { + const apiRef = useGridApiContext(); + const rootProps = useGridRootProps(); + + const totalRowCount = useGridSelector(apiRef, gridRowCountSelector); + const visibleRowCount = useGridSelector(apiRef, gridExpandedRowCountSelector); + const noRows = totalRowCount === 0; + const loading = useGridSelector(apiRef, gridRowsLoadingSelector); + + const showNoRowsOverlay = !loading && noRows; + const showNoResultsOverlay = !loading && totalRowCount > 0 && visibleRowCount === 0; + + let overlayType: GridOverlayType = null; + let loadingOverlayVariant: GridLoadingOverlayVariant | null = null; + + if (showNoRowsOverlay) { + overlayType = 'noRowsOverlay'; + } + + if (showNoResultsOverlay) { + overlayType = 'noResultsOverlay'; + } + + if (loading) { + overlayType = 'loadingOverlay'; + loadingOverlayVariant = + rootProps.slotProps?.loadingOverlay?.[noRows ? 'noRowsVariant' : 'variant'] || null; + } + + return { overlayType, loadingOverlayVariant }; +}; diff --git a/packages/x-data-grid/src/models/gridSlotsComponentsProps.ts b/packages/x-data-grid/src/models/gridSlotsComponentsProps.ts index 20c787042a25..6ea9c9f27613 100644 --- a/packages/x-data-grid/src/models/gridSlotsComponentsProps.ts +++ b/packages/x-data-grid/src/models/gridSlotsComponentsProps.ts @@ -27,6 +27,7 @@ import type { GridColumnHeadersProps } from '../components/GridColumnHeaders'; import type { GridDetailPanelsProps } from '../components/GridDetailPanels'; import type { GridPinnedRowsProps } from '../components/GridPinnedRows'; import type { GridColumnsManagementProps } from '../components/columnsManagement/GridColumnsManagement'; +import type { GridLoadingOverlayProps } from '../components/GridLoadingOverlay'; import type { GridRowCountProps } from '../components/GridRowCount'; import type { GridColumnHeaderSortIconProps } from '../components/columnHeaders/GridColumnHeaderSortIcon'; @@ -94,7 +95,7 @@ export interface GridSlotProps { filterPanel: GridFilterPanelProps & FilterPanelPropsOverrides; footer: GridFooterContainerProps & FooterPropsOverrides; footerRowCount: GridRowCountProps & FooterRowCountOverrides; - loadingOverlay: GridOverlayProps & LoadingOverlayPropsOverrides; + loadingOverlay: GridLoadingOverlayProps & LoadingOverlayPropsOverrides; noResultsOverlay: GridOverlayProps & NoResultsOverlayPropsOverrides; noRowsOverlay: GridOverlayProps & NoRowsOverlayPropsOverrides; pagination: Partial & PaginationPropsOverrides; diff --git a/packages/x-data-grid/src/utils/utils.ts b/packages/x-data-grid/src/utils/utils.ts index cc682162e59f..097ed683d9b2 100644 --- a/packages/x-data-grid/src/utils/utils.ts +++ b/packages/x-data-grid/src/utils/utils.ts @@ -190,9 +190,16 @@ function mulberry32(a: number): () => number { }; } -export function randomNumberBetween(seed: number, min: number, max: number): () => number { +/** + * Create a random number generator from a seed. The seed + * ensures that the random number generator produces the + * same sequence of 'random' numbers on every render. It + * returns a function that generates a random number between + * a specified min and max. + */ +export function createRandomNumberGenerator(seed: number): (min: number, max: number) => number { const random = mulberry32(seed); - return () => min + (max - min) * random(); + return (min: number, max: number) => min + (max - min) * random(); } export function deepClone(obj: Record) { diff --git a/scripts/x-data-grid-premium.exports.json b/scripts/x-data-grid-premium.exports.json index 12f86b8d6196..2e5ddf65f11b 100644 --- a/scripts/x-data-grid-premium.exports.json +++ b/scripts/x-data-grid-premium.exports.json @@ -399,6 +399,8 @@ { "name": "GridLeafNode", "kind": "Interface" }, { "name": "GridLoadIcon", "kind": "Variable" }, { "name": "GridLoadingOverlay", "kind": "Variable" }, + { "name": "GridLoadingOverlayProps", "kind": "Interface" }, + { "name": "GridLoadingOverlayVariant", "kind": "TypeAlias" }, { "name": "GridLocaleText", "kind": "Interface" }, { "name": "GridLocaleTextApi", "kind": "Interface" }, { "name": "GridLogicOperator", "kind": "Enum" }, diff --git a/scripts/x-data-grid-pro.exports.json b/scripts/x-data-grid-pro.exports.json index 22f8ca72e6d0..170949555457 100644 --- a/scripts/x-data-grid-pro.exports.json +++ b/scripts/x-data-grid-pro.exports.json @@ -363,6 +363,8 @@ { "name": "GridLeafNode", "kind": "Interface" }, { "name": "GridLoadIcon", "kind": "Variable" }, { "name": "GridLoadingOverlay", "kind": "Variable" }, + { "name": "GridLoadingOverlayProps", "kind": "Interface" }, + { "name": "GridLoadingOverlayVariant", "kind": "TypeAlias" }, { "name": "GridLocaleText", "kind": "Interface" }, { "name": "GridLocaleTextApi", "kind": "Interface" }, { "name": "GridLogicOperator", "kind": "Enum" }, diff --git a/scripts/x-data-grid.exports.json b/scripts/x-data-grid.exports.json index ec3c2edb53e8..68d226d15178 100644 --- a/scripts/x-data-grid.exports.json +++ b/scripts/x-data-grid.exports.json @@ -325,6 +325,8 @@ { "name": "GridLeafNode", "kind": "Interface" }, { "name": "GridLoadIcon", "kind": "Variable" }, { "name": "GridLoadingOverlay", "kind": "Variable" }, + { "name": "GridLoadingOverlayProps", "kind": "Interface" }, + { "name": "GridLoadingOverlayVariant", "kind": "TypeAlias" }, { "name": "GridLocaleText", "kind": "Interface" }, { "name": "GridLocaleTextApi", "kind": "Interface" }, { "name": "GridLogicOperator", "kind": "Enum" },