From c12af026985d9b76b96e9380c286833e6e423cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=AD=D0=BB=D1=8C=D0=B4=D0=B0=D1=80?= <61377022+EldarMuhamethanov@users.noreply.github.com> Date: Wed, 6 Nov 2024 14:34:51 +0300 Subject: [PATCH] fix(Galery): fix autoPlay when dragging slides (#7877) * fix(Galery): fix autoPlay when dragging slides * fix(HorizontalScroll): remove unused useEffect * fix(Gallery): rm useImperativeHandler and return controls from hook * fix(Gallery): rm import * fix(useAutoPlay): use useCallback * fix(useAutoPlay): rm import (cherry picked from commit 3661e98a0a3df99584a8d90623e240c44cde5427) --- .../vkui/src/components/Gallery/Gallery.tsx | 11 ++- .../vkui/src/components/Gallery/hooks.test.ts | 35 +++++++- packages/vkui/src/components/Gallery/hooks.ts | 82 +++++++++++-------- 3 files changed, 92 insertions(+), 36 deletions(-) diff --git a/packages/vkui/src/components/Gallery/Gallery.tsx b/packages/vkui/src/components/Gallery/Gallery.tsx index 72c681f6d6..988fcc3339 100644 --- a/packages/vkui/src/components/Gallery/Gallery.tsx +++ b/packages/vkui/src/components/Gallery/Gallery.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { clamp } from '../../helpers/math'; import { useIsClient } from '../../hooks/useIsClient'; +import { callMultiple } from '../../lib/callMultiple'; import { BaseGallery } from '../BaseGallery/BaseGallery'; import { CarouselBase } from '../BaseGallery/CarouselBase/CarouselBase'; import type { BaseGalleryProps } from '../BaseGallery/types'; @@ -23,6 +24,8 @@ export const Gallery = ({ onChange, bullets, looped, + onDragStart, + onDragEnd, ...props }: GalleryProps): React.ReactNode => { const [localSlideIndex, setSlideIndex] = React.useState(initialSlideIndex); @@ -46,7 +49,11 @@ export const Gallery = ({ [isControlled, onChange, slideIndex], ); - useAutoPlay(timeout, slideIndex, () => handleChange((slideIndex + 1) % childCount)); + const autoPlayControls = useAutoPlay({ + timeout, + slideIndex, + onNext: () => handleChange((slideIndex + 1) % childCount), + }); // prevent invalid slideIndex // any slide index is invalid with no slides, just keep it as is @@ -69,6 +76,8 @@ export const Gallery = ({ 0 && bullets} slideIndex={safeSlideIndex} onChange={handleChange} diff --git a/packages/vkui/src/components/Gallery/hooks.test.ts b/packages/vkui/src/components/Gallery/hooks.test.ts index 77703cc19f..1d618f1846 100644 --- a/packages/vkui/src/components/Gallery/hooks.test.ts +++ b/packages/vkui/src/components/Gallery/hooks.test.ts @@ -10,7 +10,7 @@ describe(useAutoPlay, () => { jest.spyOn(document, 'visibilityState', 'get').mockImplementation(() => visibilityState); - renderHook(() => useAutoPlay(100, 0, callback)); + renderHook(() => useAutoPlay({ timeout: 100, slideIndex: 0, onNext: callback })); jest.runAllTimers(); expect(callback).toHaveBeenCalledTimes(1); @@ -29,8 +29,39 @@ describe(useAutoPlay, () => { jest.useFakeTimers(); const callback = jest.fn(); - renderHook(() => useAutoPlay(0, 0, callback)); + renderHook(() => useAutoPlay({ timeout: 0, slideIndex: 0, onNext: callback })); jest.runAllTimers(); expect(callback).toHaveBeenCalledTimes(0); }); + + it('check controls working', () => { + jest.useFakeTimers(); + const callback = jest.fn(); + + let visibilityState: Document['visibilityState'] = 'visible'; + + jest.spyOn(document, 'visibilityState', 'get').mockImplementation(() => visibilityState); + + const res = renderHook(() => useAutoPlay({ timeout: 100, slideIndex: 0, onNext: callback })); + jest.runAllTimers(); + expect(callback).toHaveBeenCalledTimes(1); + + // Останавливаем работу хука + res.result.current.pause(); + res.rerender(); + // Срабатывает события visibilityChange + fireEvent(document, new Event('visibilitychange')); + jest.runAllTimers(); + // Но callback не срабатыват по истечению таймеров + expect(callback).toHaveBeenCalledTimes(1); + + // Восстанавливаем работу хука + res.result.current.resume(); + res.rerender(); + // Срабатывает события visibilityChange + fireEvent(document, new Event('visibilitychange')); + jest.runAllTimers(); + // callback срабатыват по истечению таймеров + expect(callback).toHaveBeenCalledTimes(2); + }); }); diff --git a/packages/vkui/src/components/Gallery/hooks.ts b/packages/vkui/src/components/Gallery/hooks.ts index cf6f638db4..d34d6d2f04 100644 --- a/packages/vkui/src/components/Gallery/hooks.ts +++ b/packages/vkui/src/components/Gallery/hooks.ts @@ -3,49 +3,65 @@ import { useStableCallback } from '../../hooks/useStableCallback'; import { useDOM } from '../../lib/dom'; import type { TimeoutId } from '../../types'; -export function useAutoPlay( - timeout: number, - slideIndex: number, - callbackFnProp: VoidFunction, -): void { +export interface AutoPlayConfig { + timeout: number; + slideIndex: number; + onNext: VoidFunction; +} + +export function useAutoPlay({ timeout, slideIndex, onNext }: AutoPlayConfig): { + pause: VoidFunction; + resume: VoidFunction; +} { const { document } = useDOM(); - const callbackFn = useStableCallback(callbackFnProp); + const [paused, setPaused] = React.useState(false); + const timeoutRef = React.useRef(null); + const callbackFn = useStableCallback(onNext); - React.useEffect( - function initializeAutoPlay() { - if (!document || !timeout) { - return; - } + const pause = React.useCallback(() => setPaused(true), []); + const resume = React.useCallback(() => setPaused(false), []); - let timeoutId: TimeoutId = null; + // Выносим функции очистки и старта таймера в отдельные функции + const clearAutoPlayTimeout = React.useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }, []); - const stop = () => { - if (timeoutId) { - clearTimeout(timeoutId); - timeoutId = null; - } - }; + const startAutoPlayTimeout = React.useCallback(() => { + if (!document || !timeout || paused) { + return; + } - const start = () => { - switch (document.visibilityState) { - case 'visible': - stop(); - timeoutId = setTimeout(callbackFn, timeout); - break; - case 'hidden': - stop(); - } - }; + if (document.visibilityState === 'visible') { + clearAutoPlayTimeout(); + timeoutRef.current = setTimeout(callbackFn, timeout); + } else { + clearAutoPlayTimeout(); + } + }, [document, timeout, paused, clearAutoPlayTimeout, callbackFn]); - start(); + // Основной эффект для управления автопроигрыванием + React.useEffect( + function initializeAutoPlay() { + if (!document || !timeout || paused) { + return; + } - document.addEventListener('visibilitychange', start); + startAutoPlayTimeout(); + document.addEventListener('visibilitychange', startAutoPlayTimeout); return () => { - stop(); - document.removeEventListener('visibilitychange', start); + clearAutoPlayTimeout(); + document.removeEventListener('visibilitychange', startAutoPlayTimeout); }; }, - [document, timeout, slideIndex, callbackFn], + [document, timeout, slideIndex, startAutoPlayTimeout, clearAutoPlayTimeout, paused], ); + + return { + resume, + pause, + }; }