feat: dots() self-gating fallback control module
This commit is contained in:
+47
@@ -0,0 +1,47 @@
|
|||||||
|
import type { Carousel } from './index.ts';
|
||||||
|
|
||||||
|
const NOOP = () => {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wire the developer's existing dot elements to a carousel.
|
||||||
|
* Progressive fallback: no-ops where native ::scroll-marker is supported, so the
|
||||||
|
* native marker group is the sole control there (no double dots).
|
||||||
|
* @param c - carousel instance
|
||||||
|
* @param container - element whose children are the dot controls
|
||||||
|
* @returns cleanup function (a noop when native markers are used)
|
||||||
|
*/
|
||||||
|
export function dots(c: Carousel, container: HTMLElement): () => void {
|
||||||
|
// Native markers present → let CSS own the controls.
|
||||||
|
if (typeof CSS !== 'undefined' && CSS.supports('selector(::scroll-marker)')) {
|
||||||
|
return NOOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = Array.from(container.children) as HTMLElement[];
|
||||||
|
|
||||||
|
const handlers = items.map((el, i) => {
|
||||||
|
const h = () => c.scrollToIndex(i);
|
||||||
|
el.addEventListener('click', h);
|
||||||
|
return h;
|
||||||
|
});
|
||||||
|
|
||||||
|
function mark(active: number) {
|
||||||
|
items.forEach((el, i) => {
|
||||||
|
if (i === active) {
|
||||||
|
el.setAttribute('aria-current', 'true');
|
||||||
|
el.classList.add('active');
|
||||||
|
} else {
|
||||||
|
el.removeAttribute('aria-current');
|
||||||
|
el.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
mark(c.index);
|
||||||
|
const off = c.on('change', mark);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
off();
|
||||||
|
items.forEach((el, i) => {
|
||||||
|
el.removeEventListener('click', handlers[i]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { afterEach, expect, test, vi } from 'vitest';
|
||||||
|
import { dots } from '../src/dots.ts';
|
||||||
|
|
||||||
|
function fakeCarousel() {
|
||||||
|
let cb: (i: number) => void = () => {};
|
||||||
|
return {
|
||||||
|
index: 0,
|
||||||
|
count: 3,
|
||||||
|
next: vi.fn(),
|
||||||
|
prev: vi.fn(),
|
||||||
|
scrollToIndex: vi.fn(),
|
||||||
|
on: (_e: string, fn: (i: number) => void) => {
|
||||||
|
cb = fn;
|
||||||
|
return () => {};
|
||||||
|
},
|
||||||
|
destroy: vi.fn(),
|
||||||
|
fire: (i: number) => cb(i),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function dotContainer() {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.innerHTML = '<button></button><button></button><button></button>';
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clicking a dot scrolls to its index', () => {
|
||||||
|
const c = fakeCarousel();
|
||||||
|
const container = dotContainer();
|
||||||
|
dots(c as never, container);
|
||||||
|
(container.children[2] as HTMLButtonElement).click();
|
||||||
|
expect(c.scrollToIndex).toHaveBeenCalledWith(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('change marks the active dot with aria-current', () => {
|
||||||
|
const c = fakeCarousel();
|
||||||
|
const container = dotContainer();
|
||||||
|
dots(c as never, container);
|
||||||
|
c.fire(1);
|
||||||
|
expect(container.children[1].getAttribute('aria-current')).toBe('true');
|
||||||
|
expect(container.children[0].hasAttribute('aria-current')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no-ops (no listeners wired) when native ::scroll-marker is supported', () => {
|
||||||
|
vi.stubGlobal('CSS', { supports: () => true }); // jsdom has no CSS global
|
||||||
|
|
||||||
|
const c = fakeCarousel();
|
||||||
|
const container = dotContainer();
|
||||||
|
const cleanup = dots(c as never, container);
|
||||||
|
(container.children[0] as HTMLButtonElement).click();
|
||||||
|
expect(c.scrollToIndex).not.toHaveBeenCalled();
|
||||||
|
expect(typeof cleanup).toBe('function'); // noop cleanup, safe to call
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user