Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detect if a client device supports :hover and :focus states

Sounds like a simple problem, but turns out to be quite challenging to solve. For some website I have contents that are only to be shown if a user hovers/focuses a link. The link however has a target itself.

If one of those links is clicked by a touch screen user the browser instantly goes to the href location. This means the hover contents are never visible!

This is why users which do not have a mouse (or another device to hover like a magic remote control) should see alternative content. But how can I detect this?

$(document).on('click','#my-menu-inner > ul > li > a',function(e) {

if(clientHasInputDeviceSupportingHover()) { 
  return true;
} else {
  e.preventDefault();
  $('#for-no-hover-visitors').html('');
  $(this).clone().appendTo('#for-no-hover-visitors');
  $(this).next().clone().appendTo('#for-no-hover-visitors');
}

});

function clientHasInputDeviceSupportingHover() {
    // HOW CAN I DETECT THIS???
    if($('#checkhover:checked').length > 0) {
      return true;
    }
    return false;
}
.clearfix::after {
    content: "";
    clear: both;
    display: table;
}

#my-menu-inner > ul {
  margin:10px;
  width:100%;
  background-color:yellow;
  list-style-type:none;
  position:relative;
}

#my-menu-inner > ul > li {
  float:left;
  margin:20px;
}

#my-menu-inner > ul > li > a {
  padding:20px;
  border:1px solid black;
  display:block;
}

#my-menu-inner > ul > li > div.sub {
   position:absolute;
   top:calc(100%  - 20px);
   background-color:red;
   padding:40px;
   display:none;
   left:0;
   width:100vw;
}

#my-menu-inner > ul > li a:hover + div.sub, #my-menu-inner > ul > li a:focus + div.sub,
#my-menu-inner > ul > li > div.sub:hover, #my-menu-inner > ul > li > div.sub:focus {
    display:block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

Simulate for Client supporting hover: <input type="checkbox" id="checkhover" />

<div id="my-menu">
  <div id="my-menu-inner">
    <ul class="clearfix">
      <li>
        <a href="http://www.example.com/foo/">foo</a>
        <div class="sub">
          <ul>
            <li><a href="http://www.example.com/mobile/">mobile</a></li>
            <li><a href="http://www.example.com/users/">users</a></li>
          </ul>
        </div>
      </li>
      <li>
        <a href="http://www.example.com/bar/">bar</a>
        <div class="sub">
          <ul>
            <li><a href="http://www.example.com/never/">never</a></li>
            <li><a href="http://www.example.com/see-me/">see me</a></li>
          </ul>
        </div>
      </li>
    </ul>
  </div>
</div>

<div id="for-no-hover-visitors"></div>

The problem is clientHasInputDeviceSupportingHover(). What is the most reliable way to find this out?

What we know so far

It is possible to detect a touch device: What's the best way to detect a 'touch screen' device using JavaScript?

Mouse detection at least might work"onclick": How to detect if a device has mouse support?

In general there are a lot of different possible input devices: https://en.wikipedia.org/wiki/Input_device#Pointing_device

A generic / more reliable solution would be very welcome.

like image 584
Blackbam Avatar asked Jan 01 '23 18:01

Blackbam


2 Answers

The W3C seems to have recognized this problem and has introduced the hover feature:

The hover media feature is used to query the user’s ability to hover over elements on the page with the primary pointing device. If a device has multiple pointing devices, the hover media feature must reflect the characteristics of the “primary” pointing device, as determined by the user agent. (To query the capabilities of any available pointing devices, see the any-hover media feature.)

There is even a media query to check if there is any possibility to hover:

The any-pointer and any-hover media features are identical to the pointer and hover media features, but they correspond to the union of capabilities of all the pointing devices available to the user. In the case of any-pointer, more than one of the values can match, if different pointing devices have different characteristics.

Code samples:

/* Primary input mechanism system can 
   hover over elements with ease */
@media (hover: hover) { ... }

/* Primary input mechanism cannot hover 
   at all or cannot conveniently hover 
   (e.g., many mobile devices emulate hovering
   when the user performs an inconvenient long tap), 
   or there is no primary pointing input mechanism */
@media (hover: none) { ... }

/* One or more available input mechanism(s) 
   can hover over elements with ease */
@media (any-hover: hover) { ... }


/* One or more available input mechanism(s) cannot 
   hover (or there are no pointing input mechanisms) */
@media (any-hover: none) { ... }

Official draft: https://drafts.csswg.org/mediaqueries/#hover

This feature is still at risk, but I really hope it will be fully supported soon as it is already widely supported: https://caniuse.com/#feat=css-media-interaction

Further read: https://css-tricks.com/touch-devices-not-judged-size/

For Chrome test your device here: https://googlechrome.github.io/samples/media-hover-pointer/

Test with JavaScript: https://jsfiddle.net/Blackbam/zkd2cs0t/16/

The best solution for now is most probably to use those media queries with a fallback solution using touch detection via document.createEvent("TouchEvent"); and mouse detection via mousemove.hasMouse.

like image 88
Blackbam Avatar answered Jan 05 '23 04:01

Blackbam


One approach, if you're able to use up-to-date CSS (and check the compatibility table in the linked resources):

/* Here we use media queries to test the device's
   support for the 'pointer' property, here we
   check if the 'fine' property-value is supported
   by the device: */
@media (pointer:fine) {
   /* Here we set variables to use in styling
      the subsequent content which will identify
      the pointer support: */
   :root {
    --hasTouch: orangered;
  }
}

@media (pointer:coarse) {
   :root {
    --hasTouch: limegreen;
  }
}

@media (pointer:fine) and (pointer:coarse) {
   :root {
    --hasTouch: skyblue;
  }
}

/* Note that we don't specify the variable
   in the event of the device not supporting
   the 'pointer' property or property-value;
   therefore if the <div> is white (#fff) then
   we're using the default value from the
   var() function: */

div {
  background-color: var(--hasTouch, #fff);
  height: 5em;
  border: 2px solid #000;
}

*,
 ::before,
 ::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

@media (pointer:fine) {
   :root {
    --hasTouch: orangered;
  }
}

@media (pointer:coarse) {
   :root {
    --hasTouch: limegreen;
  }
}

@media (pointer:fine) and (pointer:coarse) {
   :root {
    --hasTouch: skyblue;
  }
}

div {
  background-color: var(--hasTouch, #fff);
  height: 5em;
  border: 2px solid #000;
}

ul {
  width: 80vw;
  margin: 0.5em auto;
  list-style-type: none;
}

li::before {
  content: '';
  display: inline-block;
  width: 1em;
  height: 1em;
  background-color: currentColor;
  border-radius: 50%;
  border: 2px solid #000;
}

.failure {
  color: #000;
}

.orangered::before {
  color: orangered;
}

.limegreen::before {
  color: limegreen;
}

.skyblue::before {
  color: skyblue;
}
<div></div>
<ul>
  <li class="failure"> - Doesn't appear to understand <code>pointer</code>.</li>
  <li class="orangered"> - Probably uses a mouse.</li>
  <li class="limegreen"> - Probably uses touch</li>
  <li class="skyblue"> - Could maybe have both mouse <em>and</em> touch.</li>
</ul>

JS Fiddle demo.

References:

  • @media.
  • @media (pointer) property.
  • CSS custom properties (--*).
like image 33
David Thomas Avatar answered Jan 05 '23 03:01

David Thomas