多选列表框要能纯键盘操作,需要同时处理三件事:焦点在选项间移动、选中状态与视觉同步、读屏能读出当前项。缺一件,键盘操作就会中断。

键位分工
| 按键 | 行为 | 焦点变化 |
|---|---|---|
| 上下方向键 | 在选项间移动 | 移动 |
| 空格 | 切换当前项选中 | 不移动 |
| Enter | 确认选择并关闭 | 不移动 |
| Escape | 放弃变更并关闭 | 不移动 |
| Tab | 离开整个控件 | 移出 |
上下键移动的是活动项,与已选中项是两套状态。把两者混用会导致方向键一按就改变选中结果。
结构
<ul role='listbox' aria-multiselectable='true' tabindex='0'>
<li role='option' aria-selected='false' id='opt-1'>选项一</li>
<li role='option' aria-selected='false' id='opt-2'>选项二</li>
</ul>
listbox 承担焦点,option 只表达选中状态。焦点始终落在容器上,活动项用 aria-activedescendant 指向当前 option 的 id。
状态处理
list.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') { move(1); e.preventDefault(); }
if (e.key === ' ') { toggle(active); e.preventDefault(); }
});
方向键需要调用 preventDefault,否则页面会跟着滚动。空格同理,默认行为是翻页。
边界处理
- 移到首尾时停止,不做环绕,避免焦点跳到另一端造成误操作。
- 活动项必须滚动进可视区,用 scrollIntoView 配合 block 取 nearest。
- 关闭后把焦点还给触发元素,键盘操作才不会丢失位置。
- 选中变化需要同步更新 aria-selected,只改样式对读屏无效。
