import { effect, h, signal } from '@ui-arch/frame'; import type { SelectProps } from './types.js'; /*** 下拉选择:自绘弹层替代原生 select,value 信号双向绑定;支持 ↑↓/Home/End 键盘导航,点选项 / 点外部 / Esc 关闭 ***/ export function select(props: SelectProps): HTMLElement { // —— 状态集中声明 —— const open = signal(false); // 弹层开合 const value = props.value ?? signal(''); // 选中值;未传信号则内部持有 const labelOf = (v: string) => props.options.find((o) => o.value === v)?.label ?? ''; const current = () => labelOf(value()); // 触发器显示文案(响应式) const pick = (v: string) => { value(v); open(false); props.onChange?.(v); }; // —— 触发器 —— const trigger = h( 'button', { type: 'button', class: 'ua-select-trigger', 'aria-haspopup': 'listbox', 'aria-expanded': () => String(open()), disabled: props.disabled, // 布尔或信号禁用,h() 直接透传(原生 button 拦截点击) onClick: () => open(!open()), }, h('span', { class: () => `ua-select-value${value() ? '' : ' is-empty'}` }, () => current() || props.placeholder || ''), h('span', { class: 'ua-select-arrow' }, '▾'), ); // —— 选项弹层 —— const options = props.options.map((opt) => h( 'button', { type: 'button', class: () => `ua-option${opt.value === value() ? ' ua-option-selected' : ''}`, role: 'option', 'aria-selected': () => String(opt.value === value()), onClick: () => pick(opt.value), }, opt.label, ), ); const pop = h('div', { class: () => `ua-select-pop${open() ? ' ua-open' : ''}`, role: 'listbox' }, ...options); const root = h('div', { class: () => `ua-select${open() ? ' ua-open' : ''}` }, trigger, pop); // 根带开合类,供箭头旋转 root.addEventListener('keydown', (e: KeyboardEvent) => { if (e.key === 'Escape') { open(false); trigger.focus(); return; } // Esc 关闭并回焦触发器 if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); // 阻止页面滚动 if (!open()) { open(true); options[0]?.focus(); return; } // 未展开:展开并聚焦首个选项 const idx = options.indexOf(document.activeElement as HTMLElement); // 展开中:在选项间环形移动 const dir = e.key === 'ArrowDown' ? 1 : -1; const next = (idx + dir + options.length) % options.length; options[next]?.focus(); return; } if (e.key === 'Home' && open()) { e.preventDefault(); options[0]?.focus(); return; } // Home 跳首项 if (e.key === 'End' && open()) { e.preventDefault(); options[options.length - 1]?.focus(); } // End 跳末项 }); document.addEventListener('click', (e) => { if (root.contains(e.target as Node)) return; // 卫语句:组件内点击不处理 open(false); // 点外部关闭 }); effect(() => open()); // 建立依赖,保证开合信号生效 return root; }