头图

浅谈 :hover 伪类选择器在 touch 环境如何禁止生效的问题

:hover 伪类选择器用于定义元素被鼠标悬停时的样式,但它在 touch 触屏操作的环境下表现就不是那么让人满意了,主要是触屏操作并不像鼠标那样有所谓的悬停状态,所以得点击后才能生效,它也没有像鼠标挪开这样的概念,所以得等到元素失焦后才会失效,这就造成了有时样式并不符合预期结果的情况,因此有人会希望触屏操作时不要让这个选择器生效,而本篇文章就来探讨一下如何实现这一点。


01. 使用 @media 媒体查询

Media Queries Level 4 开始,浏览器新增了 hover 的查询条件,用于检测当前环境的 "主要" 输入设备是否支持 :hover 选择器,它有 hovernone 两个值,前者是符合而后者是不符合,因此使用 @media (hover:hover){ } 即可实现支持 :hover 选择器的判断,而使用 @media (hover:none){ } 则可以实现不支持 :hover 选择器的判断,下面是一个关于该查询条件的简单案例。

<style>
#button01:hover{background-color:#f00;color:#fff;}
@media (hover:hover){ #button02:hover{background-color:#f00;color:#fff;} }
</style>

<button id="button01" type="button">button01</button>
<button id="button02" type="button">button02</button>

↓ View & Code ↑

以上截图是在 Windows7 的 Chrome 中操作的结果,我们为两个按钮都设置了 :hover 样式,当样式生效时背景会变红而文字会变白,但第二个按钮的样式生效条件是主要输入设备支持 :hover 选择器才行,在 PC 模式下由于使用的是鼠标,所以两个按钮在悬停时样式都会生效,但之后我们切换为移动模拟状态,此时主要输入设备变换为触屏,因此第二个按钮在被点击后 :hover 样式就不会生效了。

这很完美?其实不然,首先是这个媒体查询条件在一些低版本的浏览器中不被支持,例如说 IE11 就不支持,存在些许兼容问题,其次是有些既支持鼠标又支持触屏的设备如 Surface,在判断 "主要" 输入设备时有问题,它们默认的主要输入设备是触屏,但插上鼠标后这个判断却不会自动变化,所以就会导致这个查询条件也无法生效,你可以参考 patrickhlauke.github.io 了解更多关于查询异常的信息。


02. 使用 JavaScript 监听

使用媒体查询的手段虽然简单,却存在着一些兼容问题,虽说随着浏览器的升级,在未来兼容肯定会有所改善,但目前确是无可奈何,如果你希望兼容性更好一些,或许可以试着借助 JS,最简单的做法,就是通过绑定 mousemove 事件和 touchstart 事件,当判断到了 mousemove 是在 touchstart 触发了 500 毫秒后才触发的,那就算是使用了鼠标,否则就是使用了触屏,下面是一个简单的例子。

<script>
(function(){

// 代码来自于 StackOverflow,XJ 进行了些许的调整
// https://stackoverflow.com/a/30303898/8079246/
var isMouse = false;  // 是否处于使用 mouse 状态
var lastTouched = 0;  // touchstart 最后触发时间

// 在 touchstart 触发 500ms 后才触发了 mousemove 
// 就算是使用了鼠标设备,因此添加 isMouse 的类名
document.addEventListener('mousemove', function(){
    if(isMouse === true || Date.now() - lastTouched <= 500){ return };
    document.body.classList.add('isMouse');
    isMouse = true;
}, true);

// 每次触发了 touchstart 就更新 lastTouched 变量
// 每次触发 touchstart 就移除 isMouse 的类名状态
document.addEventListener('touchstart', function(){
    lastTouched = Date.now();
    if(isMouse === false){ return };
    document.body.classList.remove('isMouse');
    isMouse = false;
}, true);

})();
</script>

<style>
#button01:hover{background-color:#f00;color:#fff;}
.isMouse #button02:hover{background-color:#f00;color:#fff;}
</style>

<button id="button01" type="button">button01</button>
<button id="button02" type="button">button02</button>

↓ View & Code ↑

以上截图是在 Windows7 的 Chrome 中操作的结果,我们为两个按钮都设置了 :hover 样式,当样式生效时背景会变红而文字会变白,但第二个按钮的样式生效条件是存在 isMouse 类名,也就是有鼠标时才生效,在 PC 模式下由于使用的是鼠标,所以两个按钮在悬停时样式都会生效,但之后我们切换为移动模拟状态,此时 JS 判断到没有鼠标,因此第二个按钮在被点击后 :hover 样式就不会生效。

这样就行了吧?并不是呢!这种判断存在一个问题,就是当目标节点被按住时,如果按住的时间超过 500 毫秒,它就会误判为是使用了鼠标设备,并且触屏设备在长按时往往会变成圈选文本或弹出菜单,此时操作就可能会出错,在上面的截图中,最后也展现了长按导致 #button02 错误响应的情况,那么要如何解决呢?这涉及到了不同浏览器的交互细节,实在是不好处理,建议直接使用现成的插件吧。


03. 使用 xj.operate 方案

XJ 自己编写了一个 xj.operate 插件用于解决本文所聚焦的问题,它有以下三个特点,首先是它存在的目的为了分清当前操作究竟是鼠标还是触屏,而不是仅用于判断是否支持 :hover,所以运用范围就更广泛了一些,其次是它解决了长按会导致误判的问题,但你也可以通过设置来让长按触发 :hover 变成一种特性,最后是它提供了一些方法,允许我们临时性的转换状态,以便于实现一些特殊需求。

<!-- 在引入插件后,如果当前环境是使用鼠标进行操作的,html 标签会被添加 xj-operate-mouse 的类名 -->
<script src="https://cdn.jsdelivr.net/gh/xjZone/xj.operate@0.6.0/dist/xj.operate.min.js"></script>

<style>
#button01:hover{background-color:#f00;color:#fff;}
.xj-operate-mouse #button02:hover{background-color:#f00;color:#fff;}
</style>

<button id="button01" type="button">button01</button>
<button id="button02" type="button">button02</button>

↓ View & Code ↑

上面的代码和第二章的代码是相同的,只是 JS 改成了插件的 CDN,类名从 isMouse 改成了 xj-operate-mouse,在截图中我们可以看到,在触屏环境下即使长按 #button02 也不会出现错误响应的情况了,如果你觉得 xj-operate-mouse 类名太长了,还是希望使用较短的 isMouse 或其他自定义类名,插件也能通过配置来实现,关于插件更多的细节,可以查看插件的文档,这里就不展开讲了。

实际上业界中也存在其他用于解决 hover 查询兼容的方案,例如说较为出名的 mq4-hover-shim,但根据 XJ 的实测,发现这个方案也不是很好用,它借助 window.matchMedia() 去判断浏览器是否支持 hover 查询,但我们在上面第一章节中就说过,有些设备在进行查询时会出现错误,所以 window.matchMedia() 的判断可能跟着出错,因此意义并不大,相比之下或许 xj.operate 还更加靠谱些。


参考内容

张鑫旭 - CSS any-hover any-pointer media 查询与交互体验提升
Patrick H. Lauke - 交互媒体特性及其潜力(对于不正确的假设)

Microsoft - @media 的 hover 查询在 Win11 的 Surface 中无效
bugs.chromium - @media 的 hover 查询在在 Win10 设备上无效

StackOverflow - 如何删除或忽略在触摸设备上的悬停样式
StackOverflow - 如何防止触摸设备上按钮的粘滞悬停效果
StackOverflow - 如何在移动端的浏览器上禁用掉悬停效果

MDN - @media hover
MDN - @media any-hover

MDN - @media pointer
MDN - @media any-pointer

MDN - Pointer events
MDN - PointerEvent

patrickhlauke - 对于 @media 媒体查询的 hover / pointer 条件在各种浏览器中的结果测试
patrickhlauke - Touch 相关事件和 Pointer 相关事件在不同环境和不同浏览器中测试的结果

玄魂 - (翻译)整合鼠标、触摸和触控笔事件的 Pointer Event Api
ACGTOFE - 把鼠标、触摸屏、触控笔统一起来,Pointer Events 介绍

xiaoc_ - Surface 笔记本上的手势事件
xiaoc_ - Surface 笔记本上的手势事件

XJ.Chen - xj.operate


xjArea
12 声望0 粉丝