Farewell mouse/touch events, welcome pointer events

Jack Martin
4 min readJan 22, 2022

--

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 and touch 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!

--

--