0

I expect DOM event handling to be synchronous, regardless of the internal sequence of events in a browser.

For reasons I don't understand the click handler is massively delayed (varying between 30-90ms on my machine, Macbook Pro M1 Max, Chrome 126.0.6478.63), so both the code in setTimeout and in requestAnimationFrame run before the click handler runs.

Can someone explain this behavior?

See this MVCE:

const css = `:host(:focus-within) div { outline: 1px solid blue; display: inline-block; }`;
const sheet = new CSSStyleSheet();
sheet.replaceSync(css);

customElements.define(
  "a-b",
  class extends HTMLElement {
    input = document.createElement("input");
    btn = Object.assign(document.createElement("button"), {
      textContent: "Click me"
    });

    constructor() {
      super().attachShadow({ mode: "open" }).adoptedStyleSheets.push(sheet);
      this.shadowRoot.append(this.input, this.btn);
      this.addEventListener("focusin", this.handleFocusIn);
      this.addEventListener("click", this.handleClick);
    }

    handleFocusIn() {
      output.value += `focusin: ${Date.now()}\n`;
      setTimeout(() => {
        // How is this executed before the click listener runs??
        output.value += `timeout: ${Date.now()}\n`;
      }, 0);
      requestAnimationFrame(() => {
        // How is this executed before the click listener runs??
        output.value += `rAF: ${Date.now()}\n`;
      });
    }
    handleClick() {
      output.value += `click: ${Date.now()}\n----------------------\n`;
    }
  }
);
textarea {
  height: 12em;
  margin-top: 2em;
  width: 16em;
}
  
<a-b></a-b>
<br>
<textarea id="output"></textarea>

6
  • 1
    A click is only considered a click when you release the mouse button. As you take a few tenths of a second between the mouse down (and focus in) and the mouse up events, the JS engine has time to deal with other events such as RAF and the timer that expires. Commented Jun 24, 2024 at 14:24
  • @trincot That explains the varying "delay". Would I have to listen for mousedown instead? Commented Jun 24, 2024 at 14:26
  • 1
    Depends on what you want to achieve, but the click event will not come before mouseup. So if you're only interested in the mousedown, then yes, use that event. Note that it is triggered before focusin. Commented Jun 24, 2024 at 14:27
  • @trincot Care providing that as an answer? Commented Jun 24, 2024 at 14:37
  • 1
    @mplungjan I need to handle "clicks" in a way that does not allow code which is just queued but not yet on the stack to run before I handled the "click". In my real world code I need to provide a guard in one of the two event listeners that makes sure the other one doesn't run. The timeout resets that guard. The real world component opens a dropdown on focusin, and should also toggle the dropdown via the click on the button. Currently I have set up a guard that is supposed to prevent toggling the dropdown twice when the button is clicked. Commented Jun 24, 2024 at 14:46

1 Answer 1

1

The delay you see corresponds to the period you have the mouse button down. The click event only triggers when the mouse button is released. As Mozilla contributors write:

click fires after both the mousedown and mouseup events have fired, in that order.

In case of a mouse button triggering these events, the time you need to release that button again is typically a few tenths of a second, which explains your measurements.

Depending on your use case it might be more useful to listen to the mousedown event, which will occur just before the focusin event.

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.