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

Restore List Scroll Position on Edit and Create Views side effects #9774

Merged
merged 10 commits into from Apr 16, 2024
14 changes: 13 additions & 1 deletion packages/ra-core/src/core/Resource.tsx
Expand Up @@ -5,6 +5,7 @@ import { isValidElementType } from 'react-is';

import { ResourceProps } from '../types';
import { ResourceContextProvider } from './ResourceContextProvider';
import { RestoreScrollPosition } from '../routing/RestoreScrollPosition';

export const Resource = (props: ResourceProps) => {
const { create, edit, list, name, show } = props;
Expand All @@ -17,7 +18,18 @@ export const Resource = (props: ResourceProps) => {
)}
{show && <Route path=":id/show/*" element={getElement(show)} />}
{edit && <Route path=":id/*" element={getElement(edit)} />}
{list && <Route path="/*" element={getElement(list)} />}
{list && (
<Route
path="/*"
element={
<RestoreScrollPosition
storeKey={`${name}.list.scrollPosition`}
>
{getElement(list)}
</RestoreScrollPosition>
}
/>
)}
{props.children}
</Routes>
</ResourceContextProvider>
Expand Down
30 changes: 30 additions & 0 deletions packages/ra-core/src/routing/RestoreScrollPosition.tsx
@@ -0,0 +1,30 @@
import { ReactNode } from 'react';
import { useRestoreScrollPosition } from './useRestoreScrollPosition';

/**
* A component that tracks the scroll position and restores it when the component mounts.
* @param children The content to render
* @param key The key under which to store the scroll position in the store
* @param debounceMs The debounce time in milliseconds
*
* @example
* import { ListBase, RestoreScrollPosition } from 'ra-core';
*
* const MyCustomList = (props) => (
* <RestoreScrollPosition key="my-list>
Copy link
Member

Choose a reason for hiding this comment

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

syntax error, missing quote

djhi marked this conversation as resolved.
Show resolved Hide resolved
* <ListBase {...props} />
* </RestoreScrollPosition>
* };
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
*/
export const RestoreScrollPosition = ({
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
children,
storeKey,
debounce = 250,
}: {
storeKey: string;
debounce?: number;
children: ReactNode;
}) => {
useRestoreScrollPosition(storeKey, debounce);
return children;
};
1 change: 1 addition & 0 deletions packages/ra-core/src/routing/index.ts
Expand Up @@ -6,5 +6,6 @@ export * from './useBasename';
export * from './useCreatePath';
export * from './useRedirect';
export * from './useScrollToTop';
export * from './useRestoreScrollPosition';
export * from './types';
export * from './TestMemoryRouter';
5 changes: 4 additions & 1 deletion packages/ra-core/src/routing/useRedirect.ts
Expand Up @@ -71,7 +71,10 @@ export const useRedirect = () => {
} else {
// redirection to an internal link
navigate(createPath({ resource, id, type: redirectTo }), {
state: { _scrollToTop: true, ...state },
state:
redirectTo === 'list'
? state
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
: { _scrollToTop: true, ...state },
});
return;
}
Expand Down
71 changes: 71 additions & 0 deletions packages/ra-core/src/routing/useRestoreScrollPosition.ts
@@ -0,0 +1,71 @@
import { useEffect } from 'react';
import { useStore } from '../store';
import { debounce } from 'lodash';
import { useLocation } from 'react-router';

/**
* A hook that tracks the scroll position and restores it when the component mounts.
* @param storeKey The key under which to store the scroll position in the store
* @param debounceMs The debounce time in milliseconds
*
* @example
* import { ListBase, useRestoreScrollPosition } from 'ra-core';
*
* const MyCustomList = (props) => {
* useRestoreScrollPosition('my-list');
* return <ListBase {...props} />;
* };
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
*/
export const useRestoreScrollPosition = (
storeKey: string,
debounceMs = 250
) => {
const [position, setPosition] = useTrackScrollPosition(
storeKey,
debounceMs
);
const location = useLocation();

useEffect(() => {
if (position != null && location.state?._scrollToTop !== true) {
setPosition(undefined);
window.scrollTo(0, position);
}
// We only want to run this effect on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
};

/**
* A hook that tracks the scroll position and stores it.
* @param storeKey The key under which to store the scroll position in the store
* @param debounceMs The debounce time in milliseconds
*
* @example
* import { ListBase, useTrackScrollPosition } from 'ra-core';
*
* const MyCustomList = (props) => {
* useTrackScrollPosition('my-list');
* return <ListBase {...props} />;
* };
*/
export const useTrackScrollPosition = (storeKey: string, debounceMs = 250) => {
const [position, setPosition] = useStore(storeKey);

useEffect(() => {
if (typeof window === 'undefined') {
return;
}
const handleScroll = debounce(() => {
setPosition(window.scrollY);
}, debounceMs);

window.addEventListener('scroll', handleScroll);

return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [debounceMs, setPosition]);

return [position, setPosition];
};