How to prevent default handling of touch events?

TL;DR: I was missing { passive: false } when registering event handlers.

The issue I had with preventDefault() with Chrome was due to their scrolling "intervention" (read: breaking the web IE-style). In short, because the handlers that don't call preventDefault() can be handled faster, a new option was added to addEventListener named passive. If set to true then event handler promises not to call preventDefault (if it does, the call will be ignored). Chrome however decided to go a step further and make {passive: true} default (since version 56).

Solution is calling the event listener with passive explicitly set to false:

window.addEventListener('touchmove', ev => {
  if (weShouldStopDefaultScrollAndZoom) {
}, { passive: false });

Note that this negatively impacts performance.

As a side note, it seems I misunderstood touch-action CSS, however I still can't use it because it needs to be set before touch sequence starts. But if this is not the case, it is probably more performant and seems to be supported on all applicable platforms (Safari on Mac does not support it, but on iOS it does). This post says it best:

For your case you probably want to mark your text area (or whatever) 'touch-action: none' to disable scrolling/zooming without disabling all the other behaviors.

The CSS property should be set on the component and not on document as I did it:

<div style="touch-action: none;">
  ... my component ...

In my case I will still need to use passive event handlers, but it's good to know the options... Hope it helps someone.

Try using an if statement to see if there is more than one touch:

document.body.addEventListener("touchmove", ev => {
  if (ev.touches.length > 1) {
}, true);