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] Scroll restoration #15623

Merged
merged 10 commits into from
Jan 15, 2025
59 changes: 59 additions & 0 deletions docs/data/data-grid/scrolling/ScrollRestoration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import {
DataGridPro,
useGridApiRef,
gridVisibleColumnDefinitionsSelector,
gridExpandedSortedRowIdsSelector,
} from '@mui/x-data-grid-pro';
import { useDemoData } from '@mui/x-data-grid-generator';

export default function ScrollRestoration() {
const apiRef = useGridApiRef();

const [coordinates, setCoordinates] = React.useState({
rowIndex: 0,
colIndex: 0,
});

const { data, loading } = useDemoData({
dataSet: 'Commodity',
rowLength: 100,
});

React.useEffect(() => {
const { rowIndex, colIndex } = coordinates;
apiRef.current.scrollToIndexes(coordinates);
const id = gridExpandedSortedRowIdsSelector(apiRef)[rowIndex];
const column = gridVisibleColumnDefinitionsSelector(apiRef)[colIndex];
apiRef.current.setCellFocus(id, column.field);
}, [apiRef, coordinates]);

const handleCellClick = (params) => {
const rowIndex = gridExpandedSortedRowIdsSelector(apiRef).findIndex(
(id) => id === params.id,
);
const colIndex = gridVisibleColumnDefinitionsSelector(apiRef).findIndex(
(column) => column.field === params.field,
);
setCoordinates({ rowIndex, colIndex });
};

return (
<Box sx={{ width: '100%' }}>
<Box sx={{ height: 400 }}>
<DataGridPro
apiRef={apiRef}
onCellClick={handleCellClick}
hideFooter
loading={loading}
{...data}
initialState={{
...data.initialState,
scroll: { top: 1000, left: 1000 },
}}
/>
</Box>
</Box>
);
}
60 changes: 60 additions & 0 deletions docs/data/data-grid/scrolling/ScrollRestoration.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import {
DataGridPro,
useGridApiRef,
gridVisibleColumnDefinitionsSelector,
gridExpandedSortedRowIdsSelector,
GridCellParams,
} from '@mui/x-data-grid-pro';
import { useDemoData } from '@mui/x-data-grid-generator';

export default function ScrollRestoration() {
const apiRef = useGridApiRef();

const [coordinates, setCoordinates] = React.useState({
rowIndex: 0,
colIndex: 0,
});

const { data, loading } = useDemoData({
dataSet: 'Commodity',
rowLength: 100,
});

React.useEffect(() => {
const { rowIndex, colIndex } = coordinates;
apiRef.current.scrollToIndexes(coordinates);
const id = gridExpandedSortedRowIdsSelector(apiRef)[rowIndex];
const column = gridVisibleColumnDefinitionsSelector(apiRef)[colIndex];
apiRef.current.setCellFocus(id, column.field);
}, [apiRef, coordinates]);

const handleCellClick = (params: GridCellParams) => {
const rowIndex = gridExpandedSortedRowIdsSelector(apiRef).findIndex(
(id) => id === params.id,
);
const colIndex = gridVisibleColumnDefinitionsSelector(apiRef).findIndex(
(column) => column.field === params.field,
);
setCoordinates({ rowIndex, colIndex });
};

return (
<Box sx={{ width: '100%' }}>
<Box sx={{ height: 400 }}>
<DataGridPro
apiRef={apiRef}
onCellClick={handleCellClick}
hideFooter
loading={loading}
{...data}
initialState={{
...data.initialState,
scroll: { top: 1000, left: 1000 },
}}
/>
</Box>
</Box>
);
}
13 changes: 13 additions & 0 deletions docs/data/data-grid/scrolling/ScrollRestoration.tsx.preview
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Box sx={{ height: 400 }}>
<DataGridPro
apiRef={apiRef}
onCellClick={handleCellClick}
hideFooter
loading={loading}
{...data}
initialState={{
...data.initialState,
scroll: { top: 1000, left: 1000 },
}}
/>
</Box>
8 changes: 8 additions & 0 deletions docs/data/data-grid/scrolling/scrolling.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@

{{"demo": "ScrollPlayground.js", "bg": "inline"}}

## Scroll restoration

You can restore scroll to a previous position by definining `initialState.scroll` values `{ top: number, left: number }`. The Data Grid will mount at the specified scroll offset in pixels.

Check warning on line 17 in docs/data/data-grid/scrolling/scrolling.md

View workflow job for this annotation

GitHub Actions / runner / vale

[vale] reported by reviewdog 🐶 [Google.Will] Avoid using 'will'. Raw Output: {"message": "[Google.Will] Avoid using 'will'.", "location": {"path": "docs/data/data-grid/scrolling/scrolling.md", "range": {"start": {"line": 17, "column": 137}}}, "severity": "WARNING"}

The following demo explores the usage of scroll restoration:

{{"demo": "ScrollRestoration.js", "bg": "inline"}}

## apiRef

The grid exposes a set of methods that enables all of these features using the imperative `apiRef`. To know more about how to use it, check the [API Object](/x/react-data-grid/api-object/) section.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,12 @@ const GridVirtualScrollbar = React.forwardRef<HTMLDivElement, GridVirtualScrollb
useOnMount(() => {
const scroller = apiRef.current.virtualScrollerRef.current!;
const scrollbar = scrollbarRef.current!;
scroller.addEventListener('scroll', onScrollerScroll, { capture: true });
scrollbar.addEventListener('scroll', onScrollbarScroll, { capture: true });
const eventOptions: AddEventListenerOptions = { capture: true, passive: true };
lauri865 marked this conversation as resolved.
Show resolved Hide resolved
scroller.addEventListener('scroll', onScrollerScroll, eventOptions);
scrollbar.addEventListener('scroll', onScrollbarScroll, eventOptions);
return () => {
scroller.removeEventListener('scroll', onScrollerScroll, { capture: true });
scrollbar.removeEventListener('scroll', onScrollbarScroll, { capture: true });
scroller.removeEventListener('scroll', onScrollerScroll, eventOptions);
scrollbar.removeEventListener('scroll', onScrollbarScroll, eventOptions);
};
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import * as React from 'react';
import clsx from 'clsx';
import { styled, SxProps, Theme } from '@mui/system';
import composeClasses from '@mui/utils/composeClasses';
import useEnhancedEffect from '@mui/utils/useEnhancedEffect';
import { useGridRootProps } from '../../hooks/utils/useGridRootProps';
import { getDataGridUtilityClass } from '../../constants/gridClasses';
import { DataGridProcessedProps } from '../../models/props/DataGridProps';
import { useGridApiContext } from '../../hooks/utils/useGridApiContext';

type OwnerState = DataGridProcessedProps;

Expand All @@ -28,10 +30,15 @@ const GridVirtualScrollerContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & { sx?: SxProps<Theme> }
>(function GridVirtualScrollerContent(props, ref) {
const apiRef = useGridApiContext();
const rootProps = useGridRootProps();
const overflowedContent = !rootProps.autoHeight && props.style?.minHeight === 'auto';
const classes = useUtilityClasses(rootProps, overflowedContent);

useEnhancedEffect(() => {
apiRef.current.publishEvent('virtualScrollerContentSizeChange');
}, [apiRef, props.style]);

lauri865 marked this conversation as resolved.
Show resolved Hide resolved
return (
<VirtualScrollerContentRoot
ref={ref}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ export const useGridVirtualScroller = () => {
const rowsMeta = useGridSelector(apiRef, gridRowsMetaSelector);
const selectedRowsLookup = useGridSelector(apiRef, selectedIdsLookupSelector);
const currentPage = useGridVisibleRows(apiRef, rootProps);
const gridRootRef = apiRef.current.rootElementRef;
const mainRef = apiRef.current.mainElementRef;
const scrollerRef = apiRef.current.virtualScrollerRef;
const scrollbarVerticalRef = apiRef.current.virtualScrollbarVerticalRef;
Expand Down Expand Up @@ -203,7 +202,8 @@ export const useGridVirtualScroller = () => {
* work that's not necessary. Thus we store the context at the start of the scroll in `frozenContext`, and the rows
* that are part of this old context will keep their same render context as to avoid re-rendering.
*/
const scrollPosition = React.useRef(EMPTY_SCROLL_POSITION);
const scrollPosition = React.useRef(rootProps.initialState?.scroll ?? EMPTY_SCROLL_POSITION);
const ignoreNextScrollEvent = React.useRef(false);
const previousContextScrollPosition = React.useRef(EMPTY_SCROLL_POSITION);
const previousRowContext = React.useRef(EMPTY_RENDER_CONTEXT);
const renderContext = useGridSelector(apiRef, gridRenderContextSelector);
Expand Down Expand Up @@ -346,6 +346,11 @@ export const useGridVirtualScroller = () => {
};

const handleScroll = useEventCallback((event: React.UIEvent) => {
if (ignoreNextScrollEvent.current) {
ignoreNextScrollEvent.current = false;
return;
}

const { scrollTop, scrollLeft } = event.currentTarget;

// On iOS and macOS, negative offsets are possible when swiping past the start
Expand Down Expand Up @@ -596,20 +601,6 @@ export const useGridVirtualScroller = () => {
return size;
}, [columnsTotalWidth, contentHeight, needsHorizontalScrollbar]);

React.useEffect(() => {
apiRef.current.publishEvent('virtualScrollerContentSizeChange');
}, [apiRef, contentSize]);

useEnhancedEffect(() => {
// TODO a scroll reset should not be necessary
if (enabledForColumns) {
scrollerRef.current!.scrollLeft = 0;
}
if (enabledForRows) {
scrollerRef.current!.scrollTop = 0;
}
}, [enabledForColumns, enabledForRows, gridRootRef, scrollerRef]);

useEnhancedEffect(() => {
if (listView) {
scrollerRef.current!.scrollLeft = 0;
Expand All @@ -627,6 +618,31 @@ export const useGridVirtualScroller = () => {
left: scrollPosition.current.left,
renderContext: initialRenderContext,
});

if (rootProps.initialState?.scroll && scrollerRef.current) {
const scroller = scrollerRef.current;
const { top, left } = rootProps.initialState.scroll;

if (left > 0) {
scroller.scrollLeft = left;
ignoreNextScrollEvent.current = true;
}
romgrk marked this conversation as resolved.
Show resolved Hide resolved

const unsubscribeContentSizeChange = apiRef.current.subscribeEvent(
'virtualScrollerContentSizeChange',
() => {
if (scroller) {
scroller.scrollTop = top;
scroller.scrollLeft = left;
}
unsubscribeContentSizeChange();
},
);

return unsubscribeContentSizeChange;
}

return undefined;
});

apiRef.current.register('private', {
Expand Down
1 change: 1 addition & 0 deletions packages/x-data-grid/src/models/gridStateCommunity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,5 @@ export interface GridInitialStateCommunity {
columns?: GridColumnsInitialState;
preferencePanel?: GridPreferencePanelInitialState;
density?: GridDensityState;
scroll?: { top: number; left: number };
}
Loading