1

I am subscribing to the click event on a button element. That seems to work on mobile devices if you tap the button, but if you hold it in for a while and then release it, it doesn't trigger. At least that's how it works on my very recent Android phone using Chrome.

So I would like to trigger something when the button is released. Thus, I'm assuming I should not go for touchstart because it triggers when the button is first pressed.

  • Do I need to subscribe multiple events? Which ones?
  • If I subscribe to multiple events, to I need to preventDefault()?

I found this article, but it's from 2013: https://www.html5rocks.com/en/mobile/touchandmouse/

HelloWorld
  • 3,381
  • 5
  • 32
  • 58
  • 1
    Maybe the `mouseup` event? If you use jQuery, you could try using the jQuery-mobile library. – Barmar Jan 24 '19 at 01:10
  • 1
    I think press-hold on a touch device is considered equivalent to a right click, which triggers the `contextmenu` event. – Barmar Jan 24 '19 at 01:12
  • @Bamar mouseup no good: https://stackoverflow.com/questions/14805225/whats-the-difference-between-mouseup-and-click-events. You can release the mouse somewhere else. And I refuse to add jquery mobile for such a mundane task :) – HelloWorld Jan 24 '19 at 01:13
  • Maybe `touchend` can works for you. – gugadev Jan 24 '19 at 01:30

3 Answers3

2

After some tries, it seems that all browsers do not behave the same here...

In both Firefox and Chrome on Android, the click event gets discarded by the contextmenu event, I couldn't test on Safari yet. But, in Firefox the only event to fire after is the touchend one, while on Chrome it seems the touchcancel one fires sometimes (apparently when the text in the button gets selected).

So to get this, you'd listen for the touchend event which unlike the mouseup event will also fire if you do move the cursor outside of the element after you started to click, but won't fire if you started the click outside and then moved the cursor over the element, and for the touchcancel event, which may fire a bit before the user released there touch (but it is the latest event we can get...).

Now, to avoid both click and touchend/touchcancel events to fire on the same action, you'd need to debounce your event handler (i.e force it to run only once in a given lapse of time), and probably also to prevent the default behavior of the contextmenu event.

// simple logs:
['click', 'touchstart', 'touchend', 'touchcancel', 'contextmenu'].forEach(type => {
  btn.addEventListener(type, e => console.log(e.type));
});

// what you need:
btn.oncontextmenu = e => e.preventDefault(); // prevent the context menu on long touch
btn.onclick = btn.ontouchend = btn.ontouchcancel = debounce(e => {
  console.log('Correctly handled the click event');
}, 100);

function debounce(fn, timeout=0) {
  let flag = false;
  return function() {
    if (!flag) {
      flag = true;
      setTimeout(_ => flag = false, timeout);
      fn.call(...arguments);
    }
  };
}
<button id="btn">
click me
</button>

Beware though that the default action of the button's click won't happen either (for instance if this button was in a <form>, the form won't get submitted in case of long-touch.

Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Hi, I am pretty sure you know a good duplicate for this one https://stackoverflow.com/q/54344996/8620333 or you can give an answer ... no one is really answering the *why* of the question. – Temani Afif Jan 24 '19 at 12:21
  • @TemaniAfif it is basically a variation of https://stackoverflow.com/questions/49269614/why-does-reflow-need-to-be-triggered-for-css-transitions but this may not be such a good dupe target, so I provided an answer. – Kaiido Jan 24 '19 at 14:37
2

So I tried it my way, and it seems to work correctly. It triggers on mouse click, short tap and long tap as well. Since the question is about Chrome, I tested it on Chrome 71.0.3578.99 on Android.

First, you need to suppress user selection on the button, otherwise, a long tap will not be performed.

Secondly, why is everybody so much concerned about releasing mouse/finger somewhere else? The only reason to start click/tap and then move/slide away is that the user has changed his mind and does not want to execute whatever the button says it executes.

Finally, in my code, I remove and add event listeners as needed. That is only because of the question structure: this code will work on both desktop and mobile devices. It also will not break on devices supporting mouse AND touchscreen.

To answer the questions with bullet points:

  • Yes, at least two at once – click and touchstart/touchend.
  • Yes, you need it to prevent double-triggering on short tap (if you remove it from my code, you will see that on short tap it triggers touchend and then click events). You may prefer to assume that if a touch event fired once, then the user has no mouse connected to the device - in this case simply remove the whole content of clickFunc() apart from //Your code: it will work fine.

const button = document.getElementById("button");
function clickFunc(e){
  if(e.touches){
    e.preventDefault(); //Try to comment it out: double triggers on short tap
    button.removeEventListener("touchend", clickFunc);
    button.addEventListener("click", clickFunc);
    console.log('tapped!');
  }else{
   console.log('clicked!');
  }
  //
  //Your code here
}
function touchFunc(){
  button.removeEventListener("click", clickFunc);
  button.addEventListener("touchend", clickFunc);
}
window.onload=function(){
  button.addEventListener("click", clickFunc);
  button.addEventListener("touchstart", touchFunc);
}
#button{
  user-select: none; /*find crossbrowser solution!*/

  /*For testing purposes*/
  margin-top: 100px;
  margin-left: 100px;
  font-size: 48px; 
  padding: 10px 0; 
}
<!DOCTYPE html>
<html lang="en">
<head>
 <title>Clicks and Touches</title>
</head>
<body>
 <button id="button">Click me!</button>
</body>
</html>
Jaro
  • 117
  • 7
1

Handling touch and mouse clicks without double triggering:

function handleInteraction(evt) {
  evt.preventDefault()
  console.log('interacted')
  // your code...
}
var el = document.getElementById('el');
el.addEventListener('touchstart', handleInteraction)
el.addEventListener('click', handleInteraction)
#el {
  cursor: pointer;
  width: 100px;
  height: 100px;
  background-color: #bcd1ea;
  text-align: center;
  line-height: 100px;
  color: #246dad;
}
<div id='el'>
el
</div>
Artee
  • 824
  • 9
  • 19