UNPKG

@evermade/overflow-slider

Version:

Accessible slider that is powered by overflow: auto.

234 lines (232 loc) 9.54 kB
/** * Autoplay plugin for Overflow Slider * * Loops slides infinitely, always respects reduced-motion, * provides Play/Pause controls, and shows a progress bar. * * @param {AutoplayPluginArgs} args * @returns {(slider: Slider) => void} */ function AutoplayPlugin(args) { return (slider) => { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q; const opts = { delayInMs: (_a = args === null || args === void 0 ? void 0 : args.delayInMs) !== null && _a !== void 0 ? _a : 5000, texts: { play: (_c = (_b = args === null || args === void 0 ? void 0 : args.texts) === null || _b === void 0 ? void 0 : _b.play) !== null && _c !== void 0 ? _c : 'Play', pause: (_e = (_d = args === null || args === void 0 ? void 0 : args.texts) === null || _d === void 0 ? void 0 : _d.pause) !== null && _e !== void 0 ? _e : 'Pause', }, icons: { play: (_g = (_f = args === null || args === void 0 ? void 0 : args.icons) === null || _f === void 0 ? void 0 : _f.play) !== null && _g !== void 0 ? _g : '<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393"/></svg>', pause: (_j = (_h = args === null || args === void 0 ? void 0 : args.icons) === null || _h === void 0 ? void 0 : _h.pause) !== null && _j !== void 0 ? _j : '<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5m5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5"/></svg>', }, container: (_k = args === null || args === void 0 ? void 0 : args.container) !== null && _k !== void 0 ? _k : null, classNames: { autoplayButton: (_m = (_l = args === null || args === void 0 ? void 0 : args.classNames) === null || _l === void 0 ? void 0 : _l.autoplayButton) !== null && _m !== void 0 ? _m : 'overflow-slider__autoplay', }, movementType: (_o = args === null || args === void 0 ? void 0 : args.movementType) !== null && _o !== void 0 ? _o : 'view', stopOnHover: (_p = args === null || args === void 0 ? void 0 : args.stopOnHover) !== null && _p !== void 0 ? _p : true, loop: (_q = args === null || args === void 0 ? void 0 : args.loop) !== null && _q !== void 0 ? _q : true, }; let intervalId = null; let rafId = null; let startTime = 0; let manualPause = false; // a11y: respect reduced motion preference if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { return; } /** * Create Play/Pause button and insert into DOM * @private * @returns {HTMLButtonElement} */ const createButton = () => { var _a; const btn = document.createElement('button'); btn.type = 'button'; btn.className = opts.classNames.autoplayButton; setButtonPaused(btn); // initialize CSS var btn.style.setProperty('--autoplay-delay-progress', '0'); if (opts.container) { opts.container.appendChild(btn); } else { (_a = slider.container.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(btn, slider.container); } // click toggles manual play/pause btn.addEventListener('click', () => { if (intervalId) { manualPause = true; stop(); } else { manualPause = false; start(); } }); // always pause on hover/focus (but don't clear manualPause) const pausableInteractionStart = ['focusin']; if (opts.stopOnHover) { pausableInteractionStart.push('mouseenter'); } const pausableInteractionEnd = ['focusout']; if (opts.stopOnHover) { pausableInteractionEnd.push('mouseleave'); } pausableInteractionStart.forEach(evt => slider.container.addEventListener(evt, () => { if (intervalId) stop(); })); pausableInteractionEnd.forEach(evt => slider.container.addEventListener(evt, () => { if (!intervalId && !manualPause) start(); })); return btn; }; const btn = createButton(); /** * Compute next slide, reset timer * * @private */ function scrollNext() { if (opts.movementType === 'view') { const scrollLeft = slider.getScrollLeft(); const viewWidth = slider.getInclusiveClientWidth(); const totalWidth = slider.getInclusiveScrollWidth(); if (scrollLeft + viewWidth >= totalWidth) { if (opts.loop) { slider.moveToSlide(0); } else { stop(); btn.style.setProperty('--autoplay-delay-progress', '0'); } } else { slider.moveToDirection('next'); } } else { const nextIdx = (slider.activeSlideIdx + 1) % slider.details.slideCount; if (slider.canMoveToSlide(nextIdx)) { slider.moveToSlide(nextIdx); } else { if (opts.loop) { slider.moveToSlide(0); } else { stop(); btn.style.setProperty('--autoplay-delay-progress', '0'); } } } // reset progress timer startTime = performance.now(); // restart the progress loop if (rafId !== null) { cancelAnimationFrame(rafId); } tick(); } /** * Animation-loop to update CSS var * * @private */ function tick() { const now = performance.now(); const pct = Math.min(((now - startTime) / opts.delayInMs) * 100, 100); btn.style.setProperty('--autoplay-delay-progress', `${Math.round(pct)}`); if (pct < 100) { rafId = requestAnimationFrame(tick); } } /** * Start autoplay * * @returns {void} */ function start() { if (intervalId) clearInterval(intervalId); if (rafId) cancelAnimationFrame(rafId); setButtonPlaying(btn); // reset timer and animate progress startTime = performance.now(); tick(); intervalId = window.setInterval(scrollNext, opts.delayInMs); } /** * Stop autoplay * * @param {boolean} [fromManual=false] Whether this stop was user‐initiated * @returns {void} */ function stop(fromManual = false) { if (intervalId) clearInterval(intervalId); if (rafId) cancelAnimationFrame(rafId); intervalId = rafId = null; if (fromManual) { manualPause = true; } setButtonPaused(btn); } /** * Set button state to “playing” * * @private * @param {HTMLElement} b */ function setButtonPlaying(b) { b.setAttribute('aria-pressed', 'true'); b.setAttribute('aria-label', opts.texts.pause); const frag = document.createRange() .createContextualFragment(opts.icons.pause); b.innerHTML = ''; b.appendChild(frag); } /** * Set button state to “paused” * * @private * @param {HTMLElement} b */ function setButtonPaused(b) { b.setAttribute('aria-pressed', 'false'); b.setAttribute('aria-label', opts.texts.play); const frag = document.createRange() .createContextualFragment(opts.icons.play); b.innerHTML = ''; b.appendChild(frag); b.style.setProperty('--autoplay-delay-progress', '0'); } /** * Pause/resume when slider enters/leaves viewport * @private */ function observeVisibility() { const observer = new IntersectionObserver(entries => { for (const e of entries) { if (!e.isIntersecting && intervalId) { stop(); } else if (e.isIntersecting && !intervalId && !manualPause) { start(); } } }, { threshold: 0 }); observer.observe(slider.container); } // Initialize autoplay start(); observeVisibility(); }; } export { AutoplayPlugin as default };