diff --git a/.ua/backlog.json b/.ua/backlog.json index 30a7dee..7910c8b 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -703,5 +703,7 @@ } , { "id": "g43", "title": "select/menu/popover/image 的 Esc 接入统一弹层栈", "motivation": "四个弹层组件各自监听 document 的 Esc:多层叠加(如 popover 内再开 image 预览)时一次 Esc 全部关闭,与 modal/drawer 的 pushOverlay 栈行为不一致", "acceptance": "四个组件展开期入 Esc 栈、收起即释放;多层叠加时 Esc 后进先出逐层关闭;焦点归还触发器;npm run check 全绿", "status": "done", "notes": "自补:一致性修复;复用 overlay.ts 的 pushOverlay 栈" } + , + { "id": "g44", "title": "accordion 标题行方向键导航", "motivation": "标题是原生 button 只有 Enter/Space,行间移动要靠 Tab 逐个跳,与 tabs/tree 的 ↑↓/Home/End 键盘约定不一致", "acceptance": "↑↓ 在标题行间环形移动、Home/End 首尾直达,均跳过禁用行;焦点不在标题行不拦截;npm run check 全绿", "status": "done", "notes": "自补:可达性对齐;复用环形回绕取模模式" } ] } diff --git a/packages/skin/ui/src/accordion.ts b/packages/skin/ui/src/accordion.ts index 85ea33e..a320907 100644 --- a/packages/skin/ui/src/accordion.ts +++ b/packages/skin/ui/src/accordion.ts @@ -51,5 +51,25 @@ export function accordion(props: AccordionProps): HTMLElement { ), ); - return h('div', { class: 'ua-accordion' }, ...rows); + const root = h('div', { class: 'ua-accordion' }, ...rows); + + // —— 键盘导航:↑↓ 行间环形移动、Home/End 首尾直达,均跳过禁用行(对齐 tabs/tree 约定) —— + const heads = (): HTMLButtonElement[] => rows.map((r) => r.firstElementChild as HTMLButtonElement); // 各行标题按钮 + const focusFrom = (start: number, dir: 1 | -1): void => { + const len = heads().length; + for (let n = 1; n <= len; n++) { + const cand = heads()[(((start + dir * n) % len) + len) % len]; // 环形取候选:越界回绕保证落在 [0, len) + if (!cand.disabled) { cand.focus(); return; } // 跳过禁用行,绕满一圈即止 + } + }; + root.addEventListener('keydown', (e: KeyboardEvent) => { + const list = heads(); + const idx = list.indexOf(document.activeElement as HTMLButtonElement); + if (idx < 0) return; // 卫语句:焦点不在标题行 + if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); focusFrom(idx, e.key === 'ArrowDown' ? 1 : -1); } + else if (e.key === 'Home') { e.preventDefault(); focusFrom(-1, 1); } // 从头正向找第一个可用 + else if (e.key === 'End') { e.preventDefault(); focusFrom(0, -1); } // 从尾反向找最后一个可用 + }); + + return root; }