69 lines
2.1 KiB
TypeScript
69 lines
2.1 KiB
TypeScript
/** Rotation timing from the spec ("Featured experts carousel"). */
|
|
export const FEATURED_INTERVAL_MS = 2800;
|
|
|
|
export type FeaturedSlot = "c0" | "l1" | "r1" | "l2" | "r2" | "l3" | "r3";
|
|
|
|
/**
|
|
* Where item `i` sits when `current` is in the center: by its signed offset
|
|
* from the current index, wrapped so the shorter way round wins. With two
|
|
* items the other one is a single right neighbor, never an empty pair.
|
|
*/
|
|
export function featuredSlot(i: number, current: number, count: number): FeaturedSlot {
|
|
let off = (((i - current) % count) + count) % count;
|
|
if (off > count / 2) off -= count;
|
|
if (off === 0) return "c0";
|
|
if (off === 1) return "r1";
|
|
if (off === -1) return "l1";
|
|
if (off === 2) return "r2";
|
|
if (off === -2) return "l2";
|
|
return off > 0 ? "r3" : "l3";
|
|
}
|
|
|
|
export interface FeaturedAutoplay {
|
|
readonly index: number;
|
|
readonly paused: boolean;
|
|
pause(): void;
|
|
resume(): void;
|
|
stop(): void;
|
|
}
|
|
|
|
/**
|
|
* Advances the index every `intervalMs` while not paused. A single item never
|
|
* rotates. The caller stops it on unmount.
|
|
*/
|
|
export function startFeaturedAutoplay(opts: {
|
|
count: number;
|
|
onChange: (index: number) => void;
|
|
intervalMs?: number;
|
|
start?: number;
|
|
}): FeaturedAutoplay {
|
|
const { count, onChange, intervalMs = FEATURED_INTERVAL_MS } = opts;
|
|
let index = opts.start ?? 0;
|
|
let paused = false;
|
|
const timer =
|
|
count > 1
|
|
? setInterval(() => {
|
|
if (paused) return;
|
|
index = (index + 1) % count;
|
|
onChange(index);
|
|
}, intervalMs)
|
|
: null;
|
|
return {
|
|
get index() { return index; },
|
|
get paused() { return paused; },
|
|
pause() { paused = true; },
|
|
resume() { paused = false; },
|
|
stop() { if (timer) clearInterval(timer); },
|
|
};
|
|
}
|
|
|
|
/**
|
|
* WAI-ARIA carousel pattern: a rotating carousel must not announce every
|
|
* slide, so the region is `off` while autoplay runs and `polite` when the
|
|
* person has paused it (hover, focus) or it never rotates (reduced motion,
|
|
* a single expert).
|
|
*/
|
|
export function featuredLiveMode(opts: { rotating: boolean; paused: boolean }): "off" | "polite" {
|
|
return opts.rotating && !opts.paused ? "off" : "polite";
|
|
}
|