Skip to content

Commit

Permalink
feat: add autorotate
Browse files Browse the repository at this point in the history
  • Loading branch information
webmarkyn authored and nerdyman committed Nov 26, 2023
1 parent 4cb4c33 commit 3acd18b
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 9 deletions.
4 changes: 3 additions & 1 deletion example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ export const images = [
];

function App(props: Partial<ReactImageTurntableProps>) {
return <ReactImageTurntable images={images} {...props} />;
return (
<ReactImageTurntable images={images} autoRotate={{ enabled: true, speed: 200 }} {...props} />
);
}

export default App;
2 changes: 2 additions & 0 deletions src/ReactImageTurntable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ export const ReactImageTurntable: FC<ReactImageTurntableFullProps> = ({
style,
tabIndex = 0,
movementSensitivity = 20,
autoRotate = { enabled: false },
...props
}) => {
const { ref, activeImageIndex } = useTurntableState({
initialImageIndex,
imagesCount: images.length - 1,
movementSensitivity,
autoRotate,
});

const rootStyle: CSSProperties = {
Expand Down
31 changes: 30 additions & 1 deletion src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { useEffect, useRef, useState } from 'react';
import type { ReactImageTurntableProps } from './types';

interface UseTurntableStateProps
extends Required<Pick<ReactImageTurntableProps, 'initialImageIndex' | 'movementSensitivity'>> {
extends Required<
Pick<ReactImageTurntableProps, 'initialImageIndex' | 'movementSensitivity' | 'autoRotate'>
> {
/** Number of images starting from zero. */
imagesCount: number;
}
Expand All @@ -12,14 +14,38 @@ export const useTurntableState = ({
initialImageIndex,
imagesCount,
movementSensitivity,
autoRotate,
}: UseTurntableStateProps) => {
const [activeImageIndex, setActiveImageIndex] = useState(initialImageIndex);
const [hasUserInteracted, setHasUserInteracted] = useState(false);
const intervalIdRef = useRef<NodeJS.Timeout | null>(null);
const ref = useRef<HTMLDivElement>(null);

useEffect(() => {
setActiveImageIndex(initialImageIndex);
}, [initialImageIndex]);

const clearAutoRotateInterval = () => {
if (intervalIdRef.current) {
clearInterval(intervalIdRef.current);
intervalIdRef.current = null;
}
};

useEffect(() => {
if (autoRotate.enabled && !hasUserInteracted && !intervalIdRef.current) {
intervalIdRef.current = setInterval(() => {
setActiveImageIndex((prevIndex) => {
const nextIndex = prevIndex + 1;
return nextIndex > imagesCount ? 0 : nextIndex;
});
}, autoRotate.speed || 200);
}
if (hasUserInteracted) clearAutoRotateInterval();

return () => clearAutoRotateInterval();
}, [autoRotate, hasUserInteracted, imagesCount]);

useEffect(() => {
const target = ref.current as HTMLDivElement;
let prevDragPosition = 0;
Expand All @@ -33,6 +59,7 @@ export const useTurntableState = ({
};

const handleKeyDown = (ev: KeyboardEvent) => {
setHasUserInteracted(true);
if (ev.key === 'ArrowLeft') {
decrementActiveIndex();
} else if (ev.key === 'ArrowRight') {
Expand All @@ -41,6 +68,7 @@ export const useTurntableState = ({
};

const handlePointerMove = (ev: PointerEvent) => {
setHasUserInteracted(true);
const distanceDragged = prevDragPosition - ev.clientX;

if (distanceDragged <= -movementSensitivity) {
Expand All @@ -60,6 +88,7 @@ export const useTurntableState = ({
};

const handlePointerDown = (ev: PointerEvent) => {
setHasUserInteracted(true);
if (ev.button == 2) {
return;
}
Expand Down
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ export interface ReactImageTurntableProps {
initialImageIndex?: number;
/** The amount a "drag" has to move before an image changes to next or previous. */
movementSensitivity?: number;
autoRotate?: {
/** Automatically rotate the turntable. */
enabled: boolean;
/** The speed (in ms) at which the turntable rotates. */
speed?: number;
};
}

/** Base props *and* all available HTML element props. */
Expand Down
25 changes: 18 additions & 7 deletions test/ExampleRepo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,33 +80,44 @@ test.describe('Example Repo', () => {

test('Should navigate on pointer drag', async ({ page }) => {
const component = getComponentRoot(page);
const initialValueNow = await component.getAttribute('aria-valuenow').then(Number);

await component.click();
await expect(component).toBeFocused();

page.waitForTimeout(200);

// Should automatically rotate until mouse down is fired.
const lastValueNow = await component.getAttribute('aria-valuenow').then(Number);
await expect(lastValueNow).toBeGreaterThan(initialValueNow);

// Fire initial click to set dragging origin.
await page.mouse.move(512, 200);
await page.mouse.down();

page.waitForTimeout(200);
await expect(component).toHaveAttribute('aria-valuenow', lastValueNow.toString());

// Should navigate forwards when dragging right while mouse is down.
await expect(component).toHaveAttribute('aria-valuenow', '1');
await page.mouse.move(512 + 20, 200);
await expect(component).toHaveAttribute('aria-valuenow', '2');
await expect(component).toHaveAttribute('aria-valuenow', (lastValueNow + 1).toString());
await page.mouse.move(512 + 40, 200);
await expect(component).toHaveAttribute('aria-valuenow', '3');
await expect(component).toHaveAttribute('aria-valuenow', (lastValueNow + 2).toString());

// Should **not** navigate when mouse up is fired.
await page.mouse.up();
await page.mouse.move(512 + 60, 200);
await expect(component).toHaveAttribute('aria-valuenow', '3');
await expect(component).toHaveAttribute('aria-valuenow', (lastValueNow + 2).toString());
await page.mouse.down();

// Should navigate backwards when dragging right while mouse is down.
await page.mouse.move(512 + 20, 200);
await expect(component).toHaveAttribute('aria-valuenow', '2');
await expect(component).toHaveAttribute('aria-valuenow', (lastValueNow + 1).toString());
await page.mouse.move(512, 200);
await expect(component).toHaveAttribute('aria-valuenow', '1');
await page.mouse.move(512 - 20, 200);
await expect(component).toHaveAttribute('aria-valuenow', lastValueNow.toString());
for (let i = 0; i < lastValueNow; i++) {
await page.mouse.move(512 - i * 20, 200);
}
await expect(component).toHaveAttribute('aria-valuenow', '36');

await page.mouse.up();
Expand Down

0 comments on commit 3acd18b

Please sign in to comment.