如何在触摸设备上防止按钮粘滞悬停效果

213

我创建了一个旋转木马,并添加了上一个和下一个按钮,这些按钮始终可见。这些按钮有悬停状态,它们会变成蓝色。在iPad等触摸设备上,悬停状态是粘性的,因此在点击后按钮会保持蓝色。但我不希望出现这种情况。

  • 我可以为每个按钮添加一个no-hover类,并在ontouchend时更改CSS如下:button:not(.no-hover):hover { background-color: blue; } 但这可能对性能影响较大,并且不能正确处理像Chromebook Pixel(具有触摸屏和鼠标)这样的设备。

  • 我可以向documentElement添加touch类,并将我的CSS更改如下:html:not(.touch) button:hover { background-color: blue;} 但是这也不能在同时具有触摸和鼠标的设备上正确工作。

我更喜欢在ontouchend时删除悬停状态。但似乎这是不可能的。将焦点放在另一个元素上并不能删除悬停状态。手动点击其他元素可以,但我似乎无法在JavaScript中触发该事件。

我找到的所有解决方案都不完美。是否有完美的解决方案?


6
https://css-tricks.com/solving-sticky-hover-states-with-media-hover-hover/ - dasfdsa
这是一个相当不错的解决方案,@dasfdsa!然而,它并不适用于同时支持触摸屏和鼠标的设备。 - Chris
如果使用Tailwind CSS,它会像这样:[@media(hover:hover)]:hover:bg-red-400。只有在前面的任意变量为真时,它才会应用该悬停样式。 - zenw0lf
30个回答

165

由于CSS媒体查询4级的这部分现在已经自2018年以来被广泛实现, 您可以使用以下内容:

@media (hover: hover) {
    button:hover {
        background-color: blue;
    }
}

或者用英文说:"如果浏览器支持真正的悬停(例如有类似鼠标的主要输入设备),则在悬停在上时应用此样式。"

对于没有实现此功能的浏览器(或在原始回答发布时没有实现的浏览器),我编写了一个polyfill来处理这个问题。使用它,您可以将上述未来的CSS转换为:

html.my-true-hover button:hover {
    background-color: blue;
}

(一种变体的.no-touch技术)然后使用来自检测悬停支持的同一polyfill的一些客户端JavaScript,您可以相应地切换my-true-hover类的存在。
$(document).on('mq4hsChange', function (e) {
    $(document.documentElement).toggleClass('my-true-hover', e.trueHover);
});

谢谢!对于我的需求来说,这在大多数浏览器中都得到了支持http://caniuse.com/#search=media%20queries,并且运行得非常出色,谢谢! - ragamufin
4
请参考 http://caniuse.com/#feat=css-media-interaction,你会发现它在Firefox和IE11中不被支持 :( 因此你需要使用polyfill。 - CherryDT
7
现在在移动浏览器中广泛支持,并且效果非常好。我认为这应该是被接受的答案。 - Trevor Burnham
3
这并没有真正解决双输入设备的问题。(我是从Surface Book 2上写这篇文章的。)你可以通过触摸在按下按钮后移动鼠标/触摸板,但这只是一个不完美的解决方案。我们真正需要的是一个仅对指针悬停有效的伪类。 - Coderer
1
不得不在每个悬停规则周围放置笨重的@media块真是太糟糕了 - 他们真的搞砸了这个API。 - Glenn Maynard
显示剩余3条评论

57

您可以通过在DOM中暂时删除链接来移除悬停状态。请参见http://testbug.handcraft.com/ipad.html


CSS中有:

:hover {background:red;}

在JS代码中,您有以下内容:

function fix()
{
    var el = this;
    var par = el.parentNode;
    var next = el.nextSibling;
    par.removeChild(el);
    setTimeout(function() {par.insertBefore(el, next);}, 0)
}

然后在你的 HTML 中有:

<a href="#" ontouchend="this.onclick=fix">test</a>

3
@Chris 好的,你说得对。我把示例改成在ontouchend事件中设置onclick处理程序。 - Sjoerd Visscher
4
请考虑在您的答案中添加最少量的演示代码。谢谢!http://stackoverflow.com/help/how-to-answer - 2540625
1
@SjoerdVisscher 我已经将代码粘贴在答案中了。StackOverflow 喜欢代码出现在答案中,因为链接可能会失效。(而且在这种情况下,需要不仅要点击链接,还需要查看源代码,并找出哪些部分是所讨论的技术)。 - Darren Cook
2
@KevinBorders 是的,对于某些设备,在移除和重新插入元素之间的时间延迟可能会非常明显。不幸的是,在我的 Android 4.4 设备上,如果没有使用 setTimeout 就这样做是行不通的。 - Rodney
1
@DarrenCook 但是这样删除元素再重新添加是否是个好主意呢?我猜这会导致“卡顿”并且与应用所需的60fps丝般顺畅的要求产生冲突。 - Ethan
显示剩余6条评论

30

这是一个常见的问题,没有完美的解决方案。鼠标悬停行为在使用鼠标时非常有用,但在触摸操作时大多数情况下会产生负面影响。使问题更加严重的是,某些设备支持触摸和鼠标(同时!)例如Chromebook Pixel和Surface。

我发现最干净的解决方案是仅在设备不支持触摸输入时启用悬停行为。

var isTouch =  !!("ontouchstart" in window) || window.navigator.msMaxTouchPoints > 0;

if( !isTouch ){
    // add class which defines hover behavior
}

确实,在一些设备上(可能支持hover)你会失去hover的效果,但是有时hover的影响不仅仅局限于链接本身,例如,也许当鼠标悬停在元素上时您想要显示一个菜单。这种方法允许您测试触摸功能的存在,并可能有条件地附加不同的事件。

我已经在iPhone、iPad、Chromebook Pixel、Surface和各种Android设备上进行了测试。我不能保证它在添加了通用USB触摸输入设备(如笔)时能够正常工作。


1
太棒了,这对我有用,不像社区维基/Darren Cook的回答。 - Jannik
1
好答案。这是一个类似的解决方案:https://dev59.com/gmQm5IYBdhLWcg3w6ShR#39787268 - Lorenzo Polidori
不错的解决方案!不过对于同时支持触摸和鼠标的设备无效。 - Chris

19

您可以覆盖不支持悬停的设备的悬停效果。像这样:

.my-thing {
    color: #BADA55;
}

.my-thing:hover {
    color: hotpink;
}

@media (hover: none) {
    .my-thing {
        color: #BADA55;
    }
}

已在iOS 12上进行测试和验证

感谢https://dev59.com/IGgu5IYBdhLWcg3wWV2F#50285058指出这一点。


1
这是更现代的解决方案,不需要JavaScript或DOM操作,具有完整的浏览器支持(除IE外),应该被接受的答案。 - funkju
另请查看:https://css-tricks.com/solving-sticky-hover-states-with-media-hover-hover/ - dasfdsa
1
这也没有解决双模设备(触摸屏笔记本电脑等)的问题。如果浏览器同时具有触摸和指针输入,则“hover:none”将不成立。 - Coderer

15

从2020年开始,您可以在媒体查询内添加悬停样式

@media (hover: hover) and (pointer: fine) {
    /* css hover class/style */
}

这个媒体查询表示样式将在不模拟:hover的浏览器上工作,因此它不适用于触摸浏览器。


1
这适用于仅支持触摸的浏览器,即没有其他指针的触摸屏。它不能解决例如触摸屏笔记本电脑的问题。 - Coderer
你在这里使用 and (pointer: fine) 覆盖了哪些情况?仅使用 (hover: hover) 是否足够? - Jo Liss

10

使用Modernizr,您可以专门针对非触摸设备定位悬停效果:

(注意:此示例不适用于 StackOverflow 的代码片段系统,请查看 jsfiddle

/* this one is sticky */
#regular:hover, #regular:active {
  opacity: 0.5;
}

/* this one isn't */
html.no-touch #no-touch:hover, #no-touch:active {
  opacity: 0.5;
}

请注意,:active 不需要使用 .no-touch 来定位,因为它在移动端和桌面端都可以正常工作。

这是我认为最好的答案,但示例是错误的。它显示了触摸和非触摸设备的相同悬停状态。只有在html标签上存在.no-touch时,它才应用悬停状态。否则,此答案获得我的认可。 - morrisbret
3
问题在于现在到处都是带有触摸屏幕的鼠标设备,所以你不能再依赖这种方法了。但我并没有看到其他更好的方法……这真是一个两难的境地。 - dudewad
我想,其中一种方法是同时使用Modernizr和一个库,例如mobile-detect.js,以确保它是手机或平板电脑。 - Gavin
你是救星啊,非常感谢!这个在移动设备上完美运行了。 - Shivam
这并不能解决多输入设备的问题。no-touch类被应用于我的Surface Book 2(触摸板+触摸屏),但是:hover规则在你的fiddle链接中仍然存在。 - Coderer

9
$("#elementwithhover").click(function() { 
  // code that makes element or parent slide or otherwise move out from under mouse. 

  $(this).clone(true).insertAfter($(this));
  $(this).remove();
});

你可以使用on()来改进这个答案,而不是click()。通过从DOM中删除元素并重新附加它,我失去了点击事件。当我使用on重写我的函数时,绑定到元素,然后删除和添加就可以工作了。例如:`$('body').on('click', '#elementwithhover',function() { // clone, insertafter, remove }); 如果你做出这个改变,我会投赞成票的。 - Will Lanni
clone true 会保留新元素上的点击事件,该新元素取代了被卡住的悬停元素。原始元素在克隆后从 DOM 中被移除。 - Vernard Sloggett
太棒了,兄弟!十倍感谢。 - Kaloyan Stamatov
搜索了好几个小时终于找到了解决方法,非常感谢! - phil917

8

来自解决移动设备上粘滞悬停的4种方法:这是一种根据用户当前输入类型动态添加或删除文档中的"can touch"类的方法。它也适用于混合设备,用户可能在触摸和鼠标/触控板之间切换:

<script>

;(function(){
    var isTouch = false //var to indicate current input type (is touch versus no touch) 
    var isTouchTimer 
    var curRootClass = '' //var indicating current document root class ("can-touch" or "")

    function addtouchclass(e){
        clearTimeout(isTouchTimer)
        isTouch = true
        if (curRootClass != 'can-touch'){ //add "can-touch' class if it's not already present
            curRootClass = 'can-touch'
            document.documentElement.classList.add(curRootClass)
        }
        isTouchTimer = setTimeout(function(){isTouch = false}, 500) //maintain "istouch" state for 500ms so removetouchclass doesn't get fired immediately following a touch event
    }

    function removetouchclass(e){
        if (!isTouch && curRootClass == 'can-touch'){ //remove 'can-touch' class if not triggered by a touch event and class is present
            isTouch = false
            curRootClass = ''
            document.documentElement.classList.remove('can-touch')
        }
    }

    document.addEventListener('touchstart', addtouchclass, false) //this event only gets called when input type is touch
    document.addEventListener('mouseover', removetouchclass, false) //this event gets called when input type is everything from touch to mouse/ trackpad
})();

</script>

这是我发现的最好的方法,如果你将计时器增加到1000毫秒,还可以覆盖长按功能,请参见这里。太棒了! - lowtechsun

3
我本来想发布自己的解决方案,但是在检查是否有人已经发布了它之后,我发现 @Rodney 差不多做到了。然而,他错过了最后一个关键点,使它变得无用,至少在我的情况下是这样。我的意思是,我也通过 mouseentermouseleave 事件检测添加/删除了相同的 .fakeHover 类,但仅凭这一点,它几乎完全像 "真正的" :hover。我的意思是:当您在表格中单击元素时,它不会检测到您已经“离开”它 - 因此保持“fakehover”状态。
我所做的就是简单地监听 click,因此当我“点击”按钮时,我手动触发了 mouseleave
这是我的最终代码:
.fakeHover {
    background-color: blue;
}


$(document).on('mouseenter', 'button.myButton',function(){
    $(this).addClass('fakeHover');
});

$(document).on('mouseleave', 'button.myButton',function(){
    $(this).removeClass('fakeHover');
});

$(document).on('button.myButton, 'click', function(){
    $(this).mouseleave();
});

这样一来,当您使用鼠标简单“悬停”在按钮上时,就可以保留您通常的hover功能。好吧,几乎全部都是:唯一的缺点是,在用鼠标点击按钮后,它不会处于hover状态。就像您点击并迅速将指针移出按钮一样。但对我来说,这已经足够了。


3
这是我在研究其他答案后想到的解决方案,可以支持仅触摸、仅鼠标或混合使用的用户。
为悬停效果创建单独的“hover”类。默认情况下将此“hover”类添加到我们的按钮中。
我们不希望在一开始检测到触摸支持并禁用所有悬停效果。正如其他人所提到的,混合设备正在变得越来越流行;人们可能具有触摸支持,但想要使用鼠标,反之亦然。因此,只有当用户实际触摸按钮时才删除悬停类。
接下来的问题是,如果用户想在触摸按钮后继续使用鼠标怎么办?为了解决这个问题,我们需要找到一个适当的时机来重新添加已删除的悬停类。
但是,我们不能立即添加它,因为悬停状态仍然处于活动状态。我们也可能不想销毁并重新创建整个按钮。
因此,我想到了使用忙等算法(使用setInterval)来检查悬停状态。一旦悬停状态被停用,我们就可以添加回悬停类并停止忙等,将我们带回原始状态,使用户可以使用鼠标或触摸。
我知道忙等并不是很好,但我不确定是否有适当的事件。我考虑在mouseleave事件中将其添加回去,但它并不是非常健壮。例如,在触摸按钮后出现警报时,鼠标位置会移动,但mouseleave事件不会触发。

var button = document.getElementById('myButton');

button.ontouchstart = function(e) {
  console.log('ontouchstart');
  $('.button').removeClass('button-hover');
  startIntervalToResetHover();
};

button.onclick = function(e) {
  console.log('onclick');
}

var intervalId;

function startIntervalToResetHover() {
  // Clear the previous one, if any.
  if (intervalId) {
    clearInterval(intervalId);
  }
  
  intervalId = setInterval(function() {
    // Stop if the hover class already exists.
    if ($('.button').hasClass('button-hover')) {
      clearInterval(intervalId);
      intervalId = null;
      return;
    }
    
    // Checking of hover state from 
    // https://dev59.com/omox5IYBdhLWcg3ww3GV#8981521.
    var isHovered = !!$('.button').filter(function() {
      return $(this).is(":hover");
    }).length;
    
    if (isHovered) {
      console.log('Hover state is active');
    } else {
      console.log('Hover state is inactive');
      $('.button').addClass('button-hover');
      console.log('Added back the button-hover class');
      
      clearInterval(intervalId);
      intervalId = null;
    }
  }, 1000);
}
.button {
  color: green;
  border: none;
}

.button-hover:hover {
  background: yellow;
  border: none;
}

.button:active {
  border: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button id='myButton' class='button button-hover'>Hello</button>

编辑:我尝试的另一种方法是在ontouchstart或ontouchend中调用e.preventDefault()。当按钮被触摸时,它似乎可以停止悬停效果,但也会停止按钮点击动画,并防止在触摸按钮时调用onclick函数,因此您需要在ontouchstart或ontouchend处理程序中手动调用它们。这不是一个很干净的解决方案。


谢谢,我没有使用基于间隔的方法,但是你在最后提到的preventDefault信息非常有帮助——我在其他地方都没有看到过这个! - Coderer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接