iter(g29): 新增 carousel 轮播组件
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
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-label': `第 ${i + 1} 张` }, 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-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 paused = false; // 悬停期间暂停自动播放
|
||||
root.addEventListener('pointerenter', () => { paused = true; });
|
||||
root.addEventListener('pointerleave', () => { paused = false; });
|
||||
window.setInterval(() => { if (!paused) go(index() + 1); }, props.autoplay);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
@@ -32,6 +32,7 @@ export { rate } from './rate.js';
|
||||
export { empty } from './empty.js';
|
||||
export { divider } from './divider.js';
|
||||
export { list } from './list.js';
|
||||
export { carousel } from './carousel.js';
|
||||
export { descriptions } from './descriptions.js';
|
||||
export { statistic } from './statistic.js';
|
||||
export { timeline } from './timeline.js';
|
||||
|
||||
@@ -420,4 +420,14 @@ export interface ListProps {
|
||||
onItemClick?: (item: ListItem, index: number) => void; // 条目点击回调;提供后条目可点击并带 hover 反馈
|
||||
}
|
||||
|
||||
/*** 轮播入参 ***/
|
||||
export interface CarouselProps {
|
||||
items: unknown[]; // 每张幻灯片内容,顺序即播放顺序
|
||||
index?: Signal<number>; // 当前下标信号;不传则内部维护
|
||||
autoplay?: number; // 自动前进间隔(毫秒);缺省不自动播放
|
||||
height?: number; // 固定高度像素;缺省由内容撑开
|
||||
label?: string; // 区域无障碍名称;缺省「轮播」
|
||||
onChange?: (index: number) => void; // 切换回调(点击/箭头/键盘/自动都触发)
|
||||
}
|
||||
|
||||
export type { ColumnDef, RowKey }; // 再导出常用内核类型,方便调用方
|
||||
|
||||
Reference in New Issue
Block a user