Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Galery): fix autoPlay when dragging slides #7877

Merged
18 changes: 17 additions & 1 deletion packages/vkui/src/components/Gallery/Gallery.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
'use client';

import { useRef } from 'react';
import * as React from 'react';
import { noop } from '@vkontakte/vkjs';
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';
Expand All @@ -25,6 +28,8 @@ export const Gallery = ({
onChange,
bullets,
looped,
onDragStart,
onDragEnd,
...props
}: GalleryProps): React.ReactNode => {
const [localSlideIndex, setSlideIndex] = React.useState(initialSlideIndex);
Expand All @@ -36,6 +41,10 @@ export const Gallery = ({
);
const childCount = slides.length;
const isClient = useIsClient();
const autoPlayControls = useRef<{ pause: VoidFunction; resume: VoidFunction }>({
pause: noop,
resume: noop,
});

const handleChange: GalleryProps['onChange'] = React.useCallback(
(current: number) => {
Expand All @@ -48,7 +57,12 @@ export const Gallery = ({
[isControlled, onChange, slideIndex],
);

useAutoPlay(timeout, slideIndex, () => handleChange((slideIndex + 1) % childCount));
useAutoPlay({
timeout,
slideIndex,
onNext: () => handleChange((slideIndex + 1) % childCount),
controls: autoPlayControls,
});

// prevent invalid slideIndex
// any slide index is invalid with no slides, just keep it as is
Expand All @@ -71,6 +85,8 @@ export const Gallery = ({
<Component
dragDisabled={isControlled && !onChange}
{...props}
onDragStart={callMultiple(onDragStart, autoPlayControls.current.pause)}
onDragEnd={callMultiple(onDragEnd, autoPlayControls.current.resume)}
bullets={childCount > 0 && bullets}
slideIndex={safeSlideIndex}
onChange={handleChange}
Expand Down
44 changes: 42 additions & 2 deletions packages/vkui/src/components/Gallery/hooks.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fireEvent, renderHook } from '@testing-library/react';
import { noop } from '@vkontakte/vkjs';
import { useAutoPlay } from './hooks';

describe(useAutoPlay, () => {
Expand All @@ -10,7 +11,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);

Expand All @@ -29,8 +30,47 @@ 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();
const controls = {
current: {
pause: noop,
resume: noop,
},
};

let visibilityState: Document['visibilityState'] = 'visible';

jest.spyOn(document, 'visibilityState', 'get').mockImplementation(() => visibilityState);

const res = renderHook(() =>
useAutoPlay({ timeout: 100, slideIndex: 0, onNext: callback, controls }),
);
jest.runAllTimers();
expect(callback).toHaveBeenCalledTimes(1);

// Останавливаем работу хука
controls.current.pause();
res.rerender();
// Срабатывает события visibilityChange
fireEvent(document, new Event('visibilitychange'));
jest.runAllTimers();
// Но callback не срабатыват по истечению таймеров
expect(callback).toHaveBeenCalledTimes(1);

// Восстанавливаем работу хука
controls.current.resume();
res.rerender();
// Срабатывает события visibilityChange
fireEvent(document, new Event('visibilitychange'));
jest.runAllTimers();
// callback срабатыват по истечению таймеров
expect(callback).toHaveBeenCalledTimes(2);
});
});
81 changes: 48 additions & 33 deletions packages/vkui/src/components/Gallery/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,66 @@
import { type RefObject, useImperativeHandle } from 'react';
import * as React from 'react';
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;
controls?: RefObject<{
pause: VoidFunction;
resume: VoidFunction;
}>;
}

export function useAutoPlay({ timeout, slideIndex, onNext, controls }: AutoPlayConfig): void {
const { document } = useDOM();
const callbackFn = useStableCallback(callbackFnProp);
const [paused, setPaused] = React.useState(false);
const timeoutRef = React.useRef<TimeoutId>(null);
const callbackFn = useStableCallback(onNext);

React.useEffect(
function initializeAutoPlay() {
if (!document || !timeout) {
return;
}
// Выносим функции очистки и старта таймера в отдельные функции
const clearAutoPlayTimeout = React.useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
}, []);

let timeoutId: TimeoutId = null;
const startAutoPlayTimeout = React.useCallback(() => {
if (!document || !timeout || paused) {
return;
}

const stop = () => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
};
if (document.visibilityState === 'visible') {
clearAutoPlayTimeout();
timeoutRef.current = setTimeout(callbackFn, timeout);
andrey-medvedev-vk marked this conversation as resolved.
Show resolved Hide resolved
} else {
clearAutoPlayTimeout();
}
}, [document, timeout, paused, clearAutoPlayTimeout, callbackFn]);

const start = () => {
switch (document.visibilityState) {
case 'visible':
stop();
timeoutId = setTimeout(callbackFn, timeout);
break;
case 'hidden':
stop();
}
};
useImperativeHandle(controls, () => ({
pause: () => setPaused(true),
resume: () => setPaused(false),
}));
inomdzhon marked this conversation as resolved.
Show resolved Hide resolved

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],
);
}
Loading