Keeping Up with JavaScript Pointer Events
Since JavaScript’s creation in 1995, the web has changed beyond recognition. One example of this is how we’ve gone from lugging a cursor across the screen to navigating with the tap of a finger.
But this innovation comes with consequences that have changed the fundamental way events work within the browser. We’ve encountered many of these issues while developing our Web SDK, so this blog post exists to help get you up to speed with the biggest changes you should be aware of.
The Pointer Events API
In the beginning, we had the humble Mouse Event API, which included the mouseup
and mousedown
events.
Since then, the rising tide of smartphones and other touchscreen devices meant these two events were no longer enough. In response, we gained a set of complementary events — touchup
and touchdown
— to implement touch-specific behavior such as multi-touch gestures.
And although for a while, touch-enabled browsers fell back to the Mouse Event API for compatibility, there was no true multi-input type API, until now: As of December 2019, all major browsers support the pointer API, and those that don’t support polyfills.
If you replace your touchup
and mouseup
events with pointerup
events, you can use one event listener and the browser will be able to determine if the pointer is a mouse or a touch-based device.
This new event is especially useful for multi-pointer devices such as the Surface, where the user can switch between a tablet, touch, and a mouse in the same session.
You can read more about this in the Mozilla documentation.
Click Delay
When smartphone browsers first arrived around 2008, the web wasn’t optimized for these shiny new devices, and indeed, many websites still aren’t.
As you can see, the elements on the page are far too small to read, and so the browser offers multiple ways to zoom in.
One of these, the double tap, allows the user to zoom in to a specific section of the page.
Because of this gesture, the browser needs to wait before informing JavaScript that there has been a click, just in case there has been a double tap. After the last touchup
event, there’s a 300–350 ms delay before the click
event. This is definitely one of the main reasons the mobile web has such a sluggish reputation.
Luckily, today there’s a polyfill that can mitigate this issue.
However, browsers won’t wait for a double tap if you indicate that a website is optimized for mobile, since the user will no longer need to zoom in.
In modern browsers, this can be done by adding the following to the metadata:
<meta name="viewport" content="width=device-width, initial-scale=1">
If you don’t have the ability to modify a page’s metadata, you can also use the touch-action
property on an element in CSS to disable the zooming. This will have the same result:
touch-action: pan-x pan-y;
By indicating you only want the user to be able to perform panning gestures to scroll the page, the browser will automatically disable the delay that normally waits for the double tap before zooming, since it knows the user isn’t allowed to zoom.
Passive Event Listeners
Many browser events come with default event handlers. An example of this is handling navigating to another page when you click on a link.
And as you might know, we’re able to ask the browser to not run a default event handling code by using the preventDefault
method when we’re passed an event.
However, this feature impacted the performance of scrolling on many websites across the web, so passive event listeners were introduced.
Every time you scroll by touching the screen or the wheel of your mouse, the browser needs to check that your code won’t cancel the default actions. This means it’ll wait a certain amount of time, just in case, resulting in poor performance.
Passive event listeners are a way of indicating to the browser that you have no intention of ever canceling default actions, telling the browser it can handle the events straight away.
If you already have event listeners on the window
, body
, or document
element for touches and mouse events, and if the event listeners don’t cancel the default actions, the good news is that they’ll be treated as passive by default.
However, if you do want to cancel the default action, you’ll now have to opt in. Luckily, this is as simple as providing new options when you create the event listener:
window.addEventListener("touchstart", func, {passive: false} );
State Update Delay
Although we now have ways of eliminating click delays and passive event listeners, one remaining thorn in the side of the mobile browsing realm is one we recently encountered ourselves: state.
Certain actions by the user will impact the state of the webpage; the most common one is text selection. When a user taps on a page, it can cause text to either select or deselect.
There’s a widely supported (but technically draft) event called selectionchanged
, which will allow you to detect when this happens and use window.getSelection
to read the current state.
However, you occasionally still need to read this value during a mouse or keyboard event, as PSPDFKit does, to check what the best way to handle it is, depending on what’s already selected and what you just tapped.
We discovered window.getSelection
isn’t updated until the event is handled. Browsers fire events such as pointerup
before a change is committed because your code could cancel that event (even if you mark it as passive).
That means if you read the getSelection
state during the event handler, you’ll get the text selection state before the browser processes the user’s actions.
A quick solution for handling this is to record the last user-initiated event and then to use that when the text selection changes:
var lastUserEvent = null; document.addEventListener('selectionchange', () => { if (lastUserEvent) { // Do something based on the mouse or keyboard. lastUserEvent = null; } else { // Do something based on the text just changing. } }) // Add this same listener for every user event you're interested in, // i.e `pointerdown` or `keyup`. // document.addEventListener('pointerup', (event) => { lastUserEvent = event })
Sometimes you might find you need to control the timing of when you handle these changes. For this, you can use setTimeout
, but you must bear in mind that the delay before the browser updates the selection state differs across mobile and desktop:
window.addEventListner('mouseup', () => { setTimeout(() => { window.getSelection() // This will have the true value. }, 0) })
The code above will break on mobile browsers due to the click delay that’s still used for text selection.
Because the user can double-tap to tell the browser to select entire paragraphs, it’ll still wait 300–350 ms before committing the selection change.
This means, for these browsers, you’ll account for that delay when reading this value:
window.addEventListner('mouseup', () => { setTimeout(() => { window.getSelection() // This will have the true value. }, (isMobile) ? 250 : 0) // Wait 250 ms for mobile browsers. })
When the timeout fires, you’ll have the true value of getSelected
.
Conclusion
In this post, we talked about how you can provide a smooth experience to mobile users, how to work with both touch and mouse devices, and the best practices for handling events for performance.
I hope this post serves as a primer on the current state of events in modern JavaScript on the web, and that you can use it to improve the performance and reliability of your applications.