- From: Dan Clark <notifications@github.com>
- Date: Thu, 02 Apr 2026 09:14:27 -0700
- To: WICG/webcomponents <webcomponents@noreply.github.com>
- Cc: Subscribed <subscribed@noreply.github.com>
- Message-ID: <WICG/webcomponents/issues/1098/4178949684@github.com>
dandclark left a comment (WICG/webcomponents#1098)
Thanks @alice for the deep investigation on this and @jakearchibald for raising the issue.
I was working on a few more test cases for this alongside landing changes for this from a draft @alice started to the [Chromium prototype](https://chromium-review.googlesource.com/c/chromium/src/+/7722304). One interesting aspect I didn't see mentioned in the above discussion is that since events like `command` don't bubble, not all elements in the event path actually get the event.
Consider this test case:
```html
<x-component id="outerC">
<template shadowRootMode="open">
<button id="button" command="--foo" commandFor="outerMiddleC">Do Foo</button>
<x-component id="outerMiddleC">
<template shadowRootMode="open" shadowRootReferenceTarget="targetDiv">
<x-component id="innerMiddleC">
<template shadowRootMode="open">
<x-component id="innerC">
<template shadowRootMode="open">
<slot id="innerCSlot"><!--innerMiddleCSlot with targetDiv--></slot>
</template>
<slot id="innerMiddleCSlot"><!--targetDiv--></slot>
</x-component>
</template>
<div id="targetDiv"></div>
</x-component>
</template>
</x-component>
</template>
</x-component>
```
With the algorithm discussed in this thread, the event path for the `command` event ends up being:
`[targetDiv, innerMiddleCSlot, innerCSlot, innerC.shadowRoot, innerC, innerMiddleC.shadowRoot, innerMiddleC, outerMiddleC.shadowRoot, outerMiddleC, outerC.shadowRoot]`
Of note here is that `innerC` and `innerMiddleC` are in the event path, because of the step `"if shadow root is not the root of target, and event does not have a source or shadow root is not the root of event's source, return shadow root's host;"` when running `get the parent` on `innerC.shadowRoot` and `innerMiddleC.shadowRoot`.
*But*, we don't fire `command` on `innerC` or `innerMiddleC`. We *do* fire it on `outerMiddleC`.
To understand why this happens, we first reference step 6.14 of [dispatch an event](https://dom.spec.whatwg.org/#concept-event-dispatch), which will only invoke a non-bubbling event if the `shadow-adjusted target` is non-null:
> 14. [For each](https://infra.spec.whatwg.org/#list-iterate) struct of event’s [path](https://dom.spec.whatwg.org/#event-path):
> 1. If struct’s [shadow-adjusted target](https://dom.spec.whatwg.org/#event-path-shadow-adjusted-target) is non-null, then set event’s [eventPhase](https://dom.spec.whatwg.org/#dom-event-eventphase) attribute to [AT_TARGET](https://dom.spec.whatwg.org/#dom-event-at_target).
> 1. Otherwise:
> 1. If event’s [bubbles](https://dom.spec.whatwg.org/#dom-event-bubbles) attribute is false, then [continue](https://infra.spec.whatwg.org/#iteration-continue).
> 2. Set event’s [eventPhase](https://dom.spec.whatwg.org/#dom-event-eventphase) attribute to [BUBBLING_PHASE](https://dom.spec.whatwg.org/#dom-event-bubbling_phase).
> 3. [Invoke](https://dom.spec.whatwg.org/#concept-event-listener-invoke) with struct, event, "bubbling", and legacyOutputDidListenersThrowFlag if given.
So, when is `shadow-adjusted target` non-null? This is set up earlier in [dispatch an event](https://dom.spec.whatwg.org/#concept-event-dispatch) when the event path is being built, in steps 6.9.6 and 6.9.8.
> 6. If parent is a [Window](https://html.spec.whatwg.org/multipage/nav-history-apis.html#window) object, or parent is a [node](https://dom.spec.whatwg.org/#concept-node) and target’s [root](https://dom.spec.whatwg.org/#concept-tree-root) is a [shadow-including inclusive ancestor](https://dom.spec.whatwg.org/#concept-shadow-including-inclusive-ancestor) of parent:
> 1. If isActivationEvent is true, event’s [bubbles](https://dom.spec.whatwg.org/#dom-event-bubbles) attribute is true, activationTarget is null, and parent has [activation behavior](https://dom.spec.whatwg.org/#eventtarget-activation-behavior), then set activationTarget to parent.
> 2. [Append to an event path](https://dom.spec.whatwg.org/#concept-event-path-append) with event, parent, null, relatedTarget, touchTargets, and slot-in-closed-tree.
> 7. Otherwise, if parent is relatedTarget, then set parent to null.
> 8. Otherwise:
> 1. Set target to parent.
> 2. If isActivationEvent is true, activationTarget is null, and target has [activation behavior](https://dom.spec.whatwg.org/#eventtarget-activation-behavior), then set activationTarget to target.
> 3. [Append to an event path](https://dom.spec.whatwg.org/#concept-event-path-append) with event, parent, target, relatedTarget, touchTargets, and slot-in-closed-tree.
When `parent` is `innerC` or `innerMiddleC`, we enter the case in step 6 since `targetDiv`'s `root` (`outerMiddleC.shadowRoot`) is a shadow-including inclusive ancestor, so `shadow-adjusted target` (the third argument to [`append to an event path`](https://dom.spec.whatwg.org/#concept-event-path-append)) is set to null. So the event won't fire on these elements.
When `parent` is `outerMiddleC`, we do not enter the case in step 6 since `targetDiv`'s `root` (`outerMiddleC.shadowRoot`) is **not** a shadow-including inclusive ancestor. So we fall through to the `8. Otherwise` step, setting `shadow-adjusted target` (the third argument to [`append to an event path`](https://dom.spec.whatwg.org/#concept-event-path-append)) to `target` which was set to `parent`. So the event will fire on `outerMiddleC`.
This seems like the behavior we want, where the event fires on the `targetDiv` and on `outerMiddleC` which is the shadow host in the same tree as the source button, and not on the shadow hosts in-between. But the way we got that result for this double-slotting case was not obvious to me so I thought it was worth it to write it out.
--
Reply to this email directly or view it on GitHub:
https://github.com/WICG/webcomponents/issues/1098#issuecomment-4178949684
You are receiving this because you are subscribed to this thread.
Message ID: <WICG/webcomponents/issues/1098/4178949684@github.com>
Received on Thursday, 2 April 2026 16:14:32 UTC