Let's imagine a day without touch and click. How would it be? I can't imagine a day without these two actions now. When I was a user, I used it unconsciously and never thought deeply about the relationship between these two actions. It wasn't until I became a front-end developer and encountered issues caused by the difference between touch and click that I started to think about the relationship between the two actions.
In this article, I would like to share the common point and differences between touch and click, along with potential issues and solutions, based on my experience as someone who has just jumped into front-end development.
While maintaining the TOAST UI open source project, I saw a function similar to the one below.
function getConvertedEventType(type) {
if (isMobile()) {
if (type === 'mousedown') {
type = 'touchstart';
} else if (type === 'click') {
type = 'touchend';
}
}
return type;
}
This function converts the event type so that a touch event can be used instead of a mouse event for a mobile device. It changes the mousedown
event type to the touchstart
event type and the click
event type to the touchend
event type. Therefore, the event listener for click is changed to an event listener for touch.
The method using the event type conversion function has worked without any problem so far. But an unexpected issue occurred.
If we use a touch event listener using the event type conversion function in TOAST UI DatePicker, the drop-down element disappears when you touchend
the date. But as shown in the figure above, we encountered an issue where, after the drop-down element disappears, the element beneath it is clicked. What caused this to happen?
You said they're Gganbu!
To find the cause of this issue, let's talk about touch and click first. So far, we've used the event type conversion function without any problem. What do touch and click have in common so that the event type can be converted 1:1, and what are the differences between them that require the conversion of event types?
Touch and click have one simple but crucial point in common: pressing and releasing an element on the screen to interact with the screen. That is, both actions are a sequence of a pressing action and a releasing action. For this reason, we can change touch and click and support them simply by converting the event type as with the event type conversion function.
If the two actions are so similar, why do we need an event type conversion function?
mousemove
action that connects the two click behaviors. For touch, on the other hand, we move the finger over the first element and touch, and then move any finger over the second element and touch. The screen cannot figure out the action that connects the two touch behaviors.In the early days when mobile devices started becoming popular, it wasn't hard to find OSs or applications with little consideration of the aforementioned common point and differences. This made UI inconvenient and awkward for users, and the developers could not help hear complaints. Eventually these problems were soon fixed in the way we are now familiar with.
Do you really believe they're still the same?
But I ran into a problem. To see what's causing the problem, let's look at the difference between click and touch in JavaScript.
Click has click
as an event type for this, but touch does not. So instead, let's perform touch and click using the touchstart
event type, which is an event that occurs when a touch starts.
function createParagraph(text) {
const el = document.createElement('p');
el.innerText = text;
return el
}
const printEl = document.getElementById('print');
document.addEventListener('click', () => {
printEl.appendChild(createParagraph('click'));
});
document.addEventListener('touchstart', () => {
printEl.appendChild(createParagraph('touch'));
});
When clicking with a mouse, only a click
event occurs. When performing a touch, however, a click
event also occurs after the touchstart
event. If so, does the click
event always occur when a touch is performed?
We mentioned that touch and click are a sequence of pressing and releasing actions. In the JavaScript event, the event for each separate action is fired separately. Event types related to touch include touchstart
, touchemove
, and touchend
, and event types related to click include mousedown
, mousemove
, mouseup
, and click
.
Now, let's check the order in which each event occurs.
The code executed in the example above outputs the generated event types as follows.
function createParagraph(text) {
const el = document.createElement('p');
el.innerText = text;
return el
}
const printEl = document.getElementById('print');
['touchstart', 'touchmove', 'touchend', 'mousedown', 'mousemove', 'mouseup', 'click'].forEach(eventType => {
document.addEventListener(eventType, () => {
printEl.appendChild(createParagraph(eventType));
// Move the scroll to the bottom.
window.scrollTo(0, document.body.scrollHeight);
});
})
After performing the test, we found out that the events occur in the order shown above. An interesting point is that the click
event occurs along with the touch events only when a simple touch action is performed, and even the mouse events occur together. (This is a general order for occurrence of events, and the order might vary by browser.) This means that we can perform the desired action without using the event type conversion function, because mouse events also occur when we perform a simple touch.
But why use the event type conversion function to change the event listener for click to the event listener for touch? This is because the click
event generally does not occur immediately after the touch event occurs.
Let's see if that's actually the case.
/*
* To check the delay, console.time and console.timeEnd have been used. Check out the following link for more detailed explanation.
* https://developer.mozilla.org/en/docs/Web/API/Console/time
* You can also measure the delay using performance.now.
* https://developer.mozilla.org/en/docs/Web/API/Performance/now
*/
document.addEventListener('touchstart', () => {
console.time('touch-click delay');
});
document.addEventListener('click', () => {
console.timeEnd('touch-click delay');
});
The code above is used to check the delay between the occurrence of the touchstart
event and the occurrence of the click
event. If we run this code, we can see that there is a delay of approximately 300ms
from the occurrence of the touchstart
event until the occurrence of the click
event. But the maximum delay for having the user feel that the system is reacting instantaneously is 100ms. If so, why has the delay been added, which is long enough to make the user feel that the response is slow?
Unlike mouse actions, touch actions have various gestures, and double-tap zoom is one of them. Suppose you want to double-tap zoom and there is a button in that position.
As shown above, if there is no delay, we will click the button on the first touch. If the button doesn't affect the action we're trying to do, that's fine. Otherwise, we won't be able to perform the gesture.
What if there is a delay? Look at the picture above. The button is touched on the first touch, but has not been clicked yet. In the meantime, if we touch again, the click is not performed and we can perform the gesture as intended.
But delays can cause problems as in my previous experience. When the date of TOAST UI DatePicker is touched, a touchend
event occurs and the drop-down element disappears. However, a click
event occurs at that position after 300ms
. But the drop-down element we intended to click is no longer there, so the button beneath it is clicked.
Shall we be Gganbu?
Now that we've examined the problem, let's fix it.
In general, pages to be displayed on mobile devices are created to be optimized for mobile devices. In this case, the size of the element on the page might be large enough that a gesture such as double-tap zoom might not be necessary. From Chrome 32 released in 2014, there is no delay generated for sites optimized for mobile pages. (Most other browsers work in the same way now.) If the width of the viewport is the same as the width of the device screen, it is called a mobile-optimized site.
Now, let's add the following syntax to the <head>
tag of the delay example page.
<meta name="viewport" content="width=device-width">
We simply specified the width of the viewport, but we can see that the delay of 300ms
has been removed. You can also use the following style to remove the touch-click delay for that element, even though it is not a recommended method.
* {
touch-action: manipulation;
}
The method above is not effective if you create components to be used inside the page rather than the entire page, like TOAST UI. So, let's use the event type conversion function to change a mouse event listener to a touch event listener. Even in this case, mouse events will still occur, so it can cause unintended side effects as in my experience. If mouse events are not fired like other touch actions shown in the event occurrence sequence we saw earlier, we don't have to care about this problem.
This can be resolved by calling preventDefault()
on touchstart
or touchend
.
The code executed in the example above is as follows.
function createParagraph(text) {
const el = document.createElement('p');
el.innerText = text;
return el
}
const printEl = document.getElementById('print');
document.addEventListener('click', () => {
printEl.appendChild(createParagraph('click'));
});
document.addEventListener('touchend', (ev) => {
ev.preventDefault();
printEl.appendChild(createParagraph('touch'));
});
The test shows that only the click
event occurs when the mouse is clicked, but unlike the previous example, only the touch event occurs when touch is performed. The example above is quite similar to the first example, but a listener is assigned to the touchend
event instead of touchstart
and preventDefault()
is called internally to prevent the occurrence of mouse event.
We knew that the purpose of calling preventDefault()
in "Removing Mouse Events" is to suppress mouse events, but why did we change touchstart
to touchend
? It is related to one of the options of the third argument of addEventListener
, passive
. According to the spec, the passive
option defaults to false
, but in some browsers (especially Chrome and Firefox) the default value is set to true
for touchstart
and touchmove
events to improve scroll performance. If the touchstart
event is used, when the passive
option is set to true
, even if preventDefault()
is called inside the callback function, only a console warning is output and preventDefault()
does not work properly.
In addition, the event object passed as the first argument to the listener is also different, which is MouseEvent
and TouchEvent
Therefore, to use the property used in MouseEvent
, you need to select an appropriate property among touches
, targetTouches
, and changedTouches
of TouchEvent
, and then find and use the appropriate Touch
object in the property.
Touch and click, we're Gganbu.
In this article, we examined the issue from my experience and how to use the event type conversion function properly to solve this issue, while minimizing code duplication and supporting touch and click without any problem. Make sure that you know the order of occurrence of touch or click events, and don't forget to use removal of delay and mouse events appropriately.
Since the market share of the mobile has overtaken that of desktop for the first time, the importance of mobile web browsing has continued to increase. But the market share of desktop browsing is still fairly large, so we cannot focus solely on mobile browsing. Therefore, it is critical to clearly distinguish between touch and click, which are the most basic elements of user interaction in web browsing, and to use them correctly.
I hope that many developers who started jumping into front-end development can use this article as a cornerstone to support more features in mobile web browsing.