正确实现搜索框与下拉列表需使用 role="combobox" 的可聚焦输入框,关联 role="listbox" 容器及 role="option" 选项,并通过键盘事件手动管理焦点、选中态和展开状态,确保无障碍与跨端兼容。

搜索框与下拉列表的 HTML 结构必须满足可聚焦和语义化
纯 <div> 套 <code><ul></ul> 的“伪下拉”无法被键盘自然访问,屏幕阅读器也难以识别。正确起点是让输入框本身可聚焦、有明确的 role="combobox",并用 aria-expanded 和 aria-controls 关联下拉容器。
关键点:
-
<input type="text" role="combobox" aria-expanded="false" aria-controls="suggestions-list">—— 必须显式声明角色和控制关系 - 下拉容器用
<div role="listbox" id="suggestions-list">,不用 <code><ul></ul>(语义不匹配) - 每条建议项用
<div role="option" data-value="xxx">,不能用 <code><li>(除非包裹在role="listbox"内且设role="none",但更易出错) - 初始状态建议容器
display: none或visibility: hidden,但不能移除 DOM —— 否则aria-controls失效 - 对
<input>绑定keydown,检测event.key为"ArrowDown"/"ArrowUp"/"Enter"/"Escape" -
event.preventDefault()必须调用,否则浏览器会执行原生行为(如光标移动、表单提交) - 用
querySelectorAll('[role="option"]')获取全部选项,维护一个当前索引(currentIndex),按方向键增减并调用optionEl.focus() -
Enter时取当前聚焦项的dataset.value填入输入框,并触发搜索;Escape时收起下拉、恢复输入框焦点 - 所有
[role="option"]必须设tabindex="-1",确保能被脚本聚焦但不参与自然 Tab 顺序 - 鼠标点击某选项时,除了填值、收起下拉,还要立即执行
optionEl.focus(),把焦点“锚定”住 - 监听
input的blur事件:若下拉处于展开态,延迟setTimeout(() => { if (!container.matches(':hover')) hide() }, 200),给用户留出移到下拉区域的时间 - 避免在
mousedown时直接preventDefault—— 这会干扰屏幕阅读器的点击读取 - 方向键导航的
focus()必须发生在keydown回调内,不能挪到异步操作(如 debounce 后)里 - 不要依赖
setTimeout延迟聚焦 —— iOS 下大概率失败 - 如果用了虚拟滚动或动态渲染建议项,确保新插入的
[role="option"]元素在挂载后立刻设好tabindex="-1",否则focus()无效
键盘导航必须接管 ArrowUp/ArrowDown + Enter/Escape 的默认行为
浏览器不会自动为自定义下拉实现方向键导航。你得监听 keydown,阻止默认行为,并手动管理焦点和选中态。
常见错误现象:ArrowDown 触发了页面滚动、Enter 提交了表单、Tab 跳出了下拉直接到下一个表单控件。
实操建议:
focus 管理要防住“焦点丢失”导致导航中断
用户用鼠标点击某条建议,或 Tab 离开输入框再返回,都可能让当前焦点脱离下拉结构,键盘导航就断了。
这不是小问题:Chrome 和 Firefox 对 role="option" 元素的焦点支持不一致,某些情况下点击后焦点不会自动落到 option 上。
解决方案:
移动端 Safari 的 focus() 行为需要降级兼容
iOS Safari 在非用户手势触发的 JS 中调用 element.focus() 会被静默忽略,导致方向键导航在真机上完全失效。
这不是 bug,是策略限制:它只允许在 click、touchend、keydown(且 key 是 Enter/Space)等明确用户意图的事件回调里聚焦。
所以:
aria-activedescendant 的使用场景 —— 它适用于输入框自身保持焦点、仅通过属性指示“当前高亮项”的模式(比如复杂富文本编辑器),而搜索建议这种需要真实聚焦选项以便屏幕阅读器朗读完整内容的,还是老实用 focus() 更稳妥。











