diff --git a/packages/vkui/src/components/Gallery/Gallery.tsx b/packages/vkui/src/components/Gallery/Gallery.tsx index 63acff6b0b..01b240fc32 100644 --- a/packages/vkui/src/components/Gallery/Gallery.tsx +++ b/packages/vkui/src/components/Gallery/Gallery.tsx @@ -3,6 +3,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'; @@ -25,6 +26,8 @@ export const Gallery = ({ onChange, bullets, looped, + onDragStart, + onDragEnd, ...props }: GalleryProps): React.ReactNode => { const [localSlideIndex, setSlideIndex] = React.useState(initialSlideIndex); @@ -48,7 +51,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 @@ -71,6 +78,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, + }; }