lit icon indicating copy to clipboard operation
lit copied to clipboard

Event target stack incorrect when `wa-page` is imported

Open tpluscode opened this issue 2 weeks ago • 4 comments

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

tpluscode avatar Dec 10 '25 15:12 tpluscode

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 avatar Dec 11 '25 10:12 tpluscode

@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-open operation a bit, since it comes before the custom-element-open operation where we actually make the element renderer instance and attribute-part where we evaluate the bindings. We might have to mark a slotted-element-open operation as having a dynamic name, then special case slot=${...} bindings to find the associated slotStack item and set its name.

    I wonder if we actually need separate slotted-element-open and custom-element-open operations too, since we only push on to the slotStack for custom elements?

  • Similar thing forslot-element-open: we need to mark the ones with dynamic slot names, then when we see a name attribute, 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.

justinfagnani avatar Dec 12 '25 18:12 justinfagnani

cc @kyubisation

justinfagnani avatar Dec 12 '25 18:12 justinfagnani

I wonder if your example has any dynamic slot attributes on elements, and that's the remaining problem?

Not 100% certain, but I don think there are any such cases here

tpluscode avatar Dec 12 '25 20:12 tpluscode

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>
        `;
    }
}

kyubisation avatar Dec 20 '25 21:12 kyubisation

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

tpluscode avatar Dec 20 '25 21:12 tpluscode

I'm still investigating on how to best fix this.

By the way, is this even valid?

tpluscode avatar Dec 20 '25 21:12 tpluscode

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.

kyubisation avatar Dec 20 '25 22:12 kyubisation

On second thought, it could make sense in some dynamic scenarios

tpluscode avatar Dec 20 '25 22:12 tpluscode

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.

KonnorRogers avatar Dec 21 '25 21:12 KonnorRogers