为什么调用 .focus() 后父元素的 :has(:focus-visible) 不匹配?
结论
.focus() 会尝试将焦点移到目标元素,但不会强制它匹配 :focus-visible。在示例中,点击触发按钮后,目标按钮可能已经获得焦点,却没有匹配 :focus-visible,父元素的 div:has(:focus-visible) 也就不会匹配。
浏览器会判断当前情境是否需要显示焦点提示,因此不能仅凭这个现象认定 :has() 失效。如果要求容器内有元素获得焦点时就显示红色边框,使用 :focus-within 更合适。
为什么按钮和输入框表现不同?
:focus 表示元素当前拥有焦点;:focus-visible 还要求浏览器判断当前情境需要显示焦点提示。
浏览器通常会结合交互方式、元素类型和此前的焦点状态来判断:
- 使用 Tab 等键盘操作聚焦按钮时,按钮通常会匹配
:focus-visible,帮助用户识别键盘焦点的位置。 - 使用鼠标点击触发按钮,再通过脚本聚焦另一个按钮时,目标按钮可能只匹配
:focus。 - 文本输入框需要接收键盘输入,即使通过鼠标或脚本获得焦点,也通常会匹配
:focus-visible。
脚本移动焦点时,此前获得焦点的元素是否匹配 :focus-visible 也可能影响结果。因此,不能认为通过脚本聚焦的按钮都不会匹配。同一段 .focus() 代码在不同交互路径下可能得到不同结果,具体行为也可能受浏览器和用户设置影响。
() 在这里如何匹配?
原来的选择器:
div:has(:focus-visible) {
border: 2px solid red;
}
表示选择内部存在匹配 :focus-visible 的后代元素的 div。
它不会改变后代元素的焦点状态,也不会让已经获得焦点的按钮自动匹配 :focus-visible。如果目标按钮只有 :focus 状态,父元素就不满足这个条件。
所有聚焦方式都需要显示边框时,使用
将容器样式改为:
div {
border: 1px solid black;
margin: 10px 0;
}
div:focus-within {
border: 2px solid red;
}
只要 div 自身或其后代元素拥有焦点,就会匹配 :focus-within,后代无需同时匹配 :focus-visible。原来的 JavaScript 不需要修改。
如果希望继续用 :has(),也可以写成:
div:has(:focus) {
border: 2px solid red;
}
在示例结构中,这样也能根据子元素是否获得焦点来显示边框。区别在于,div:has(:focus) 检查后代元素,而 div:focus-within 也包含 div 自身获得焦点的情况。
如何确认焦点与选择器状态?
可以在现有按钮点击回调中,紧接着 .focus() 检查状态:
document.querySelector('#trigger-button').addEventListener('click', () => {
const button = document.querySelector('#button');
button.focus();
console.log('active:', document.activeElement === button);
console.log(':focus:', button.matches(':focus'));
console.log(':focus-visible:', button.matches(':focus-visible'));
console.log(
'parent matches:',
button.parentElement.matches('div:has(:focus-visible)')
);
});
如果前两项为 true,而 :focus-visible 为 false,说明聚焦已经成功,差异出在 :focus-visible 的匹配条件上。如果 document.activeElement 也不是目标按钮,需要先排查元素是否可以获得焦点。
复现时应分别测试鼠标点击触发和键盘操作触发。在开发者工具控制台里单独调用 .focus(),交互上下文可能与原示例不同。调整容器边框时,也应保留清晰的键盘焦点提示,避免直接全局移除 outline。
备注:内容仅供参考。
