0de9b6e66e
- core/table-core: Headless 表格内核(排序/筛选/分页/选择插件 + 冒烟 9 项) - core/frame: signal/computed/effect + hyperscript 微框架(零依赖) - skin/ui: 16 个 DOM 组件(含自绘 select、滑动指示条 tabs、行选择 dataTable、form 表单校验) - design/tokens: --ua-* 设计令牌(light/dark) - tools/cli: ua 命令行(index/list/info/docs/style/mcp)+ MCP 注册表服务 - apps: demo 演示站(esbuild 单文件)+ docs 双轨文档(人类版 + llms-full.txt)
55 lines
2.2 KiB
TypeScript
55 lines
2.2 KiB
TypeScript
import { effect, h } from '@ui-arch/frame';
|
|
import type { TabsProps } from './types.js';
|
|
|
|
/*** 标签页:active 信号双向绑定 + 滑动指示条 + 方向键环形导航;内容由调用方按 active 派生 ***/
|
|
export function tabs(props: TabsProps): HTMLElement {
|
|
// —— 状态集中声明 ——
|
|
const buttons: HTMLButtonElement[] = []; // 页签按钮,顺序与 tabs 一致
|
|
const indicator = h('span', { class: 'ua-tabs-indicator' });
|
|
const head = h('div', { class: 'ua-tabs-head', role: 'tablist' }, indicator);
|
|
|
|
/*** 同步激活态与指示条几何位置 ***/
|
|
const sync = () => {
|
|
const value = props.active?.();
|
|
for (const btn of buttons) {
|
|
const on = btn.dataset.value === value;
|
|
btn.classList.toggle('ua-tab-active', on);
|
|
btn.setAttribute('aria-selected', String(on));
|
|
btn.tabIndex = on ? 0 : -1; // 漫游 tabindex:仅激活页签可 Tab 聚焦
|
|
if (on) {
|
|
indicator.style.left = `${btn.offsetLeft}px`;
|
|
indicator.style.width = `${btn.offsetWidth}px`;
|
|
}
|
|
}
|
|
};
|
|
|
|
for (const tab of props.tabs) {
|
|
const btn = h('button', { class: 'ua-tab', role: 'tab', type: 'button' }, tab.label) as HTMLButtonElement;
|
|
btn.dataset.value = tab.value;
|
|
btn.disabled = tab.disabled ?? false;
|
|
btn.addEventListener('click', () => {
|
|
if (btn.disabled) return; // 卫语句:禁用页签不响应
|
|
if (props.active) props.active(tab.value); // 用户切换写回信号
|
|
props.onChange?.(tab.value);
|
|
});
|
|
buttons.push(btn);
|
|
head.append(btn);
|
|
}
|
|
|
|
head.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
const delta = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0;
|
|
if (!delta) return; // 卫语句:非方向键放行
|
|
const list = buttons.filter((b) => !b.disabled);
|
|
const i = list.indexOf(document.activeElement as HTMLButtonElement);
|
|
if (i < 0) return; // 卫语句:焦点不在页签上
|
|
e.preventDefault();
|
|
const next = list[(i + delta + list.length) % list.length]; // 环形移动
|
|
next.focus();
|
|
next.click();
|
|
});
|
|
|
|
effect(sync);
|
|
requestAnimationFrame(sync); // 挂载后再量一次布局,否则指示条宽度为 0
|
|
return h('div', { class: 'ua-tabs' }, head);
|
|
}
|