React/Preact 如何将焦点作为状态函数实现日期选择器无障碍焦点管理
焦点管理的核心思路
- 不要把焦点当成纯命令式的DOM操作来写,要把焦点指向的语义标识作为受控状态收敛到顶层Calendar组件,把“DOM上的
focus()调用”当成状态变化后的副作用。这完全符合UI = f(state)的核心逻辑:状态决定哪个元素应该被聚焦,DOM层面的调用只是把状态同步到真实界面的动作。 - 顶层Calendar只需要维护三类核心内容,从根源上避免焦点混乱:
- 状态层:当前激活的视图(CenturyView/YearView/MonthView)、当前焦点对应的语义值(根据视图不同,对应具体年份、月份、日期)
- 引用层:用
useRef维护一个映射表,key是语义化的标识符(比如year-2024、month-05、date-2024-05-20),value是对应的DOM元素。这个映射表不需要放到响应式状态里,因为DOM节点的注册/注销是实时操作,不需要触发重渲染。 - 注册方法:对外暴露一个
registerNode(key, element)的方法,让各个子组件在渲染每个可聚焦元素的时候,把对应的DOM节点注册到顶层的映射表里。
- 这种收敛式的状态管理模式,比手动在各个子组件之间透传ref、调用
focus()的方式要稳定得多,后期改导航规则也只需要改顶层的逻辑,不用动各个子组件的代码。
跨兄弟组件的焦点切换实现
- 因为所有焦点逻辑都收敛在顶层,兄弟组件之间完全不需要持有对方的引用、也不需要跨组件通信,切换流程非常简单:
- 要从MonthView切到YearView的时候,只需要更新顶层状态:把激活视图切为YearView,同时把焦点语义值设为当前选中日期对应的月份值
- YearView组件挂载完成后,在
useEffect里根据当前的焦点语义值,从顶层的节点映射表里找到对应的DOM元素,执行focus()动作即可
- 这个模式彻底规避了兄弟组件乱传引用导致的焦点丢失、错乱问题:不管多少个视图、多少层嵌套,所有的焦点跳转指令都从顶层发出,子组件只负责把状态同步到自己的DOM上。
键盘方向键导航的实现方案
- 把所有键盘事件的监听统一绑定在Calendar组件的根DOM节点上,不要分散到各个子组件单独监听。利用事件冒泡机制,不管当前焦点落在哪个子组件的元素上,键盘事件都会冒泡到根节点,统一处理:
- 在根节点的键盘事件处理函数里,根据当前激活的视图、当前焦点值,计算出下一个需要聚焦的语义值
- 直接更新顶层的
focusedValue状态即可,后续的焦点落地、DOM操作完全交给各个子组件的副作用处理
- 各视图的导航规则可以直接对齐WAI-ARIA的日期选择器规范,常规逻辑包括:
- MonthView场景:左右方向键逐天切换,上下方向键逐周切换,Enter/Space键确认选中,Esc键触发返回/关闭弹窗
- YearView场景:左右方向键逐月份切换,上下方向键按行切换月份
- CenturyView场景:方向键按行列切换可选择的年份
- 几个容易踩的坑:
- 方向键导航时不要因为跨月/跨年就立刻触发视图切换,先只更新焦点值,等用户按下确认键再切换视图,不然会因为DOM重挂载导致焦点丢失
- 弹窗形态的日期选择器关闭时,一定要把焦点回退到触发弹窗的源按钮,不要让焦点直接停留在body上
- 可聚焦的元素要加上正确的
tabindex,确保Tab键导航的顺序和视觉顺序一致。
内容的提问来源于stack exchange,提问作者Harshal Patil
相关产品推荐
相关产品推荐

