Search code examples
javascripthtmldomdom-eventsshadow-dom

When callbacks won't be called in event listeners?


Well I wrote the following code:

<script>
setTimeout(() => {
  document.body.innerHTML = "<div></div>";
  let outerHost = document.querySelectorAll("div")[0];
  outerHost.attachShadow({mode: "open"});
  outerHost.shadowRoot.innerHTML = "<style>div {width: 100px; height: 100px; background-color: red}</style><div></div><div id='host-shadow'></div>";

  let target = outerHost.shadowRoot.childNodes[2];
  target.attachShadow({mode: "open"});
  target.shadowRoot.innerHTML = "<div id='node-text' style='background-color: green'>Hello</div>";
  let relatedTarget = target.shadowRoot.childNodes[0];

  let ev = new Event("mouseover");
  ev.relatedTarget = relatedTarget;
  target.addEventListener("mouseover", (e) => console.log(e.target, e.relatedTarget));
  target.dispatchEvent(ev);
});
</script>

It creates two shadow roots and to the id="host-shadow" appoint event listener mouseover.

If I set my cursor onto id="node-text" and then move it to the id="host-shadow" I won't get the invocation of callback of the event listener. But If I do it by synthetic method I get the callback invocation. I'am stuck!

Look at the spec:

If target is not relatedTarget or target is event’s relatedTarget, then:

If this condition passing examination, then you getting your callback invocation, otherwise you won't.

As I can compare not sythetic invocation and synthetic, them invocations don't pass the examination. Why does synthetic invocation pass and call callback?

My question: what actually do this condition that I specify above?


Solution

  • It seems you were assuming you could set the Event's relatedTarget by setting a property with that name on the instance object. That is not how this works. The relatedTarget in your quote of the specs is an internal property of the instance, and it's not exposed to JS. It's currently called like that in the specs, but it could be changed tomorrow to another name without any consequences, because it's not exposed.

    For a property to have an influence, it needs to be exposed as a setter on the Interface. Currently the Event interface only exposes the cancelBubble and returnValue attributes as settable. All other attributes are readonly. You can set a few more properties like .type, .bubbles, .cancelable and .composed through the constructor call, and the call to dispatchEvent() will determine the .target and .timestamp one, but relatedTarget is simply not of this list and there is nothing you can do to set it.

    Now, the MouseEvent interface does expose its internal relatedTarget through the readonly .relatedTarget attribute, and it allows setting it through the eponymous property of the MouseEventInit dictionary. So for a MouseEvent, you can actually set it.

    Setting it to what you tried to do, the callback will not be called, because the "result of retargeting event’s relatedTarget against target" would be target (we hit the third bullet in the algo and thus return A directly which is target), but the event's relatedTarget (the one we did set) is not target.

    <div></div>
    <script>
    setTimeout(() => {
      let outerHost = document.querySelectorAll("div")[0];
      outerHost.attachShadow({mode: "open"});
      outerHost.shadowRoot.innerHTML = "<style>div {width: 100px; height: 100px; background-color: red}</style><div></div><div id='host-shadow'></div>";
    
      let target = outerHost.shadowRoot.childNodes[2];
      target.attachShadow({mode: "open"});
      target.shadowRoot.innerHTML = "<div id='node-text' style='background-color: green'>Hello</div>";
      let relatedTarget = target.shadowRoot.childNodes[0];
    
      // Use the MouseEvent interface to set relatedTarget
      let ev = new MouseEvent("mouseover", { relatedTarget });
      // Won't fire
      target.addEventListener("mouseover", (e) => console.log(e.target, e.relatedTarget));
      target.dispatchEvent(ev);
      console.log("Event dispatched");
    });
    </script>