javascriptgoogle-chromedomdom-eventssynchronous

How does setTimeout in a focusIn handler resolve before the click handler runs?


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>


Solution

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