Skip to content

被偷走的焦点

背景:鼠标悬停后键盘焦点位置丢失,鼠标与键盘操作不一致。

过程:悬停更新键盘焦点,保持鼠标与键盘焦点同步。


你用键盘上下选到第三项,伸手用鼠标点了一下第五项,再按键盘往下——咦,怎么从第四项开始?

"因为有两个焦点,"外交官说,"一个是视觉焦点——鼠标悬停高亮。另一个是键盘焦点——按上下键时从哪开始移动。写代码的人忘了同步,两个就各玩各的。"

menu.ts 里,键盘事件更新 keyboardIndex,鼠标悬停只加 hover 类:

typescript
function onMouseOver(item) {
    item.classList.add('hover');       // 忘了更新 keyboardIndex
}

"所以用户觉得'我明明选了第五项,按往下键怎么跳到第四项'?"AI 问。

"就是这样。键盘焦点和鼠标焦点脱节了。"


为什么会忘

"写代码的时候,先想键盘怎么用,再想鼠标怎么用,"外交官说,"两个是分开想的,就容易忘同步。"

"但用户的心智模型里,焦点就是焦点,没有'键盘焦点'和'鼠标焦点'之分。"

"说得好——实现细节不要泄露到用户体验里。"


怎么修

鼠标悬停时更新 keyboardIndex,一行代码:

typescript
function onMouseOver(item, index) {
    keyboardIndex = index;  // 同步键盘焦点
    updateHighlight();
}

只有鼠标→键盘这个方向有问题。键盘操作时鼠标没动,悬停位置不变,用户不会觉得奇怪。


可访问性

"视障用户靠键盘导航,如果键盘焦点和视觉焦点不一致……"AI 意识到。

"会更严重,"外交官说,"不过我们的 SlideMenu 用的是 div + 自定义键盘事件,不是原生 focus。屏幕阅读器的支持是另一个层级的问题——可访问性是一个大工程,要单独做。"

先修眼前的——同步键盘和鼠标。长远的,以后再说。


本质:一致性

"同一扇门,推也能开、拉也能开,但推开是左边、拉开是右边?"

"就是这个感觉,"外交官笑了,"说不出哪里不对,但就是觉得别扭。"

好的 UI 是——不管你用什么方式操作,结果都是你预期的。


第十五颗石子

第十五颗黄石子落进"已处理"的堆里。中优项十五颗,全部清了。

这最后一颗,只加了一行代码。但它代表的是"一致性"——最小的一颗,意义不小。

"中优项全了。剩下的呢?"

"三十颗绿石子——低优,"外交官看着桌上那一小堆,"做了更好,不做也行。但小事也是事。一颗一颗捡,捡一颗是一颗。"

他伸手拿起第一颗绿石子。


附录:键盘交互设计检查清单

检查项说明
方向键导航上/下/左/右键可在菜单项间移动
回车确认Enter 触发选中项操作
Esc 退出Escape 关闭菜单/弹窗
鼠标键盘同步鼠标悬停位置 = 键盘焦点位置
初始焦点菜单打开时焦点在合理的默认位置
循环导航到最后一项再往下回到第一项
可访问性使用原生 focus 或 aria-activedescendant

教训:用户的心智模型里,焦点就是焦点。程序员为了"方便"拆成两个,拆完了忘了同步,用户就懵了。实现细节不要泄露到用户体验里。