37

I am trying to do something similar to what embedded Google maps do. My component should ignore single touch (allowing user to scroll page) and pinch outside of itself (allowing user to zoom page), but should react to double touch (allowing user to navigate inside the component) and disallow any default action in this case.

How do I prevent default handling of touch events, but only in the case when user is interacting with my component with two fingers?

What I have tried:

  1. I tried capturing onTouchStart, onTouchMove and onTouchEnd. It turns out that on FF Android the first event that fires when doing pinch on component is onTouchStart with a single touch, then onTouchStart with two touches, then onTouchMove. But calling event.preventDefault() or event.stopPropagation() in onTouchMove handler doesn't (always) stop page zoom/scroll. Preventing event escalation in the first call to onTouchStart does help - unfortunately at that time I don't know yet if it's going to be multitouch or not, so I can't use this.

  2. Second approach was setting touch-action: none on document.body. This works with Chrome Android, but I could only make it work with Firefox Android if I set this on all elements (except for my component). So while this is doable, it seems like it could have unwanted side effects and performance issues. EDIT: Further testing revealed that this works for Chrome only if the CSS is set before the touch has started. In other words, if I inject CSS styles when I detect 2 fingers then touch-action is ignored. So this is not useful on Chrome.

  3. I have also tried adding a new event listener on component mount:

    document.body.addEventListener("touchmove", ev => {
      ev.preventDefault();
      ev.stopImmediatePropagation();
    }, true);
    

    (and the same for touchstart). Doing so works in Firefox Android, but does nothing on Chrome Android.

I am running out of ideas. Is there a reliable cross-browser way to achieve what Google apparently did, or did they use multiple hacks and lots of testing on every browser to make it work? I would appreciate if someone pointed out an error in my approach(es) or propose a new way.

johndodo
  • 17,247
  • 15
  • 96
  • 113

4 Answers4

54

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) {
    ev.preventDefault();
    ev.stopImmediatePropagation();
  };
}, { 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 ...
</div>

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

johndodo
  • 17,247
  • 15
  • 96
  • 113
  • I would really like to get a more concise answer out of this. Right now, I implemented an extra `touchmove` event and added css to the component. Are both necessary or can we do without the css on the component? – T.Woody Aug 01 '19 at 18:59
  • 1
    Only one of them is needed. CSS `touch-action` option is more performant (and nicer), but it only works if you set it in advance (before the `touchstart` event). In my case this was not an option because I needed different behaviour based on the number to fingers, so I had to wait for touchstart. – johndodo Aug 08 '19 at 14:52
  • If using Pointer Events (`pointerdown`, `pointermove`), then `preventDefault` and `stopImmediatePropagation` don't have any effect on default touch actions. One must explicitly prevent the default actions using `touchstart` and `touchmove`. – The Conspiracy Sep 28 '22 at 23:02
1

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

document.body.addEventListener("touchmove", ev => {
  if (ev.touches.length > 1) {
    ev.preventDefault();
    ev.stopImmediatePropagation();
  }
}, true);
fig
  • 364
  • 2
  • 11
  • Thanks! I already tried that (see point 3 in my question). However this doesn't prevent scrolling / zooming on Chrome for Android. Note that I have trouble preventing default action (after detecting 2 fingers), not figuring out if it needs to be prevented. – johndodo Mar 31 '18 at 12:23
1

This is my idea:

I used one div with opacity: 0 to cover the map with z-index > map z-index

And I will detect in the covered div. If I detect 2 fingers touched in this covered div, I will display: none this div to allow user can use 2 finger in this map.

Otherwise, in document touchEnd I will recover this covered div using display: block to make sure we can scroll.

Tan Duong
  • 2,124
  • 1
  • 11
  • 17
-1

In my case I have solved it with

@HostListener('touchmove', ['$event'])
    public onTouch(event: any): void {
    event.stopPropagation();
    console.log('onTouch', event);
}
Leonardo Pineda
  • 990
  • 8
  • 10