touchend-and-click-events

How to support touch and click without firing twice, and without disabling other event support.

by Nic Fontaine

HTML

<header>
<h4>Event</h4>
<div id='out'>(none)</div>
</header>
<main id='main'>
<h3>Problem</h3>
<ul>
<li>This text body, <b>&lt;main&gt;</b>, has both <b>touchend</b> &amp; <b>click</b> events</li>
<li>Normally, a mobile <b>tap</b> triggers <b>both</b> &mdash; <b>Click</b> happens last</li>
<li><b>preventDefault()</b> in touchend would be fine if we only cared about 1 event, but since our event is on <b>main</b> that would kill scrolling</li>
<li>We only want to <b>fire one</b> of our <b>2 events</b></li>
<li>...And not kill other events</li>
<li>...And support <b>mouse, mobile, and touchscreen</b> (mouse &amp; mobile)</li>
<hr>
<h3>Solution</h3>
<li>On <b>touchend</b>, we flag <b>evActive</b> as true</li>
<li><b>Click</b> only runs if <b>evActive === false</b></li>
<li><b>Click</b> resets <b>evActive</b> to false, in case of both events - from mouse &amp; touchscreen</li>
<li><b>Touchend</b> also resets evActive, with 50ms delay that covers mobile taps' touchend + click</li>
<li>We use <b>touchend</b> instead of <b>touchstart</b> b/c the delay between <b>touchstart</b> &amp; <b>click</b> can be somewhat long if held down before release.</li>
<hr>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent egestas finibus mauris eu gravida. Nunc convallis eget tellus a auctor. Nam ultricies ex ut nunc ultricies, eget porttitor est malesuada. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Suspendisse nibh nunc, maximus eu sapien ac, convallis ultricies quam. Sed dictum lorem ut libero ornare, quis porta tellus pellentesque. Vivamus vitae feugiat metus. Proin eleifend congue sapien, at convallis neque. Sed dignissim euismod porttitor. Pellentesque molestie dictum tellus, vel pretium risus tincidunt sit amet.
</p>
<p>
Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Integer cursus porttitor eros facilisis pretium. Curabitur tempor mi ac congue efficitur. Donec interdum justo...

CSS

body {
  padding: 0;
  margin: 0;
  font-family: sans-serif;
  line-height: 1.5;
}
header {
  position: sticky;
  top: 0;
  box-shadow: 0 0 10px rgba(0,0,0,0.5);
  text-align: center;
  padding: 0.5rem;
  font-weight: bold;
  background: deeppink;
  color: #fff;
}
h3 {
  text-align: center;
}
ul {
  padding-left: 1rem;
}
li {
  margin-bottom: 0.6rem;
}
h4 {
  margin: 0;
  color: rgba(0,0,0,0.3);
}
main {
  padding: 1rem 1rem 1rem;
  user-select: none;
  color: #777;
}
hr {
  border: none;
  height: 1px;
  background: #eee;
  margin: 2rem 0; 
}

JavaScript

(function() {

  // DOM
  const out = document.getElementById("out")
  const main = document.getElementById("main")
  
  // Flag that touchstart has triggered
  var evActive = false
  
  main.addEventListener("touchstart", function(e) {
  	readout("TOUCHSTART")
    evActive = true
  })

  // Events
  main.addEventListener("touchend", function(e) {
    readout("TOUCHEND")
    // Set to true, so we can check in "click"
    evActive = true
    // There's no event fired after "click" to use & reset, so we have to reset w/ a timeout..
    // ...this is long enough to cover a long press & release
    setTimeout(function() { evActive = false }, 100)
  })

  main.addEventListener("click", function(e) {
    // Only fire if touchstart hasn't fired...
    // ...i.e. we're probably on desktop, no touchscreen, & relying on "click" events
    if (!evActive) {
      readout("CLICK")
    }
    evActive = false
  })

  // Show visual readout of events
  function readout(str) {
    out.innerHTML = str
    setTimeout(function() {
      out.innerHTML = "&nbsp;"
    },400)
  }

})()