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
Original file line number Diff line number Diff line change
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
33 changes: 33 additions & 0 deletions packages/ra-core/src/routing/RestoreScrollPosition.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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 { RestoreScrollPosition } from 'ra-core';
*
* const MyCustomPage = () => {
* <RestoreScrollPosition key="my-list">
* <div>
* <h1>My Custom Page</h1>
* <VeryLongContent />
* </div>
* </RestoreScrollPosition>
* };
*/
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;
};
2 changes: 2 additions & 0 deletions packages/ra-core/src/routing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ export * from './AdminRouter';
export * from './BasenameContextProvider';
export * from './linkToRecord';
export * from './resolveRedirectTo';
export * from './RestoreScrollPosition';
export * from './useBasename';
export * from './useCreatePath';
export * from './useRedirect';
export * from './useScrollToTop';
export * from './useRestoreScrollPosition';
export * from './types';
export * from './TestMemoryRouter';
7 changes: 6 additions & 1 deletion packages/ra-core/src/routing/useRedirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,12 @@ export const useRedirect = () => {
} else {
// redirection to an internal link
navigate(createPath({ resource, id, type: redirectTo }), {
state: { _scrollToTop: true, ...state },
state:
// We force the scrollToTop except when navigating to a list
// where this is already done by <RestoreScrollPosition> in <Resource>
redirectTo === 'list'
? state
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
: { _scrollToTop: true, ...state },
});
return;
}
Expand Down
83 changes: 83 additions & 0 deletions packages/ra-core/src/routing/useRestoreScrollPosition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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 { useRestoreScrollPosition } from 'ra-core';
*
* const MyCustomPage = () => {
* useRestoreScrollPosition('my-list');
*
* return (
* <div>
* <h1>My Custom Page</h1>
* <VeryLongContent />
* </div>
* );
* };
*/
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 { useTrackScrollPosition } from 'ra-core';
*
* const MyCustomPage = () => {
* useTrackScrollPosition('my-list');
*
* return (
* <div>
* <h1>My Custom Page</h1>
* <VeryLongContent />
* </div>
* );
* };
*/
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];
};
Loading