Skip to content

Commit

Permalink
[DataGrid] Scroll restoration (#16208)
Browse files Browse the repository at this point in the history
Co-authored-by: Lauri <[email protected]>
  • Loading branch information
cherniavskii and lauri865 authored Jan 16, 2025
1 parent 3719061 commit fd668fb
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 9 deletions.
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 @@ The following demo explores the usage of this API:

{{"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.

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 @@ -136,11 +136,12 @@ const GridVirtualScrollbar = forwardRef<HTMLDivElement, GridVirtualScrollbarProp
useOnMount(() => {
const scroller = apiRef.current.virtualScrollerRef.current!;
const scrollbar = scrollbarRef.current!;
scroller.addEventListener('scroll', onScrollerScroll, { capture: true });
scrollbar.addEventListener('scroll', onScrollbarScroll, { capture: true });
const options = { capture: true, passive: true };
scroller.addEventListener('scroll', onScrollerScroll, options);
scrollbar.addEventListener('scroll', onScrollbarScroll, options);
return () => {
scroller.removeEventListener('scroll', onScrollerScroll, { capture: true });
scrollbar.removeEventListener('scroll', onScrollbarScroll, { capture: true });
scroller.removeEventListener('scroll', onScrollerScroll, options);
scrollbar.removeEventListener('scroll', onScrollbarScroll, options);
};
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,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 @@ -342,6 +343,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 @@ -589,9 +595,18 @@ export const useGridVirtualScroller = () => {
return size;
}, [columnsTotalWidth, contentHeight, needsHorizontalScrollbar]);

React.useEffect(() => {
apiRef.current.publishEvent('virtualScrollerContentSizeChange');
}, [apiRef, contentSize]);
const onContentSizeApplied = React.useCallback(
(node: HTMLDivElement | null) => {
if (!node) {
return;
}
apiRef.current.publishEvent('virtualScrollerContentSizeChange', {
columnsTotalWidth,
contentHeight,
});
},
[apiRef, columnsTotalWidth, contentHeight],
);

useEnhancedEffect(() => {
if (!isRenderContextReady.current) {
Expand Down Expand Up @@ -619,6 +634,55 @@ export const useGridVirtualScroller = () => {
});

isRenderContextReady.current = true;

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

// On initial mount, if we have columns available, we can restore the horizontal scroll immediately, but we need to skip the resulting scroll event, otherwise we would recalculate the render context at position top=0, left=restoredValue, but the initial render context is already calculated based on the initial value of scrollPosition ref.
const isScrollRestored = {
top: !(top > 0),
left: !(left > 0),
};
if (!isScrollRestored.left && columnsTotalWidth) {
scroller.scrollLeft = left;
ignoreNextScrollEvent.current = true;
isScrollRestored.left = true;
}

// For the sake of completeness, but I'm not sure if contentHeight is ever available at this point. Maybe when virtualisation is disabled?
if (!isScrollRestored.top && contentHeight) {
scroller.scrollTop = top;
ignoreNextScrollEvent.current = true;
isScrollRestored.top = true;
}

// To restore the vertical scroll, we need to wait until the rows are available in the DOM (otherwise there's nowhere to scroll), but before paint to avoid reflows
if (!isScrollRestored.top || !isScrollRestored.left) {
const unsubscribeContentSizeChange = apiRef.current.subscribeEvent(
'virtualScrollerContentSizeChange',
(params) => {
if (!isScrollRestored.left && params.columnsTotalWidth) {
scroller.scrollLeft = left;
ignoreNextScrollEvent.current = true;
isScrollRestored.left = true;
}
if (!isScrollRestored.top && params.contentHeight) {
scroller.scrollTop = top;
ignoreNextScrollEvent.current = true;
isScrollRestored.top = true;
}
if (isScrollRestored.left && isScrollRestored.top) {
unsubscribeContentSizeChange();
}
},
);

return unsubscribeContentSizeChange;
}
}

return undefined;
});

apiRef.current.register('private', {
Expand Down Expand Up @@ -650,6 +714,7 @@ export const useGridVirtualScroller = () => {
getContentProps: () => ({
style: contentSize,
role: 'presentation',
ref: onContentSizeApplied,
}),
getRenderZoneProps: () => ({ role: 'rowgroup' }),
getScrollbarVerticalProps: () => ({ ref: scrollbarVerticalRef, role: 'presentation' }),
Expand Down
7 changes: 6 additions & 1 deletion packages/x-data-grid/src/models/events/gridEventLookup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,12 @@ export interface GridEventLookup
* Fired when the content size used by the `GridVirtualScroller` changes.
* @ignore - do not document.
*/
virtualScrollerContentSizeChange: {};
virtualScrollerContentSizeChange: {
params: {
columnsTotalWidth: number;
contentHeight: number;
};
};
/**
* Fired when the content is scrolled by the mouse wheel.
* It's attached to the "mousewheel" event.
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 };
}

0 comments on commit fd668fb

Please sign in to comment.