import { effect, h } from '@ui-arch/frame'; import type { SegmentedProps } from './types.js'; /*** 分段控制器:胶囊分段条 + 滑动滑块,value 信号双向绑定,←→/Home/End 环形导航,整组或单项可禁用 ***/ export function segmented(props: SegmentedProps): HTMLElement { // —— 状态集中声明 —— const buttons: HTMLButtonElement[] = []; // 分段按钮,顺序与 options 一致 const thumb = h('span', { class: 'ua-seg-thumb', 'aria-hidden': 'true' }); // 滑块:仅装饰,不参与可访问性树 const root = h('div', { class: `ua-segmented ua-segmented-${props.size ?? 'md'}`, role: 'radiogroup' }, thumb); /*** 整组禁用判定:布尔直接取,信号在此读取以纳入 effect 依赖 ***/ const groupOff = (): boolean => (typeof props.disabled === 'function' ? props.disabled() : props.disabled === true); /*** 同步选中态、禁用态与滑块几何(挂载前量不到宽度,由 rAF 二次测量兜底) ***/ const sync = (): void => { const off = groupOff(); const box = root.getBoundingClientRect(); root.classList.toggle('is-disabled', off); for (const btn of buttons) { const on = btn.dataset.value === props.value(); const dead = off || btn.dataset.dead === '1'; // 整组禁用或单项禁用 btn.classList.toggle('is-active', on); btn.setAttribute('aria-checked', String(on)); btn.setAttribute('aria-disabled', String(dead)); btn.disabled = dead; btn.tabIndex = on ? 0 : -1; // 漫游 tabindex:仅选中项可 Tab 聚焦 if (!on) continue; // 滑块只跟选中项走 const rect = btn.getBoundingClientRect(); thumb.style.width = `${rect.width}px`; thumb.style.transform = `translateX(${rect.left - box.left - root.clientLeft}px)`; // 减去容器边框,滑块与按钮严丝合缝 } }; for (const opt of props.options) { const btn = h('button', { type: 'button', class: 'ua-seg-btn', role: 'radio' }, opt.label) as HTMLButtonElement; btn.dataset.value = opt.value; if (opt.disabled) btn.dataset.dead = '1'; // 单项禁用标记,交给 sync 统一置 disabled btn.addEventListener('click', () => { if (btn.disabled) return; // 卫语句:禁用项不响应 props.value(opt.value); // 用户切换写回信号 props.onChange?.(opt.value); }); buttons.push(btn); root.append(btn); } root.addEventListener('keydown', (e: KeyboardEvent) => { const map: Record = { ArrowRight: 1, ArrowLeft: -1 }; // 左右移动,首尾环形 const steer = map[e.key]; if (!steer) 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 + steer + list.length) % list.length]; // 环形取模 next.focus(); next.click(); }); effect(sync); requestAnimationFrame(sync); // 挂载后再量一次布局,否则滑块宽度为 0 return root; }