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

feat(ScrollSaver): Add component to save scroll position of elements with scroll box #6011

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
434 changes: 434 additions & 0 deletions packages/vkui/src/components/ScrollSaver/Readme.md

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as React from 'react';
import { usePatchChildrenRef } from '../../hooks/usePatchChildrenRef';

Check failure on line 2 in packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx

View workflow job for this annotation

GitHub Actions / Run linters

Cannot find module '../../hooks/usePatchChildrenRef' or its corresponding type declarations.
import { HasRootRef } from '../../types';
import { ScrollSaveModeType } from './types';
import { useScrollSaver } from './useScrollSaver';

export interface ScrollSaverProps {
/*
* Уникальный идентификатор элемента скролл которого надо запомнить.
* Важно задавать id, так как на одной панели может понадобится запомнить позиции нескольких скролл-боксов.
**/
id: string;
/*
* Режим сохранения скролла: по умолчанию `forward`.
* `forward` - позиция скролла сохраняется только при переходе вперёд и восстанавливается при переходе назад.
* `always` - позиция скролла сохраняется и при переходе вперёд и при переходе назад.
**/
saveMode?: ScrollSaveModeType;
/*
* Если передан реакт-компонент, то он должен поддерживать getRootRef.
**/
children: React.ReactElement<HasRootRef<any>> | React.ReactElement;
/*
* Режим для получения рефа на элемент для скролла через getRef проп, вместо getRootRef (актуально для компонента HorizontalScroll)
**/
useGetRef?: boolean;
}

/**
* Компонент-обертка для сохранения позиции скролла элемента при переходах между View и Panel.
* По умолчанию позволяет восстановить значение скролла при возвращении назад.
*
* @see https://vkcom.github.io/VKUI/#/ScrollSaver
*/
export const ScrollSaver = (props: ScrollSaverProps) => {
const [childrenRef, children] = usePatchChildrenRef(props.children, props.useGetRef);
useScrollSaver(childrenRef, props.id, props.saveMode);

return children;
};
34 changes: 34 additions & 0 deletions packages/vkui/src/components/ScrollSaver/ScrollSaverContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as React from 'react';

export type ScrollSaverCache = {
[key: string]: {
inlineStart: number;
blockStart: number;
};
};

export const ScrollSaverContext = React.createContext<React.MutableRefObject<ScrollSaverCache>>({
current: {},
});
export const ScrollSaverContextProvider = ({
value,
children,
}: React.PropsWithChildren<{ value?: ScrollSaverCache }>) => {
const valueRef = {
current: value || {},
};

return <ScrollSaverContext.Provider value={valueRef}>{children}</ScrollSaverContext.Provider>;
};

export const useScrollSaverCache = () => {
const contextCacheRef = React.useContext(ScrollSaverContext);
return contextCacheRef.current;
};

export const useClearScrollSaverCache = () => {
const cacheRef = React.useContext(ScrollSaverContext);
return () => {
cacheRef.current = {};
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ScrollSaverProps } from './ScrollSaver';
import { useScrollSaver } from './useScrollSaver';

export interface ScrollSaverWithCustomRefProps<T = HTMLElement>
extends Omit<ScrollSaverProps, 'useGetRef'> {
elementRef: React.RefObject<T>;
}

/* Компонентный Вариант useScrollSaver хука для динамического рендеринга, чтобы можно было пробросить и использовать любой ref
* children проп можно передать, но ref из него браться не будет */
export function ScrollSaverWithCustomRef(props: ScrollSaverWithCustomRefProps) {
useScrollSaver(props.elementRef, props.id, props.saveMode);

return props.children;
}
1 change: 1 addition & 0 deletions packages/vkui/src/components/ScrollSaver/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type ScrollSaveModeType = 'forward' | 'always';
66 changes: 66 additions & 0 deletions packages/vkui/src/components/ScrollSaver/useScrollSaver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as React from 'react';
import { useNavId } from '../../components/NavIdContext/useNavId';
import { useNavDirection } from '../../components/NavTransitionDirectionContext/NavTransitionDirectionContext';
import { useIsomorphicLayoutEffect } from '../../lib/useIsomorphicLayoutEffect';
import { useScrollSaverCache } from './ScrollSaverContext';
import { ScrollSaveModeType } from './types';

export function useScrollSaver<T extends HTMLElement>(
elementRef: React.RefObject<T>,
id: string,
saveMode: ScrollSaveModeType = 'forward',
) {
const uniqueId = useUniqueId(id);
const direction = useNavDirection();
const scrollSaverCache = useScrollSaverCache();

useIsomorphicLayoutEffect(
function handleScrollPosition() {
const scrollId = uniqueId;
const refNode = elementRef.current;

function restoreScrollPosition() {
if (!refNode) {
return;
}

const scrollPosition = scrollSaverCache[scrollId];
if (!scrollPosition) {
return;
}

const shouldRestoreMovingBackwards = direction === 'backwards' && saveMode === 'forward';
const shouldRestoreMovingForwards = saveMode === 'always';
const shouldRestore = shouldRestoreMovingBackwards || shouldRestoreMovingForwards;
if (!shouldRestore) {
return;
}

const { inlineStart, blockStart } = scrollSaverCache[scrollId];
refNode.scrollLeft = inlineStart;
refNode.scrollTop = blockStart;
}

restoreScrollPosition();

return function saveScrollPositionOnUnmount() {
if (!refNode) {
return;
}
scrollSaverCache[scrollId] = {
inlineStart: refNode.scrollLeft,
blockStart: refNode.scrollTop,
};
};
},
[direction, uniqueId, saveMode, elementRef],
);

return elementRef;
}

function useUniqueId(id: string) {
const { view: viewId, panel: panelId } = useNavId();
const uniqueId = `${viewId}-${panelId}-${id}`;
return uniqueId;
}
Loading
Loading