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 Skeleton loading overlay support #13293

Merged
merged 35 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5242c9a
[DataGrid] Add GridSkeletonLoadingOverlay
KenanYusuf May 27, 2024
a2b1bb0
allow users to set a variant and noRowsVariant through loadingOverlay…
KenanYusuf May 29, 2024
d32fa8b
fix selector usage
KenanYusuf May 29, 2024
d45dfdd
use useGridApiEventHandler for event subscriptions
KenanYusuf May 29, 2024
e8c9e0f
remove skeleton export
KenanYusuf May 29, 2024
e05cf1d
apply solid background colour to skeleton overlay
KenanYusuf May 30, 2024
4373460
rename random number generator function and expand on comment
KenanYusuf May 30, 2024
f9d5442
Update packages/x-data-grid/src/components/GridSkeletonLoadingOverlay…
KenanYusuf May 31, 2024
d745a3e
update GridLoadingOverlay to use props destructuring convention
KenanYusuf May 31, 2024
6cf0ffa
update RNG comments
KenanYusuf May 31, 2024
e91886f
Update packages/x-data-grid/src/components/GridSkeletonLoadingOverlay…
KenanYusuf May 31, 2024
721e78a
reuse GridSkeletonCell component in skeleton loading overlay
KenanYusuf May 31, 2024
ceb9c7e
make skeleton loader flow with scroll container
KenanYusuf Jun 3, 2024
e98680e
import syntax
KenanYusuf Jun 3, 2024
09118fb
update classes docs
KenanYusuf Jun 4, 2024
650ae4e
overlays documentation
KenanYusuf Jun 4, 2024
7c226b6
remove skeleton overlay class from public api
KenanYusuf Jun 4, 2024
bdbfeb5
support pinned columns
KenanYusuf Jun 5, 2024
6413802
remove row hover style and expose rowSkeleton class
KenanYusuf Jun 5, 2024
5d54f01
add filler cell if the columns do not take up the full width of the row
KenanYusuf Jun 5, 2024
5e736e9
Merge branch 'master' into skeleton-loading-overlay
KenanYusuf Jun 6, 2024
eda2557
set width var directly on skeleton cell
KenanYusuf Jun 21, 2024
7de9a56
fix pinned cell offset position during resize
KenanYusuf Jun 21, 2024
027cf4b
Merge branch 'master' into skeleton-loading-overlay
KenanYusuf Jun 21, 2024
78641bc
Merge branch 'master' into skeleton-loading-overlay
KenanYusuf Jun 24, 2024
5f295da
Merge branch 'master' into skeleton-loading-overlay
KenanYusuf Jun 26, 2024
0e0e8cc
Update custom loading overlay example
KenanYusuf Jun 26, 2024
5567ef3
add scrollbar filler cell
KenanYusuf Jun 26, 2024
be974f5
separate variant demo into multiple demos
KenanYusuf Jun 26, 2024
7bd1b49
Update docs/data/data-grid/overlays/LoadingOverlaySkeleton.tsx
KenanYusuf Jul 1, 2024
c6f6617
Merge branch 'mui:master' into skeleton-loading-overlay
KenanYusuf Jul 2, 2024
6dc2f5d
update demos
KenanYusuf Jul 2, 2024
65da878
optimizations from code review
KenanYusuf Jul 3, 2024
c92a3a3
fix sectionLength and sectionIndex for middle section skeleton cells
KenanYusuf Jul 3, 2024
ba615d4
Merge branch 'mui:master' into skeleton-loading-overlay
KenanYusuf Jul 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 66 additions & 3 deletions packages/x-data-grid/src/components/GridLoadingOverlay.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,66 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import LinearProgress from '@mui/material/LinearProgress';
import CircularProgress from '@mui/material/CircularProgress';
import { Theme, SystemStyleObject } from '@mui/system';
import { GridOverlay, GridOverlayProps } from './containers/GridOverlay';
import { GridSkeletonLoadingOverlay } from './GridSkeletonLoadingOverlay';
import { useGridApiContext } from '../hooks/utils/useGridApiContext';

const GridLoadingOverlay = React.forwardRef<HTMLDivElement, GridOverlayProps>(
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;
sx: SystemStyleObject<Theme>;
}
> = {
'circular-progress': {
component: CircularProgress,
sx: {},
},
'linear-progress': {
component: LinearProgress,
sx: { display: 'block' },
},
skeleton: {
component: GridSkeletonLoadingOverlay,
sx: {
display: 'block',
background: 'var(--DataGrid-containerBackground)',
KenanYusuf marked this conversation as resolved.
Show resolved Hide resolved
},
},
};

const GridLoadingOverlay = React.forwardRef<HTMLDivElement, GridLoadingOverlayProps>(
function GridLoadingOverlay(props, ref) {
const {
variant = 'circular-progress',
noRowsVariant = 'circular-progress',
sx,
...other
} = props;
const apiRef = useGridApiContext();
const rowsCount = apiRef.current.getRowsCount();
MBilalShafi marked this conversation as resolved.
Show resolved Hide resolved
const activeVariant = LOADING_VARIANTS[rowsCount === 0 ? noRowsVariant : variant];

return (
<GridOverlay ref={ref} {...props}>
<CircularProgress />
<GridOverlay ref={ref} sx={{ ...activeVariant.sx, ...sx }} {...other}>
<activeVariant.component />
</GridOverlay>
);
},
Expand All @@ -18,11 +71,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 };
139 changes: 139 additions & 0 deletions packages/x-data-grid/src/components/GridSkeletonLoadingOverlay.tsx
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you confirm that it works well with the showCellVerticalBorder prop?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tested and found that it worked well with the showCellVerticalBorder prop when it was not overflowing horizontally, but the right border disappeared when it became scrollable. This turned out to be because I was not providing the correct sectionIndex and sectionLength values to the shouldCellShowRightBorder function.

Fixed here c92a3a3

Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import * as React from 'react';
import Skeleton from '@mui/material/Skeleton';
import { styled } from '@mui/system';
Copy link
Member

@oliviertassinari oliviertassinari May 30, 2024

Choose a reason for hiding this comment

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

We should import from Material UI here no? Same for all instances, until we deprecate @mui/material/styles for @mui/system. But until then, we need the theme. mui/material-ui#40594

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@oliviertassinari I wasn't sure because I found other instances in the same directory where styled is imported from @mui/system e.g. https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/components/base/GridOverlays.tsx#L3

Copy link
Member

Choose a reason for hiding this comment

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

@oliviertassinari I had a similar discussion on Slack last week with quite a different answer: https://mui-org.slack.com/archives/C0170JAM7ML/p1717077013655759

It would be great to align on this topic to avoid back and forth between those two import paths

Copy link
Member

@oliviertassinari oliviertassinari Jun 3, 2024

Choose a reason for hiding this comment

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

This sounds compatible, if the style of the component needs to access the theme directly, it must import from @mui/material/styles for now, since we don't force people to provide a theme, it needs the default value for the theme value accessed. Otherwise, import from @mui/system.

In the future, everything can be imported from @mui/system (meaning Pigment CSS).

Copy link
Member

@MBilalShafi MBilalShafi Jun 25, 2024

Choose a reason for hiding this comment

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

In the future, everything can be imported from @mui/system (meaning Pigment CSS).

Is there a timeline for this? With v6?

It might impact a priority OKR for the Data Grid where we want to remove all @mui/material imports.

Copy link
Member

Choose a reason for hiding this comment

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

Is there a timeline for this? With v6?

I don't know. I'm actually not even sure it's the direction taken by the MUI System team. cc @brijeshb42 and @siriwatknp. At least, in my mind. @mui/material should include nothing else than Base UI components that are styled. I could see Pigment System as where all the theming and specific styling things of Material UI to be hosted or have it under a separate brand like Material UI System.

import { unstable_useForkRef as useForkRef } from '@mui/utils';
import { useGridApiContext } from '../hooks/utils/useGridApiContext';
import {
gridColumnPositionsSelector,
gridColumnsTotalWidthSelector,
gridDimensionsSelector,
gridVisibleColumnDefinitionsSelector,
useGridApiEventHandler,
useGridSelector,
} from '../hooks';
import { GridColType, GridEventListener } from '../models';
import { createRandomNumberGenerator } from '../utils/utils';

const DEFAULT_COLUMN_WIDTH_RANGE = [40, 80] as const;

const COLUMN_WIDTH_RANGE_BY_TYPE: Partial<Record<GridColType, [number, number]>> = {
number: [40, 60],
string: [40, 80],
date: [40, 60],
dateTime: [60, 80],
singleSelect: [40, 80],
} as const;

const colWidthVar = (index: number) => `--colWidth-${index}`;

const SkeletonOverlay = styled('div')({
KenanYusuf marked this conversation as resolved.
Show resolved Hide resolved
display: 'grid',
overflow: 'hidden',
});

const SkeletonCell = styled('div')({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
borderBottom: '1px solid var(--DataGrid-rowBorderColor)',
});

const GridSkeletonLoadingOverlay = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(function GridSkeletonLoadingOverlay(props, forwardedRef) {
const ref = React.useRef<HTMLDivElement>(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 = allVisibleColumns.slice(0, inViewportCount);
MBilalShafi marked this conversation as resolved.
Show resolved Hide resolved

const children = React.useMemo(() => {
// The random number generator is used to determine the width of each skeleton element.
// The seed ensures the width of each skeleton element width remains the same across renders and does not flicker.
KenanYusuf marked this conversation as resolved.
Show resolved Hide resolved
const randomNumberBetween = createRandomNumberGenerator(12345);
const array: React.ReactNode[] = [];

for (let i = 0; i < skeletonRowsCount; i += 1) {
// eslint-disable-next-line no-restricted-syntax
for (const column of columns) {
const [min, max] = column.type
? COLUMN_WIDTH_RANGE_BY_TYPE[column.type] ?? DEFAULT_COLUMN_WIDTH_RANGE
: DEFAULT_COLUMN_WIDTH_RANGE;
const width = Math.round(randomNumberBetween(min, max));
const isCircular = column.type === 'boolean' || column.type === 'actions';

array.push(
<SkeletonCell
key={`skeleton-column-${i}-${column.field}`}
sx={{ justifyContent: column.align }}
>
<Skeleton
width={isCircular ? '1.3em' : `${width}%`}
height={isCircular ? '1.3em' : '1.2em'}
variant={isCircular ? 'circular' : 'text'}
sx={{ mx: 1 }}
/>
</SkeletonCell>,
);
}
array.push(<SkeletonCell key={`skeleton-filler-column-${i}`} />);
}
return array;
}, [skeletonRowsCount, columns]);

const [initialColWidthVariables, gridTemplateColumns] = columns.reduce(
([initialSize, templateColumn], column, i) => {
const varName = colWidthVar(i);
initialSize[varName] = `${column.computedWidth}px`;
templateColumn += ` var(${varName})`;
return [initialSize, templateColumn];
},
[{} as Record<string, string>, ''],
);

// Sync the horizontal scroll of the overlay with the grid
const handleScrollChange: GridEventListener<'scrollPositionChange'> = (params) => {
if (ref.current) {
ref.current.scrollLeft = params.left;
}
};
useGridApiEventHandler(apiRef, 'scrollPositionChange', handleScrollChange);

// Sync the column resize of the overlay columns with the grid
const handleColumnResize: GridEventListener<'columnResize'> = (params) => {
const columnIndex = columns.findIndex((column) => column.field === params.colDef.field);
ref.current?.style.setProperty(colWidthVar(columnIndex), `${params.width}px`);
};
useGridApiEventHandler(apiRef, 'columnResize', handleColumnResize);

return (
<SkeletonOverlay
ref={handleRef}
{...props}
style={{
gridTemplateColumns: `${gridTemplateColumns} 1fr`,
gridAutoRows: dimensions.rowHeight,
...initialColWidthVariables,
...props.style,
}}
>
{children}
</SkeletonOverlay>
);
});

export { GridSkeletonLoadingOverlay };
6 changes: 3 additions & 3 deletions packages/x-data-grid/src/components/cell/GridSkeletonCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import {
unstable_capitalize as capitalize,
} from '@mui/utils';
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';

const randomWidth = randomNumberBetween(10000, 20, 80);
const randomWidth = createRandomNumberGenerator(10000);

export interface GridSkeletonCellProps {
width: number;
Expand Down Expand Up @@ -39,7 +39,7 @@ function GridSkeletonCell(props: React.HTMLAttributes<HTMLDivElement> & GridSkel
const rootProps = useGridRootProps();
const ownerState = { classes: rootProps.classes, align };
const classes = useUtilityClasses(ownerState);
const contentWidth = Math.round(randomWidth());
const contentWidth = Math.round(randomWidth(20, 80));

return (
<div className={classes.root} style={{ height, maxWidth: width, minWidth: width }} {...other}>
Expand Down
3 changes: 2 additions & 1 deletion packages/x-data-grid/src/models/gridSlotsComponentsProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

// Overrides for module augmentation
Expand Down Expand Up @@ -91,7 +92,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<TablePaginationProps> & PaginationPropsOverrides;
Expand Down
11 changes: 9 additions & 2 deletions packages/x-data-grid/src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>) {
Expand Down