Appearance
被偷走的焦点
背景:鼠标悬停后键盘焦点位置丢失,鼠标与键盘操作不一致。
过程:悬停更新键盘焦点,保持鼠标与键盘焦点同步。
你用键盘上下选到第三项,伸手用鼠标点了一下第五项,再按键盘往下——咦,怎么从第四项开始?
"因为有两个焦点,"外交官说,"一个是视觉焦点——鼠标悬停高亮。另一个是键盘焦点——按上下键时从哪开始移动。写代码的人忘了同步,两个就各玩各的。"
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 |
教训:用户的心智模型里,焦点就是焦点。程序员为了"方便"拆成两个,拆完了忘了同步,用户就懵了。实现细节不要泄露到用户体验里。