Farewell mouse/touch events, welcome pointer events
Over the past few years i’ve been happily maintaining a library for joysticks in React — It works great, but there’s a number of small problems:
- We have to handle
mouse
andtouch
events separately — in order to support multi-touch functionality, we need to access the touches field which doesn’t exist on mouse events - If we start dragging the joystick and the cursor leaves the viewport, we’re unable to track the angle and velocity of the cursor
So let’s explore these inside the react-joystick-component
project:
In in terms of registering the listener the key difference is that we need to track the identifier of the touch so that we can differentiate the interactions — We do not have this issue with mouse events as the cursor is typically a singleton.
if (e.type === 'mousedown') {
window.addEventListener(InteractionEvents.MouseUp, this._boundMouseUp);
window.addEventListener(InteractionEvents.MouseMove, this._boundMouseMove);
} else {
this._touchIdentifier = e.targetTouches[0].identifier;
window.addEventListener(InteractionEvents.TouchEnd, this._boundMouseUp);
window.addEventListener(InteractionEvents.TouchMove, this._boundMouseMove);
}
How can we integrate this with the pointer API?
Simplifying the listener
The event listener registration above becomes the below
window.addEventListener('pointerup', this._boundMouseUp);
window.addEventListener('pointermove', this._boundMouseMove);
this._touchIdentifier = e.targetTouches[0].identifier;
Multi-touch events
Given that our move listener is on the window
, we need to identify between the different event sources.
Current implementation
if(event.targetTouches && event.targetTouches[0].identifier !== this._touchIdentifier){
return;
}
I see this as a code smell/polluted logic — we’re duck checking the event to see if it has touch data and then if the identifiers don’t match up, we’re bailing out.
Pointer implementation
Although the pointerId
is slightly redundant for mouse events, we remove a lot of logic with this change.
if(event.pointerId !== this._touchIdentifier) return;
Getting the position of the pointer
Current Implementation
Like elsewhere, we had some clunky code to handle the positioning for both touch and mouse events — we can now simplify this
let absoluteX = null;
let absoluteY = null;
if (event instanceof MouseEvent) {
absoluteX = event.clientX;
absoluteY = event.clientY;
} else {
absoluteX = event.targetTouches[0].clientX;
absoluteY = event.targetTouches[0].clientY;
}
Becomes
const absoluteX = event.clientX;
const absoluteY = event.clientY;
Handling touchend in multitouch environments
Given that multiple touches could be happening when the touchend
event listener fires, we had to do some iteration to check which touch to deregister
if(event.touches){
for(const touch of event.touches){
// this touch id is still in the TouchList, so this touchend should be ignored
if(touch.identifier === this._touchIdentifier){
return;
}
}
}
This can now be simplified:
if(event.pointerId !== this._touchIdentifier) return;
Easier TypeScript integration
Another smell in this codebase is that we have methods handling multiple types of events, which caused typing issues — I couldn’t get this to work with the MouseEvent | TouchEvent
union so I resorted to any
:
private _mouseDown(e: MouseEvent| any) {
Now that the event trigger is standardised we can properly type the method:
private _pointerDown(e: PointerEvent) {
Handling cursor position outside of the viewport
For this we can use the setPointerCapture method — This binds the subsequent pointer events to the specific element we’re dragging
Registering
window.addEventListener(InteractionEvents.PointerUp, this._boundPounterUp);
window.addEventListener(InteractionEvents.PointerMove, this._boundPointerMove);
this._pointerId = e.pointerId
// We're setting this directly on the React stick ref
this._stickRef.current.setPointerCapture(this._pointerId);
Voila, problem solved
Not quite
One of the main purposes of this refactor was to standardise mouse and touch events, however if we have a look at the touch emulator, we can see a problem:
Hmm.. well.. it’s broken.
After a bit of digging I stumbled across the touchAction CSS selector. Essentially what’s happening is that the PointerEvent
is getting cancelled by the TouchEvent
. Setting touch-action:none
stops this.
And there you have it, standardised pointer events. Feel free to have a look at the commit — https://github.com/elmarti/react-joystick-component/commit/129fd6dd091788e5e8a16fc342a1cbfbcb55c662 I added in a load of cleanup too, but you should be able to get a general understanding from it.
While you’re here, I thought i’d also mention i’m hiring a full stack engineer at my company — Noala — let’s work together! https://www.linkedin.com/jobs/view/2848986961/
Thanks for reading!