user1031947
user1031947

Reputation: 6664

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

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..)

Upvotes: 14

Views: 3324

Answers (2)

nathnolt
nathnolt

Reputation: 525

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 ...
}

Upvotes: 0

amitp
amitp

Reputation: 1335

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>

Upvotes: 5

Related Questions