0

https://jsbin.com/qogewewomi/1/edit?html,js,output

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>

<body>
  <test-test></test-test>
  <button id='out'>Outside Shadow DOM</button>
</body>

</html>
customElements.define('test-test', class extends HTMLElement {
  constructor() {
    super();

    const node = document.createElement('template');
    node.innerHTML = '<button id="in">Inside Shadow DOM</button>';
    this.attachShadow({
      mode: 'open'
    }).appendChild(node.content);

    this.shadowRoot.querySelector('#in').addEventListener('click', e => {
      console.log(e.target);
      setTimeout(() => {
        console.log(e.target);
      });
    });
  }
});

document.querySelector('#out').addEventListener('click', e => {
  console.log(e.target);
  setTimeout(() => {
    console.log(e.target);
  });
});

I found these inconsistent behaviors inside an event listener inside and outside of shadow DOM. When the Inside Shadow DOM button is clicked, the console outputs:

<button id="in">Inside Shadow DOM</button>
<test-test>...</test-test>

When the Outside Shadow DOM button is clicked, the console outputs:

<button id="out">Outside Shadow DOM</button>
<button id="out">Outside Shadow DOM</button>

Tested in Chrome, FireFox and Safari. They all have these inconsistent behaviors. I don't know if this is expected behavior or a bug?

Update: This question should not be closed. The other one doesn't answer this question.

2 Answers2

3

It is expected behavior, not a bug.

Explaining it will take too many characters.

See:


In my simple words:

Javascript is single-threaded.
The (e) Event is a global Object passed around all Event Handlers

When you use a SetTimeout the Event content can/will be different

I rewrote your test code:

<shadow-element id="lightcoral" title=One></shadow-element>
<script>
  function log(label, color, scope, evt) {
    let composedTarget = (evt.composed && evt.composedPath());
    console.log(`%c ${label} \t%c ${evt.target.id} `, `background:${color}`, `background:${evt.target.id};color:white`, '\n\ttarget:', evt.target, "\n\tthis:", scope.nodeName || "window", "\n\tcurrentTarget", evt.currentTarget, '\n\tclickedTarget:', evt.clickedTarget, "\n\tcomposed:", evt.composed ? "True" : "False", "composedPath[0]:", composedTarget[0]);
  }

  customElements.define('shadow-element', class extends HTMLElement {
    constructor() {
      super().attachShadow({mode:'open'})
             .innerHTML = `<button id=green>click ${this.title}</button>`;
      let button = this.shadowRoot.querySelector('button');
      button.onclick = e => {
        let savedTarget = e.target;
        e.clickedTarget = e.target;
        button.onclick = false; //prevent double capture
        log(`clicked element:${this.id}`, 'lightgreen', this, e);
        setTimeout(() => {
          log('timeout element', 'red;color:yellow', this, e)
        }, 500);
      };
      //this.onclick = button.onclick;
    }
  });

</script>

To output:

target now is the <shadow-element> because once the setTimeout runs the global Event was passed all the way up the DOM.

currentTarget tells you all Event processing is done
https://developer.mozilla.org/en-US/docs/Web/API/Event/currentTarget

clickedTarget demonstrates you can set custom properties on that global Event object (being passed around). And thus 'save' the target you clicked. But.. other events (or the element.onclick function call below) could overwrite it, so better set a custom variable savedTarget in the correct scope, and use that in your setTimeout


You can see how targetchanges by setting a click handler on the element itself.

target becomes the <shadow-element> the moment the Event bubbles up the DOM and 'escapes' shadowDOM

Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49
-1

The main point being made there is that Events that do fire on elements in the Shadow DOM are retargeted, in order to hide the Shadow DOM from the Light one. This means that during its lifetime the Event object will get its target changed.

  • First at the capturing phase, while the Event object still hasn't reached the Shadow DOM, the Light container (here <test-test>) will be the target of that Event object.
  • Then inside the Shadow DOM, it will get changed to the actual Shadow target (<button>).
  • Finally, at the bubbling phase when getting out of the Shadow DOM, it will be set back to the Light container.

So when you log it from the timeout, all the event phases will have occurred and the Event object will be in this final state with the Light container as .target.

customElements.define('test-test', class extends HTMLElement {
  constructor() {
    super();

    const node = document.createElement('template');
    node.innerHTML = '<button id="in">Inside Shadow DOM</button>';
    this.attachShadow({
      mode: 'open'
    }).appendChild(node.content);

    this.shadowRoot.addEventListener('click', e => {
      console.log("[capturing phase] in Shadow DOM", e.target);
    }, { capture: true });
    this.shadowRoot.addEventListener('click', e => {
      console.log("[bubbling phase] in Shadow DOM", e.target);
    }, { capture: false });

  }
});

document.addEventListener('click', e => {
  console.log( "[capturing phase] in Light DOM", e.target);
}, { capture: true });
document.addEventListener('click', e => {
  console.log( "[bubbling phase] in Light DOM", e.target);
}, { capture: false });
<test-test></test-test>
leonheess
  • 16,068
  • 14
  • 77
  • 112
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • So does it mean if I want to use the same target in setTimeout in shadow dom, I will have to store the variable locally? –  Aug 28 '20 at 09:43
  • 1
    @QianChen yes you need to grab it from when the event fires synchronously (you could also get it from outside the open Shadow DOM through `e.composedPath()[0]`, but this returns an empty Array after a timeout.) – Kaiido Aug 28 '20 at 10:51
  • Depends on where you want to use it. Add it as variable within the listener scope and it is only available within that scope. Add it as extra property on the event and is available to all handlers the event is processed by. – Danny '365CSI' Engelman Aug 28 '20 at 14:04