元素被移除时焦点需要有承接目标
别名: 焦点承接 · focus restoration on removal · 焦点丢失 · activeElement removed
概念解释
焦点是文档里指向某一个节点的指针。当这个节点被删掉、卸载、过滤出列表或提交后从树上消失,指针无处可指。浏览器通常把焦点丢到 body,或置于一种「没有活动元素」的状态。下一次按键从页顶重新开始,读屏报出整页标题,用户不知道刚才的任务做到哪了。承接目标(focus successor)是在移除发生之前就定好的下一个可聚焦节点:相邻项、父容器、邻近的主操作,用来接住那根指针。
它处理的是「正握着焦点的那个节点被销毁」这一刻,不是浮层关闭后把焦点还回触发按钮,也不是路由切换后把焦点放到新页的主内容。
机制
document.activeElement 必须引用一棵仍然连在文档上的节点。移除(remove()、框架卸载、虚拟列表回收、display: none 导致不可聚焦)使这个引用失效。规范没有要求引擎把焦点移到「逻辑上下一项」;常见回退是 body,移动端 WebView 上甚至会暂时没有焦点。键盘用户的下一次 Tab 于是从文档起始处的第一个可聚焦节点走,空间方向键则可能完全无响应。屏幕阅读器跟随焦点,会把这次跳跃朗读成一次新的页面定位。
承接必须抢在移除之前:先算下一项(删除列表项时是下一项或上一项,空列表时是「添加」或列表容器),对它 focus(),再删。先删再找,活动元素已经是 body,再移动就会造成一次可被察觉的焦点闪烁,读屏会报两次。承接节点本身必须可聚焦;tabindex="-1" 的容器可以程序聚焦,但不能假定用户能 Tab 到一个从未进入顺序的装饰性包装。
边界
用户焦点根本不在被删节点上时,不必强行移动——去抢别人正在用的控件会造成更严重的打断。自动刷新的表格若每秒重绘当前行,频繁把焦点塞给「下一项」会让键盘无法停留。无限滚动回收视口外节点时,若焦点还在被回收的行上,承接规则仍然适用,不能只因为「还在同一份数据里」就假定焦点还在。原生 <dialog> 关闭、路由卸载整棵树,属于把焦点交还触发点或新页入口的另一种交接,不要和「列表中删掉当前项」共用同一个后继启发式。富文本、canvas 内部的自定义插入符不走 activeElement,需要自己的插入符承接。
怎么落地
- 在会销毁当前焦点的操作里(删除、提交后消失、筛选掉当前行、wizard 走完一步卸载面板),先指定承接节点并
focus(),再改 DOM。 - 列表删除:下一项,否则上一项,否则列表的空态操作或列表容器(
tabindex="-1")。表单提交成功后控件消失:焦点移到结果标题或下一项任务,不要留在body。 - 不要在无关节点上抢焦点;不要先删再补救。
- 验证:把焦点放在将被移除的控件上,执行移除,立刻看
document.activeElement是不是你指定的承接者,且该节点可见。按一次 Tab,确认不会跳到页顶 logo。用读屏听这次移动是否只报一次、报的是不是后继而不是整页。