Skip to content

Commit

Permalink
fix(Galery): fix autoPlay when dragging slides (#7877)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
EldarMuhamethanov authored Nov 6, 2024
1 parent 5775640 commit 3661e98
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 36 deletions.
11 changes: 10 additions & 1 deletion packages/vkui/src/components/Gallery/Gallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -25,6 +26,8 @@ export const Gallery = ({
onChange,
bullets,
looped,
onDragStart,
onDragEnd,
...props
}: GalleryProps): React.ReactNode => {
const [localSlideIndex, setSlideIndex] = React.useState(initialSlideIndex);
Expand All @@ -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
Expand All @@ -71,6 +78,8 @@ export const Gallery = ({
<Component
dragDisabled={isControlled && !onChange}
{...props}
onDragStart={callMultiple(onDragStart, autoPlayControls.pause)}
onDragEnd={callMultiple(onDragEnd, autoPlayControls.resume)}
bullets={childCount > 0 && bullets}
slideIndex={safeSlideIndex}
onChange={handleChange}
Expand Down
35 changes: 33 additions & 2 deletions packages/vkui/src/components/Gallery/hooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);
});
});
82 changes: 49 additions & 33 deletions packages/vkui/src/components/Gallery/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TimeoutId>(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,
};
}

0 comments on commit 3661e98

Please sign in to comment.