Search code examples
javascriptpointer-events

'pointerup' event not firing on desktop, works fine on mobile


I'm trying to come up with a cross-device code that handles pointer events. I'm running this code successfully on Chrome/Android (using USB debugging), but the Chrome desktop just acts up and keeps firing pointermove after the mouse has been released.

(Another problem is that the moves are not as smooth as on the mobile)

Playable demo

These SO posts don't solve my problem:


Solution

  • The "pointerup" Event is assigned to the #canvas, but such event will never occur because the mouse is actually above the generated DIV circle.
    Since your circles are just visual helpers, set in CSS

    .dot {
      /* ... */
      pointer-events: none;
    }
    

    Also, make sure to use Event.preventDefault() on "pointerdown".

    Regarding the other strategies for a seamless experience, both on desktop and on mobile (touch):

    • assign only the "pointerdown" Event to a desired Element (canvas in your case)
    • use the window object for all the other events

    Edited example:

    const canvas = document.getElementById('canvas');
    
    function startTouch(ev) {
      ev.preventDefault();
    
      const dot = document.createElement('div');
      dot.classList.add('dot');
      dot.id = ev.pointerId;
      dot.style.left = `${ev.pageX}px`;
      dot.style.top = `${ev.pageY}px`;
      dot.style.backgroundColor = `hsl(${ev.pointerId * 50 % 360}, 80%, 50%)`;
      document.body.append(dot);
      canvas.setPointerCapture(ev.pointerId);
    }
    
    function moveTouch(ev) {
      const dot = document.getElementById(ev.pointerId);
      if (!dot) return;
    
      dot.style.left = `${ev.pageX}px`;
      dot.style.top = `${ev.pageY}px`;
    }
    
    function endTouch(ev) {
      const dot = document.getElementById(ev.pointerId);
      if (!dot) return;
      canvas.releasePointerCapture(ev.pointerId);
      removeDot(dot);
    }
    
    function removeDot(dot) {
      dot.remove();
    }
    
    canvas.addEventListener("pointerdown", startTouch);
    canvas.addEventListener("pointermove", moveTouch);
    canvas.addEventListener("pointerup", endTouch);
    canvas.addEventListener("pointercancel", endTouch);
    * {
      margin: 0;
      box-sizing: border-box;
    }
    
    body {
      margin: 10vh;
    }
    
    .dot {
      pointer-events: none;
      position: absolute;
      width: 4rem;
      height: 4rem;
      border-radius: 50%;
      background-color: deeppink;
      translate: -50% -50%;
      left: calc(var(--x) * 1px);
      top: calc(var(--y) * 1px);
    }
    
    #canvas {
      height: 80vh;
      background-color: black;
      touch-action: none;
    }
    <div id="canvas"></div>

    The code needs also this improvement:

    • Don't query the DOM inside a pointermove event

    Using CSS vars

    As per the comments section here's a viable solution that uses custom properties CSS variables and JS's CSSStyleDeclaration.setProperty() method.
    Basically the --x and --y CSS properties values are updated from the pointerdown/move event handlers to reflect the current clientX and clientY values:

    const el = (sel, par) => (par || document).querySelector(sel);
    const elNew = (tag, prop) => Object.assign(document.createElement(tag), prop);
    
    const canvas = document.getElementById("canvas");
    
    const pointersDots = (parent) => {
    
      const elParent = typeof parent === "string" ? el(parent) : parent;
      const dots = new Map();
    
      const moveDot = (elDot, {clientX: x,clientY: y}) => {
        elDot.style.setProperty("--x", x);
        elDot.style.setProperty("--y", y);
      };
    
      const onDown = (ev) => {
        ev.preventDefault();
        const elDot = elNew("div", { className: "dot" });
        elDot.style.backgroundColor = `hsl(${ev.pointerId * 50 % 360}, 80%, 50%)`;
        moveDot(elDot, ev);
        elParent.append(elDot);
        dots.set(ev.pointerId, elDot);
        canvas.setPointerCapture(ev.pointerId);
      };
    
      const onMove = (ev) => {
        if (dots.size === 0) return;
        const elDot = dots.get(ev.pointerId);
        moveDot(elDot, ev);
      };
    
      const onUp = (ev) => {
        if (dots.size === 0) return;
        const elDot = dots.get(ev.pointerId);
        elDot.remove();
        dots.delete(ev.pointerId);
        canvas.releasePointerCapture(ev.pointerId);
      };
    
      canvas.addEventListener("pointerdown", onDown);
      canvas.addEventListener("pointermove", onMove);
      canvas.addEventListener("pointerup", onUp);
      canvas.addEventListener("pointercancel", onUp);
    };
    
    // Init: Pointers helpers
    pointersDots("#canvas");
    * {
      margin: 0;
    }
    
    .dot {
      --x: 0;
      --y: 0;
      pointer-events: none;
      position: absolute;
      width: 2rem;
      height: 2rem;
      border-radius: 50%;
      background-color: deeppink;
      transform: translate(-50%, -50%);
      left: calc(var(--x) * 1px);
      top: calc(var(--y) * 1px);
    }
    
    #canvas {
      margin: 10vh;
      height: 80vh;
      background-color: black;
      touch-action: none;
    }
    <div id="canvas"></div>