From 96da754ca157fad6d5c13ce64d3037499c50275d Mon Sep 17 00:00:00 2001 From: Andrey Medvedev Date: Wed, 18 Oct 2023 17:26:29 +0300 Subject: [PATCH 01/10] Add ScrollSaver component-wrapper --- .../components/ScrollSaver/ScrollSaver.tsx | 94 +++++++++++++++++++ packages/vkui/src/index.ts | 1 + 2 files changed, 95 insertions(+) create mode 100644 packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx diff --git a/packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx b/packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx new file mode 100644 index 0000000000..6bd37a530d --- /dev/null +++ b/packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx @@ -0,0 +1,94 @@ +import * as React from 'react'; +import { useNavDirection } from '../../components/NavTransitionDirectionContext/NavTransitionDirectionContext'; +export { useNavId } from '../../components/NavIdContext/useNavId'; +import { usePatchChildrenRef } from '../../hooks/usePatchChildrenRef'; +import { useIsomorphicLayoutEffect } from '../../lib/useIsomorphicLayoutEffect'; +import { HasRootRef } from '../../types'; + +export interface ScrollSaverProps { + /* + * Уникальный идентификатор элемента скролл которого надо запомнить. + * Важно задавать id, так как на одной панели может понадобится запомнить позиции нескольких скролл-боксов. + **/ + id: string; + /* + * Если передан реакт-компонент, то он должен поддерживать getRootRef. + **/ + children: React.ReactElement> | React.ReactElement; + /* + * Режим для получения рефа на элемент для скролла через getRef проп, вместо getRootRef (актуально для компонента HorizontalScroll) + **/ + useGetRef?: boolean; + /* + * Режим сохранения скролла: по умолчанию `forward`. + * `forward` - позиция скролла сохраняется только при переходе вперёд и восстанавливается при переходе назад. + * `always` - позиция скролла сохраняется и при переходе вперёд и при переходе назад. + **/ + saveMode?: 'forward' | 'always'; +} + +let scrollSaverCache: { [key: string]: { inlineStart: number; blockStart: number } } = {}; + +/** + * Компонент-обертка для сохранения позиции скролла элемента при переходах между View и Panel. + * По умолчанию позволяет восстановить значение скролла при возвращении назад. + */ +export function ScrollSaver({ + id, + children: childrenProp, + saveMode = 'forward', + useGetRef, +}: ScrollSaverProps) { + const uniqueId = useUniqueId(id); + const [childrenRef, children] = usePatchChildrenRef(childrenProp, useGetRef); + const direction = useNavDirection(); + + useIsomorphicLayoutEffect( + function handleScrollPosition() { + const scrollId = uniqueId; + const childrenNode = childrenRef.current; + + function restoreScrollPosition() { + const scrollPosition = scrollSaverCache[scrollId]; + + const shouldRestoreMovingBackwards = direction === 'backwards' && saveMode === 'forward'; + const shouldRestoreMovingForwards = saveMode === 'always'; + const shouldRestore = shouldRestoreMovingBackwards || shouldRestoreMovingForwards; + if (!shouldRestore) { + // should I clean up the cache + return; + } + + if (!scrollPosition) { + return; + } + + const { inlineStart, blockStart } = scrollSaverCache[scrollId]; + if (inlineStart) { + childrenNode.scrollLeft = inlineStart; + } + if (blockStart) { + childrenNode.scrollTop = blockStart; + } + } + + restoreScrollPosition(); + + return function saveScrollPositionOnUnmount() { + scrollSaverCache[scrollId] = { + inlineStart: childrenNode.scrollLeft, + blockStart: childrenNode.scrollTop, + }; + }; + }, + [direction, uniqueId, saveMode, childrenRef], + ); + + return children; +} + +function useUniqueId(id: string) { + const { view: viewId, panel: panelId } = useNavId(); + const uniqueId = `${viewId}-${panelId}-${id}`; + return uniqueId; +} diff --git a/packages/vkui/src/index.ts b/packages/vkui/src/index.ts index d4ce796773..8ebd76a39f 100644 --- a/packages/vkui/src/index.ts +++ b/packages/vkui/src/index.ts @@ -343,6 +343,7 @@ export type { PopoverOnShownChange, PopoverContentRenderProp, } from './components/Popover/Popover'; +export { ScrollSaver } from './components/ScrollSaver/ScrollSaver'; /** * HOCs From 35445e77ce05096579947a3596381f6add067a14 Mon Sep 17 00:00:00 2001 From: Andrey Medvedev Date: Wed, 18 Oct 2023 17:28:02 +0300 Subject: [PATCH 02/10] Update View Readme to use ScrollSaver with HorizontalScroll --- packages/vkui/src/components/View/Readme.md | 50 +++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/vkui/src/components/View/Readme.md b/packages/vkui/src/components/View/Readme.md index ab57e5488d..bad35c1956 100644 --- a/packages/vkui/src/components/View/Readme.md +++ b/packages/vkui/src/components/View/Readme.md @@ -1,3 +1,4 @@ +<<<<<<< HEAD Базовый компонент для создания панелей. - В качестве `children` принимает коллекцию [Panel](#/Panel). У каждой [Panel](#/Panel) должен быть @@ -263,7 +264,49 @@ Xук возвращает правильное значение даже есл На третьем `View` пример со свайпом в iOS от левого края назад, где видно, что панель на которую идёт переход определяет его тип в самом начале свайпа. +======= +## [useNavDirection(): определение типа перехода (вперёд/назад), с которым была отрисована панель.](#/View?id=usenavdirection_example) + +>>>>>>> ffd276e66 (Update View Readme to use ScrollSaver with HorizontalScroll) ```jsx +const albumItems = [ + { + id: 1, + title: 'Команда <3', + size: 4, + thumb_src: 'https://sun9-33.userapi.com/ODk8khvW97c6aSx_MxHXhok5byDCsHEoU-3BwA/sO-lGf_NjN4.jpg', + }, + { + id: 2, + title: 'Зингер', + size: 22, + thumb_src: 'https://sun9-60.userapi.com/bjwt581hETPAp4oY92bDcRvMymyfCaEsnojaUA/_KWQfS-MAd4.jpg', + }, + { + id: 3, + title: 'Медиагалерея ВКонтакте', + size: 64, + thumb_src: 'https://sun9-26.userapi.com/YZ5-1A6cVgL7g1opJGQIWg1Bl5ynfPi8p41SkQ/IYIUDqGkkBE.jpg', + }, +]; + +const largeImageStyles = { + width: 220, + height: 124, + borderRadius: 4, + boxSizing: 'border-box', + border: 'var(--vkui--size_border--regular) solid var(--vkui--color_image_border_alpha)', + objectFit: 'cover', +}; + +const AlbumItems = () => { + return albumItems.map(({ id, title, size, thumb_src }) => ( + + + + )); +}; + const Content = () => { const direction = useNavDirection(); @@ -297,6 +340,13 @@ const Content = () => { : 'не определено'} {spinner} + + +
+ +
+
+
); }; From fcc65df3c1bc41e34567a934f88e6fbad7ed33d1 Mon Sep 17 00:00:00 2001 From: Andrey Medvedev Date: Thu, 19 Oct 2023 14:24:00 +0300 Subject: [PATCH 03/10] Add ScrollSaverContext with ability to clear the cache --- .../components/ScrollSaver/ScrollSaver.tsx | 5 ++- .../ScrollSaver/ScrollSaverContext.tsx | 34 +++++++++++++++++++ packages/vkui/src/index.ts | 6 ++++ 3 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 packages/vkui/src/components/ScrollSaver/ScrollSaverContext.tsx diff --git a/packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx b/packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx index 6bd37a530d..873c272fed 100644 --- a/packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx +++ b/packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx @@ -4,6 +4,7 @@ export { useNavId } from '../../components/NavIdContext/useNavId'; import { usePatchChildrenRef } from '../../hooks/usePatchChildrenRef'; import { useIsomorphicLayoutEffect } from '../../lib/useIsomorphicLayoutEffect'; import { HasRootRef } from '../../types'; +import { useScrollSaverCache } from './ScrollSaverContext'; export interface ScrollSaverProps { /* @@ -27,8 +28,6 @@ export interface ScrollSaverProps { saveMode?: 'forward' | 'always'; } -let scrollSaverCache: { [key: string]: { inlineStart: number; blockStart: number } } = {}; - /** * Компонент-обертка для сохранения позиции скролла элемента при переходах между View и Panel. * По умолчанию позволяет восстановить значение скролла при возвращении назад. @@ -42,6 +41,7 @@ export function ScrollSaver({ const uniqueId = useUniqueId(id); const [childrenRef, children] = usePatchChildrenRef(childrenProp, useGetRef); const direction = useNavDirection(); + const scrollSaverCache = useScrollSaverCache(); useIsomorphicLayoutEffect( function handleScrollPosition() { @@ -55,7 +55,6 @@ export function ScrollSaver({ const shouldRestoreMovingForwards = saveMode === 'always'; const shouldRestore = shouldRestoreMovingBackwards || shouldRestoreMovingForwards; if (!shouldRestore) { - // should I clean up the cache return; } diff --git a/packages/vkui/src/components/ScrollSaver/ScrollSaverContext.tsx b/packages/vkui/src/components/ScrollSaver/ScrollSaverContext.tsx new file mode 100644 index 0000000000..d5b3473050 --- /dev/null +++ b/packages/vkui/src/components/ScrollSaver/ScrollSaverContext.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; + +export type ScrollSaverCache = { + [key: string]: { + inlineStart: number; + blockStart: number; + }; +}; + +export const ScrollSaverContext = React.createContext>({ + current: {}, +}); +export const ScrollSaverContextProvider = ({ + value, + children, +}: React.PropsWithChildren<{ value?: ScrollSaverCache }>) => { + const valueRef = { + current: value || {}, + }; + + return {children}; +}; + +export const useScrollSaverCache = () => { + const contextCacheRef = React.useContext(ScrollSaverContext); + return contextCacheRef.current; +}; + +export const useClearScrollSaverCache = () => { + const cacheRef = React.useContext(ScrollSaverContext); + return () => { + cacheRef.current = {}; + }; +}; diff --git a/packages/vkui/src/index.ts b/packages/vkui/src/index.ts index 8ebd76a39f..857308cd54 100644 --- a/packages/vkui/src/index.ts +++ b/packages/vkui/src/index.ts @@ -344,6 +344,12 @@ export type { PopoverContentRenderProp, } from './components/Popover/Popover'; export { ScrollSaver } from './components/ScrollSaver/ScrollSaver'; +export { + ScrollSaverContextProvider, + useScrollSaverCache, + useClearScrollSaverCache, + type ScrollSaverCache, +} from './components/ScrollSaver/ScrollSaverContext'; /** * HOCs From 72eddf8847b093b6de6d2887214a98cf12679e23 Mon Sep 17 00:00:00 2001 From: Andrey Medvedev Date: Thu, 19 Oct 2023 17:36:26 +0300 Subject: [PATCH 04/10] Split ScrollSaver to components ScrollSaver is a component-wrapper which expects children and search for the ref in the children props ScrollSaverWithoutChildren is a component that receives ref from the outside as elementRef prop to support dynamic scroll control. useScrollSaver is a hook that allows to use scroll saver using hook. Expects ref as a prop to control scroll position --- .../components/ScrollSaver/ScrollSaver.tsx | 77 ++++++++++++------- 1 file changed, 51 insertions(+), 26 deletions(-) diff --git a/packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx b/packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx index 873c272fed..2af7dc9871 100644 --- a/packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx +++ b/packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx @@ -6,49 +6,37 @@ import { useIsomorphicLayoutEffect } from '../../lib/useIsomorphicLayoutEffect'; import { HasRootRef } from '../../types'; import { useScrollSaverCache } from './ScrollSaverContext'; -export interface ScrollSaverProps { +interface ScrollSaverHookProps { /* * Уникальный идентификатор элемента скролл которого надо запомнить. * Важно задавать id, так как на одной панели может понадобится запомнить позиции нескольких скролл-боксов. **/ id: string; - /* - * Если передан реакт-компонент, то он должен поддерживать getRootRef. - **/ - children: React.ReactElement> | React.ReactElement; - /* - * Режим для получения рефа на элемент для скролла через getRef проп, вместо getRootRef (актуально для компонента HorizontalScroll) - **/ - useGetRef?: boolean; /* * Режим сохранения скролла: по умолчанию `forward`. * `forward` - позиция скролла сохраняется только при переходе вперёд и восстанавливается при переходе назад. * `always` - позиция скролла сохраняется и при переходе вперёд и при переходе назад. **/ saveMode?: 'forward' | 'always'; + /* Ref элемента, скроллом которого надо управлять */ + ref: React.RefObject; } -/** - * Компонент-обертка для сохранения позиции скролла элемента при переходах между View и Panel. - * По умолчанию позволяет восстановить значение скролла при возвращении назад. - */ -export function ScrollSaver({ - id, - children: childrenProp, - saveMode = 'forward', - useGetRef, -}: ScrollSaverProps) { +function useScrollSaver({ ref, id, saveMode = 'forward' }: ScrollSaverHookProps) { const uniqueId = useUniqueId(id); - const [childrenRef, children] = usePatchChildrenRef(childrenProp, useGetRef); const direction = useNavDirection(); const scrollSaverCache = useScrollSaverCache(); useIsomorphicLayoutEffect( function handleScrollPosition() { const scrollId = uniqueId; - const childrenNode = childrenRef.current; + const refNode = ref.current; function restoreScrollPosition() { + if (!refNode) { + return; + } + const scrollPosition = scrollSaverCache[scrollId]; const shouldRestoreMovingBackwards = direction === 'backwards' && saveMode === 'forward'; @@ -64,28 +52,65 @@ export function ScrollSaver({ const { inlineStart, blockStart } = scrollSaverCache[scrollId]; if (inlineStart) { - childrenNode.scrollLeft = inlineStart; + refNode.scrollLeft = inlineStart; } if (blockStart) { - childrenNode.scrollTop = blockStart; + refNode.scrollTop = blockStart; } } restoreScrollPosition(); return function saveScrollPositionOnUnmount() { + if (!refNode) { + return; + } scrollSaverCache[scrollId] = { - inlineStart: childrenNode.scrollLeft, - blockStart: childrenNode.scrollTop, + inlineStart: refNode.scrollLeft, + blockStart: refNode.scrollTop, }; }; }, - [direction, uniqueId, saveMode, childrenRef], + [direction, uniqueId, saveMode, ref], ); + return ref; +} + +interface ScrollSaverProps extends Omit { + /* + * Если передан реакт-компонент, то он должен поддерживать getRootRef. + **/ + children: React.ReactElement> | React.ReactElement; + /* + * Режим для получения рефа на элемент для скролла через getRef проп, вместо getRootRef (актуально для компонента HorizontalScroll) + **/ + useGetRef?: boolean; +} + +/** + * Компонент-обертка для сохранения позиции скролла элемента при переходах между View и Panel. + * По умолчанию позволяет восстановить значение скролла при возвращении назад. + */ +export function ScrollSaver(props: ScrollSaverProps) { + const [childrenRef, children] = usePatchChildrenRef(props.children, props.useGetRef); + useScrollSaver({ ref: childrenRef, id: props.id, saveMode: props.saveMode }); + return children; } +interface ScrollSaverWithoutChildren extends Omit { + elementRef: React.RefObject; + children?: React.ReactElement | null; +} + +/* Компонентный Вариант useScrollSaver хука для динамического рендеринга, чтобы можно было пробросить и использовать любой ref */ +export function ScrollSaverWithoutChildren(props: ScrollSaverWithoutChildren) { + useScrollSaver({ ref: props.elementRef, id: props.id, saveMode: props.saveMode }); + + return props.children; +} + function useUniqueId(id: string) { const { view: viewId, panel: panelId } = useNavId(); const uniqueId = `${viewId}-${panelId}-${id}`; From dafe33c50a17b1fdfa4b3d46cbb4c06f1e8f3b99 Mon Sep 17 00:00:00 2001 From: Andrey Medvedev Date: Fri, 20 Oct 2023 14:52:45 +0300 Subject: [PATCH 05/10] Shake ScrolSaver types Use arguments in the hook --- .../components/ScrollSaver/ScrollSaver.tsx | 83 +++++++++---------- packages/vkui/src/index.ts | 6 +- 2 files changed, 44 insertions(+), 45 deletions(-) diff --git a/packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx b/packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx index 2af7dc9871..cca70b417f 100644 --- a/packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx +++ b/packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx @@ -6,7 +6,7 @@ import { useIsomorphicLayoutEffect } from '../../lib/useIsomorphicLayoutEffect'; import { HasRootRef } from '../../types'; import { useScrollSaverCache } from './ScrollSaverContext'; -interface ScrollSaverHookProps { +interface ScrollSaverProps { /* * Уникальный идентификатор элемента скролл которого надо запомнить. * Важно задавать id, так как на одной панели может понадобится запомнить позиции нескольких скролл-боксов. @@ -18,11 +18,32 @@ interface ScrollSaverHookProps { * `always` - позиция скролла сохраняется и при переходе вперёд и при переходе назад. **/ saveMode?: 'forward' | 'always'; - /* Ref элемента, скроллом которого надо управлять */ - ref: React.RefObject; + /* + * Если передан реакт-компонент, то он должен поддерживать getRootRef. + **/ + children: React.ReactElement> | React.ReactElement; + /* + * Режим для получения рефа на элемент для скролла через getRef проп, вместо getRootRef (актуально для компонента HorizontalScroll) + **/ + useGetRef?: boolean; +} + +/** + * Компонент-обертка для сохранения позиции скролла элемента при переходах между View и Panel. + * По умолчанию позволяет восстановить значение скролла при возвращении назад. + */ +export function ScrollSaver(props: ScrollSaverProps) { + const [childrenRef, children] = usePatchChildrenRef(props.children, props.useGetRef); + useScrollSaver(childrenRef, props.id, props.saveMode); + + return children; } -function useScrollSaver({ ref, id, saveMode = 'forward' }: ScrollSaverHookProps) { +export function useScrollSaver( + elementRef: React.RefObject, + id: ScrollSaverProps['id'], + saveMode: ScrollSaverProps['saveMode'] = 'forward', +) { const uniqueId = useUniqueId(id); const direction = useNavDirection(); const scrollSaverCache = useScrollSaverCache(); @@ -30,7 +51,7 @@ function useScrollSaver({ ref, id, saveMode = 'forward' }: ScrollSaverHookProps) useIsomorphicLayoutEffect( function handleScrollPosition() { const scrollId = uniqueId; - const refNode = ref.current; + const refNode = elementRef.current; function restoreScrollPosition() { if (!refNode) { @@ -38,6 +59,9 @@ function useScrollSaver({ ref, id, saveMode = 'forward' }: ScrollSaverHookProps) } const scrollPosition = scrollSaverCache[scrollId]; + if (!scrollPosition) { + return; + } const shouldRestoreMovingBackwards = direction === 'backwards' && saveMode === 'forward'; const shouldRestoreMovingForwards = saveMode === 'always'; @@ -46,17 +70,9 @@ function useScrollSaver({ ref, id, saveMode = 'forward' }: ScrollSaverHookProps) return; } - if (!scrollPosition) { - return; - } - const { inlineStart, blockStart } = scrollSaverCache[scrollId]; - if (inlineStart) { - refNode.scrollLeft = inlineStart; - } - if (blockStart) { - refNode.scrollTop = blockStart; - } + refNode.scrollLeft = inlineStart; + refNode.scrollTop = blockStart; } restoreScrollPosition(); @@ -71,42 +87,21 @@ function useScrollSaver({ ref, id, saveMode = 'forward' }: ScrollSaverHookProps) }; }; }, - [direction, uniqueId, saveMode, ref], + [direction, uniqueId, saveMode, elementRef], ); - return ref; -} - -interface ScrollSaverProps extends Omit { - /* - * Если передан реакт-компонент, то он должен поддерживать getRootRef. - **/ - children: React.ReactElement> | React.ReactElement; - /* - * Режим для получения рефа на элемент для скролла через getRef проп, вместо getRootRef (актуально для компонента HorizontalScroll) - **/ - useGetRef?: boolean; -} - -/** - * Компонент-обертка для сохранения позиции скролла элемента при переходах между View и Panel. - * По умолчанию позволяет восстановить значение скролла при возвращении назад. - */ -export function ScrollSaver(props: ScrollSaverProps) { - const [childrenRef, children] = usePatchChildrenRef(props.children, props.useGetRef); - useScrollSaver({ ref: childrenRef, id: props.id, saveMode: props.saveMode }); - - return children; + return elementRef; } -interface ScrollSaverWithoutChildren extends Omit { +interface ScrollSaverWithoutChildrenProps + extends Omit { elementRef: React.RefObject; - children?: React.ReactElement | null; } -/* Компонентный Вариант useScrollSaver хука для динамического рендеринга, чтобы можно было пробросить и использовать любой ref */ -export function ScrollSaverWithoutChildren(props: ScrollSaverWithoutChildren) { - useScrollSaver({ ref: props.elementRef, id: props.id, saveMode: props.saveMode }); +/* Компонентный Вариант useScrollSaver хука для динамического рендеринга, чтобы можно было пробросить и использовать любой ref + * children проп можно передать, но ref из него браться не будет */ +export function ScrollSaverWithoutChildren(props: ScrollSaverWithoutChildrenProps) { + useScrollSaver(props.elementRef, props.id, props.saveMode); return props.children; } diff --git a/packages/vkui/src/index.ts b/packages/vkui/src/index.ts index 857308cd54..0783887753 100644 --- a/packages/vkui/src/index.ts +++ b/packages/vkui/src/index.ts @@ -343,7 +343,11 @@ export type { PopoverOnShownChange, PopoverContentRenderProp, } from './components/Popover/Popover'; -export { ScrollSaver } from './components/ScrollSaver/ScrollSaver'; +export { + ScrollSaver, + ScrollSaverWithoutChildren, + useScrollSaver, +} from './components/ScrollSaver/ScrollSaver'; export { ScrollSaverContextProvider, useScrollSaverCache, From 8d4c33134a8bbade0d8e875d0b460414a3ab4d87 Mon Sep 17 00:00:00 2001 From: Andrey Medvedev Date: Fri, 20 Oct 2023 15:15:18 +0300 Subject: [PATCH 06/10] Update Readme to show only ScrollSaver Example --- packages/vkui/src/components/View/Readme.md | 76 ++++++++++++++++++++- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/packages/vkui/src/components/View/Readme.md b/packages/vkui/src/components/View/Readme.md index bad35c1956..f8ae291f2e 100644 --- a/packages/vkui/src/components/View/Readme.md +++ b/packages/vkui/src/components/View/Readme.md @@ -1,4 +1,5 @@ <<<<<<< HEAD +<<<<<<< HEAD Базовый компонент для создания панелей. - В качестве `children` принимает коллекцию [Panel](#/Panel). У каждой [Panel](#/Panel) должен быть @@ -266,6 +267,13 @@ Xук возвращает правильное значение даже есл ======= ## [useNavDirection(): определение типа перехода (вперёд/назад), с которым была отрисована панель.](#/View?id=usenavdirection_example) +======= +В примере ниже на View1 и View2 сохраняются позиции скролла HorizontalScroll используя ScrollSaver, useScrollSaver и ScrollSaverWithoutChildren. +Позиция сохраняется как при переходе между View так и при переходе между Panel +При возврате на View1 c других View мы сбрасываем весь кэш ScrollSaver. + +ScrollSaverWtihoutChildren - это версия useScrollSaver обёрнутая в компонент для динамического рендеринга, но она требудет преедачи пропа elementRef. +>>>>>>> cffef2295 (Update Readme to show only ScrollSaver Example) >>>>>>> ffd276e66 (Update View Readme to use ScrollSaver with HorizontalScroll) ```jsx @@ -340,8 +348,20 @@ const Content = () => { : 'не определено'} {spinner} - - + + ); +}; + +const ContentWithScrollSaverComponent = () => { + const horizontalScrollRef = useRef(); + + return ( +
+ + With ScrollSaver component + + +
@@ -351,6 +371,44 @@ const Content = () => { ); }; +const ContentWithScrollSaverWithoutChildren = () => { + const horizontalScrollRef = useRef(); + + const horizontalRef = useRef(); + return ( +
+ + With ScrollSaverWithoutChildren - doesn't look for children ref + + + +
+ +
+
+
+
+ ); +}; + +const ContentWithScrollSaverHook = () => { + const horizontalScrollRef = useRef(); + useScrollSaver({ elementRef: horizontalScrollRef, id: 'horizontal-scroll-saver-hook' }); + + return ( +
+ + With ScrollSaverHook + + +
+ +
+
+
+ ); +}; + const Example = () => { const [activeView, setActiveView] = useState('view1'); const [activePanel, setActivePanel] = useState(1); @@ -378,8 +436,14 @@ const Example = () => { [pushSwipeViewHistory, activeView], ); + const cache = useScrollSaverCache(); + const clearScrollCache = useClearScrollSaverCache(); handleActiveViewSet = React.useCallback( (view) => { + if (view === 'view1') { + clearScrollCache(); + } + console.log(cache); if (view === 'swipeView') { const defaultSwipeViewActivePanel = 1; setSwipeViewHistory([`swipeView.${defaultSwipeViewActivePanel}`]); @@ -387,7 +451,7 @@ const Example = () => { } setActiveView(view); }, - [activePanel], + [activePanel, clearScrollCache, cache], ); const navigationButtons = ( @@ -410,16 +474,19 @@ const Example = () => { Панель 1.1 {navigationButtons} + Панель 1.2 {navigationButtons} + Панель 1.3 {navigationButtons} + @@ -427,16 +494,19 @@ const Example = () => { Панель 2.1 {navigationButtons} + Панель 2.2 {navigationButtons} + Панель 2.3 {navigationButtons} + Date: Fri, 20 Oct 2023 15:37:09 +0300 Subject: [PATCH 07/10] Fix issues in example --- packages/vkui/src/components/View/Readme.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/vkui/src/components/View/Readme.md b/packages/vkui/src/components/View/Readme.md index f8ae291f2e..f960cc9dff 100644 --- a/packages/vkui/src/components/View/Readme.md +++ b/packages/vkui/src/components/View/Readme.md @@ -372,28 +372,26 @@ const ContentWithScrollSaverComponent = () => { }; const ContentWithScrollSaverWithoutChildren = () => { - const horizontalScrollRef = useRef(); - const horizontalRef = useRef(); return (
With ScrollSaverWithoutChildren - doesn't look for children ref - - + +
-
+
); }; const ContentWithScrollSaverHook = () => { const horizontalScrollRef = useRef(); - useScrollSaver({ elementRef: horizontalScrollRef, id: 'horizontal-scroll-saver-hook' }); + useScrollSaver(horizontalScrollRef, 'horizontal-scroll-saver-hook'); return (
From 57f7af3df3936f7a74ee4bd14e17c53f87786d2c Mon Sep 17 00:00:00 2001 From: Andrey Medvedev Date: Fri, 20 Oct 2023 16:48:05 +0300 Subject: [PATCH 08/10] Fix import/export of hook --- packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx b/packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx index cca70b417f..8449741fef 100644 --- a/packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx +++ b/packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; +import { useNavId } from '../../components/NavIdContext/useNavId'; import { useNavDirection } from '../../components/NavTransitionDirectionContext/NavTransitionDirectionContext'; -export { useNavId } from '../../components/NavIdContext/useNavId'; import { usePatchChildrenRef } from '../../hooks/usePatchChildrenRef'; import { useIsomorphicLayoutEffect } from '../../lib/useIsomorphicLayoutEffect'; import { HasRootRef } from '../../types'; From 0b3dc0bcfcab4f8e07064af82ca7ad04d3b504e0 Mon Sep 17 00:00:00 2001 From: Andrey Medvedev Date: Mon, 23 Oct 2023 17:31:12 +0300 Subject: [PATCH 09/10] Add ScrollSaver doc --- .../vkui/src/components/ScrollSaver/Readme.md | 434 ++++++++++++++++++ .../components/ScrollSaver/ScrollSaver.tsx | 2 + packages/vkui/src/components/View/Readme.md | 352 ++++++++++---- styleguide/config.js | 1 + 4 files changed, 687 insertions(+), 102 deletions(-) create mode 100644 packages/vkui/src/components/ScrollSaver/Readme.md diff --git a/packages/vkui/src/components/ScrollSaver/Readme.md b/packages/vkui/src/components/ScrollSaver/Readme.md new file mode 100644 index 0000000000..969f4ee047 --- /dev/null +++ b/packages/vkui/src/components/ScrollSaver/Readme.md @@ -0,0 +1,434 @@ +Компонент-обертка для сохранения позиции скролла элемента при переходах между View и Panel. +По умолчанию позволяет восстановить значение скролла при возвращении назад. + +
+ +Примеры использования **ScrollSaver**: + +- С компонентом Div. + +```jsx static + +
+ +
+
+``` + +- С компонентом HorizontalScroll требуется задать проп withGetRef, потому что область скролла HorizontalScroll доступна не через getRootRef, а через getRef. + +```jsx static + + +
+ +
+
+
+``` + +- ScrollSaverWtihoutChildren берёт ref не из children а из пропа elementRef. + +```jsx static + + +
+ +
+
+
+``` + +- useScrollSaver хук + +```jsx static +const ContentWithScrollSaverHook = () => { + const horizontalScrollRef = useRef(); + useScrollSaver(horizontalScrollRef, 'horizontal-scroll-saver-hook'); + + return ( +
+ + With ScrollSaverHook + + +
+ +
+
+
+ ); +}; +``` + +
+ +## Хук useClearScrollSaverCache() позволяет получить функцию глобальной очистки кэша ScrollSaver. + +```jsx static +const clearScrollCache = useClearScrollSaverCache(); +``` + +
+ +В примере ниже на View1 и View2 сохраняются позиции скролла HorizontalScroll используя **ScrollSaver**, **useScrollSaver** и **ScrollSaverWithoutChildren**. +Позиция сохраняется как при переходе между View так и при переходе между Panel +При возврате на View1 c других View мы сбрасываем весь кэш ScrollSaver. + +```jsx +const albumItems = [ + { + id: 1, + title: 'Команда <3', + size: 4, + thumb_src: 'https://sun9-33.userapi.com/ODk8khvW97c6aSx_MxHXhok5byDCsHEoU-3BwA/sO-lGf_NjN4.jpg', + }, + { + id: 2, + title: 'Зингер', + size: 22, + thumb_src: 'https://sun9-60.userapi.com/bjwt581hETPAp4oY92bDcRvMymyfCaEsnojaUA/_KWQfS-MAd4.jpg', + }, + { + id: 3, + title: 'Медиагалерея ВКонтакте', + size: 64, + thumb_src: 'https://sun9-26.userapi.com/YZ5-1A6cVgL7g1opJGQIWg1Bl5ynfPi8p41SkQ/IYIUDqGkkBE.jpg', + }, +]; + +const largeImageStyles = { + width: 220, + height: 124, + borderRadius: 4, + boxSizing: 'border-box', + border: 'var(--vkui--size_border--regular) solid var(--vkui--color_image_border_alpha)', + objectFit: 'cover', +}; + +const AlbumItems = () => { + return albumItems.map(({ id, title, size, thumb_src }) => ( + + + + )); +}; + +const Content = () => { + const direction = useNavDirection(); + + const [spinner, setSpinner] = useState(null); + + React.useEffect( + function simulateDataLoadingWhenMovingForwards() { + let timerId = null; + const loadData = () => { + setSpinner(); + timerId = setTimeout(() => setSpinner(null), 1000); + }; + + if (direction !== 'backwards') { + loadData(); + } + + return () => clearTimeout(timerId); + }, + [direction], + ); + + return ( +
+ + Направление перехода:{' '} + {direction === 'forwards' + ? 'вперёд' + : direction === 'backwards' + ? 'назад' + : 'не определено'} + + {spinner} +
+ ); +}; + +const ContentWithScrollSaverComponent = () => { + return ( +
+ + With ScrollSaver component + + + +
+ +
+
+
+
+ ); +}; + +const ContentWithScrollSaverWithoutChildren = () => { + const horizontalRef = useRef(); + return ( +
+ + With ScrollSaverWithoutChildren - doesn't look for children ref + + + +
+ +
+
+
+
+ ); +}; + +const ContentWithScrollSaverHook = () => { + const horizontalScrollRef = useRef(); + useScrollSaver(horizontalScrollRef, 'horizontal-scroll-saver-hook'); + + return ( +
+ + With ScrollSaverHook + + +
+ +
+
+
+ ); +}; + +const Example = () => { + const [activeView, setActiveView] = useState('view1'); + const [activePanel, setActivePanel] = useState(1); + + const [swipeViewHistory, setSwipeViewHistory] = useState([`swipeView.${activePanel}`]); + const pushSwipeViewHistory = React.useCallback((panel) => { + setSwipeViewHistory((prevHistory) => [...prevHistory, `swipeView.${panel}`]); + }, []); + const onSwipeBack = React.useCallback(() => { + const newHistory = swipeViewHistory.slice(0, -1); + setSwipeViewHistory(newHistory); + + const newActiveSwipeViewPanel = newHistory[newHistory.length - 1]; + const swipeViewPanel = +newActiveSwipeViewPanel.split('swipeView.')[1]; + setActivePanel(swipeViewPanel); + }, [swipeViewHistory]); + + handleActivePanelSet = React.useCallback( + (panel) => { + setActivePanel(panel); + if (activeView === 'swipeView') { + pushSwipeViewHistory(panel); + } + }, + [pushSwipeViewHistory, activeView], + ); + + const cache = useScrollSaverCache(); + const clearScrollCache = useClearScrollSaverCache(); + handleActiveViewSet = React.useCallback( + (view) => { + if (view === 'view1') { + clearScrollCache(); + } + console.log(cache); + if (view === 'swipeView') { + const defaultSwipeViewActivePanel = 1; + setSwipeViewHistory([`swipeView.${defaultSwipeViewActivePanel}`]); + setActivePanel(defaultSwipeViewActivePanel); + } + setActiveView(view); + }, + [activePanel, clearScrollCache, cache], + ); + + const navigationButtons = ( + + ); + + const swipeViewActivePanel = swipeViewHistory[swipeViewHistory.length - 1]; + return ( + + + + + + + Панель 1.1 + {navigationButtons} + + + + + Панель 1.2 + {navigationButtons} + + + + + Панель 1.3 + {navigationButtons} + + + + + + + Панель 2.1 + {navigationButtons} + + + + + Панель 2.2 + {navigationButtons} + + + + + Панель 2.3 + {navigationButtons} + + + + + + + П.1 iOS Swipe Back + {navigationButtons} + + + + П.2 iOS Swipe Back + {navigationButtons} + + + + П.3 iOS Swipe Back + {navigationButtons} + + + + + + + + ); +}; + +function NavigationButtons({ activePanel, activeView, setActiveView, setActivePanel }) { + return ( + <> + + {activeView === 'view1' ? ( + <> + Перейти на View 1 + setActiveView('view2')}>Перейти на View 2 + setActiveView('swipeView')}> + Перейти на пример с iOS Swipe Back + + + ) : activeView === 'view2' ? ( + <> + setActiveView('view1')}>Назад View 1 + Перейти на View 2 + setActiveView('swipeView')}> + Перейти на пример с iOS Swipe Back + + + ) : ( + activeView === 'swipeView' && ( + <> + setActiveView('view1')}>Назад на View 1 + setActiveView('view2')}>Назад на View 2 + Перейти на пример с iOS Swipe Back + + ) + )} + + + + + {activeView === 'view1' ? ( + <> + {Array.from({ length: 3 }, (_, index) => ( + + ))} + + ) : activeView === 'view2' ? ( + <> + {Array.from({ length: 3 }, (_, index) => ( + + ))} + + ) : ( + activeView === 'swipeView' && ( + + ) + )} + + + + + + ); +} + +function PanelNavigationButton({ activePanel, viewNumber, panelNumber, setActivePanel }) { + const goOrBack = activePanel <= panelNumber ? 'Перейти' : 'Назад'; + return ( + setActivePanel(panelNumber)}> + {goOrBack} на панель {viewNumber}.{panelNumber} + + ); +} + +function SwipeViewNavigationButton({ activePanel, setActivePanel }) { + return ( + <> + {activePanel === 1 && ( + Перейдите на панель 2 чтобы можно было свайпнуть назад + )} + {activePanel > 1 && ( + Теперь свайпните от левого края направо, чтобы вернуться + )} + {activePanel < 3 && ( + setActivePanel(activePanel + 1)}> + Перейти на панель {activePanel + 1} + + )} + + ); +} + +; +``` diff --git a/packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx b/packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx index 8449741fef..578531a734 100644 --- a/packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx +++ b/packages/vkui/src/components/ScrollSaver/ScrollSaver.tsx @@ -31,6 +31,8 @@ interface ScrollSaverProps { /** * Компонент-обертка для сохранения позиции скролла элемента при переходах между View и Panel. * По умолчанию позволяет восстановить значение скролла при возвращении назад. + * + * @see https://vkcom.github.io/VKUI/#/ScrollSaver */ export function ScrollSaver(props: ScrollSaverProps) { const [childrenRef, children] = usePatchChildrenRef(props.children, props.useGetRef); diff --git a/packages/vkui/src/components/View/Readme.md b/packages/vkui/src/components/View/Readme.md index f960cc9dff..2d7d5ac7e8 100644 --- a/packages/vkui/src/components/View/Readme.md +++ b/packages/vkui/src/components/View/Readme.md @@ -1,5 +1,6 @@ <<<<<<< HEAD <<<<<<< HEAD +<<<<<<< HEAD Базовый компонент для создания панелей. - В качестве `children` принимает коллекцию [Panel](#/Panel). У каждой [Panel](#/Panel) должен быть @@ -274,47 +275,261 @@ Xук возвращает правильное значение даже есл ScrollSaverWtihoutChildren - это версия useScrollSaver обёрнутая в компонент для динамического рендеринга, но она требудет преедачи пропа elementRef. >>>>>>> cffef2295 (Update Readme to show only ScrollSaver Example) +======= +Базовый компонент для создания панелей. В качестве `children` принимает коллекцию `Panel`. +У каждой `Panel` должен быть уникальный `id`. Свойство `activePanel` определяет какая `Panel` активна. + +При смене значения свойства `activePanel` происходит плавный переход от одной панели к другой. +Как только он заканчивается, вызывается свойство-функция `onTransition`. + +Чтобы понять был это переход вперёд или назад можно воспользоваться хуком [`useNavDirection()`](#/View?id=usenavdirection_example). Этот хук работает даже если анимации выключены (``). +>>>>>>> af7abaa36 (Add ScrollSaver doc) >>>>>>> ffd276e66 (Update View Readme to use ScrollSaver with HorizontalScroll) ```jsx -const albumItems = [ - { - id: 1, - title: 'Команда <3', - size: 4, - thumb_src: 'https://sun9-33.userapi.com/ODk8khvW97c6aSx_MxHXhok5byDCsHEoU-3BwA/sO-lGf_NjN4.jpg', - }, - { - id: 2, - title: 'Зингер', - size: 22, - thumb_src: 'https://sun9-60.userapi.com/bjwt581hETPAp4oY92bDcRvMymyfCaEsnojaUA/_KWQfS-MAd4.jpg', - }, - { - id: 3, - title: 'Медиагалерея ВКонтакте', - size: 64, - thumb_src: 'https://sun9-26.userapi.com/YZ5-1A6cVgL7g1opJGQIWg1Bl5ynfPi8p41SkQ/IYIUDqGkkBE.jpg', - }, -]; - -const largeImageStyles = { - width: 220, - height: 124, - borderRadius: 4, - boxSizing: 'border-box', - border: 'var(--vkui--size_border--regular) solid var(--vkui--color_image_border_alpha)', - objectFit: 'cover', +const [activePanel, setActivePanel] = useState('panel1'); + + + + Panel 1 + +
+ setActivePanel('panel2')}>Go to panel 2 +
+ + + + Panel 2 + +
+ setActivePanel('panel3')}>Go to panel 3 +
+ + + + Panel 3 + +
+ setActivePanel('panel1')}>Back to panel 1 +
+ + +; +``` + +
+ +## [iOS Swipe Back](https://vkcom.github.io/VKUI/#/View?id=iosswipeback) + +В iOS есть возможность свайпнуть от левого края назад, чтобы перейти на предыдущую панель. Для того, чтобы +повторить такое поведение в VKUI, нужно: + +- Передать во `View` коллбек `onSwipeBack` — он сработает при завершении анимации свайпа. Поменяйте в нем `activePanel` и обновите `history`. +- Передать во `View` проп `history` — массив из id панелей в порядке открытия. Например, если пользователь из `main` перешел в `profile`, а оттуда попал в `education`, то `history=['main', 'profile', 'education']`. +- Обернуть ваше приложение в `ConfigProvider` — он определит, открыто приложение в webview клиента VK или в браузере (там есть свой swipe back, который будет конфликтовать с нашим). Для проверки в браузере форсируйте определение webview: ``. + +**Блокировка свайпа (вариант #1)** + +Компоненты, которые сами обрабатывают жесты (например, карта или кастомный компонент по типу карусели), могут конфликтовать со свайпбеком. Вот как можно это решить: + +- либо повесьте на них свойство `data-vkui-swipe-back={false}`; +- либо вызывайте `event.stopPropagation()` на событие `onStartX` компонента [Touch](#/Touch). + +
+ +**Блокировка свайпа (вариант #2)** + +Для блокирования свайпа по вашему условию есть коллбек `onSwipeBackStart()` (см. **Свойства и методы**) + +```tsx static +import * as React from 'react'; +import { type ViewProps, View } from '@vkontakte/vkui'; + +type ViewOnSwipeBackStartProp = Required['onSwipeBackStart']; + +const App = () => { + const handleSwipeBackStart = React.useCallback((activePanel) => {}, []); + return ; +}; +``` + +```jsx +import vkBridge from '@vkontakte/vk-bridge'; + +const App = () => { + const [history, setHistory] = useState(['main']); + const activePanel = history[history.length - 1]; + + const go = React.useCallback((panel) => { + setHistory((prevHistory) => [...prevHistory, panel]); + }, []); + const goBack = React.useCallback(() => { + setHistory((prevHistory) => prevHistory.slice(0, -1)); + }, []); + + const handleProfileClick = React.useCallback(() => go('profile'), [go]); + const handleSettingsClick = React.useCallback(() => go('settings'), [go]); + + const [userName, setUserName] = React.useState(''); + const [popoutWithRestriction, setPopoutWithRestriction] = React.useState(null); + + const handleSwipeBackStartForPreventIfNeeded = React.useCallback( + (activePanel) => { + if (activePanel === 'settings') { + if (userName !== '') { + return; + } + + setPopoutWithRestriction( + setPopoutWithRestriction(null)} + />, + ); + + return 'prevent'; + } + }, + [userName], + ); + + return ( + + + + + + + + + + + + + + + + ); +}; + +const MainPanelContent = ({ onProfileClick }) => { + return ( + + Main + +
+ + Профиль + +
+ + + ); }; -const AlbumItems = () => { - return albumItems.map(({ id, title, size, thumb_src }) => ( - - - - )); +const ProfilePanelContent = ({ onSettingsClick }) => { + return ( + + Профиль + + Теперь свайпните от левого края направо, чтобы вернуться +
+ Здесь свайпбек отключен +
+
+ + + Настройки + + + Gallery} + description="Полностью блокирует свайпбэк (за счёт event.stopPropagation() на onStartX компонента Touch)" + > + +
+ +
+ + + HorizontalScroll} + description="Свайпбэк срабатывает либо если мы тянем за левый край экрана, либо если позиция горизонтального скролла равна нулю" + > + +
+ {getRandomUsers(15).map((user) => ( + + + + ))} +
+
+
+ + ); +}; + +const SettingsPanelContent = ({ name, onChangeName }) => { + const handleNameChange = React.useCallback( + (event) => { + onChangeName(event.target.value.trim()); + }, + [onChangeName], + ); + + return ( + + Настройки + + Пример с блокированием свайпбека пока не будет выполнено условие + + + + + + ); }; + + +; +``` + +
+ +## [useNavDirection(): определение типа перехода (вперёд/назад), с которым была отрисована панель.](#/View?id=usenavdirection_example) + +Хук `useNavDirection()` возвращает одно из трёх значений: + +- `undefined` означает, что компонент был смонтирован без перехода (тип перехода может быть не определён при самом первом монтировании приложения, когда ещё не было переходов между [View](#/View) и [Panel](#/Panel)); +- `"forwards"` переход вперёд; +- `"backwards"` переход назад. + +Xук возвращает правильное значение даже если анимация **выключена** через [ConfigProvider](#/ConfigProvider) (``). + +Значение известно ещё до завершения анимации и определяется один раз, при первом монтировании панели. + +Этот хук можно использовать для определения типа анимации перехода не только между `Panel` внутри одного `View`, но и между `View` внутри `Root`. + +
+ +Хук также работает в режиме [iOS Swipe Back](#/View?id=iosswipeback). Тип перехода известен как только пользователь начал движение. + +
+ +В примере ниже c помощью спиннера имитируется загрузка данных если панель отрисована с анимацией перехода вперед. +Используется два `View` и по три `Panel` компонента в каждом, чтобы показать, что тип перехода известен как при переходе между `View`, так и при переходе между `Panel`. + +На третьем `View` пример со свайпом в iOS от левого края назад, где видно, что панель на которую идёт переход определяет его тип в самом начале свайпа. + +```jsx const Content = () => { const direction = useNavDirection(); @@ -352,61 +567,6 @@ const Content = () => { ); }; -const ContentWithScrollSaverComponent = () => { - const horizontalScrollRef = useRef(); - - return ( -
- - With ScrollSaver component - - - -
- -
-
-
-
- ); -}; - -const ContentWithScrollSaverWithoutChildren = () => { - const horizontalRef = useRef(); - return ( -
- - With ScrollSaverWithoutChildren - doesn't look for children ref - - - -
- -
-
-
-
- ); -}; - -const ContentWithScrollSaverHook = () => { - const horizontalScrollRef = useRef(); - useScrollSaver(horizontalScrollRef, 'horizontal-scroll-saver-hook'); - - return ( -
- - With ScrollSaverHook - - -
- -
-
-
- ); -}; - const Example = () => { const [activeView, setActiveView] = useState('view1'); const [activePanel, setActivePanel] = useState(1); @@ -434,14 +594,8 @@ const Example = () => { [pushSwipeViewHistory, activeView], ); - const cache = useScrollSaverCache(); - const clearScrollCache = useClearScrollSaverCache(); handleActiveViewSet = React.useCallback( (view) => { - if (view === 'view1') { - clearScrollCache(); - } - console.log(cache); if (view === 'swipeView') { const defaultSwipeViewActivePanel = 1; setSwipeViewHistory([`swipeView.${defaultSwipeViewActivePanel}`]); @@ -449,7 +603,7 @@ const Example = () => { } setActiveView(view); }, - [activePanel, clearScrollCache, cache], + [activePanel], ); const navigationButtons = ( @@ -472,19 +626,16 @@ const Example = () => { Панель 1.1 {navigationButtons} - Панель 1.2 {navigationButtons} - Панель 1.3 {navigationButtons} - @@ -492,19 +643,16 @@ const Example = () => { Панель 2.1 {navigationButtons} - Панель 2.2 {navigationButtons} - Панель 2.3 {navigationButtons} - Date: Mon, 23 Oct 2023 19:55:47 +0300 Subject: [PATCH 10/10] Move components to files --- .../vkui/src/components/ScrollSaver/Readme.md | 20 ++--- .../components/ScrollSaver/ScrollSaver.tsx | 87 ++----------------- .../ScrollSaver/ScrollSaverWithCustomRef.ts | 15 ++++ .../vkui/src/components/ScrollSaver/types.ts | 1 + .../components/ScrollSaver/useScrollSaver.ts | 66 ++++++++++++++ packages/vkui/src/index.ts | 10 ++- 6 files changed, 104 insertions(+), 95 deletions(-) create mode 100644 packages/vkui/src/components/ScrollSaver/ScrollSaverWithCustomRef.ts create mode 100644 packages/vkui/src/components/ScrollSaver/types.ts create mode 100644 packages/vkui/src/components/ScrollSaver/useScrollSaver.ts diff --git a/packages/vkui/src/components/ScrollSaver/Readme.md b/packages/vkui/src/components/ScrollSaver/Readme.md index 969f4ee047..692b91acbe 100644 --- a/packages/vkui/src/components/ScrollSaver/Readme.md +++ b/packages/vkui/src/components/ScrollSaver/Readme.md @@ -27,16 +27,16 @@ ``` -- ScrollSaverWtihoutChildren берёт ref не из children а из пропа elementRef. +- ScrollSaverWithCustomRef берёт ref не из children а из пропа elementRef. ```jsx static - +
-
+ ``` - useScrollSaver хук @@ -71,7 +71,7 @@ const clearScrollCache = useClearScrollSaverCache();
-В примере ниже на View1 и View2 сохраняются позиции скролла HorizontalScroll используя **ScrollSaver**, **useScrollSaver** и **ScrollSaverWithoutChildren**. +В примере ниже на View1 и View2 сохраняются позиции скролла HorizontalScroll используя **ScrollSaver**, **useScrollSaver** и **ScrollSaverWithCustomRef**. Позиция сохраняется как при переходе между View так и при переходе между Panel При возврате на View1 c других View мы сбрасываем весь кэш ScrollSaver. @@ -168,20 +168,20 @@ const ContentWithScrollSaverComponent = () => { ); }; -const ContentWithScrollSaverWithoutChildren = () => { +const ContentWithScrollSaverWithCustomRef = () => { const horizontalRef = useRef(); return (
- With ScrollSaverWithoutChildren - doesn't look for children ref + With ScrollSaverWithCustomRef - doesn't look for children ref - +
-
+
); }; @@ -283,7 +283,7 @@ const Example = () => { Панель 1.3 {navigationButtons} - +
@@ -303,7 +303,7 @@ const Example = () => { Панель 2.3 {navigationButtons} - + { const [childrenRef, children] = usePatchChildrenRef(props.children, props.useGetRef); useScrollSaver(childrenRef, props.id, props.saveMode); return children; -} - -export function useScrollSaver( - elementRef: React.RefObject, - id: ScrollSaverProps['id'], - saveMode: ScrollSaverProps['saveMode'] = '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; -} - -interface ScrollSaverWithoutChildrenProps - extends Omit { - elementRef: React.RefObject; -} - -/* Компонентный Вариант useScrollSaver хука для динамического рендеринга, чтобы можно было пробросить и использовать любой ref - * children проп можно передать, но ref из него браться не будет */ -export function ScrollSaverWithoutChildren(props: ScrollSaverWithoutChildrenProps) { - useScrollSaver(props.elementRef, props.id, props.saveMode); - - return props.children; -} - -function useUniqueId(id: string) { - const { view: viewId, panel: panelId } = useNavId(); - const uniqueId = `${viewId}-${panelId}-${id}`; - return uniqueId; -} +}; diff --git a/packages/vkui/src/components/ScrollSaver/ScrollSaverWithCustomRef.ts b/packages/vkui/src/components/ScrollSaver/ScrollSaverWithCustomRef.ts new file mode 100644 index 0000000000..08c1cd3ffb --- /dev/null +++ b/packages/vkui/src/components/ScrollSaver/ScrollSaverWithCustomRef.ts @@ -0,0 +1,15 @@ +import { ScrollSaverProps } from './ScrollSaver'; +import { useScrollSaver } from './useScrollSaver'; + +export interface ScrollSaverWithCustomRefProps + extends Omit { + elementRef: React.RefObject; +} + +/* Компонентный Вариант useScrollSaver хука для динамического рендеринга, чтобы можно было пробросить и использовать любой ref + * children проп можно передать, но ref из него браться не будет */ +export function ScrollSaverWithCustomRef(props: ScrollSaverWithCustomRefProps) { + useScrollSaver(props.elementRef, props.id, props.saveMode); + + return props.children; +} diff --git a/packages/vkui/src/components/ScrollSaver/types.ts b/packages/vkui/src/components/ScrollSaver/types.ts new file mode 100644 index 0000000000..ec905250d5 --- /dev/null +++ b/packages/vkui/src/components/ScrollSaver/types.ts @@ -0,0 +1 @@ +export type ScrollSaveModeType = 'forward' | 'always'; diff --git a/packages/vkui/src/components/ScrollSaver/useScrollSaver.ts b/packages/vkui/src/components/ScrollSaver/useScrollSaver.ts new file mode 100644 index 0000000000..fe2f02057b --- /dev/null +++ b/packages/vkui/src/components/ScrollSaver/useScrollSaver.ts @@ -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( + elementRef: React.RefObject, + 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; +} diff --git a/packages/vkui/src/index.ts b/packages/vkui/src/index.ts index 0783887753..ad436abded 100644 --- a/packages/vkui/src/index.ts +++ b/packages/vkui/src/index.ts @@ -343,11 +343,13 @@ export type { PopoverOnShownChange, PopoverContentRenderProp, } from './components/Popover/Popover'; +export { ScrollSaver, type ScrollSaverProps } from './components/ScrollSaver/ScrollSaver'; export { - ScrollSaver, - ScrollSaverWithoutChildren, - useScrollSaver, -} from './components/ScrollSaver/ScrollSaver'; + ScrollSaverWithCustomRef, + type ScrollSaverWithCustomRefProps, +} from './components/ScrollSaver/ScrollSaverWithCustomRef'; +export { useScrollSaver } from './components/ScrollSaver/useScrollSaver'; +export { type ScrollSaveModeType } from './components/ScrollSaver/types'; export { ScrollSaverContextProvider, useScrollSaverCache,