From decdcfd3bd6e04fcbef7f7e3c11b06e5d84ecfdb Mon Sep 17 00:00:00 2001 From: Inomdzhon Mirdzhamolov Date: Fri, 9 Feb 2024 23:07:41 +0300 Subject: [PATCH] fix(PullToRefresh): rm global touchmove listener MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Глобальный обработчик touchmove влиял на поведение элементов с горизонтальным скроллом (например, HorizontalScroll) – нужно ровно по оси X проводить пальцем, чтобы заработало. После удаления проблему, из-за которой был добавлен этот обработчик, не смог воспроизвести. Даже если такая проблема есть, то думаю она не критичней, в отличии от блокировки горизонтального скролла. Удалил тест, который проверял глобальный обработчик. - Заодно поправил проблему, когда при работе спиннера можно было бы потянуть сверху вниз и вызвать нативный pull-to-refresh – для исправления, мы сохраняем св-во `overscroll-behavior` на `document.body` пока не закончится обновление. Вынес логику в эффект. - Избавился от `useTimeout`, т.к. он скорее усложняет код, чем облегчает его. - Добавил больше данные в примере Storybook, чтобы увеличить высоту контента под скролл. --- .../PullToRefresh/PullToRefresh.stories.tsx | 2 +- .../PullToRefresh/PullToRefresh.test.tsx | 28 +++-- .../PullToRefresh/PullToRefresh.tsx | 113 +++++++----------- 3 files changed, 63 insertions(+), 80 deletions(-) diff --git a/packages/vkui/src/components/PullToRefresh/PullToRefresh.stories.tsx b/packages/vkui/src/components/PullToRefresh/PullToRefresh.stories.tsx index 9d3a5f9f3e..5215ca07d7 100644 --- a/packages/vkui/src/components/PullToRefresh/PullToRefresh.stories.tsx +++ b/packages/vkui/src/components/PullToRefresh/PullToRefresh.stories.tsx @@ -23,7 +23,7 @@ export default story; type Story = StoryObj; -const initUsers = getRandomUsers(10); +const initUsers = getRandomUsers(20); export const Example: Story = { render: function Render() { diff --git a/packages/vkui/src/components/PullToRefresh/PullToRefresh.test.tsx b/packages/vkui/src/components/PullToRefresh/PullToRefresh.test.tsx index e050c6ead8..d6a660d23f 100644 --- a/packages/vkui/src/components/PullToRefresh/PullToRefresh.test.tsx +++ b/packages/vkui/src/components/PullToRefresh/PullToRefresh.test.tsx @@ -116,11 +116,6 @@ describe(PullToRefresh, () => { const hasDefaultStart = fireEvent.mouseDown(screen.getByTestId('xxx'), { clientY: 0 }); expect(hasDefaultStart).toBe(toHaveDefault); }; - it('prevents during refresh', () => { - renderRefresher(); - firePull(screen.getByTestId('xxx')); - expectEvents(false); - }); it('releases after refresh', () => { const { setFetching } = renderRefresher(); firePull(screen.getByTestId('xxx')); @@ -153,7 +148,7 @@ describe(PullToRefresh, () => { it('disables native pull-to-refresh while pulling', async () => { const component = render( - + , { baseElement: document.documentElement }, ); @@ -162,12 +157,27 @@ describe(PullToRefresh, () => { // класс присутствует пока пуллим firePull(component.getByTestId('xxx'), { end: false }); - act(jest.runAllTimers); expect(document.querySelector('.vkui--disable-overscroll-behavior')).toBeTruthy(); - // класс удаляется когда отпускаем + component.rerender( + + + , + ); + fireEvent.mouseUp(component.getByTestId('xxx'), { clientY: 500 }); - act(jest.runAllTimers); + expect(document.querySelector('.vkui--disable-overscroll-behavior')).toBeTruthy(); + + // пока идёт обновление, класс не удаляется, чтобы не вызывалось нативное поведение + firePull(component.getByTestId('xxx'), { end: true }); + expect(document.querySelector('.vkui--disable-overscroll-behavior')).toBeTruthy(); + + component.rerender( + + + , + ); + expect(document.querySelector('.vkui--disable-overscroll-behavior')).toBeFalsy(); }); }); diff --git a/packages/vkui/src/components/PullToRefresh/PullToRefresh.tsx b/packages/vkui/src/components/PullToRefresh/PullToRefresh.tsx index 39ea411120..bdf25d9ba1 100644 --- a/packages/vkui/src/components/PullToRefresh/PullToRefresh.tsx +++ b/packages/vkui/src/components/PullToRefresh/PullToRefresh.tsx @@ -1,12 +1,9 @@ import * as React from 'react'; import { classNames } from '@vkontakte/vkjs'; import { clamp } from '../../helpers/math'; -import { useGlobalEventListener } from '../../hooks/useGlobalEventListener'; import { usePlatform } from '../../hooks/usePlatform'; import { usePrevious } from '../../hooks/usePrevious'; -import { useTimeout } from '../../hooks/useTimeout'; import { DOMProps, useDOM } from '../../lib/dom'; -import { coordY, VKUITouchEvent } from '../../lib/touch'; import { useIsomorphicLayoutEffect } from '../../lib/useIsomorphicLayoutEffect'; import { AnyFunction, HasChildren } from '../../types'; import { ScrollContextInterface, useScroll } from '../AppRoot/ScrollContext'; @@ -16,18 +13,18 @@ import TouchRootContext from '../Touch/TouchContext'; import { PullToRefreshSpinner } from './PullToRefreshSpinner'; import styles from './PullToRefresh.module.css'; -function cancelEvent(event: any) { +const WAIT_FETCHING_TIMEOUT_MS = 1000; + +function cancelEvent(event: TouchEvent) { + /* istanbul ignore if: неясно в какой ситуации `event` из `Touch` может быть не определён */ if (!event) { return false; } - while (event.originalEvent) { - event = event.originalEvent; - } - if (event.preventDefault && event.cancelable) { - event.preventDefault(); + if ('preventDefault' in event.originalEvent && event.originalEvent.cancelable) { + event.originalEvent.preventDefault(); } - if (event.stopPropagation) { - event.stopPropagation(); + if ('stopPropagation' in event.originalEvent) { + event.originalEvent.stopPropagation(); } return false; } @@ -45,11 +42,6 @@ export interface PullToRefreshProps extends DOMProps, TouchProps, HasChildren { scroll?: ScrollContextInterface; } -const TOUCH_MOVE_EVENT_PARAMS = { - cancelable: true, - passive: false, -}; - /** * @see https://vkcom.github.io/VKUI/#/PullToRefresh */ @@ -102,10 +94,7 @@ export const PullToRefresh = ({ } }, [touchDown, resetRefreshingState]); - const { set: setWaitFetchingTimeout, clear: clearWaitFetchingTimeout } = useTimeout( - onRefreshingFinish, - 1000, - ); + const waitFetchingTimeoutId = React.useRef(); useIsomorphicLayoutEffect(() => { if (prevIsFetching !== undefined && prevIsFetching && !isFetching) { @@ -115,21 +104,22 @@ export const PullToRefresh = ({ useIsomorphicLayoutEffect(() => { if (prevIsFetching !== undefined && !prevIsFetching && isFetching) { - clearWaitFetchingTimeout(); + clearTimeout(waitFetchingTimeoutId.current); } - }, [isFetching, prevIsFetching, clearWaitFetchingTimeout]); + }, [isFetching, prevIsFetching]); const runRefreshing = React.useCallback(() => { if (!refreshing && onRefresh) { // cleanup if the consumer does not start fetching in 1s - setWaitFetchingTimeout(); + clearTimeout(waitFetchingTimeoutId.current); + waitFetchingTimeoutId.current = setTimeout(onRefreshingFinish, WAIT_FETCHING_TIMEOUT_MS); setRefreshing(true); setSpinnerY((prevSpinnerY) => (platform === 'ios' ? prevSpinnerY : initParams.refreshing)); onRefresh(); } - }, [refreshing, onRefresh, setWaitFetchingTimeout, platform, initParams.refreshing]); + }, [refreshing, onRefresh, onRefreshingFinish, platform, initParams.refreshing]); useIsomorphicLayoutEffect(() => { if (prevTouchDown !== undefined && prevTouchDown && !touchDown) { @@ -138,6 +128,7 @@ export const PullToRefresh = ({ } else if (refreshing && !isFetching) { // only iOS can start refresh before gesture end resetRefreshingState(); + /* istanbul ignore if: TODO написать тест */ } else { // refreshing && isFetching: refresh in progress // OR !refreshing && !canRefresh: pull was not strong enough @@ -158,56 +149,44 @@ export const PullToRefresh = ({ runRefreshing, ]); - const startYRef = React.useRef(0); - - const onTouchStart = (e: TouchEvent) => { - if (refreshing) { - cancelEvent(e); - } - setTouchDown(true); - startYRef.current = e.startY; + useIsomorphicLayoutEffect( + function toggleBodyOverscrollBehavior() { + /* istanbul ignore if: невозможный кейс, т.к. в SSR эффекты не вызываются. Проверка на будущее, если вдруг эффект будет вызываться. */ + if (!document) { + return; + } - if (document) { - // eslint-disable-next-line no-restricted-properties - document.documentElement.classList.add('vkui--disable-overscroll-behavior'); - } - }; + if (touchDown || refreshing) { + // eslint-disable-next-line no-restricted-properties + document.documentElement.classList.add('vkui--disable-overscroll-behavior'); + } - const shouldPreventTouchMove = (event: VKUITouchEvent) => { - if (watching || refreshing) { - return true; - } + return () => { + // eslint-disable-next-line no-restricted-properties + document.documentElement.classList.remove('vkui--disable-overscroll-behavior'); + }; + }, + [touchDown, refreshing], + ); - /* Нам нужно запретить touchmove у документа как только стало понятно, что - * начинается pull. - * состояния watching и refreshing устанавливаются слишком поздно и браузер - * может успеть начать нативный pull to refresh. - * - * Этот код является запасным вариантом, на случай, если css свойство - * overscroll-behavior не поддерживается - * */ - const shiftY = coordY(event) - startYRef.current; - const pageYOffset = scroll?.getScroll().y; - const isRefreshGestureStarted = pageYOffset === 0 && shiftY > 0 && touchDown; - return isRefreshGestureStarted; - }; + const startYRef = React.useRef(0); - const onWindowTouchMove = (event: VKUITouchEvent) => { - if (shouldPreventTouchMove(event)) { - event.preventDefault(); - event.stopPropagation(); + const onTouchStart = (event: TouchEvent) => { + if (refreshing) { + cancelEvent(event); + return; } + setTouchDown(true); + startYRef.current = event.startY; }; - useGlobalEventListener(document, 'touchmove', onWindowTouchMove, TOUCH_MOVE_EVENT_PARAMS); - - const onTouchMove = (e: TouchEvent) => { - const { isY, shiftY } = e; + const onTouchMove = (event: TouchEvent) => { + const { isY, shiftY } = event; const { start, max } = initParams; const pageYOffset = scroll?.getScroll().y; if (watching && touchDown) { - cancelEvent(e); + cancelEvent(event); const { positionMultiplier, maxY } = initParams; @@ -225,7 +204,7 @@ export const PullToRefresh = ({ runRefreshing(); } } else if (isY && pageYOffset === 0 && shiftY > 0 && !refreshing && touchDown) { - cancelEvent(e); + cancelEvent(event); touchY.current = shiftY; setWatching(true); @@ -237,12 +216,6 @@ export const PullToRefresh = ({ const onTouchEnd = () => { setWatching(false); setTouchDown(false); - - // восстанавливаем overscroll behavior - if (document) { - // eslint-disable-next-line no-restricted-properties - document.documentElement.classList.remove('vkui--disable-overscroll-behavior'); - } }; const spinnerTransform = `translate3d(0, ${spinnerY}px, 0)`;