Feat/carousel impl #1
@@ -0,0 +1,48 @@
|
||||
import type { Carousel } from './index.ts';
|
||||
|
||||
/** Options for autoplay. */
|
||||
export type AutoplayOptions = {
|
||||
/** ms between advances */
|
||||
interval: number;
|
||||
/** element whose hover/focus pauses playback (usually the track wrapper) */
|
||||
root: HTMLElement;
|
||||
};
|
||||
|
||||
const prefersReducedMotion = () =>
|
||||
typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
/**
|
||||
* Auto-advance a carousel, pausing on hover/focus. Never starts when the user
|
||||
* prefers reduced motion (WCAG / respects OS setting). A visible pause control
|
||||
* (WCAG 2.2.2) is the developer's responsibility — wire it to the returned stop().
|
||||
* @param c - carousel instance
|
||||
* @param opts - interval (ms) and the root element to bind pause events to
|
||||
* @returns stop function that halts playback and removes listeners
|
||||
*/
|
||||
export function autoplay(c: Carousel, opts: AutoplayOptions): () => void {
|
||||
let timer: ReturnType<typeof setInterval> | undefined;
|
||||
const reduce = prefersReducedMotion();
|
||||
|
||||
const tick = () => c.next();
|
||||
const start = () => {
|
||||
if (!reduce) timer ??= setInterval(tick, opts.interval);
|
||||
};
|
||||
const pause = () => {
|
||||
clearInterval(timer);
|
||||
timer = undefined;
|
||||
};
|
||||
|
||||
opts.root.addEventListener('pointerenter', pause);
|
||||
opts.root.addEventListener('pointerleave', start);
|
||||
opts.root.addEventListener('focusin', pause);
|
||||
opts.root.addEventListener('focusout', start);
|
||||
start();
|
||||
|
||||
return () => {
|
||||
pause();
|
||||
opts.root.removeEventListener('pointerenter', pause);
|
||||
opts.root.removeEventListener('pointerleave', start);
|
||||
opts.root.removeEventListener('focusin', pause);
|
||||
opts.root.removeEventListener('focusout', start);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
|
||||
import { autoplay } from '../src/autoplay.ts';
|
||||
|
||||
function fakeCarousel(count = 3) {
|
||||
let index = 0;
|
||||
return {
|
||||
get index() {
|
||||
return index;
|
||||
},
|
||||
count,
|
||||
next: vi.fn(() => {
|
||||
index = (index + 1) % count;
|
||||
}),
|
||||
prev: vi.fn(),
|
||||
scrollToIndex: vi.fn(),
|
||||
on: () => () => {},
|
||||
destroy: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
// jsdom has no matchMedia — stub it. reduce = whether reduce-motion matches.
|
||||
function stubMatchMedia(reduce: boolean) {
|
||||
vi.stubGlobal('matchMedia', (q: string) => ({
|
||||
matches: reduce && q.includes('reduce'),
|
||||
media: q,
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
}));
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
stubMatchMedia(false);
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
test('advances every interval', () => {
|
||||
const c = fakeCarousel();
|
||||
autoplay(c as never, { interval: 1000, root: document.createElement('div') });
|
||||
vi.advanceTimersByTime(2000);
|
||||
expect(c.next).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('pause on pointerenter, resume on pointerleave', () => {
|
||||
const c = fakeCarousel();
|
||||
const root = document.createElement('div');
|
||||
autoplay(c as never, { interval: 1000, root });
|
||||
root.dispatchEvent(new Event('pointerenter'));
|
||||
vi.advanceTimersByTime(3000);
|
||||
expect(c.next).not.toHaveBeenCalled();
|
||||
root.dispatchEvent(new Event('pointerleave'));
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(c.next).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('does not start under prefers-reduced-motion', () => {
|
||||
stubMatchMedia(true);
|
||||
const c = fakeCarousel();
|
||||
autoplay(c as never, { interval: 1000, root: document.createElement('div') });
|
||||
vi.advanceTimersByTime(5000);
|
||||
expect(c.next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('stop() halts and removes listeners', () => {
|
||||
const c = fakeCarousel();
|
||||
const stop = autoplay(c as never, { interval: 1000, root: document.createElement('div') });
|
||||
stop();
|
||||
vi.advanceTimersByTime(5000);
|
||||
expect(c.next).not.toHaveBeenCalled();
|
||||
});
|
||||
Reference in New Issue
Block a user