From e3bb41deb821787307b13323f7eb98df1372571e Mon Sep 17 00:00:00 2001 From: Andrey Medvedev Date: Wed, 17 Jan 2024 14:43:16 +0300 Subject: [PATCH] fix(View): Restore scroll position on swipe back cancel (#6325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-picked из v6. Мы очень хитро восстанавливаем позицию скролла, надеясь на то, что это всегда будет работать в useEffect, который сработает перед обработкой события transition (перед завершением анимации), где окончательно сбросятся все состояния. Но в случае, если пользователь вернул панель назад, мы просто сбрасываем состояние компонента, отвечающее, за свайп. Логика, отвечающая за восстановление скролла не срабатывает, потому что это не считается как `failure`. Вот тут мы решаем `swipeBack` был успешным (`success`), был отменён (`fail`), то есть свайп не закончен, либо это вообще не считается свайпом, потому что панель по завершении жеста осталась на той же позиции. https://github.com/VKCOM/VKUI/blob/a4719b49f887c2584eec6655d72e373e62409c59/packages/vkui/src/components/View/View.tsx#L301-L316 Если это не свайп вовсе (пользователь вернул панель на место), то мы просто сбрасываем состояние свайпа с помощью функции `onSwipeBackCancel`. Но в такой ситуации не сработает условие для восстановления скролла при отмене свайпа. https://github.com/VKCOM/VKUI/blob/8dbb1de9855af8c772abcb719848175654e39a8a/src/components/View/View.tsx#L494-L500 - caused by #5725 -- Изменения Вынес логику по восстановлению скролла при отмене свайпа в отдельный useEffect, потому что изначальный слишком большой. Смотрю на переменные `prevSwipingBack`, `swipingBack`, чтобы понять был ли всё же свайп, потому что `swipeBackResult` нам ни о чем не скажет, даже если бы мы его устанавливали, то он не был бы тут же очищен в том же рендере из-за вызова `onSwipeBackCancel`. Также проверяю `prevSwipeBackShift`, который равен нулю, то есть смещения после жеста у панели нет, что значит, что пользователь жестом свайпа вернул панель туда откуда взял. --- .../vkui/src/components/View/View.test.tsx | 19 +++++++++-- packages/vkui/src/components/View/View.tsx | 33 ++++++++++++++++--- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/packages/vkui/src/components/View/View.test.tsx b/packages/vkui/src/components/View/View.test.tsx index f3a120191d..da2d64660c 100644 --- a/packages/vkui/src/components/View/View.test.tsx +++ b/packages/vkui/src/components/View/View.test.tsx @@ -255,7 +255,8 @@ describe('View', () => { expect(document.getElementById('p1')).toBeTruthy(); expect(document.getElementById('p2')).toBeNull(); }); - it('restores scroll after swipeBack', () => { + + it('restores scroll after cancelled swipeBack (mouse up during the move)', () => { let y = 101; scrollsCache['scroll']['p1'] = 22; const [MockScroll, scrollTo] = mockScrollContext(() => y); @@ -268,7 +269,21 @@ describe('View', () => { }); fireEvent.mouseUp(view); rerender(); - expect(scrollTo).toBeCalledWith(0, 22); + expect(scrollTo).toHaveBeenCalledWith(0, 22); + }); + + it('restores scroll when swipeBack cancelled because user moves panel back to starting point', () => { + const currentScrollPosition = 22; + const startPosition = { clientX: 0, clientY: 100 }; + const [MockScroll, scrollTo] = mockScrollContext(() => currentScrollPosition); + const { view, rerender, SwipeBack } = setupSwipeBack({ Wrapper: MockScroll }); + fireEvent.mouseDown(view, startPosition); + fireEvent.mouseMove(view, { clientX: SWIPE_BACK_SHIFT_THRESHOLD, clientY: 100 }); + fireEvent.mouseMove(view, startPosition); + fireEvent.mouseUp(view); + + rerender(); + expect(scrollTo).toHaveBeenCalledWith(0, currentScrollPosition); }); describe('horizontal scrollable elements', () => { diff --git a/packages/vkui/src/components/View/View.tsx b/packages/vkui/src/components/View/View.tsx index b7d200a5ca..66e3d0e47c 100644 --- a/packages/vkui/src/components/View/View.tsx +++ b/packages/vkui/src/components/View/View.tsx @@ -119,6 +119,7 @@ export const View = ({ const prevSwipingBack = usePrevious(swipingBack); const prevBrowserSwipe = usePrevious(browserSwipe); const prevSwipeBackResult = usePrevious(swipeBackResult); + const prevSwipeBackShift = usePrevious(swipeBackShift); const prevSwipeBackPrevPanel = usePrevious(swipeBackPrevPanel); const prevOnTransition = usePrevious(onTransition); @@ -455,11 +456,6 @@ export const View = ({ ); } - // Если свайп назад отменился (когда пользователь недостаточно сильно свайпнул) - if (prevSwipeBackResult === SwipeBackResults.fail && !swipeBackResult && activePanel !== null) { - scroll?.scrollTo(0, scrolls.current[activePanel]); - } - // Закончился Safari свайп if (prevActivePanel !== activePanelProp && browserSwipe) { setBrowserSwipe(false); @@ -492,6 +488,33 @@ export const View = ({ waitTransitionFinish, ]); + React.useEffect( + function restoreScrollPositionWhenSwipeBackIsCancelled() { + // Если свайп назад отменился (когда пользователь недостаточно сильно свайпнул) + const swipeBackCancelledInTheMiddleOfAction = + prevSwipeBackResult === SwipeBackResults.fail && !swipeBackResult; + const swipeBackCancelledByMovingPanelBackToInitialPoint = + prevSwipingBack && !swipingBack && prevSwipeBackShift === 0; + + if ( + (swipeBackCancelledInTheMiddleOfAction || + swipeBackCancelledByMovingPanelBackToInitialPoint) && + activePanel !== null + ) { + scroll?.scrollTo(0, scrolls.current[activePanel]); + } + }, + [ + prevSwipeBackResult, + swipeBackResult, + prevSwipingBack, + swipingBack, + prevSwipeBackShift, + activePanel, + scroll, + ], + ); + return (