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><main></b>, has both <b>touchend</b> & <b>click</b> events</li>
<li>Normally, a mobile <b>tap</b> triggers <b>both</b> — <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 & 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 & 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> & <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 = " "
},400)
}
})()