Event target stack incorrect when `wa-page` is imported
Which package(s) are affected?
SSR (@lit-labs/ssr)
Description
I render a page where my elements are sharing some data using @lit-labs/context. When I put them in Web Awesome wa-page, I found that the context stepped propagating.
I traced it to issue with event targeting where the event target stack is incorrect, thus causing the event to never reach the right parent
Reproduction
https://gist.github.com/tpluscode/3e5bdc0768489f8376379b055ddcfe6f
Workaround
I found none
Is this a regression?
No or unsure. This never worked, or I haven't tried before.
Affected versions
[email protected] @lit-labs/[email protected] @lit-labs/[email protected]
Browser/OS/Node environment
- Brave 1.84.139 Chromium: 142.0.7444.163
- node 20.19.4
- npm 10.8.2
- macOS 15.3.2
I managed get some more details on this. Looks like the issue is when processing slots.
tl;dr;, the op 'slot-element-close' in render-value.js hits more times than 'slot-element-open'. Or, to be more precise, the 'slot-element-open' is wrongly skipped so that some slots are not appended to the target stack and then wrong elements are removed.
My first suspect are dynamic slot names as seen here https://github.com/shoelace-style/webawesome/blob/d2b5613e85074acdaf774690c65ad9ffa95750f5/packages/webawesome/src/components/page/page.ts#L346-L357
When I make them static (and more below), the target stacks look more correct but the event is still not propagated right. There is still an additional 'slot-element-close' which removes wrong element. I pushed a patch with some more console.log calls to the gist. When you run it, you'll see for example this
now LIT-SERVER-ROOT -> EX-PAGE -> WA-PAGE -> WA-DRAWER
POP SLOT - last element is SLOT
POP SLOT - last element is SLOT
POP SLOT - last element is SLOT
POP SLOT - last element is SLOT
POP SLOT - last element is WA-DRAWER
POP SLOT - last element is SLOT
POP SLOT - last element is SLOT
POP WA-PAGE
now LIT-SERVER-ROOT -> EX-PAGE
POP SLOT - last element is SLOT
POP EX-PAGE
now LIT-SERVER-ROOT
LIT-SERVER-ROOT -> PAGE-HEADER
See how wa-drawer is removed from the stack at the wrong moment. This ultimately causes my context consumer element page-header to become "detached" from its context provider element ex-page
@tpluscode thanks for the investigation. Looking at our SSR operations for slots, I can see that we don't support dynamic slot names.
On template prep for slots: https://github.com/lit/lit/blob/57e052a7f32743013b313b5109c62bd42746f4a7/packages/labs/ssr/src/lib/render-value.ts#L487
On template prep for slotted elements: https://github.com/lit/lit/blob/57e052a7f32743013b313b5109c62bd42746f4a7/packages/labs/ssr/src/lib/render-value.ts#L460
These both assume a static name or slot attribute. I wonder if your example has any dynamic slot attributes on elements, and that's the remaining problem?
I don't see why we can't support dynamic values here, but there are probably some tricky bits.
-
We might need to rethink the
slotted-element-openoperation a bit, since it comes before thecustom-element-openoperation where we actually make the element renderer instance andattribute-partwhere we evaluate the bindings. We might have to mark aslotted-element-openoperation as having a dynamic name, then special caseslot=${...}bindings to find the associatedslotStackitem and set its name.I wonder if we actually need separate
slotted-element-openandcustom-element-openoperations too, since we only push on to theslotStackfor custom elements? -
Similar thing for
slot-element-open: we need to mark the ones with dynamic slot names, then when we see anameattribute, look up the slot element and set its name.
I think these things will necessarily be done before any events will be fired that require a proper slot setup, but if not we can throw en error if an event traverses through an un-finalized slot or slotted element.
cc @kyubisation
I wonder if your example has any dynamic
slotattributes on elements, and that's the remaining problem?
Not 100% certain, but I don think there are any such cases here
Ok, I have had a look at the problem. From my understanding, the problem happens when there are multiple slots with the same name in a shadow DOM. This is the case in <wa-page> with e.g. navigation.
I'm still investigating on how to best fix this.
wa-page mock
@customElement('wa-page')
class WaPage extends LitElement {
render() {
return html`
<slot name="navigation"></slot>
<slot name="subheader"></slot>
<slot name="navigation"></slot>
`;
}
}
Superb. Thank you for investigating.
While the wa-page element is closed source now, maybe an alternative fix to the dynamic slot names is actually render a separate slot?
-<slot name=${this.view === 'desktop' ? 'navigation-header' : '___'}><div></div></slot>
+${this.view === 'desktop' ? html`<slot name='navigation-header'><div></div></slot>` : html`<slot name='___'><div></div></slot>`}
cc @KonnorRogers
I'm still investigating on how to best fix this.
By the way, is this even valid?
I'm not sure whether the spec allows this, but technically it works. It just connects slotted elements to the first instance of the named slot. I would also adapt the SSR Event path that way.
On second thought, it could make sense in some dynamic scenarios
the spec allows it, but i think we can just omit the slot with an when() directive in Lit for this case, no need to do funky slot renaming.
EDIT: the way multiple slot elements with the same name works is the last one "defined" in HTML source order "wins" and is where elements will get slotted into.