Files
UI/packages/skin/ui/src/carousel.ts
T

69 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { effect, h, signal } from '@ui-arch/frame';
import type { CarouselProps } from './types.js';
/*** 轮播:items 横排 + translateX 位移;指示点 / 左右箭头 / ←→ 键切换,autoplay 毫秒自动前进且悬停暂停 ***/
export function carousel(props: CarouselProps): HTMLElement {
// —— 状态集中声明 ——
const total = props.items.length;
const index = props.index ?? signal(0); // 未传外部信号时用内部信号,行为一致
const wrap = (i: number) => (total ? ((i % total) + total) % total : 0); // 环形取模,负数也回绕
const dots: HTMLButtonElement[] = []; // 指示点,顺序即下标
const track = h('div', { class: 'ua-carousel-track' }) as HTMLElement; // 幻灯片轨道,靠 transform 位移
const dotsBar = h('div', { class: 'ua-carousel-dots' }) as HTMLElement;
/*** 跳到指定下标(自动环形),并回传 onChange ***/
const go = (i: number) => {
if (!total) return; // 卫语句:无内容不动作
index(wrap(i));
props.onChange?.(index());
};
props.items.forEach((item, i) => {
track.append(h('div', { class: 'ua-carousel-slide', role: 'group', 'aria-roledescription': '幻灯片', 'aria-label': `第 ${i + 1} 张,共 ${total} 张` }, item));
const dot = h('button', { type: 'button', class: 'ua-carousel-dot', 'aria-label': `跳到第 ${i + 1} 张`, onClick: () => go(i) }) as HTMLButtonElement;
dots.push(dot);
dotsBar.append(dot);
});
/*** 生成左右箭头 ***/
const arrow = (dir: 1 | -1, label: string) =>
h('button', { type: 'button', class: `ua-carousel-arrow ua-carousel-${dir === 1 ? 'next' : 'prev'}`, 'aria-label': label, onClick: () => go(index() + dir) }, dir === 1 ? '›' : '‹');
const root = h(
'div',
{ class: 'ua-carousel', tabindex: '0', role: 'region', 'aria-roledescription': '轮播', 'aria-label': props.label ?? '轮播' },
track,
arrow(-1, '上一张'),
arrow(1, '下一张'),
dotsBar,
) as HTMLElement;
if (props.height) root.style.height = `${props.height}px`; // 固定高度由调用方指定,缺省随内容
effect(() => {
const i = wrap(index());
track.style.transform = `translateX(${-i * 100}%)`; // 整屏位移,配合 CSS transition 出滑动效果
dots.forEach((dot, k) => {
dot.classList.toggle('ua-carousel-dot-active', k === i);
dot.setAttribute('aria-current', String(k === i)); // 读屏标注当前张
});
});
root.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return; // 卫语句:只接管方向键
e.preventDefault();
e.stopPropagation(); // 不冒泡:避免外层 tabs/menu 同时响应
go(index() + (e.key === 'ArrowRight' ? 1 : -1));
});
if (props.autoplay && total > 1) {
let hover = false; // 悬停中
let focused = false; // 焦点在轮播内(键盘用户没有 hover,必须单独计)
root.addEventListener('pointerenter', () => { hover = true; });
root.addEventListener('pointerleave', () => { hover = false; });
root.addEventListener('focusin', () => { focused = true; });
root.addEventListener('focusout', () => { focused = false; });
window.setInterval(() => { if (!hover && !focused) go(index() + 1); }, props.autoplay); // 两者都不在才前进
}
return root;
}