skip to Main Content

I have a simple web page with a button and a div. The button has a click event listener that logs to the console when clicked. The div has a pointerdown event listener that calls setPointerCapture to capture pointer events.

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=devicexx-width, initial-scale=1.0" />
  <title>play</title>
</head>
<body>

<button id="btn">Test</button>
<div id="div" style="margin-top: 20px; height: 50px; width: 50px; background: red"></div>

<script>
  const btn = document.getElementById("btn");
  btn.addEventListener("click", () => console.log("btn clicked"));

  const div = document.getElementById("div");
  div.addEventListener("pointerdown", (e) => {
    e.target.setPointerCapture(e.pointerId);
  });
</script>

</body>
</html>

On Firefox, when I press mouse on the div, drag it onto the button and release it over the button, the button‘s click event fires. This issue doesn’t happen on Edge, Safari and Chrome.

I wonder why it happens? How can I prevent the click event fires?

I tried to manually remove that pointer capture when pointer up like this:

div.addEventListener("pointerup", (e) => {
  e.target.releasePointerCapture(e.pointerId);
});

It doesn’t help.

PS: If you want to know why I want to prevent it, it’s because it’s related to this issue: https://github.com/radix-ui/primitives/issues/2777.

2

Answers


  1. Chosen as BEST ANSWER

    Thanks for Nanigashi. I've found that this is indeed a Firefox issue: https://bugzilla.mozilla.org/show_bug.cgi?id=1556240. It has been discussed in: https://github.com/w3c/pointerevents/issues/356

    I'd like to propose another work-round: You can use pointer-events properties to temporarily disable pointer events of a component. Be cautious though. It might cause unexpected side effect.

    div.addEventListener("pointerdown", (e) => {
      document.body.style.pointerEvents = 'none';
      e.target.setPointerCapture(e.pointerId);
    });
    
    div.addEventListener("pointerup", (e) => {
      e.target.releasePointerCapture(e.pointerId);
      document.body.style.pointerEvents = 'auto';
    });
    

  2. This erroneous behavior is a currently known bug.

    A click event is a mouse/pointer down & mouse/pointer up on the same element.

    Let’s change your code a bit.

    <!DOCTYPE html><html lang="en"><head>
      <title>play</title>
    </head><body style="border: 1px solid black;">
    <button id="btn" style="margin: 10px; width: 50px;">Test</button>
    <div id="div" style="margin: 10px; height: 50px; width: 50px; background: red"></div>
    <script>
      function report( e ) {
        console.log( this.tagName, e.target.tagName, e.type );
      }
    
      const btn = document.getElementById("btn");
      btn.addEventListener( 'click',       report );
      btn.addEventListener( 'pointerdown', report );
      btn.addEventListener( 'pointerup',   report );
    
      const div = document.getElementById( 'div' );
      div.addEventListener( 'pointerdown', e => {
        console.log( 'DIV', e.target.tagName, e.type );
        e.target.setPointerCapture( e.pointerId );
      } );
      div.addEventListener( 'pointerup', report );
      div.addEventListener( 'click',     report );
    
      document.body.addEventListener( 'pointerdown', report );
      document.body.addEventListener( 'pointerup',   report );
      document.body.addEventListener( 'click',       report );
    </script>
    </body></html>
    

    The code above allows trying various things. For example

    1. Down & hold on the div, then drag & up to the right (body) correctly directs the target of the up event at the div, as it should with the pointer events captured there. Because the down event on the div bubbled up to the body, there’s a click on the body, but the click should be on the div.

      DIV  DIV  pointerdown
      BODY DIV  pointerdown
      DIV  DIV  pointerup
      BODY DIV  pointerup
      BODY BODY click
      

      The click should be:

      DIV  DIV click
      BODY DIV click
      
    2. Down & hold on the body, then drag & up on the div correctly reports the target of the up event as the div, because that’s where the up event took place. The up event bubbles to the body, so there’s a click event on the body.

      BODY BODY pointerdown
      DIV  DIV  pointerup
      BODY DIV  pointerup
      BODY BODY click
      
    3. Similarly down & hold on the body, then drag & up to the button correctly reports the target of the up event as the button. Again the up event bubbles, so there’s a click on the body.

      BODY   BODY   pointerdown
      BUTTON BUTTON pointerup
      BODY   BUTTON pointerup
      BODY   BODY   click
      
    4. However, down & hold on the div, then drag & up to the button correctly reports the up event on the div like it should, but also Firefox appears to be confused that the down and (captured) up events took place on the same element (the div), but the pointer is over the button. There should be no click event on the button.

      DIV    DIV    pointerdown
      BODY   DIV    pointerdown
      DIV    DIV    pointerup
      BODY   DIV    pointerup
      BUTTON BUTTON click
      BODY   BUTTON click
      

      As in #1, the click should be:

      DIV  DIV click
      BODY DIV click
      

    Here’s a work-around, if you’re desperate, to find the common ancestor of the pointerdown and pointerup events. (It won’t work with keyboard navigation, only pointers.) It would be better if Mozilla fixed the bug so clicks worked with setPointerCapture.

    <!DOCTYPE html><html lang="en"><head>
      <title>play</title>
    </head><body style="border: 1px solid black;">
    <button id="btn" style="margin: 10px; width: 50px;">Test</button>
    <div id="div" style="margin: 10px; height: 50px; width: 50px; background: red"></div>
    <script>( function () {
      'use strict';
    
      var
        div = document.getElementById( 'div' ),
        pathDown = [];
    
      function findClick( e ) {
        var
          target, found, i;
    
        console.log( e.type, 'on', e.target.tagName );
        if ( e.type === 'pointerdown' ) {
          // record the path of ancestors from the target to the root
          pathDown = [];
          for ( target = e.target; target; target = target.parentElement ) {
            pathDown.push( target );
          }
        } else { // pointerup
          found = false;
          // look for the first common ancestor between the "down" event and the "up" event
          for ( target = e.target; target && !found; target = target.parentElement ) {
            for ( i = 0; i < pathDown.length && !found; ++i ) {
              if ( target === pathDown[ i ] ) {
                found = target;
              }
            }
          }
          pathDown = [];
          if ( found ) {
            console.log( 'click on', found.tagName );
          }
        }
      }
      // listen on the body, because everything bubbles up eventually
      document.body.addEventListener( 'pointerdown', findClick );
      document.body.addEventListener( 'pointerup',   findClick );
    
      div.addEventListener( 'pointerdown', e => {
        e.target.setPointerCapture( e.pointerId );
      } );
    } () );</script>
    </body></html>
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search