Stop pointercancel event from firing without disabling touch scrolling on Chrome Android?
Asked Answered
B

2

11

I have a bunch of child divs in a scrollable area, something like this:

<div style='overflow: scroll;'>
    <div id='a' />
    <div id='b' />
    <div id='c' />
    ...
</div>

I listen to pointerdown events on each child, and when one fires, I setup pointermove handlers on the document. eg:

const pointerdownHandle = e => {
    e.target.releasePointerCapture(e.pointerId)
    document.addEventListener('pointermove', pointermoveHandle)
    document.addEventListener('pointerup', pointerupHandle)
}

const pointermoveHandle = e => { ... }

const pointerupHandle = {
    document.removeEventListener('pointermove', pointermoveHandle)
    document.removeEventListener('pointerup', pointerupHandle)  
}

document.getElementById('a').addEventListener('pointerdown', pointerdownHandle)

This works great on desktop and on iOS safari. However on Android Chrome, the pointercancel event fires almost immediately, breaking things.

This appears to be the expected behaviour: "The pointercancel event is fired when the browser determines that there are unlikely to be any more pointer events, or if after the pointerdown event is fired, the pointer is then used to manipulate the viewport by panning, zooming, or scrolling."

The recommended solution is to apply the css property "touch-action: none" to the parent element. And this works. However unfortunately that will also break scrolling, because now touch actions are ignored.

I have tried programatically applying the css property after the pointerdown event has fired, but this does not work. Nor does adding preventDefault / stopPropagation to pointermoveHandle.

Has anyone got a solution to this problem? How can I stop the pointercancel event from firing without disabling scrolling on the parent element?

(I realize I can fall back on touch events, but pointer events, which support pointerenter and pointerleave, are much simpler and cleaner to work with..)

Balderas answered 1/3, 2021 at 22:1 Comment(6)
Have you tried touch-action: pan-y?Igbo
yup, I have. Any kind of touch-action other than none will cause the pointercancel event to fire.Balderas
Did you manage to solve this?Contemn
@Dmitry -- no I never did. I ended up having to rewrite it to use touch events.Balderas
@Balderas thank you, I may have to do that too. I used PointerEvents in the first place because I thought the support was better across browsers :).Contemn
@Balderas wondering if you had any success with touch events? I'm having the same issue, except on iOS. Turning off pan-x and pan-y when pinch to zoom begins doesn't seem to do anything.Nickelic
F
4

You're using releasePointerCapture but I think you might want to do the exact opposite. Pointer capture directs the move events to your element, so that you don't have to put event handlers on document.

I've had to use one touch event to cancel scrolling even though I'm using pointer events everywhere else. I wasn't able to figure out how to use pointer events to cancel scrolling.

function makeDraggable(element) {
    let pos = {x: 0, y: 0}
    let dragging = false
    
    const stopScrollEvents = (event) => {
        event.preventDefault()
    }
        
    const pointerdownHandle = (event) => {
        dragging = {dx: pos.x - event.clientX, dy: pos.y - event.clientY}
        element.classList.add('dragging')
        element.setPointerCapture(event.pointerId)
    }
    
    const pointerupHandle = (event ) => {
        dragging = null
        element.classList.remove('dragging')
    }
    
    const pointermoveHandle = (event) => {
        if (!dragging) return
        pos.x = event.clientX + dragging.dx
        pos.y = event.clientY + dragging.dy
        element.style.transform = `translate(${pos.x}px, ${pos.y}px)`
    }
    
    element.addEventListener('pointerdown', pointerdownHandle)
    element.addEventListener('pointerup', pointerupHandle)
    element.addEventListener('pointercancel', pointerupHandle)
    element.addEventListener('pointermove', pointermoveHandle)
    element.addEventListener('touchstart', stopScrollEvents)
}

for (let element of document.querySelectorAll("#draggable-children > div")) {
    makeDraggable(element)
}
#draggable-children {
    width: 100%;
    height: 100%;
    min-height: 5em;
    background: #eee;
    border: 1px solid black;
}

#draggable-children > div {
    width: 5em;
    height: 1.5em;
    background: #88a;
    border: 1px solid black;
    cursor: grab;
}

#draggable-children > div.dragging {
    width: 5em;
    height: 1.5em;
    background: #aa8;
    border: 1px solid black;
    cursor: grabbing;
}
<div id="draggable-children">
    <div></div>
    <div></div>
    <div></div>
</div>
Fleta answered 27/1, 2023 at 20:18 Comment(2)
You can leave out the touchstart event by using touch-events: none in css, see this answer :)Cephalic
Agreed, touch-events: none is simpler and works in some cases. It'll probably work for the OP's example. It didn't work for many of my use cases, so I had to switch to the touchstart approach. DetailsFleta
V
0

I ran into a simmilar problem. Where I just had a draggable widget, which failed in Android. The fix was to set touch-action onto just the dragging element. pinch-zoom worked which still allows for pinch zooming, but any single finger panning event is blocked.

So in your case, I'm not sure if you're trying to make the divs inside of the scrollable area draggable, or something, but maybe touch-action: pinch-zoom would work on just the divs.

#a, #b, #c {
    touch-action: pinch-zoom;
}

If things still don't work, you could set the state of the document to disable any touch-action as soon as you start dragging things, and remove it again on pointerup or pointercancel event.

You would do this by setting a class on the body element.

.dragging {
  touch-action: pinch-zoom;
  /*touch-action: none; <- disables all touch gestures */
}
const pointerdownHandle = e => {
    document.body.classList.add('dragging')
    // ... your code ...
}
Vaivode answered 22/5, 2023 at 10:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.