How can we determine clicks or touches reliably across desktop and mobile platforms?

While working on my recent Android phone using Chrome, I noticed that subscribing to the click event on a button element only triggers when tapping the button, not when holding it down and then releasing. In order to trigger an action when the button is released, I am considering alternatives to the touchstart event which fires upon pressing the button.

  • Should I subscribe to multiple events? If so, which ones?
  • If multiple events are subscribed, do I need to use preventDefault()?

I came across this article from 2013: https://www.html5rocks.com/en/mobile/touchandmouse/

Answer №1

After experimenting with my own approach, I have successfully achieved the desired functionality. The event triggers work seamlessly on mouse click, short tap, and long tap. I conducted the test specifically on Chrome version 71.0.3578.99 on an Android device.

In order to ensure a smooth experience, it is essential to disable user selection on the button to enable proper execution of long taps.

It's interesting how many people emphasize the release of the mouse/finger elsewhere after initiating a click/tap. In most cases, moving away indicates a change of mind by the user, signaling that they no longer wish to proceed with the action associated with the button.

In my implementation, I manage event listeners dynamically based on the device type. This strategy allows for compatibility across desktop and mobile environments without any disruption on devices supporting both mouse and touchscreen functionalities.

Addressing the queries succinctly:

  • Yes, the events in question can occur simultaneously - namely, click and touchstart/touchend.
  • To prevent double-triggering during a short tap, it's necessary to include certain conditions within the code segment. By handling touch events appropriately, such as removing unnecessary actions related to mouse input, the functionality can be optimized effectively.

const button = document.getElementById("button");
function clickFunc(e){
  if(e.touches){
    e.preventDefault(); //Consider commenting out for testing purposes
    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; /*Implement 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>

Answer №2

It appears that different browsers have varying behaviors in this scenario...

When testing on Android, both Firefox and Chrome handle the click event differently. In Firefox, only the touchend event fires after the initial click, whereas Chrome sometimes triggers the touchcancel event as well (especially when text in the button is selected). I have not had a chance to test Safari yet.

To address this issue, listen for the touchend event instead of the mouseup event. This will ensure that the event still fires even if you move the cursor outside of the element after initially clicking. Additionally, be aware of the touchcancel event, which may occur slightly before the user releases their touch.

In order to prevent both the click event and the touchend/touchcancel events from firing simultaneously, consider debouncing your event handler. This means that the event handler will only run once within a specified time frame. It may also be necessary to prevent the default behavior of the contextmenu event.

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

// necessary code:
btn.oncontextmenu = e => e.preventDefault(); // prevent context menu on long touch
btn.onclick = btn.ontouchend = btn.ontouchcancel = debounce(e => {
  console.log('Successfully 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>

Keep in mind that preventing the default action of the button's click event may affect other functionalities (e.g., submitting a form if the button is inside a <form>). Proceed with caution.

Answer №3

Solving the issue of touch and mouse click events triggering twice:

function handleInteraction(evt) {
  evt.preventDefault()
  console.log('interaction detected')
  // add your code here...
}
var element = document.getElementById('element');
element.addEventListener('touchstart', handleInteraction)
element.addEventListener('click', handleInteraction)
#element {
  cursor: pointer;
  width: 100px;
  height: 100px;
  background-color: #bcd1ea;
  text-align: center;
  line-height: 100px;
  color: #246dad;
}
<div id='element'>
element
</div>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Encountered an error with the Vue Slick Carousel that states "Uncaught (in promise) TypeError: this.$

Hey there! I'm trying to incorporate a carousel using Vue Slick Carousel in my Vue component. Here's what I have so far: <vue-slick-carousel v-if="groupOne && groupOne.length > 0" v-bind="settings& ...

How to set a background image with vue.js

My website design requires 70% of the page to be covered with an image, but the image is currently blank. I am using Vue.js as my frontend framework. Below is the code snippet that is resulting in a blank space: <script setup lang="ts"> imp ...

What is the preferred method for logging out: using window.location.replace('/') or setting window.location.href to window.location.origin?

When it comes to a logout button, which is the better option: window.location.replace('/') or window.location.href=window.location.origin? Can you explain the difference between these two methods? It's my understanding that both of them remo ...

Changing the color of dots in a React Material-UI timeline

Hey there, I'm trying to change the default color of the MUI TimelineDot component. I attempted to do so by adding this code: <TimelineDot sx={{ '& .MuiTimelineDot-root': { backgroundColor: '#00FF00' }, }} /> Unfortunate ...

Trigger a popup notification with JavaScript when

Check out this snippet of code: <link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css"> <script src="https://code.jquery.com/jquery-1.12.4.js"></script> <script src="https://code.jquery.com/ui/ ...

Ways to halt a watch statement or digest cycle within Angular

At the moment, I have implemented a text input linked to a model using a $scope.watch statement to observe changes in the model. The purpose of this setup is to create an auto-complete / typeahead feature. <!-- HTML --> <input type="text" ng-mode ...

What is the process for constructing an object to resemble another object?

After collecting input data, I have created an object based on those values. Here is an example of the generated object: var generate_fields = { name: "Mike", email: "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="b4d9dddf ...

Positioning of Cloned Element in jQuery

When a user presses enter while in one of the text input fields provided, I want to add a new empty text input field directly below it - and before the other existing ones. The code snippet below currently adds the new field at the end of the container, bu ...

The JavaScript function I created to remove the last item in an array is not functioning correctly when it encounters an "else

My button has been designed to delete the last index from this.fullformula, which is a string. However, I've encountered an issue with deleting characters from this.result, which is an integer. Instead of looping over the function, it only deletes one ...

Objects beyond a distance of 1 unit start to vanish when employing ArrayCamera

After implementing an ArrayCamera to wrap my PerspectiveCamera, I noticed that objects located more than 1 unit away from the origin point (0,0,0) appear to vanish in the render. var camera = new THREE.PerspectiveCamera(); camera.viewport = new THREE.Vecto ...

Tracking a razor ajax form using pace.js has never been easier with these simple steps

I'm currently exploring the use of pace.js with my Razor ajax form. The form generates a Partial View upon submission. Pace.js, as per its documentation, automatically monitors all ajax requests lasting longer than 500ms without any additional configu ...

"Revamp your Node.js and HTML with the click of a

I recently built a basic counter for my website, but I'm having trouble with its consistency across different browsers and computers. The button seems to work only about 1/12th of the time. Any tips on making it more reliable? Any help would be great ...

Using jQuery and Bootstrap to Enhance Checkbox Button Styling

I am currently working on an asp.net webform and attempting to style my checkbox similar to the design shown here: When I try to replicate the HTML and JS code from the above link on my page, it does not seem to be functioning properly, most likely due to ...

JQuery Mobile pop-up error: Property 'is' is not defined

I am attempting to utilize jQuery to open a dialog box. I have followed the instructions provided in this link: Here is the code snippet for the dialog: <div data-role="popup" id="popupDialog" data-overlay-theme="a" data-theme="c" style="max-width ...

Turn off choices by utilizing data type attribute values within select2 version 4

I'm attempting to deactivate the options by using the data-type attribute specified for each option in select2. Unfortunately, my attempts have been unsuccessful thus far. In addition, I am encountering this error within the change event handler: ...

Utilize external modules in factory by employing Object.assign for encapsulation

In my factory function, I have the following setup: import moduleA from'./modulea' import moduleB from'./moduleb' import { componentA, componentB } from './components' module.exports = () => { //DECLARE VARIABLES SHAR ...

The header and sub-navigation are in disarray and require some help

Dear web experts, After six months of learning to code websites, I'm tackling a big project with some challenges. The recent changes to the header and subnav have thrown everything off balance, making it look wonky and not quite right. Specifically, ...

I am having trouble combining my streams with es.merge

Here is the gulp task that I am working on: var es = require('event-stream'), concat = require('gulp-concat'), templateCache = require('gulp-angular-templatecache'); var scripts = gulp.src(paths.js + '/**/*.js&a ...

Is there a way to access and invoke a exposed function of a Vue component within a default slot?

Exploring the realms of a vue playground. The functions interfaceFunction in both ChildA and ChildB are exposed. In App, these functions can be called by obtaining references to the components that expose them. This allows direct function calls from with ...

What is the best way to incorporate a loading icon onto a webpage that exclusively runs JavaScript functions?

I frequently use Ajax load icons to indicate progress during ajax requests. Is there a way to achieve the same result using regular JavaScript? For instance: $('button').on('click', function(){ showLoadingIcon(); lengthyProces ...