Skip to content

Commit

Permalink
fix(PullToRefresh): rm global touchmove listener
Browse files Browse the repository at this point in the history
- Глобальный обработчик touchmove влиял на поведение элементов с горизонтальным скроллом (например, HorizontalScroll) – нужно ровно по оси X проводить пальцем, чтобы заработало. После удаления проблему, из-за которой был добавлен этот обработчик, не смог воспроизвести. Даже если такая проблема есть, то думаю она не критичней, в отличии от блокировки горизонтального скролла. Удалил тест, который проверял глобальный обработчик.
- Заодно поправил проблему, когда при работе спиннера можно было бы потянуть сверху вниз и вызвать нативный pull-to-refresh – для исправления, мы сохраняем св-во `overscroll-behavior` на `document.body` пока не закончится обновление. Вынес логику в эффект.
- Избавился от `useTimeout`, т.к. он скорее усложняет код, чем облегчает его.
- Добавил больше данные в примере Storybook, чтобы увеличить высоту контента под скролл.
  • Loading branch information
inomdzhon committed Feb 9, 2024
1 parent b79ae18 commit decdcfd
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default story;

type Story = StoryObj<PullToRefreshProps>;

const initUsers = getRandomUsers(10);
const initUsers = getRandomUsers(20);

export const Example: Story = {
render: function Render() {
Expand Down
28 changes: 19 additions & 9 deletions packages/vkui/src/components/PullToRefresh/PullToRefresh.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down Expand Up @@ -153,7 +148,7 @@ describe(PullToRefresh, () => {
it('disables native pull-to-refresh while pulling', async () => {
const component = render(
<ConfigProvider platform="ios">
<PullToRefresh onRefresh={noop} data-testid="xxx" />
<PullToRefresh onRefresh={noop} isFetching={false} data-testid="xxx" />
</ConfigProvider>,
{ baseElement: document.documentElement },
);
Expand All @@ -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(
<ConfigProvider platform="ios">
<PullToRefresh onRefresh={noop} isFetching data-testid="xxx" />
</ConfigProvider>,
);

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(
<ConfigProvider platform="ios">
<PullToRefresh onRefresh={noop} isFetching={false} data-testid="xxx" />
</ConfigProvider>,
);

expect(document.querySelector('.vkui--disable-overscroll-behavior')).toBeFalsy();
});
});
113 changes: 43 additions & 70 deletions packages/vkui/src/components/PullToRefresh/PullToRefresh.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
}
Expand All @@ -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
*/
Expand Down Expand Up @@ -102,10 +94,7 @@ export const PullToRefresh = ({
}
}, [touchDown, resetRefreshingState]);

const { set: setWaitFetchingTimeout, clear: clearWaitFetchingTimeout } = useTimeout(
onRefreshingFinish,
1000,
);
const waitFetchingTimeoutId = React.useRef<NodeJS.Timeout>();

useIsomorphicLayoutEffect(() => {
if (prevIsFetching !== undefined && prevIsFetching && !isFetching) {
Expand All @@ -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) {
Expand All @@ -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
Expand All @@ -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;

Expand All @@ -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);
Expand All @@ -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)`;
Expand Down

0 comments on commit decdcfd

Please sign in to comment.