From 2ff7658d7ddb4d205122a0d860a289fde61d9b65 Mon Sep 17 00:00:00 2001 From: John Jenkins Date: Tue, 19 May 2026 23:28:59 +0100 Subject: [PATCH 1/3] feat: simplify non-shadow `` logic pt1 --- V5_PLANNING.md | 19 + .../src/runtime/_test_/dom-extras.spec.tsx | 4 +- .../_test_/hydrate-no-encapsulation.spec.tsx | 100 ++--- .../runtime/_test_/hydrate-scoped.spec.tsx | 31 +- .../_test_/hydrate-shadow-child.spec.tsx | 120 +++--- .../_test_/hydrate-shadow-in-shadow.spec.tsx | 35 +- .../_test_/hydrate-shadow-parent.spec.tsx | 91 +++-- .../runtime/_test_/hydrate-shadow.spec.tsx | 32 +- .../_test_/hydrate-slot-fallback.spec.tsx | 224 ++++++----- .../hydrate-slotted-content-order.spec.tsx | 263 +++++++------ .../runtime/_test_/lifecycle-sync.spec.tsx | 4 +- .../src/runtime/_test_/render-vdom.spec.tsx | 4 +- .../core/src/runtime/_test_/scoped.spec.tsx | 16 +- .../runtime/_test_/vdom-relocation.spec.tsx | 46 ++- packages/core/src/runtime/client-hydrate.ts | 317 ++------------- packages/core/src/runtime/dom-extras.ts | 46 +-- .../core/src/runtime/runtime-constants.ts | 13 +- .../core/src/runtime/slot-polyfill-utils.ts | 137 ++----- .../vdom-annotations.spec.tsx.snap | 6 +- .../runtime/vdom/_test_/scoped-slot.spec.tsx | 305 +++++++++++---- .../core/src/runtime/vdom/vdom-annotations.ts | 13 - packages/core/src/runtime/vdom/vdom-render.ts | 361 +++++------------- .../core/src/testing/vitest-stencil-plugin.ts | 2 +- .../build-size/kitchen-sink/stencil.config.ts | 1 - .../kitchen-sink/test-bundle-size.js | 2 +- test/build/build-size/light/stencil.config.ts | 1 - .../build-size/light/test-bundle-size.js | 2 +- test/runtime/hydrated.css | 3 - .../remove-child-patch.spec.tsx | 8 +- .../scoped-add-remove-classes/readme.md | 8 +- .../scoped/scoped-basic/scoped-basic.spec.tsx | 7 +- .../scoped-slot-append-and-prepend.spec.tsx | 28 +- .../scoped-slot-in-slot.spec.tsx | 11 +- .../scoped-slot-insertbefore.spec.tsx | 26 +- .../scoped-slot-slotted-parentnode.spec.tsx | 5 +- .../scoped-slot-text-with-sibling.spec.tsx | 13 +- .../scoped-slot-text.spec.tsx | 9 +- .../slot-array-basic.spec.tsx | 51 +-- ...slot-fallback-with-forwarded-slot.spec.tsx | 12 +- .../slot-fallback/slot-fallback.spec.tsx | 91 ++--- .../slot-hide-content.spec.tsx | 5 +- .../slots/slot-html/slot-html.spec.tsx | 64 ++-- .../slot-nested-default-order.spec.tsx | 4 +- .../slots/slot-reorder/slot-reorder.spec.tsx | 191 ++++----- .../slot-replace-wrapper.spec.tsx | 6 +- .../slot-scoped-list.spec.tsx | 5 +- .../slot-shadow-list.spec.tsx | 6 +- test/runtime/stencil.config.ts | 1 - test/runtime/vitest-setup-custom-elements.ts | 2 +- 49 files changed, 1259 insertions(+), 1492 deletions(-) delete mode 100644 test/runtime/hydrated.css diff --git a/V5_PLANNING.md b/V5_PLANNING.md index d7066213750..41edc0b763c 100644 --- a/V5_PLANNING.md +++ b/V5_PLANNING.md @@ -134,6 +134,25 @@ Modernize Stencil after 10 years: shed tech debt, embrace modern tooling, simpli ## Tasks +### 🎰 Light DOM Slot System Modernization +**Status:** In Progress + +Replace Stencil's ~15-year-old proprietary light DOM slot polyfill with a much simpler architecture using real `` elements as containers. + +**Key changes:** +- Slot references: empty text nodes → real `` elements (UA stylesheet gives them `display:contents` globally) +- Content model: siblings after a text-node marker → children *inside* `` elements +- Fallback visibility: JS traversal (`updateFallbackSlotVisibility`) → pure CSS: `slot:not(:empty)+slot-fb{display:none}` +- `assignedNodes()` / `assignedElements()`: sibling traversal → `slot.childNodes` / `slot.children` +- DOM patches (`dom-extras.ts`): complex sibling-walking → `slot.appendChild` / `slot.prepend` +- SSR: comment marker soup → real `` elements with content already inside them + +**Removes entirely:** `updateFallbackSlotVisibility`, `getSlotChildSiblings`, `checkSlotFallbackVisibility` flag, `isSlotFallback`/`isSlotReference` VNODE_FLAGS, `s-nt-` comment-node hack for unmatched text nodes, debug slot reference comment nodes + +**What stays:** `s-ol` original-location markers (re-render put-back), DOM method patching on host (still needed for transparency, but much simpler implementations), content relocation (still physically moves nodes, now *into* `` instead of adjacent) + +**Normalization:** JSX `fallback` is normalized at render time into two sibling vnodes: `` + `fallback`, so the vdom and DOM always match 1:1. + ### 🌍 `ssr-wasm` Output Target (Planned) New output target that compiles the SSR script to a standalone `.wasm` binary, callable from any language with a WASM runtime (PHP via `ext-wasm`, Java via `wasmtime-java`, Ruby via `wasmtime-rb`, Go, Rust, etc.). diff --git a/packages/core/src/runtime/_test_/dom-extras.spec.tsx b/packages/core/src/runtime/_test_/dom-extras.spec.tsx index 84dfe0fc18d..2c2e96d4d02 100644 --- a/packages/core/src/runtime/_test_/dom-extras.spec.tsx +++ b/packages/core/src/runtime/_test_/dom-extras.spec.tsx @@ -149,8 +149,8 @@ describe('dom-extras - patches for non-shadow dom methods and accessors', () => expect((specPage.root.childNodes[0].parentNode as HTMLElement).tagName).toBe('CMP-A'); expect((specPage.root.childNodes[1].parentNode as HTMLElement).tagName).toBe('CMP-A'); // @ts-ignore - expect((specPage.root.children[0].__parentNode as HTMLElement).tagName).toBe('DIV'); + expect((specPage.root.children[0].__parentNode as HTMLElement).tagName).toBe('SLOT'); // @ts-ignore - expect((specPage.root.childNodes[0].__parentNode as HTMLElement).tagName).toBe('DIV'); + expect((specPage.root.childNodes[0].__parentNode as HTMLElement).tagName).toBe('SLOT'); }); }); diff --git a/packages/core/src/runtime/_test_/hydrate-no-encapsulation.spec.tsx b/packages/core/src/runtime/_test_/hydrate-no-encapsulation.spec.tsx index 56484b6d229..211447aa9fa 100644 --- a/packages/core/src/runtime/_test_/hydrate-no-encapsulation.spec.tsx +++ b/packages/core/src/runtime/_test_/hydrate-no-encapsulation.spec.tsx @@ -195,9 +195,10 @@ describe('hydrate no encapsulation', () => { - - - light-dom + + + light-dom +
@@ -214,8 +215,9 @@ describe('hydrate no encapsulation', () => { - - light-dom + + light-dom +
@@ -255,9 +257,10 @@ describe('hydrate no encapsulation', () => { - - - light-dom + + + light-dom +
@@ -274,8 +277,9 @@ describe('hydrate no encapsulation', () => { - - light-dom + + light-dom +
@@ -317,9 +321,10 @@ describe('hydrate no encapsulation', () => {
- - - light-dom + + + light-dom + `); @@ -336,8 +341,9 @@ describe('hydrate no encapsulation', () => {
- - light-dom + + light-dom +
`); @@ -378,9 +384,10 @@ describe('hydrate no encapsulation', () => {
- - - light-dom + + + light-dom +
@@ -398,8 +405,9 @@ describe('hydrate no encapsulation', () => {
- - light-dom + + light-dom +
@@ -449,19 +457,22 @@ describe('hydrate no encapsulation', () => {
- -
- - top light-dom -
- - - middle light-dom - -
- - bottom light-dom -
+ +
+ + top light-dom +
+
+ + + middle light-dom + + +
+ + bottom light-dom +
+
@@ -479,16 +490,19 @@ describe('hydrate no encapsulation', () => {
- -
- top light-dom -
- - middle light-dom - -
- bottom light-dom -
+ +
+ top light-dom +
+
+ + middle light-dom + + +
+ bottom light-dom +
+
diff --git a/packages/core/src/runtime/_test_/hydrate-scoped.spec.tsx b/packages/core/src/runtime/_test_/hydrate-scoped.spec.tsx index 58b859ab239..48d6980eca3 100644 --- a/packages/core/src/runtime/_test_/hydrate-scoped.spec.tsx +++ b/packages/core/src/runtime/_test_/hydrate-scoped.spec.tsx @@ -28,9 +28,10 @@ describe('hydrate scoped', () => {
- - - 88mph + + + 88mph +
`); @@ -78,9 +79,10 @@ describe('hydrate scoped', () => {
- - - 88mph + + + 88mph +
`); @@ -98,9 +100,10 @@ describe('hydrate scoped', () => { expect(clientHydrated.root).toEqualHtml(` -
- - 88mph +
+ + 88mph +
`); @@ -177,9 +180,9 @@ describe('hydrate scoped', () => { expect(serverHydrated.root).toEqualHtml(` -
+

- +

`); @@ -189,14 +192,14 @@ describe('hydrate scoped', () => { html: serverHydrated.root.outerHTML, hydrateClientSide: true, }); - expect(clientHydrated.root.querySelector('p').className).toBe('hi sc-cmp-a-s sc-cmp-a'); + expect(clientHydrated.root.querySelector('p').className).toBe('hi sc-cmp-a'); expect(clientHydrated.root).toEqualHtml(`
-

- +

+

`); diff --git a/packages/core/src/runtime/_test_/hydrate-shadow-child.spec.tsx b/packages/core/src/runtime/_test_/hydrate-shadow-child.spec.tsx index bcd8ca2f534..ba125be1d8b 100644 --- a/packages/core/src/runtime/_test_/hydrate-shadow-child.spec.tsx +++ b/packages/core/src/runtime/_test_/hydrate-shadow-child.spec.tsx @@ -84,9 +84,10 @@ describe('hydrate, shadow child', () => { - - - light-dom + + + light-dom + `); @@ -146,7 +147,7 @@ describe('hydrate, shadow child', () => {
- +
`); @@ -207,7 +208,7 @@ describe('hydrate, shadow child', () => { shadow-header - + `); @@ -268,9 +269,10 @@ describe('hydrate, shadow child', () => {
- - - light-dom + + + light-dom + `); @@ -399,9 +401,10 @@ describe('hydrate, shadow child', () => {
- - - light-dom + + + light-dom +
@@ -481,24 +484,26 @@ describe('hydrate, shadow child', () => { - - - - - -
- - - cmp-b-top-text - - -
- - cmp-c -
-
-
-
+ + + + + +
+ + + cmp-b-top-text + + +
+ + cmp-c +
+
+
+
+
+
`); @@ -512,22 +517,23 @@ describe('hydrate, shadow child', () => { expect(clientHydrated.root).toEqualHtml(` - - - -
- -
-
- cmp-b-top-text - + + -
- cmp-c -
+
+ +
-
-
+ cmp-b-top-text + + +
+ cmp-c +
+
+
+ +
`); }); @@ -566,21 +572,23 @@ describe('hydrate, shadow child', () => { -
diff --git a/packages/core/src/runtime/_test_/hydrate-slot-fallback.spec.tsx b/packages/core/src/runtime/_test_/hydrate-slot-fallback.spec.tsx index b2b4999fc18..494debbabee 100644 --- a/packages/core/src/runtime/_test_/hydrate-slot-fallback.spec.tsx +++ b/packages/core/src/runtime/_test_/hydrate-slot-fallback.spec.tsx @@ -31,11 +31,12 @@ describe('hydrate, slot fallback', () => {
- - + + + Fallback text - should not be hidden - - + + Fallback element @@ -52,7 +53,8 @@ describe('hydrate, slot fallback', () => { expect(clientHydrated.root).toEqualHtml(` -
+
+ Fallback text - should not be hidden @@ -92,16 +94,17 @@ describe('hydrate, slot fallback', () => {
- - + + + Fallback text - should not be hidden - - - Fallback element - - -
-
+ + + Fallback element + +
+
+ `); const clientHydrated = await newSpecPage({ @@ -168,29 +171,32 @@ describe('hydrate, slot fallback', () => {
-
- + `); const clientHydrated = await newSpecPage({ @@ -202,21 +208,24 @@ describe('hydrate, slot fallback', () => { expect(clientHydrated.root).toEqualHtml(` -
-
-
+ `); const clientHydrated = await newSpecPage({ @@ -510,7 +533,7 @@ describe('hydrate, slot fallback', () => {
- +
@@ -518,12 +541,13 @@ describe('hydrate, slot fallback', () => {
+ Fallback content parent - should be hidden
-
+ `); }); }); diff --git a/packages/core/src/runtime/_test_/hydrate-slotted-content-order.spec.tsx b/packages/core/src/runtime/_test_/hydrate-slotted-content-order.spec.tsx index 8337e9c8747..0d1a4f24c3a 100644 --- a/packages/core/src/runtime/_test_/hydrate-slotted-content-order.spec.tsx +++ b/packages/core/src/runtime/_test_/hydrate-slotted-content-order.spec.tsx @@ -43,22 +43,23 @@ describe("hydrated components' slotted node order", () => {
- -

- slotted item 1 -

- - -

- slotted item 2 -

- - A text node -

- slotted item 3 -

- - + +

+ slotted item 1 +

+ + +

+ slotted item 2 +

+ + A text node +

+ slotted item 3 +

+ + +
`); @@ -137,19 +138,21 @@ describe("hydrated components' slotted node order", () => {
- - - - - Default slot - - + + + + + Default slot + + +
`); @@ -235,35 +238,37 @@ describe("hydrated components' slotted node order", () => {
- -

- slotted item 1a -

- - - - A text node - - - - - - - - -
- -

- slotted item 1b -

- - - - B text node - - -
-
+ +

+ slotted item 1a +

+ + + + A text node + + + + + + + + +
+ +

+ slotted item 1b +

+ + + + B text node + + +
+
+
+
`); @@ -318,22 +323,23 @@ describe("hydrated components' slotted node order", () => {
- -

- slotted item 1 -

- - -

- slotted item 2 -

- - A text node -

- slotted item 3 -

- - + +

+ slotted item 1 +

+ + +

+ slotted item 2 +

+ + A text node +

+ slotted item 3 +

+ + +
`); @@ -349,20 +355,21 @@ describe("hydrated components' slotted node order", () => { expect(clientHydrated.root).toEqualHtml(` -
- -

- slotted item 1 -

- -

- slotted item 2 -

- A text node -

- slotted item 3 -

- +
+ +

+ slotted item 1 +

+ +

+ slotted item 2 +

+ A text node +

+ slotted item 3 +

+ +
`); @@ -414,19 +421,21 @@ describe("hydrated components' slotted node order", () => {
- - - - - Default slot - - + + + + + Default slot + + +
`); @@ -494,35 +503,37 @@ describe("hydrated components' slotted node order", () => {
- -

- slotted item 1a -

- - - - A text node - - - - - - - - -
- -

- slotted item 1b -

- - - - B text node - - -
-
+ +

+ slotted item 1a +

+ + + + A text node + + + + + + + + +
+ +

+ slotted item 1b +

+ + + + B text node + + +
+
+
+
`); diff --git a/packages/core/src/runtime/_test_/lifecycle-sync.spec.tsx b/packages/core/src/runtime/_test_/lifecycle-sync.spec.tsx index 9165d2089f1..9c56c6c444c 100644 --- a/packages/core/src/runtime/_test_/lifecycle-sync.spec.tsx +++ b/packages/core/src/runtime/_test_/lifecycle-sync.spec.tsx @@ -70,7 +70,9 @@ describe('lifecycle sync', () => { expect(root).toEqualHtml(`
- + + +
`); diff --git a/packages/core/src/runtime/_test_/render-vdom.spec.tsx b/packages/core/src/runtime/_test_/render-vdom.spec.tsx index d0d5fbe3c39..0a77654b3ad 100644 --- a/packages/core/src/runtime/_test_/render-vdom.spec.tsx +++ b/packages/core/src/runtime/_test_/render-vdom.spec.tsx @@ -593,7 +593,9 @@ describe('render-vdom', () => { expect(root).toEqualHtml(` - Hello + + Hello + `); diff --git a/packages/core/src/runtime/_test_/scoped.spec.tsx b/packages/core/src/runtime/_test_/scoped.spec.tsx index eff88b9e688..030db62f951 100644 --- a/packages/core/src/runtime/_test_/scoped.spec.tsx +++ b/packages/core/src/runtime/_test_/scoped.spec.tsx @@ -42,10 +42,12 @@ describe('scoped', () => { expect(page.root).toEqualHtml(` -
- - Hola - +
+ + + Hola + +
@@ -84,8 +86,10 @@ describe('scoped', () => { expect(page.root).toEqualHtml(`
-
- hello +
+ + hello +
diff --git a/packages/core/src/runtime/_test_/vdom-relocation.spec.tsx b/packages/core/src/runtime/_test_/vdom-relocation.spec.tsx index 1f1ad578bbc..e8da45d8bc7 100644 --- a/packages/core/src/runtime/_test_/vdom-relocation.spec.tsx +++ b/packages/core/src/runtime/_test_/vdom-relocation.spec.tsx @@ -47,15 +47,17 @@ describe('vdom-relocation', () => {
-
- 1 -
-
- 2 -
-
- 3 -
+ +
+ 1 +
+
+ 2 +
+
+ 3 +
+
`); @@ -67,18 +69,20 @@ describe('vdom-relocation', () => {
-
- 1 -
-
- 2 -
-
- 3 -
-
- 4 -
+ +
+ 1 +
+
+ 2 +
+
+ 3 +
+
+ 4 +
+
`); diff --git a/packages/core/src/runtime/client-hydrate.ts b/packages/core/src/runtime/client-hydrate.ts index d67dbcf6f67..bff2c82a6b8 100644 --- a/packages/core/src/runtime/client-hydrate.ts +++ b/packages/core/src/runtime/client-hydrate.ts @@ -3,7 +3,7 @@ import { getHostRef, plt, transformTag, win } from 'virtual:platform'; import type * as d from '@stencil/core'; import { CMP_FLAGS } from '../utils/constants'; -import { internalCall, patchSlottedNode } from './dom-extras'; +import { patchSlottedNode } from './dom-extras'; import { getShadowRoot } from './element'; import { createTime } from './profile'; import { @@ -13,9 +13,7 @@ import { HYDRATE_ID, NODE_TYPE, ORG_LOCATION_ID, - SLOT_NODE_ID, TEXT_NODE_ID, - VNODE_FLAGS, } from './runtime-constants'; import { addSlotRelocateNode, patchSlotNode } from './slot-polyfill-utils'; import { getScopeId } from './styles'; @@ -48,8 +46,6 @@ export const initializeClientHydrate = ( const childRenderNodes: RenderNodeData[] = []; // nodes representing a `` element const slotNodes: RenderNodeData[] = []; - // nodes that have been slotted from outside the component - const slottedNodes: SlottedNodes[] = []; // nodes that make up this component's shadowDOM const shadowRootNodes: d.RenderNode[] = BUILD.shadowDom && shadowRoot ? [] : null; // The root VNode for this component @@ -84,7 +80,6 @@ export const initializeClientHydrate = ( hostElm, hostElm, hostId, - slottedNodes, ); let crIndex = 0; @@ -131,22 +126,16 @@ export const initializeClientHydrate = ( } if (childRenderNode.$tag$ === 'slot') { - childRenderNode.$name$ = - childRenderNode.$elm$['s-sn'] || (childRenderNode.$elm$ as any)['name'] || null; - if (childRenderNode.$children$) { - childRenderNode.$flags$ |= VNODE_FLAGS.isSlotFallback; - - if (!childRenderNode.$elm$.childNodes.length) { - // idiosyncrasy with slot fallback nodes during SSR + `serializeShadowRoot: false`: - // the slot node is created here (in `addSlot()`) via a comment node, - // but the children aren't moved into it. Let's do that now - childRenderNode.$children$.forEach((c) => { - childRenderNode.$elm$.appendChild(c.$elm$); - }); - } - } else { - childRenderNode.$flags$ |= VNODE_FLAGS.isSlotReference; + childRenderNode.$name$ = (node as HTMLSlotElement).name || null; + if (!shadowRoot) { + node['s-sr'] = true; + node['s-sn'] = (node as HTMLSlotElement).name || ''; + node['s-cr'] = hostElm['s-cr']; + patchSlotNode(node); + slotNodes.push(childRenderNode); } + } else if (childRenderNode.$tag$ === 'slot-fb') { + node['s-sn'] = node.getAttribute('name') || ''; } if (orgLocationNode && orgLocationNode.isConnected) { @@ -172,97 +161,35 @@ export const initializeClientHydrate = ( } } - const hosts: d.HostElement[] = []; - const snLen = slottedNodes.length; - let snIndex = 0; - let slotGroup: SlottedNodes; - let snGroupIdx: number; - let snGroupLen: number; - let slottedItem: SlottedNodes[0]; - let currentPos = 0; - - // Loops through all the slotted nodes we found while stepping through this component. - // creates slot relocation nodes (non-shadow) or moves nodes to their new home (shadow) - for (snIndex; snIndex < snLen; snIndex++) { - slotGroup = slottedNodes[snIndex]; - - if (!slotGroup || !slotGroup.length) continue; - - snGroupLen = slotGroup.length; - snGroupIdx = 0; - - for (snGroupIdx; snGroupIdx < snGroupLen; snGroupIdx++) { - slottedItem = slotGroup[snGroupIdx]; - - const hid = slottedItem.hostId as any; - if (!hosts[hid]) { - // Cache this host for other grouped slotted nodes - hosts[hid] = plt.$orgLocNodes$.get(slottedItem.hostId); - } - // This *shouldn't* happen as we collect all the custom elements first in `initializeDocumentHydrate` - if (!hosts[hid]) continue; - - const hostEle = hosts[hid]; - const siNode = slottedItem.node; - const siSlot = slottedItem.slot; - - if (hostEle.shadowRoot && siNode.parentElement !== hostEle) { - // shadowDOM. This slotted node got left behind. - // Move the item to the element root for native slotting - // insert node after the previous node in the slotGroup - hostEle.insertBefore(siNode, slotGroup[snGroupIdx - 1]?.node?.nextSibling); - } - - // This node is either slotted in a non-shadow host, OR *that* host is nested in a non-shadow host - if (!hostEle.shadowRoot || !shadowRoot) { - // Try to set an appropriate Content-position Reference (CR) node for this host element - - if (!siSlot['s-cr']) { - // Is a CR already set on the host? - siSlot['s-cr'] = hostEle['s-cr']; - - if (!siSlot['s-cr'] && hostEle.shadowRoot) { - // Host has shadowDOM - just use the host itself as the CR for native slotting - siSlot['s-cr'] = hostEle; - } else { - // If all else fails - just set the CR as the first child - // (9/10 if node['s-cr'] hasn't been set, the node will be at the element root) - siSlot['s-cr'] = ((hostEle as any).__childNodes || hostEle.childNodes)[0]; - } - } - // Create our 'Original Location' node - addSlotRelocateNode(siNode, siSlot, false, siNode['s-oo'] || currentPos); - - if ( - siNode.parentElement?.shadowRoot && - siNode['getAttribute'] && - siNode.getAttribute('slot') - ) { - // Remove the `slot` attribute from the slotted node: - // if it's projected from a scoped component into a shadowRoot it's slot attribute will cause it to be hidden. - // scoped components use the `s-sn` attribute to identify slotted nodes - siNode.removeAttribute('slot'); - } - + // For non-shadow: set s-sn on slotted content and create s-ol markers from children. + // Text-position comments () are cleaned up during the parent's clientHydrate pass. + if (BUILD.slotRelocation && !shadowRoot && slotNodes.length) { + let currentPos = 0; + slotNodes.forEach((slotVNode) => { + const slotElm = slotVNode.$elm$ as d.RenderNode; + Array.from(slotElm.childNodes).forEach((child) => { + const childNode = child as d.RenderNode; + childNode['s-sn'] = slotElm['s-sn']; + childNode['s-hn'] = transformTag(tagName).toUpperCase(); if ( BUILD.lightDomPatches || BUILD.slotChildNodes || (BUILD.patchAll && hostRef.$cmpMeta$.$flags$ & CMP_FLAGS.patchAll) ) { - // patch this node for accessors like `nextSibling` (et al) - patchSlottedNode(siNode); + patchSlottedNode(childNode); } - } - // Empty text nodes are never accounted on the server (they don't get a comment node, or a positional id) - // So let's manually increment their position counter for them, keeping them in the correct order in the slot - currentPos = (siNode['s-oo'] || currentPos) + 1; - } + // Use s-oo (original order from nodeId) so cross-slot document order is preserved + const pos = childNode['s-oo'] ?? currentPos; + addSlotRelocateNode(childNode, slotElm, false, pos); + currentPos = pos + 1; + }); + }); } if (BUILD.scoped && scopeId && slotNodes.length) { slotNodes.forEach((slot) => { - // Host is `scoped: true` - add the slotted scoped class to the slot parent - slot.$elm$.parentElement.classList.add(scopeId + '-s'); + // is now the direct parent of slotted nodes — add '-s' here + slot.$elm$.classList.add(scopeId + '-s'); }); } @@ -330,7 +257,6 @@ export const initializeClientHydrate = ( * @param hostElm The parent element. * @param node The node to construct the vNode tree for. * @param hostId The host ID assigned to the element by the server. - * @param slottedNodes - nodes that have been slotted * @returns - the constructed VNode */ const clientHydrate = ( @@ -341,7 +267,6 @@ const clientHydrate = ( hostElm: d.HostElement, node: d.RenderNode, hostId: string, - slottedNodes: SlottedNodes[] = [], ) => { let childNodeType: string; let childIdSplt: string[]; @@ -385,30 +310,8 @@ const clientHydrate = ( } // Test if this element was 'slotted' or is a 'slot' (with fallback). Recreate node attributes - const slotName = childVNode.$elm$.getAttribute('s-sn'); - if (typeof slotName === 'string') { - if (childVNode.$tag$ === 'slot-fb') { - // This is a slot node. Set it up and find any assigned slotted nodes - addSlot( - slotName, - childIdSplt[2], - childVNode, - node, - parentVNode, - childRenderNodes, - slotNodes, - shadowRootNodes, - slottedNodes, - ); - - if (BUILD.scoped && scopeId) { - // Host is `scoped: true` - a slot-fb node - // never goes through 'set-accessor.ts' so add the class now - node.classList.add(scopeId); - } - } - childVNode.$elm$['s-sn'] = slotName; - childVNode.$elm$.removeAttribute('s-sn'); + if (childVNode.$tag$ === 'slot-fb' && BUILD.scoped && scopeId) { + node.classList.add(scopeId); } if (childVNode.$index$ !== undefined) { // add our child VNode to a specific index of the VNode's children @@ -435,7 +338,6 @@ const clientHydrate = ( hostElm, node.shadowRoot.childNodes[i] as any, hostId, - slottedNodes, ); } } @@ -451,7 +353,6 @@ const clientHydrate = ( hostElm, nonShadowNodes[i] as any, hostId, - slottedNodes, ); } } else if (node.nodeType === NODE_TYPE.CommentNode) { @@ -507,26 +408,7 @@ const clientHydrate = ( } else if (childVNode.$hostId$ === hostId) { // This comment node is specifically for this host id - if (childNodeType === SLOT_NODE_ID) { - // Comment refers to a slot node: - // `${SLOT_NODE_ID}.${hostId}.${nodeId}.${depth}.${index}.${slotName}`; - - // Add the slot name - const slotName = (node['s-sn'] = childIdSplt[5] || ''); - - // add the `` node to the VNode tree and prepare any slotted any child nodes - addSlot( - slotName, - childIdSplt[2], - childVNode, - node, - parentVNode, - childRenderNodes, - slotNodes, - shadowRootNodes, - slottedNodes, - ); - } else if (childNodeType === CONTENT_REF_ID) { + if (childNodeType === CONTENT_REF_ID) { // `${CONTENT_REF_ID}.${hostId}`; if (BUILD.shadowDom && shadowRootNodes) { // Remove the content ref comment since it's not needed for shadow @@ -598,139 +480,6 @@ const initializeDocumentHydrate = ( const createSimpleVNode = (vnode: Partial): RenderNodeData => ({ $flags$: 0, $index$: '0', ...vnode }) as RenderNodeData; -function addSlot( - slotName: string, - slotId: string, - childVNode: RenderNodeData, - node: d.RenderNode, - parentVNode: d.VNode, - childRenderNodes: RenderNodeData[], - slotNodes: RenderNodeData[], - shadowRootNodes: d.RenderNode[], - slottedNodes: SlottedNodes[], -) { - node['s-sr'] = true; - childVNode.$name$ = slotName || null; - childVNode.$tag$ = 'slot'; - - // Find this slots' current host parent (as dictated by the VDOM tree). - // Important because where it is now in the constructed SSR markup might be different to where to *should* be - const parentNodeId = parentVNode?.$elm$ - ? parentVNode.$elm$['s-id'] || parentVNode.$elm$.getAttribute('s-id') - : ''; - - if (BUILD.shadowDom && shadowRootNodes && win.document) { - /* SHADOW */ - - // Browser supports shadowRoot and this is a shadow dom component; create an actual slot element - const slot = (childVNode.$elm$ = win.document.createElement( - childVNode.$tag$ as string, - ) as d.RenderNode); - - if (childVNode.$name$) { - // Add the slot name attribute - childVNode.$elm$.setAttribute('name', slotName); - } - - if (parentVNode.$elm$.shadowRoot && parentNodeId && parentNodeId !== childVNode.$hostId$) { - // Shadow component's slot is placed inside a nested component's shadowDOM; it doesn't belong to this host - it was forwarded by the SSR markup. - // Insert it in the root of this host; it's lightDOM. It doesn't really matter where in the host root; the component will take care of it. - internalCall(parentVNode.$elm$, 'insertBefore')( - slot, - internalCall(parentVNode.$elm$, 'children')[0], - ); - } else { - // Insert the new slot element before the slot comment - internalCall(internalCall(node, 'parentNode') as d.RenderNode, 'insertBefore')(slot, node); - } - addSlottedNodes(slottedNodes, slotId, slotName, node, childVNode.$hostId$); - - // Remove the slot comment since it's not needed for shadow - node.remove(); - - if (childVNode.$depth$ === '0') { - shadowRootNodes[childVNode.$index$ as any] = childVNode.$elm$; - } - } else { - /* NON-SHADOW */ - const slot = childVNode.$elm$ as d.RenderNode; - - // Test to see if this non-shadow component's mock 'slot' is placed inside a nested component's shadowDOM. If so, it doesn't belong here; - // it was forwarded by the SSR markup. So we'll insert it into the root of this host; it's lightDOM with accompanying 'slotted' nodes - const shouldMove = - parentNodeId && parentNodeId !== childVNode.$hostId$ && parentVNode.$elm$.shadowRoot; - - // attempt to find any mock slotted nodes which we'll move later - addSlottedNodes( - slottedNodes, - slotId, - slotName, - node, - shouldMove ? parentNodeId : childVNode.$hostId$, - ); - patchSlotNode(node); - - if (shouldMove) { - // Move slot comment node (to after any other comment nodes) - parentVNode.$elm$.insertBefore(slot, parentVNode.$elm$.children[0]); - } - } - - childRenderNodes.push(childVNode); - slotNodes.push(childVNode); - - if (!parentVNode.$children$) { - parentVNode.$children$ = []; - } - parentVNode.$children$[childVNode.$index$ as any] = childVNode; -} - -/** - * Adds groups of slotted nodes (grouped by slot ID) to this host element's 'master' array. - * We'll use this after the host element's VDOM is completely constructed to finally position and add meta required by non-shadow slotted nodes - * - * @param slottedNodes - the main host element 'master' array to add to - * @param slotNodeId - the slot node unique ID - * @param slotName - the slot node name (can be '') - * @param slotNode - the slot node - * @param hostId - the host element id where this node should be slotted - */ -const addSlottedNodes = ( - slottedNodes: SlottedNodes[], - slotNodeId: string, - slotName: string, - slotNode: d.RenderNode, - hostId: string, -) => { - let slottedNode = slotNode.nextSibling as d.RenderNode; - const group: SlottedNodes = (slottedNodes[slotNodeId as any] ||= []); - - // stop if we find another slot node (as subsequent nodes will belong to that slot) - if (!slottedNode || slottedNode.nodeValue?.startsWith(SLOT_NODE_ID + '.')) return; - - // Loop through the next siblings of the slot node, looking for nodes that match this slot's name - // slottedNode is guaranteed truthy here (checked above) and at each while re-entry (checked by while condition) - do { - const sa = slottedNode['getAttribute'] && slottedNode.getAttribute('slot'); - if ( - (sa || slottedNode['s-sn']) === slotName || - (slotName === '' && - !slottedNode['s-sn'] && - !sa && - (slottedNode.nodeType === NODE_TYPE.CommentNode || - slottedNode.nodeType === NODE_TYPE.TextNode)) - ) { - // Looking for nodes that match this slot's name, - // OR are text / comment nodes and the slot is a default slot (no name) - text / comments cannot be direct descendants of *named* slots. - // Also ignore slot fallback nodes - they're not part of the lightDOM - slottedNode['s-sn'] = slotName; - group.push({ slot: slotNode, node: slottedNode, hostId }); - } - slottedNode = slottedNode.nextSibling as d.RenderNode; - // continue *unless* we find another slot node (as subsequent nodes will belong to that slot) - } while (slottedNode && !slottedNode.nodeValue?.startsWith(SLOT_NODE_ID + '.')); -}; - /** * Steps through the node's siblings to find the next node of a specific type, with a value. * e.g. when we find a position comment ``, we need to find the next text node with a value. @@ -750,8 +499,6 @@ const findCorrespondingNode = ( return sibling; }; -type SlottedNodes = Array<{ slot: d.RenderNode; node: d.RenderNode; hostId: string }>; - interface RenderNodeData extends d.VNode { $hostId$: string; $nodeId$: string; diff --git a/packages/core/src/runtime/dom-extras.ts b/packages/core/src/runtime/dom-extras.ts index 4bd5c76fecb..14535070eda 100644 --- a/packages/core/src/runtime/dom-extras.ts +++ b/packages/core/src/runtime/dom-extras.ts @@ -6,10 +6,8 @@ import { dispatchSlotChangeEvent, findSlotFromSlottedNode, getHostSlotNodes, - getSlotChildSiblings, getSlotName, getSlottedChildNodes, - updateFallbackSlotVisibility, } from './slot-polyfill-utils'; /// HOST ELEMENTS /// @@ -95,23 +93,11 @@ export const patchSlotAppendChild = (HostElementPrototype: any) => { HostElementPrototype.__appendChild = HostElementPrototype.appendChild; HostElementPrototype.appendChild = function (this: d.RenderNode, newChild: d.RenderNode) { - const { slotName, slotNode } = findSlotFromSlottedNode(newChild, this); + const { slotNode } = findSlotFromSlottedNode(newChild, this); if (slotNode) { addSlotRelocateNode(newChild, slotNode); - - const slotChildNodes = getSlotChildSiblings(slotNode, slotName); - const appendAfter = slotChildNodes[slotChildNodes.length - 1]; - - const parent = internalCall(appendAfter, 'parentNode') as d.RenderNode; - const insertedNode: d.RenderNode = internalCall(parent, 'insertBefore')( - newChild, - appendAfter.nextSibling, - ); + const insertedNode: d.RenderNode = internalCall(slotNode, 'appendChild')(newChild); dispatchSlotChangeEvent(slotNode); - - // Check if there is fallback content that should be hidden - updateFallbackSlotVisibility(this); - return insertedNode; } return (this as any).__appendChild(newChild); @@ -130,16 +116,13 @@ export const patchSlotRemoveChild = (ElementPrototype: any) => { ElementPrototype.__removeChild = ElementPrototype.removeChild; ElementPrototype.removeChild = function (this: d.RenderNode, toRemove: d.RenderNode) { - if (toRemove && typeof toRemove['s-sn'] !== 'undefined') { - const childNodes = (this as any).__childNodes || this.childNodes; - const slotNode = getHostSlotNodes(childNodes, this.tagName, toRemove['s-sn']); - if (slotNode && toRemove.isConnected) { - toRemove.remove(); - // Check if there is fallback content that should be displayed if that - // was the last node in the slot - updateFallbackSlotVisibility(this); - return; - } + // If the node lives inside a element, remove it there directly. + // CSS `slot:not(:empty)+slot-fb{display:none}` handles fallback visibility automatically. + const slotParent = (toRemove as any).__parentNode as d.RenderNode; + if (slotParent?.['s-sr'] && toRemove.isConnected) { + toRemove.remove(); + dispatchSlotChangeEvent(slotParent); + return toRemove; } return (this as any).__removeChild(toRemove); }; @@ -167,16 +150,9 @@ export const patchSlotPrepend = (HostElementPrototype: HTMLElement) => { const slotNode = getHostSlotNodes(childNodes, this.tagName, slotName)[0]; if (slotNode) { addSlotRelocateNode(newChild, slotNode, true); - const slotChildNodes = getSlotChildSiblings(slotNode, slotName); - const appendAfter = slotChildNodes[0]; - - const parent = internalCall(appendAfter, 'parentNode') as d.RenderNode; - const toReturn = internalCall(parent, 'insertBefore')( - newChild, - internalCall(appendAfter, 'nextSibling'), - ); + internalCall(slotNode, 'prepend')(newChild); dispatchSlotChangeEvent(slotNode); - return toReturn; + return; } if (newChild.nodeType === 1 && !!newChild.getAttribute('slot')) { diff --git a/packages/core/src/runtime/runtime-constants.ts b/packages/core/src/runtime/runtime-constants.ts index ea331af8a3e..7d36f0076cf 100644 --- a/packages/core/src/runtime/runtime-constants.ts +++ b/packages/core/src/runtime/runtime-constants.ts @@ -56,7 +56,6 @@ export const NODE_TYPE = { export const CONTENT_REF_ID = 'r'; export const ORG_LOCATION_ID = 'o'; -export const SLOT_NODE_ID = 's'; export const TEXT_NODE_ID = 't'; export const COMMENT_NODE_ID = 'c'; @@ -73,12 +72,14 @@ export const DEFAULT_DOC_DATA = { }; /** - * Constant for styles to be globally applied to `slot-fb` elements for pseudo-slot behavior. - * - * Two cascading rules must be used instead of a `:not()` selector due to Stencil browser - * support as of Stencil v4. + * CSS for light DOM slot polyfill. `` gets display:contents (matches UA stylesheet, + * but explicit for SSR/test environments). Fallback visibility is CSS-driven: + * `` shows by default, hidden whenever its preceding `` has content. */ -export const SLOT_FB_CSS = 'slot-fb{display:contents}slot-fb[hidden]{display:none}'; +// slot:has(:not(slot:empty)) handles forwarded-slot chains: hide slot-fb only when +// the slot tree contains at least one non-empty-slot descendant (real content). +export const SLOT_FB_CSS = + 'slot{display:contents}slot-fb{display:contents}slot:has(:not(slot:empty))+slot-fb{display:none}'; export const XLINK_NS = 'http://www.w3.org/1999/xlink'; diff --git a/packages/core/src/runtime/slot-polyfill-utils.ts b/packages/core/src/runtime/slot-polyfill-utils.ts index 8d359476e87..98d60e78c4a 100644 --- a/packages/core/src/runtime/slot-polyfill-utils.ts +++ b/packages/core/src/runtime/slot-polyfill-utils.ts @@ -4,52 +4,6 @@ import type * as d from '@stencil/core'; import { internalCall } from './dom-extras'; import { NODE_TYPE } from './runtime-constants'; -/** - * Adjust the `.hidden` property as-needed on any nodes in a DOM subtree which - * are slot fallback nodes - `...` - * - * A slot fallback node should be visible by default. Then, it should be - * conditionally hidden if: - * - * - it has a sibling with a `slot` property set to its slot name or if - * - it is a default fallback slot node, in which case we hide if it has any - * content - * - * @param elm the element of interest - */ -export const updateFallbackSlotVisibility = (elm: d.RenderNode) => { - const childNodes = internalCall(elm, 'childNodes'); - - // is this is a stencil component? - if (elm.tagName && elm.tagName.includes('-') && elm['s-cr'] && elm.tagName !== 'SLOT-FB') { - // stencil component - try to find any slot fallback nodes - getHostSlotNodes(childNodes as any, (elm as HTMLElement).tagName).forEach((slotNode) => { - if (slotNode.nodeType === NODE_TYPE.ElementNode && slotNode.tagName === 'SLOT-FB') { - // this is a slot fallback node - if (getSlotChildSiblings(slotNode, getSlotName(slotNode), false).length) { - // has slotted nodes, hide fallback - slotNode.hidden = true; - } else { - // no slotted nodes - slotNode.hidden = false; - } - } - }); - } - - let i = 0; - for (i = 0; i < childNodes.length; i++) { - const childNode = childNodes[i] as d.RenderNode; - if ( - childNode.nodeType === NODE_TYPE.ElementNode && - internalCall(childNode, 'childNodes').length - ) { - // keep drilling down - updateFallbackSlotVisibility(childNode); - } - } -}; - /** * Get's the child nodes of a component that are actually slotted. * It does this by using root nodes of a component; for each slotted node there is a @@ -102,25 +56,6 @@ export function getHostSlotNodes( return slottedNodes; } -/** - * Get all 'child' sibling nodes of a slot node - * @param slot - the slot node to get the child nodes from - * @param slotName - the name of the slot to match on - * @param includeSlot - whether to include the slot node in the result - * @returns child nodes of the slot node - */ -export const getSlotChildSiblings = (slot: d.RenderNode, slotName: string, includeSlot = true) => { - const childNodes: d.RenderNode[] = []; - if ((includeSlot && slot['s-sr']) || !slot['s-sr']) childNodes.push(slot as any); - let node = slot; - - while ((node = node.nextSibling as any)) { - if (getSlotName(node) === slotName && (includeSlot || !node['s-sr'])) - childNodes.push(node as any); - } - return childNodes; -}; - /** * Check whether a node is located in a given named slot. * @@ -130,6 +65,11 @@ export const getSlotChildSiblings = (slot: d.RenderNode, slotName: string, inclu */ export const isNodeLocatedInSlot = (nodeToRelocate: d.RenderNode, slotName: string): boolean => { if (nodeToRelocate.nodeType === NODE_TYPE.ElementNode) { + // A forwarding slot (a rendered inside another component's children) + // matches by its own slot name rather than a `slot` attribute. + if (nodeToRelocate['s-sr'] && nodeToRelocate['s-sn'] === slotName) { + return true; + } if (nodeToRelocate.getAttribute('slot') === null && slotName === '') { // if the node doesn't have a slot attribute, and the slot we're checking // is not a named slot, then we assume the node should be within the slot @@ -207,49 +147,39 @@ export const getSlotName = (node: d.PatchedSlotNode) => : (node.nodeType === 1 && (node as Element).getAttribute('slot')) || undefined; /** - * Add `assignedElements` and `assignedNodes` methods on a fake slot node + * Add `assignedElements` and `assignedNodes` methods on a `` element. + * Content is now physically inside the slot, so these are trivial. * * @param node - slot node to patch */ export function patchSlotNode(node: d.RenderNode) { - if ((node as any).assignedElements || (node as any).assignedNodes || !node['s-sr']) return; - - const assignedFactory = (elementsOnly: boolean) => - function (opts?: { flatten: boolean }) { - const toReturn: d.RenderNode[] = []; - const slotName = this['s-sn']; - - if (opts?.flatten) { - if (BUILD.isDev) { - console.error( - 'Flattening is not supported for Stencil non-shadow slots. You can use `.childNodes` for nested slot fallback content.', - ); - } else { - console.error('Flattening not supported for Stencil non-shadow slots'); - } + if (!node['s-sr']) return; + + (node as any).assignedNodes = function (opts?: { flatten: boolean }) { + if (opts?.flatten) { + if (BUILD.isDev) { + console.error( + 'Flattening is not supported for Stencil non-shadow slots. You can use `.childNodes` for nested slot fallback content.', + ); + } else { + console.error('Flattening not supported for Stencil non-shadow slots'); } - - const parent = this['s-cr'].parentElement as d.RenderNode; - // get all light dom nodes - const slottedNodes = parent.__childNodes - ? parent.childNodes - : getSlottedChildNodes(parent.childNodes); - - (slottedNodes as d.RenderNode[]).forEach((n) => { - // find all the nodes assigned to slots we care about - if (slotName === getSlotName(n)) { - toReturn.push(n); - } - }); - - if (elementsOnly) { - return toReturn.filter((n) => n.nodeType === NODE_TYPE.ElementNode); + } + return Array.from(this.childNodes); + }.bind(node); + + (node as any).assignedElements = function (opts?: { flatten: boolean }) { + if (opts?.flatten) { + if (BUILD.isDev) { + console.error( + 'Flattening is not supported for Stencil non-shadow slots. You can use `.childNodes` for nested slot fallback content.', + ); + } else { + console.error('Flattening not supported for Stencil non-shadow slots'); } - return toReturn; - }.bind(node); - - (node as any).assignedElements = assignedFactory(true); - (node as any).assignedNodes = assignedFactory(false); + } + return Array.from(this.children); + }.bind(node); } /** @@ -258,7 +188,8 @@ export function patchSlotNode(node: d.RenderNode) { * @param elm the slot node to dispatch the event from */ export function dispatchSlotChangeEvent(elm: d.RenderNode) { - (elm as any).name = elm['s-sn'] || ''; + // Only set name for named slots — setting name='' adds a spurious empty attribute on default slots + if (elm['s-sn']) (elm as any).name = elm['s-sn']; elm.dispatchEvent( new CustomEvent('slotchange', { bubbles: false, cancelable: false, composed: false }), ); diff --git a/packages/core/src/runtime/vdom/_test_/__snapshots__/vdom-annotations.spec.tsx.snap b/packages/core/src/runtime/vdom/_test_/__snapshots__/vdom-annotations.spec.tsx.snap index 9d15d959059..fcae407780f 100644 --- a/packages/core/src/runtime/vdom/_test_/__snapshots__/vdom-annotations.spec.tsx.snap +++ b/packages/core/src/runtime/vdom/_test_/__snapshots__/vdom-annotations.spec.tsx.snap @@ -1,7 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`vdom-annotations > should add annotations when component-a-test and component-b-test is given as static component 1`] = `"
o.0.1
slotContent
o.0.2
slotContent
"`; +exports[`vdom-annotations > should add annotations when component-a-test and component-b-test is given as static component 1`] = `"
o.0.1
slotContent
o.0.2
slotContent
"`; -exports[`vdom-annotations > should add annotations when component-a-test is given as static component 1`] = `"
o.0.1
slotContent
o.0.2
slotContent
"`; +exports[`vdom-annotations > should add annotations when component-a-test is given as static component 1`] = `"
o.0.1
slotContent
o.0.2
slotContent
"`; -exports[`vdom-annotations > should add annotations when no static component is given 1`] = `"
o.0.1
slotContent
o.0.2
slotContent
"`; +exports[`vdom-annotations > should add annotations when no static component is given 1`] = `"
o.0.1
slotContent
o.0.2
slotContent
"`; diff --git a/packages/core/src/runtime/vdom/_test_/scoped-slot.spec.tsx b/packages/core/src/runtime/vdom/_test_/scoped-slot.spec.tsx index 084ef71f784..a260ef7b402 100644 --- a/packages/core/src/runtime/vdom/_test_/scoped-slot.spec.tsx +++ b/packages/core/src/runtime/vdom/_test_/scoped-slot.spec.tsx @@ -21,8 +21,9 @@ describe('scoped slot', () => { }); expect(root.firstElementChild.nodeName).toBe('SPIDER'); - expect(root.firstElementChild.childNodes[1].textContent).toBe('88'); - expect(root.firstElementChild.childNodes).toHaveLength(2); + expect(root.firstElementChild.childNodes).toHaveLength(1); + expect(root.firstElementChild.firstChild.nodeName).toBe('SLOT'); + expect(root.firstElementChild.firstChild.textContent).toBe('88'); }); it('should use components default slot text content', async () => { @@ -43,10 +44,11 @@ describe('scoped slot', () => { }); expect(root.firstElementChild.nodeName).toBe('SPIDER'); - expect(root.firstElementChild.children).toHaveLength(1); - expect(root.firstElementChild.firstElementChild.nodeName).toBe('SLOT-FB'); - expect(root.firstElementChild.firstElementChild.textContent).toBe('default content'); - expect(root.firstElementChild.firstElementChild.childNodes).toHaveLength(1); + expect(root.firstElementChild.children).toHaveLength(2); + expect(root.firstElementChild.children[0].nodeName).toBe('SLOT'); + expect(root.firstElementChild.children[1].nodeName).toBe('SLOT-FB'); + expect(root.firstElementChild.children[1].textContent).toBe('default content'); + expect(root.firstElementChild.children[1].childNodes).toHaveLength(1); }); it('should use components default slot node content', async () => { @@ -69,8 +71,9 @@ describe('scoped slot', () => { }); expect(root.firstElementChild.nodeName).toBe('SPIDER'); - expect(root.firstElementChild.firstElementChild.nodeName).toBe('SLOT-FB'); - expect(root.firstElementChild.firstElementChild.firstElementChild.textContent).toBe( + expect(root.firstElementChild.children[0].nodeName).toBe('SLOT'); + expect(root.firstElementChild.children[1].nodeName).toBe('SLOT-FB'); + expect(root.firstElementChild.children[1].firstElementChild.textContent).toBe( 'default content', ); }); @@ -93,9 +96,11 @@ describe('scoped slot', () => { }); expect(root.firstElementChild.nodeName).toBe('MONKEY'); - expect(root.firstElementChild.firstElementChild.nodeName).toBe('TIGER'); - expect(root.firstElementChild.firstElementChild.textContent).toBe('88'); - expect(root.firstElementChild.firstElementChild.childNodes).toHaveLength(1); + expect(root.firstElementChild.firstElementChild.nodeName).toBe('SLOT'); + expect(root.firstElementChild.firstElementChild.getAttribute('name')).toBe('start'); + expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('TIGER'); + expect(root.firstElementChild.firstElementChild.firstElementChild.textContent).toBe('88'); + expect(root.firstElementChild.firstElementChild.firstElementChild.childNodes).toHaveLength(1); }); it('no content', async () => { @@ -125,7 +130,9 @@ describe('scoped slot', () => { expect(root.children).toHaveLength(1); expect(root.firstElementChild.nodeName).toBe('LION'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('ION-CHILD'); - expect(root.firstElementChild.firstElementChild.children).toHaveLength(0); + expect(root.firstElementChild.firstElementChild.children).toHaveLength(1); + expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('SLOT'); + expect(root.firstElementChild.firstElementChild.firstElementChild.children).toHaveLength(0); }); it('no content, nested child slot', async () => { @@ -162,7 +169,13 @@ describe('scoped slot', () => { expect(root.firstElementChild.firstElementChild.nodeName).toBe('ION-CHILD'); expect(root.firstElementChild.firstElementChild.children).toHaveLength(1); expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('FISH'); - expect(root.firstElementChild.firstElementChild.firstElementChild.children).toHaveLength(0); + expect(root.firstElementChild.firstElementChild.firstElementChild.children).toHaveLength(1); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, + ).toBe('SLOT'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.children, + ).toHaveLength(0); }); it('should put parent content in child default slot', async () => { @@ -193,10 +206,13 @@ describe('scoped slot', () => { expect(root.firstElementChild.nodeName).toBe('HIPPO'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('ION-CHILD'); - expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('AARDVARK'); - expect(root.firstElementChild.firstElementChild.firstElementChild.textContent).toBe( - 'parent message', - ); + expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('SLOT'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, + ).toBe('AARDVARK'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.textContent, + ).toBe('parent message'); }); it('should relocate parent content after child content dynamically changes slot wrapper tag', async () => { @@ -229,7 +245,10 @@ describe('scoped slot', () => { expect(root.firstElementChild.nodeName).toBe('ION-CHILD'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('SECTION'); - expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('H1'); + expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('SLOT'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, + ).toBe('H1'); expect(root.firstElementChild.textContent).toBe('parent text'); root.innerH =
parent text update
; @@ -237,7 +256,10 @@ describe('scoped slot', () => { expect(root.firstElementChild.nodeName).toBe('ION-CHILD'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('SECTION'); - expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('H6'); + expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('SLOT'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, + ).toBe('H6'); expect(root.firstElementChild.textContent).toBe('parent text update'); const child = root.querySelector('ion-child'); @@ -246,7 +268,10 @@ describe('scoped slot', () => { expect(root.firstElementChild.nodeName).toBe('ION-CHILD'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('ARTICLE'); - expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('H6'); + expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('SLOT'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, + ).toBe('H6'); expect(root.firstElementChild.textContent).toBe('parent text update'); }); @@ -291,10 +316,14 @@ describe('scoped slot', () => { expect( root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild .nodeName, + ).toBe('SLOT'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild + .firstElementChild.nodeName, ).toBe('DINGO'); expect( root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild - .textContent, + .firstElementChild.textContent, ).toBe('parent message'); forceUpdate(root); @@ -309,10 +338,14 @@ describe('scoped slot', () => { expect( root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild .nodeName, + ).toBe('SLOT'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild + .firstElementChild.nodeName, ).toBe('DINGO'); expect( root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild - .textContent, + .firstElementChild.textContent, ).toBe('parent message'); }); @@ -411,9 +444,14 @@ describe('scoped slot', () => { expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('CHIPMUNK'); expect( root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, + ).toBe('SLOT'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild + .nodeName, ).toBe('BEAR'); expect( - root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.textContent, + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild + .textContent, ).toBe('parent message'); root.msg = 'change 1'; @@ -424,9 +462,14 @@ describe('scoped slot', () => { expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('CHIPMUNK'); expect( root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, + ).toBe('SLOT'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild + .nodeName, ).toBe('BEAR'); expect( - root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.textContent, + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild + .textContent, ).toBe('change 1'); root.msg = 'change 2'; @@ -437,9 +480,14 @@ describe('scoped slot', () => { expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('CHIPMUNK'); expect( root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, + ).toBe('SLOT'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild + .nodeName, ).toBe('BEAR'); expect( - root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.textContent, + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild + .textContent, ).toBe('change 2'); }); @@ -474,26 +522,39 @@ describe('scoped slot', () => { expect(root.firstElementChild.nodeName).toBe('ION-CHILD'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('BULL'); - expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('WHALE'); - expect(root.firstElementChild.firstElementChild.firstElementChild.textContent).toBe( - 'parent message', - ); + expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('SLOT'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, + ).toBe('WHALE'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.textContent, + ).toBe('parent message'); root.msg = 'change 1'; await waitForChanges(); expect(root.firstElementChild.nodeName).toBe('ION-CHILD'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('BULL'); - expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('WHALE'); - expect(root.firstElementChild.firstElementChild.firstElementChild.textContent).toBe('change 1'); + expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('SLOT'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, + ).toBe('WHALE'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.textContent, + ).toBe('change 1'); root.msg = 'change 2'; await waitForChanges(); expect(root.firstElementChild.nodeName).toBe('ION-CHILD'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('BULL'); - expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('WHALE'); - expect(root.firstElementChild.firstElementChild.firstElementChild.textContent).toBe('change 2'); + expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('SLOT'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, + ).toBe('WHALE'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.textContent, + ).toBe('change 2'); }); it('should allow multiple slots with same name', async () => { @@ -529,32 +590,44 @@ describe('scoped slot', () => { html: ``, }); + // mouse has 3 slots: default, start, end — falcon+eagle go into the 'start' slot expect(root.firstElementChild.nodeName).toBe('ION-CHILD'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('MOUSE'); - expect(root.firstElementChild.firstElementChild.children[0].nodeName).toBe('FALCON'); - expect(root.firstElementChild.firstElementChild.children[0].textContent).toBe('1'); - expect(root.firstElementChild.firstElementChild.children[1].nodeName).toBe('EAGLE'); - expect(root.firstElementChild.firstElementChild.children[1].textContent).toBe('2'); + expect(root.firstElementChild.firstElementChild.children[0].nodeName).toBe('SLOT'); + expect(root.firstElementChild.firstElementChild.children[1].nodeName).toBe('SLOT'); + expect(root.firstElementChild.firstElementChild.children[1].getAttribute('name')).toBe('start'); + expect(root.firstElementChild.firstElementChild.children[1].children[0].nodeName).toBe( + 'FALCON', + ); + expect(root.firstElementChild.firstElementChild.children[1].children[0].textContent).toBe('1'); + expect(root.firstElementChild.firstElementChild.children[1].children[1].nodeName).toBe('EAGLE'); + expect(root.firstElementChild.firstElementChild.children[1].children[1].textContent).toBe('2'); + expect(root.firstElementChild.firstElementChild.children[2].nodeName).toBe('SLOT'); + expect(root.firstElementChild.firstElementChild.children[2].getAttribute('name')).toBe('end'); forceUpdate(root); await waitForChanges(); expect(root.firstElementChild.nodeName).toBe('ION-CHILD'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('MOUSE'); - expect(root.firstElementChild.firstElementChild.children[0].nodeName).toBe('FALCON'); - expect(root.firstElementChild.firstElementChild.children[0].textContent).toBe('3'); - expect(root.firstElementChild.firstElementChild.children[1].nodeName).toBe('EAGLE'); - expect(root.firstElementChild.firstElementChild.children[1].textContent).toBe('4'); + expect(root.firstElementChild.firstElementChild.children[1].children[0].nodeName).toBe( + 'FALCON', + ); + expect(root.firstElementChild.firstElementChild.children[1].children[0].textContent).toBe('3'); + expect(root.firstElementChild.firstElementChild.children[1].children[1].nodeName).toBe('EAGLE'); + expect(root.firstElementChild.firstElementChild.children[1].children[1].textContent).toBe('4'); forceUpdate(root); await waitForChanges(); expect(root.firstElementChild.nodeName).toBe('ION-CHILD'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('MOUSE'); - expect(root.firstElementChild.firstElementChild.children[0].nodeName).toBe('FALCON'); - expect(root.firstElementChild.firstElementChild.children[0].textContent).toBe('5'); - expect(root.firstElementChild.firstElementChild.children[1].nodeName).toBe('EAGLE'); - expect(root.firstElementChild.firstElementChild.children[1].textContent).toBe('6'); + expect(root.firstElementChild.firstElementChild.children[1].children[0].nodeName).toBe( + 'FALCON', + ); + expect(root.firstElementChild.firstElementChild.children[1].children[0].textContent).toBe('5'); + expect(root.firstElementChild.firstElementChild.children[1].children[1].nodeName).toBe('EAGLE'); + expect(root.firstElementChild.firstElementChild.children[1].children[1].textContent).toBe('6'); }); it('should only render nested named slots and default slot', async () => { @@ -595,23 +668,46 @@ describe('scoped slot', () => { html: ``, }); + // flamingo: [slot(start), horse] — ferret in start slot + // horse: [slot(default), bullfrog] — butterfly in default slot + // bullfrog: [slot(end)] — fox in end slot expect(root.firstElementChild.nodeName).toBe('ION-CHILD'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('FLAMINGO'); - expect(root.firstElementChild.firstElementChild.children[0].nodeName).toBe('FERRET'); - expect(root.firstElementChild.firstElementChild.children[0].textContent).toBe('3'); - expect(root.firstElementChild.firstElementChild.children[1].nodeName).toBe('HORSE'); - expect(root.firstElementChild.firstElementChild.children[1].children[0].nodeName).toBe( - 'BUTTERFLY', + expect(root.firstElementChild.firstElementChild.children[0].nodeName).toBe('SLOT'); + expect(root.firstElementChild.firstElementChild.children[0].getAttribute('name')).toBe('start'); + expect(root.firstElementChild.firstElementChild.children[0].firstElementChild.nodeName).toBe( + 'FERRET', ); - expect(root.firstElementChild.firstElementChild.children[1].children[0].textContent).toBe('1'); + expect(root.firstElementChild.firstElementChild.children[0].firstElementChild.textContent).toBe( + '3', + ); + expect(root.firstElementChild.firstElementChild.children[1].nodeName).toBe('HORSE'); + expect(root.firstElementChild.firstElementChild.children[1].children[0].nodeName).toBe('SLOT'); + expect( + root.firstElementChild.firstElementChild.children[1].children[0].firstElementChild.nodeName, + ).toBe('BUTTERFLY'); + expect( + root.firstElementChild.firstElementChild.children[1].children[0].firstElementChild + .textContent, + ).toBe('1'); expect(root.firstElementChild.firstElementChild.children[1].children[1].nodeName).toBe( 'BULLFROG', ); expect( root.firstElementChild.firstElementChild.children[1].children[1].children[0].nodeName, + ).toBe('SLOT'); + expect( + root.firstElementChild.firstElementChild.children[1].children[1].children[0].getAttribute( + 'name', + ), + ).toBe('end'); + expect( + root.firstElementChild.firstElementChild.children[1].children[1].children[0].firstElementChild + .nodeName, ).toBe('FOX'); expect( - root.firstElementChild.firstElementChild.children[1].children[1].children[0].textContent, + root.firstElementChild.firstElementChild.children[1].children[1].children[0].firstElementChild + .textContent, ).toBe('2'); forceUpdate(root); @@ -619,21 +715,30 @@ describe('scoped slot', () => { expect(root.firstElementChild.nodeName).toBe('ION-CHILD'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('FLAMINGO'); - expect(root.firstElementChild.firstElementChild.children[0].nodeName).toBe('FERRET'); - expect(root.firstElementChild.firstElementChild.children[0].textContent).toBe('6'); - expect(root.firstElementChild.firstElementChild.children[1].nodeName).toBe('HORSE'); - expect(root.firstElementChild.firstElementChild.children[1].children[0].nodeName).toBe( - 'BUTTERFLY', + expect(root.firstElementChild.firstElementChild.children[0].firstElementChild.nodeName).toBe( + 'FERRET', ); - expect(root.firstElementChild.firstElementChild.children[1].children[0].textContent).toBe('4'); + expect(root.firstElementChild.firstElementChild.children[0].firstElementChild.textContent).toBe( + '6', + ); + expect(root.firstElementChild.firstElementChild.children[1].nodeName).toBe('HORSE'); + expect( + root.firstElementChild.firstElementChild.children[1].children[0].firstElementChild.nodeName, + ).toBe('BUTTERFLY'); + expect( + root.firstElementChild.firstElementChild.children[1].children[0].firstElementChild + .textContent, + ).toBe('4'); expect(root.firstElementChild.firstElementChild.children[1].children[1].nodeName).toBe( 'BULLFROG', ); expect( - root.firstElementChild.firstElementChild.children[1].children[1].children[0].nodeName, + root.firstElementChild.firstElementChild.children[1].children[1].children[0].firstElementChild + .nodeName, ).toBe('FOX'); expect( - root.firstElementChild.firstElementChild.children[1].children[1].children[0].textContent, + root.firstElementChild.firstElementChild.children[1].children[1].children[0].firstElementChild + .textContent, ).toBe('5'); forceUpdate(root); @@ -641,21 +746,30 @@ describe('scoped slot', () => { expect(root.firstElementChild.nodeName).toBe('ION-CHILD'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('FLAMINGO'); - expect(root.firstElementChild.firstElementChild.children[0].nodeName).toBe('FERRET'); - expect(root.firstElementChild.firstElementChild.children[0].textContent).toBe('9'); - expect(root.firstElementChild.firstElementChild.children[1].nodeName).toBe('HORSE'); - expect(root.firstElementChild.firstElementChild.children[1].children[0].nodeName).toBe( - 'BUTTERFLY', + expect(root.firstElementChild.firstElementChild.children[0].firstElementChild.nodeName).toBe( + 'FERRET', + ); + expect(root.firstElementChild.firstElementChild.children[0].firstElementChild.textContent).toBe( + '9', ); - expect(root.firstElementChild.firstElementChild.children[1].children[0].textContent).toBe('7'); + expect(root.firstElementChild.firstElementChild.children[1].nodeName).toBe('HORSE'); + expect( + root.firstElementChild.firstElementChild.children[1].children[0].firstElementChild.nodeName, + ).toBe('BUTTERFLY'); + expect( + root.firstElementChild.firstElementChild.children[1].children[0].firstElementChild + .textContent, + ).toBe('7'); expect(root.firstElementChild.firstElementChild.children[1].children[1].nodeName).toBe( 'BULLFROG', ); expect( - root.firstElementChild.firstElementChild.children[1].children[1].children[0].nodeName, + root.firstElementChild.firstElementChild.children[1].children[1].children[0].firstElementChild + .nodeName, ).toBe('FOX'); expect( - root.firstElementChild.firstElementChild.children[1].children[1].children[0].textContent, + root.firstElementChild.firstElementChild.children[1].children[1].children[0].firstElementChild + .textContent, ).toBe('8'); }); @@ -702,19 +816,28 @@ describe('scoped slot', () => { html: ``, }); + // test-1: seal > slot(default) > test-2 > goose > slot(default) > goat expect(root.firstElementChild.nodeName).toBe('TEST-1'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('SEAL'); - expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('TEST-2'); + expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('SLOT'); expect( root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, - ).toBe('GOOSE'); + ).toBe('TEST-2'); expect( root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild .nodeName, + ).toBe('GOOSE'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild + .firstElementChild.nodeName, + ).toBe('SLOT'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild + .firstElementChild.firstElementChild.nodeName, ).toBe('GOAT'); expect( root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild - .textContent, + .firstElementChild.firstElementChild.textContent, ).toBe('1'); forceUpdate(root); @@ -722,17 +845,25 @@ describe('scoped slot', () => { expect(root.firstElementChild.nodeName).toBe('TEST-1'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('SEAL'); - expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('TEST-2'); + expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('SLOT'); expect( root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, - ).toBe('GOOSE'); + ).toBe('TEST-2'); expect( root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild .nodeName, + ).toBe('GOOSE'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild + .firstElementChild.nodeName, + ).toBe('SLOT'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild + .firstElementChild.firstElementChild.nodeName, ).toBe('GOAT'); expect( root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild - .textContent, + .firstElementChild.firstElementChild.textContent, ).toBe('2'); forceUpdate(root); @@ -740,17 +871,25 @@ describe('scoped slot', () => { expect(root.firstElementChild.nodeName).toBe('TEST-1'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('SEAL'); - expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('TEST-2'); + expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('SLOT'); expect( root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, - ).toBe('GOOSE'); + ).toBe('TEST-2'); expect( root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild .nodeName, + ).toBe('GOOSE'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild + .firstElementChild.nodeName, + ).toBe('SLOT'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild + .firstElementChild.firstElementChild.nodeName, ).toBe('GOAT'); expect( root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild - .textContent, + .firstElementChild.firstElementChild.textContent, ).toBe('3'); }); @@ -857,8 +996,10 @@ describe('scoped slot', () => { html: `Content`, }); - expect(root.firstElementChild.children[0].nodeName).toBe('SLOT-FB'); - expect(root.firstElementChild.children[0]).toHaveAttribute('hidden'); + // contains the user content; follows — CSS hides it when slot is non-empty + expect(root.firstElementChild.children[0].nodeName).toBe('SLOT'); + expect(root.firstElementChild.children[0].firstElementChild.nodeName).toBe('SPAN'); + expect(root.firstElementChild.children[1].nodeName).toBe('SLOT-FB'); }); it("should hide the slot's fallback content for a non-shadow component when slot content passed in", async () => { @@ -879,7 +1020,9 @@ describe('scoped slot', () => { html: `Content`, }); - expect(root.firstElementChild.children[0].nodeName).toBe('SLOT-FB'); - expect(root.firstElementChild.children[0]).toHaveAttribute('hidden'); + // contains the user content; follows — CSS hides it when slot is non-empty + expect(root.firstElementChild.children[0].nodeName).toBe('SLOT'); + expect(root.firstElementChild.children[0].firstElementChild.nodeName).toBe('SPAN'); + expect(root.firstElementChild.children[1].nodeName).toBe('SLOT-FB'); }); }); diff --git a/packages/core/src/runtime/vdom/vdom-annotations.ts b/packages/core/src/runtime/vdom/vdom-annotations.ts index 191991ddd76..b18b33f8121 100644 --- a/packages/core/src/runtime/vdom/vdom-annotations.ts +++ b/packages/core/src/runtime/vdom/vdom-annotations.ts @@ -9,7 +9,6 @@ import { HYDRATE_ID, NODE_TYPE, ORG_LOCATION_ID, - SLOT_NODE_ID, STENCIL_DOC_DATA, TEXT_NODE_ID, } from '../runtime-constants'; @@ -53,9 +52,6 @@ export const insertVdomAnnotations = (doc: Document, staticComponents: string[]) if (nodeRef.nodeType === NODE_TYPE.ElementNode) { nodeRef.setAttribute(HYDRATE_CHILD_ID, childId); - if (typeof nodeRef['s-sn'] === 'string' && !nodeRef.getAttribute('slot')) { - nodeRef.setAttribute('s-sn', nodeRef['s-sn']); - } } else if (nodeRef.nodeType === NODE_TYPE.TextNode) { if (hostId === 0) { const textContent = nodeRef.nodeValue?.trim(); @@ -234,9 +230,6 @@ const insertChildVNodeAnnotations = ( if (childElm.nodeType === NODE_TYPE.ElementNode) { childElm.setAttribute(HYDRATE_CHILD_ID, childId); - if (typeof childElm['s-sn'] === 'string' && !childElm.getAttribute('slot')) { - childElm.setAttribute('s-sn', childElm['s-sn']); - } } else if (childElm.nodeType === NODE_TYPE.TextNode) { const parentNode = childElm.parentNode; const nodeName = parentNode?.nodeName; @@ -246,12 +239,6 @@ const insertChildVNodeAnnotations = ( const commentBeforeTextNode = doc.createComment(textNodeId); insertBefore(parentNode, commentBeforeTextNode as any, childElm); } - } else if (childElm.nodeType === NODE_TYPE.CommentNode) { - if (childElm['s-sr']) { - const slotName = childElm['s-sn'] || ''; - const slotNodeId = `${SLOT_NODE_ID}.${childId}.${slotName}`; - childElm.nodeValue = slotNodeId; - } } if (vnodeChild.$children$ != null) { diff --git a/packages/core/src/runtime/vdom/vdom-render.ts b/packages/core/src/runtime/vdom/vdom-render.ts index cc37904ecd3..db9686809ed 100644 --- a/packages/core/src/runtime/vdom/vdom-render.ts +++ b/packages/core/src/runtime/vdom/vdom-render.ts @@ -20,7 +20,6 @@ import { findSlotFromSlottedNode, isNodeLocatedInSlot, patchSlotNode, - updateFallbackSlotVisibility, } from '../slot-polyfill-utils'; import { h, isHost, newVNode as createVNode } from './h'; import { updateElement } from './update-element'; @@ -29,7 +28,6 @@ let scopeId: string; let contentRef: d.RenderNode | undefined; let hostTagName: string; let useNativeShadowDom = false; -let checkSlotFallbackVisibility = false; let checkSlotRelocate = false; let isSvgMode = false; @@ -61,17 +59,6 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: if (BUILD.slotRelocation && !useNativeShadowDom) { // remember for later we need to check to relocate nodes checkSlotRelocate = true; - - if (newVNode.$tag$ === 'slot') { - newVNode.$flags$ |= newVNode.$children$ - ? // slot element has fallback content - // still create an element that "mocks" the slot element - VNODE_FLAGS.isSlotFallback - : // slot element does not have fallback content - // create an html comment we'll use to always reference - // where actual slot content should sit next to - VNODE_FLAGS.isSlotReference; - } } if (BUILD.isDev && newVNode.$elm$) { @@ -86,16 +73,6 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: if (BUILD.vdomText && newVNode.$text$ != null) { // create text node elm = newVNode.$elm$ = win.document.createTextNode(newVNode.$text$) as any; - } else if (BUILD.slotRelocation && newVNode.$flags$ & VNODE_FLAGS.isSlotReference) { - // create a slot reference node - elm = newVNode.$elm$ = - BUILD.isDebug || BUILD.hydrateServerSide - ? slotReferenceDebugNode(newVNode) - : (win.document.createTextNode('') as any); - // add css classes, attrs, props, listeners, etc. - if (BUILD.vdomAttribute) { - updateElement(null, newVNode, isSvgMode); - } } else { // Only create element if we have a valid tag name if (BUILD.svg && !isSvgMode) { @@ -109,21 +86,8 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: // create element elm = newVNode.$elm$ = ( BUILD.svg - ? win.document.createElementNS( - isSvgMode ? SVG_NS : HTML_NS, - !useNativeShadowDom && - BUILD.slotRelocation && - newVNode.$flags$ & VNODE_FLAGS.isSlotFallback - ? 'slot-fb' - : (newVNode.$tag$ as string), - ) - : win.document.createElement( - !useNativeShadowDom && - BUILD.slotRelocation && - newVNode.$flags$ & VNODE_FLAGS.isSlotFallback - ? 'slot-fb' - : (newVNode.$tag$ as string), - ) + ? win.document.createElementNS(isSvgMode ? SVG_NS : HTML_NS, newVNode.$tag$ as string) + : win.document.createElement(newVNode.$tag$ as string) ) as any; if (BUILD.svg && isSvgMode && newVNode.$tag$ === 'foreignObject') { @@ -173,34 +137,24 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: // This needs to always happen so we can hide nodes that are projected // to another component but don't end up in a slot elm['s-hn'] = hostTagName; - if (BUILD.slotRelocation) { - if (newVNode.$flags$ & (VNODE_FLAGS.isSlotFallback | VNODE_FLAGS.isSlotReference)) { - // remember the content reference comment - elm['s-sr'] = true; - - // remember the content reference comment - elm['s-cr'] = contentRef; - - // remember the slot name, or empty string for default slot - elm['s-sn'] = newVNode.$name$ || ''; - - // remember the ref callback function - elm['s-rf'] = newVNode.$attrs$?.ref; - - // give this node `assignedElements` and `assignedNodes` methods - patchSlotNode(elm); - - // check if we've got an old vnode for this slot - oldVNode = - oldParentVNode && oldParentVNode.$children$ && oldParentVNode.$children$[childIndex]; - if (oldVNode && oldVNode.$tag$ === newVNode.$tag$ && oldParentVNode.$elm$) { - // we've got an old slot vnode and the wrapper is being replaced - // so let's move the old slot content to the root of the element currently being rendered - relocateToHostRoot(oldParentVNode.$elm$); - } - if (BUILD.scoped || (BUILD.hydrateServerSide && CMP_FLAGS.shadowNeedsScopedCss)) { - addRemoveSlotScopedClass(contentRef, elm, newParentVNode.$elm$, oldParentVNode?.$elm$); - } + if (BUILD.slotRelocation && !useNativeShadowDom && newVNode.$tag$ === 'slot-fb') { + elm['s-sn'] = newVNode.$name$ || ''; + if (newVNode.$name$) elm.setAttribute('name', newVNode.$name$); + } + if (BUILD.slotRelocation && !useNativeShadowDom && newVNode.$tag$ === 'slot') { + elm['s-sr'] = true; + elm['s-cr'] = contentRef; + elm['s-sn'] = newVNode.$name$ || ''; + if (newVNode.$name$) elm.setAttribute('name', newVNode.$name$); + elm['s-rf'] = newVNode.$attrs$?.ref; + patchSlotNode(elm); + + oldVNode = oldParentVNode && oldParentVNode.$children$ && oldParentVNode.$children$[childIndex]; + if (oldVNode && oldVNode.$tag$ === newVNode.$tag$ && oldParentVNode.$elm$) { + relocateToHostRoot(oldParentVNode.$elm$); + } + if (BUILD.scoped || (BUILD.hydrateServerSide && CMP_FLAGS.shadowNeedsScopedCss)) { + addSlotScopedClass(contentRef, elm); } } @@ -224,22 +178,21 @@ const relocateToHostRoot = (parentElm: Element) => { const contentRefNode = ( Array.from((host as d.RenderNode).__childNodes || host.childNodes) as d.RenderNode[] ).find((ref) => ref['s-cr']); + + // Walk elements inside parentElm and move their children back to the host root const childNodeArray = Array.from( (parentElm as d.RenderNode).__childNodes || parentElm.childNodes, ) as d.RenderNode[]; - // If we have a content ref, we need to invert the order of the nodes we're relocating - // to preserve the correct order of elements in the DOM on future relocations - for (const childNode of contentRefNode ? childNodeArray.reverse() : childNodeArray) { - // Only relocate nodes that were slotted in - if (childNode['s-sh'] != null) { - insertBefore(host, childNode, contentRefNode ?? null); - - // Reset so we can correctly move the node around again. - childNode['s-sh'] = undefined; - - // Need to tell the render pipeline to check to relocate slot content again - checkSlotRelocate = true; + for (const childNode of childNodeArray) { + if (childNode['s-sr']) { + // this is a element — move its slotted children back to the host root + const slotChildren = Array.from(childNode.childNodes) as d.RenderNode[]; + for (const slotChild of contentRefNode ? slotChildren.reverse() : slotChildren) { + insertBefore(host, slotChild, contentRefNode ?? null); + slotChild['s-sh'] = undefined; + checkSlotRelocate = true; + } } } } @@ -255,17 +208,9 @@ const relocateToHostRoot = (parentElm: Element) => { */ const putBackInOriginalLocation = (parentElm: d.RenderNode, recursive: boolean) => { plt.$flags$ |= PLATFORM_FLAGS.isTmpDisconnected; + // Content is now inside elements as children, not siblings — plain childNodes walk suffices const oldSlotChildNodes: ChildNode[] = Array.from(parentElm.__childNodes || parentElm.childNodes); - if (parentElm['s-sr']) { - let node = parentElm; - while ((node = node.nextSibling as d.RenderNode)) { - if (node && node['s-sn'] === parentElm['s-sn'] && node['s-sh'] === hostTagName) { - oldSlotChildNodes.push(node); - } - } - } - for (let i = oldSlotChildNodes.length - 1; i >= 0; i--) { const childNode = oldSlotChildNodes[i] as any; if (childNode['s-hn'] !== hostTagName && childNode['s-ol']) { @@ -366,10 +311,6 @@ const removeVnodes = (vnodes: d.VNode[], startIdx: number, endIdx: number) => { if (elm) { if (BUILD.slotRelocation) { - // we're removing this element - // so it's possible we need to show slot fallback content now - checkSlotFallbackVisibility = true; - if (elm['s-ol']) { // remove the original location comment elm['s-ol'].remove(); @@ -739,22 +680,33 @@ export const patch = (oldVNode: d.VNode, newVNode: d.VNode, isInitialRender = fa if (BUILD.vdomAttribute || BUILD.reflect) { if (BUILD.slot && tag === 'slot' && !useNativeShadowDom) { - if (oldVNode.$name$ !== newVNode.$name$) { + // Use loose equality: null and undefined both mean "no name" for a default slot + if (oldVNode.$name$ != newVNode.$name$) { newVNode.$elm$['s-sn'] = newVNode.$name$ || ''; relocateToHostRoot(newVNode.$elm$.parentElement); } } + if (BUILD.slot && tag === 'slot-fb' && !useNativeShadowDom) { + if (oldVNode.$name$ != newVNode.$name$) { + newVNode.$elm$['s-sn'] = newVNode.$name$ || ''; + if (newVNode.$name$) { + newVNode.$elm$.setAttribute('name', newVNode.$name$); + } else { + newVNode.$elm$.removeAttribute('name'); + } + } + } // either this is the first render of an element OR it's an update // AND we already know it's possible it could have changed // this updates the element's css classes, attrs, props, listeners, etc. updateElement(oldVNode, newVNode, isSvgMode, isInitialRender); } - if (BUILD.updatable && oldChildren !== null && newChildren !== null) { + if (BUILD.updatable && oldChildren != null && newChildren != null) { // looks like there's child vnodes for both the old and new vnodes // so we need to call `updateChildren` to reconcile them updateChildren(elm, oldChildren, newVNode, newChildren, isInitialRender); - } else if (newChildren !== null) { + } else if (newChildren != null) { // no old child vnodes, but there are new child vnodes to add if (BUILD.updatable && BUILD.vdomText && oldVNode.$text$ !== null) { // the old vnode was text, so be sure to clear it out @@ -766,7 +718,7 @@ export const patch = (oldVNode: d.VNode, newVNode: d.VNode, isInitialRender = fa // don't do this on initial render as it can cause non-hydrated content to be removed !isInitialRender && BUILD.updatable && - oldChildren !== null + oldChildren != null ) { // no new child vnodes, but there are old child vnodes to remove removeVnodes(oldChildren, 0, oldChildren.length - 1); @@ -813,7 +765,7 @@ const markSlotContentForRelocation = (elm: d.RenderNode) => { // tslint:disable-next-line: prefer-const let node: d.RenderNode; let hostContentNodes: NodeList; - let j; + let j: number; const children = elm.__childNodes || elm.childNodes; for (const childNode of children as unknown as d.RenderNode[]) { @@ -827,8 +779,8 @@ const markSlotContentForRelocation = (elm: d.RenderNode) => { const slotName = childNode['s-sn']; // iterate through all the nodes under the location where the host was - // originally rendered - for (j = hostContentNodes.length - 1; j >= 0; j--) { + // originally rendered - forward order so appendChild preserves source order + for (j = 0; j < hostContentNodes.length; j++) { node = hostContentNodes[j] as d.RenderNode; // check that the node is not a content reference node or a node @@ -851,10 +803,6 @@ const markSlotContentForRelocation = (elm: d.RenderNode) => { // it's possible we've already decided to relocate this node let relocateNodeData = relocateNodes.find((r) => r.$nodeToRelocate$ === node); - // made some changes to slots - // let's make sure we also double check - // fallbacks are correctly hidden or shown - checkSlotFallbackVisibility = true; // ensure that the slot-name attr is correct node['s-sn'] = node['s-sn'] || slotName; @@ -979,12 +927,7 @@ export const insertBefore = ( !!newNode['s-cr'] ) { // this is a slot node - addRemoveSlotScopedClass( - newNode['s-cr'], - newNode, - parent as d.RenderNode, - newNode.parentElement, - ); + addSlotScopedClass(newNode['s-cr'], newNode); } else if (typeof newNode['s-sn'] === 'string') { // this is a slotted node. const hostElm = newNode['s-hn'] && (parent as Element).closest?.(newNode['s-hn']); @@ -1022,23 +965,13 @@ export const insertBefore = ( }; /** - * Adds or removes a scoped class to the parent element of a slotted node. - * This is used for styling slotted content (e.g. with `::scoped(...) {...}` selectors ) - * in `scoped: true` components. + * Adds the scoped-slot class (`scopeId + '-s'`) to the `` element itself. + * Since `` is now the direct parent of slotted nodes, this replicates `::slotted()` selectors. * * @param reference - Content Reference Node. Used to get the scope id of the parent component. - * @param slotNode - the `` node to apply the class for - * @param newParent - the slots' new parent element that requires the scoped class - * @param oldParent - optionally, an old parent element that may no longer require the scoped class + * @param slotNode - the `` node to apply the class to */ -function addRemoveSlotScopedClass( - reference: d.RenderNode, - slotNode: d.RenderNode, - newParent: Element, - oldParent?: Element, -) { - // if the new node to move is slotted, - // find it's original parent component and see if has a scope id +function addSlotScopedClass(reference: d.RenderNode, slotNode: d.RenderNode) { let slotScopeId: string; if ( reference && @@ -1048,30 +981,8 @@ function addRemoveSlotScopedClass( (reference.parentNode as d.RenderNode)['s-sc'] && (slotScopeId = slotNode['s-si'] || (reference.parentNode as d.RenderNode)['s-sc']) ) { - const scopeName = slotNode['s-sn']; - const hostName = slotNode['s-hn']; - - // we found the original parent component's scoped id - // let's add a scoped-slot class to this slotted node's parent - newParent.classList?.add(slotScopeId + '-s'); - - if (oldParent && oldParent.classList?.contains(slotScopeId + '-s')) { - let child = ((oldParent as d.RenderNode).__childNodes || - oldParent.childNodes)[0] as d.RenderNode; - let found = false; - - while (child) { - if (child['s-sn'] !== scopeName && child['s-hn'] === hostName && !!child['s-sr']) { - found = true; - break; - } - child = child.nextSibling as d.RenderNode; - } - - // there are no other slots in the old parent - // let's remove the scoped-slot class - if (!found) oldParent.classList.remove(slotScopeId + '-s'); - } + // is now the direct parent of slotted nodes — add '-s' here + slotNode.classList?.add(slotScopeId + '-s'); } } /** @@ -1083,6 +994,41 @@ interface RelocateNodeData { $nodeToRelocate$: d.RenderNode; } +/** + * Split any `` vnode that carries fallback children into two consecutive + * sibling vnodes: `` (the container) and `fallback`. + * This is done in-place on the vnode tree before patching so the vdom and DOM + * always match 1:1 and the CSS `slot:not(:empty)+slot-fb{display:none}` rule + * can drive fallback visibility without any JS traversal. + * @param vnode the vnode to normalize + */ +const normalizeSlotVNodes = (vnode: d.VNode): void => { + if (!vnode.$children$) return; + const children = vnode.$children$; + let i = 0; + while (i < children.length) { + const child = children[i]; + if (child && child.$tag$ === 'slot' && child.$children$) { + const fallbackVNode: d.VNode = { + $flags$: 0, + $tag$: 'slot-fb', + $children$: child.$children$, + $attrs$: null, + $key$: null, + $name$: child.$name$ ?? null, + $text$: null, + $elm$: null, + }; + child.$children$ = null; + children.splice(i + 1, 0, fallbackVNode); + i += 2; + } else { + if (child) normalizeSlotVNodes(child); + i++; + } + } +}; + /** * The main entry point for Stencil's virtual DOM-based rendering engine * @@ -1181,9 +1127,9 @@ render() { if (BUILD.slotRelocation) { contentRef = hostElm['s-cr']; - - // always reset - checkSlotFallbackVisibility = false; + if (!useNativeShadowDom) { + normalizeSlotVNodes(rootVnode); + } } // synchronous patch @@ -1220,125 +1166,35 @@ render() { for (const relocateData of relocateNodes) { const nodeToRelocate = relocateData.$nodeToRelocate$; - const slotRefNode = relocateData.$slotRefNode$; + const slotRefNode = relocateData.$slotRefNode$; // the element if (nodeToRelocate.nodeType === NODE_TYPE.ElementNode && isInitialLoad) { - // Store the initial value of `hidden` so we can reset it later when - // moving nodes around. nodeToRelocate['s-ih'] = !!nodeToRelocate.hidden; } if (slotRefNode) { - const parentNodeRef = slotRefNode.parentNode; - // When determining where to insert content, the most simple case would be - // to relocate the node immediately following the slot reference node. We do this - // by getting a reference to the node immediately following the slot reference node - // since we will use `insertBefore` to manipulate the DOM. - // - // If there is no node immediately following the slot reference node, then we will just - // end up appending the node as the last child of the parent. - let insertBeforeNode = slotRefNode.nextSibling as d.RenderNode | null; - - // If the node we're currently planning on inserting the new node before is an element, - // we need to do some additional checks to make sure we're inserting the node in the correct order. - // The use case here would be that we have multiple nodes being relocated to the same slot. So, we want - // to make sure they get inserted into their new home in the same order they were declared in their original location. + // Move the node into the element if it isn't already there. + // Forward iteration in markSlotContentForRelocation ensures source order is preserved. if ( - !BUILD.hydrateServerSide && - insertBeforeNode && - insertBeforeNode.nodeType === NODE_TYPE.ElementNode + (nodeToRelocate as d.PatchedSlotNode).__parentNode !== slotRefNode && + nodeToRelocate.parentNode !== slotRefNode ) { - let orgLocationNode = nodeToRelocate['s-ol']?.previousSibling as d.RenderNode | null; - while (orgLocationNode) { - let refNode = (orgLocationNode['s-nr'] as d.RenderNode) ?? null; - - if ( - refNode && - refNode['s-sn'] === nodeToRelocate['s-sn'] && - parentNodeRef === - ((refNode as d.PatchedSlotNode).__parentNode || refNode.parentNode) - ) { - refNode = refNode.nextSibling as d.RenderNode | null; - - // If the refNode is the same node to be relocated or another element's slot reference, keep searching to find the - // correct relocation target - while (refNode === nodeToRelocate || refNode?.['s-sr']) { - refNode = refNode?.nextSibling as d.RenderNode | null; - } - - if (!refNode || !refNode['s-nr']) { - insertBeforeNode = refNode; - break; - } - } - - orgLocationNode = orgLocationNode.previousSibling as d.RenderNode | null; - } - } + insertBefore(slotRefNode, nodeToRelocate, null, isInitialLoad); - const parent = - (nodeToRelocate as d.PatchedSlotNode).__parentNode || nodeToRelocate.parentNode; - const nextSibling = - (nodeToRelocate as d.PatchedSlotNode).__nextSibling || nodeToRelocate.nextSibling; - if ((!insertBeforeNode && parentNodeRef !== parent) || nextSibling !== insertBeforeNode) { - // we've checked that it's worth while to relocate - // since that the node to relocate - // has a different next sibling or parent relocated - - if (nodeToRelocate !== insertBeforeNode) { - // Add it back to the dom but in its new home - // If we get to this point and `insertBeforeNode` is `null`, that means - // we're just going to append the node as the last child of the parent. Passing - // `null` as the second arg here will trigger that behavior. - insertBefore(parentNodeRef, nodeToRelocate, insertBeforeNode, isInitialLoad); - - // // If this is a comment node that represents a hidden text node, convert it back to text - if ( - nodeToRelocate.nodeType === NODE_TYPE.CommentNode && - nodeToRelocate.nodeValue.startsWith('s-nt-') - ) { - // create a text node to replace the comment node - const textNode = win.document.createTextNode( - nodeToRelocate.nodeValue.replace(/^s-nt-/, ''), - ) as any; - // Copy over Stencil properties - textNode['s-hn'] = nodeToRelocate['s-hn']; // host (component) name - textNode['s-sn'] = nodeToRelocate['s-sn']; // slot name - textNode['s-sh'] = nodeToRelocate['s-sh']; // host (component) that this node is slotted to - textNode['s-sr'] = nodeToRelocate['s-sr']; // slot reference node - textNode['s-ol'] = nodeToRelocate['s-ol']; // original location marker - textNode['s-ol']['s-nr'] = textNode; // point original location marker to new text node - - insertBefore(nodeToRelocate.parentNode, textNode, nodeToRelocate, isInitialLoad); - nodeToRelocate.parentNode.removeChild(nodeToRelocate); - } - - // Reset the `hidden` value back to what it was defined as originally - // This solves a problem where a `slot` is dynamically rendered and `hidden` may have - // been set on content originally, but now it has a slot to go to so it should have - // the value it was defined as having in the DOM, not what we overrode it to. - if ( - nodeToRelocate.nodeType === NODE_TYPE.ElementNode && - nodeToRelocate.tagName !== 'SLOT-FB' - ) { - nodeToRelocate.hidden = nodeToRelocate['s-ih'] ?? false; - } + if (nodeToRelocate.nodeType === NODE_TYPE.ElementNode) { + nodeToRelocate.hidden = nodeToRelocate['s-ih'] ?? false; } } - if (nodeToRelocate && typeof slotRefNode['s-rf'] === 'function') { + if (typeof slotRefNode['s-rf'] === 'function') { slotRefNode['s-rf'](slotRefNode); } } else if (nodeToRelocate.nodeType === NODE_TYPE.ElementNode) { - // this node doesn't have a slot home to go to, so let's hide it + // no slot home — hide the element nodeToRelocate.hidden = true; } } } - if (checkSlotFallbackVisibility) { - updateFallbackSlotVisibility(rootVnode.$elm$); - } - // done moving nodes around // allow the disconnect callback to work again plt.$flags$ &= ~PLATFORM_FLAGS.isTmpDisconnected; @@ -1383,13 +1239,6 @@ render() { flushQueuedRefCallbacks(); }; -// slot comment debug nodes only created with the `--debug` flag -// otherwise these nodes are text nodes w/out content -const slotReferenceDebugNode = (slotVNode: d.VNode) => - win.document?.createComment( - ` (host=${hostTagName.toLowerCase()})`, - ); - const originalLocationDebugNode = (nodeToRelocate: d.RenderNode): any => win.document?.createComment( `org-location for ` + diff --git a/packages/core/src/testing/vitest-stencil-plugin.ts b/packages/core/src/testing/vitest-stencil-plugin.ts index 4d62bcf6e37..0e52214f6a2 100644 --- a/packages/core/src/testing/vitest-stencil-plugin.ts +++ b/packages/core/src/testing/vitest-stencil-plugin.ts @@ -45,7 +45,7 @@ export function stencilVitestPlugin(): VitePlugin { currentDirectory: process.cwd(), module: 'esm', proxy: null, - sourceMap: false, + sourceMap: true, style: null, styleImportData: 'queryparams', target: 'es2022', diff --git a/test/build/build-size/kitchen-sink/stencil.config.ts b/test/build/build-size/kitchen-sink/stencil.config.ts index eb4d86099c8..4af88f0e7b2 100644 --- a/test/build/build-size/kitchen-sink/stencil.config.ts +++ b/test/build/build-size/kitchen-sink/stencil.config.ts @@ -9,7 +9,6 @@ export const config: Config = { }, { type: 'standalone', - externalRuntime: false, }, { type: 'ssr', diff --git a/test/build/build-size/kitchen-sink/test-bundle-size.js b/test/build/build-size/kitchen-sink/test-bundle-size.js index 3c393ea30a6..4936cca5ff8 100644 --- a/test/build/build-size/kitchen-sink/test-bundle-size.js +++ b/test/build/build-size/kitchen-sink/test-bundle-size.js @@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const distDir = path.join(__dirname, 'dist', 'loader-bundle', 'bundlesize-kitchen-sink'); -const maxBundleSize = 28 * 1024; // 28KB in bytes (~27KB non-gzipped, ~10KB gzipped) +const maxBundleSize = 26 * 1024; // 26KB in bytes (~26KB non-gzipped, ~9KB gzipped) console.log('\nChecking bundle size (kitchen-sink)...'); diff --git a/test/build/build-size/light/stencil.config.ts b/test/build/build-size/light/stencil.config.ts index 13bd15412af..867119c115b 100644 --- a/test/build/build-size/light/stencil.config.ts +++ b/test/build/build-size/light/stencil.config.ts @@ -6,7 +6,6 @@ export const config: Config = { { type: 'loader-bundle', hashFileNames: false }, { type: 'standalone', - externalRuntime: false, }, { type: 'global-style', diff --git a/test/build/build-size/light/test-bundle-size.js b/test/build/build-size/light/test-bundle-size.js index 6c03eb4175b..a0da48306da 100644 --- a/test/build/build-size/light/test-bundle-size.js +++ b/test/build/build-size/light/test-bundle-size.js @@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const distDir = path.join(__dirname, 'dist', 'loader-bundle', 'bundlesize-non-shadow'); -const maxBundleSize = 20 * 1024; // 20KB in bytes (non-gzipped - ~7KB gzipped) +const maxBundleSize = 19 * 1024; // 19KB in bytes (non-gzipped - ~7KB gzipped) console.log('\nChecking bundle size (non-shadow)...'); diff --git a/test/runtime/hydrated.css b/test/runtime/hydrated.css deleted file mode 100644 index 8b45cd670d7..00000000000 --- a/test/runtime/hydrated.css +++ /dev/null @@ -1,3 +0,0 @@ -:not(:defined) { - visibility: hidden; -} diff --git a/test/runtime/src/components/dom/remove-child-patch/remove-child-patch.spec.tsx b/test/runtime/src/components/dom/remove-child-patch/remove-child-patch.spec.tsx index 6a5a2742420..38f56cc03ae 100644 --- a/test/runtime/src/components/dom/remove-child-patch/remove-child-patch.spec.tsx +++ b/test/runtime/src/components/dom/remove-child-patch/remove-child-patch.spec.tsx @@ -21,8 +21,8 @@ describe('remove-child-patch', () => { await waitForExist('remove-child-patch.hydrated'); const host = document.querySelector('remove-child-patch')!; document.querySelector('#remove-child-button')!.addEventListener('click', () => { - const slotContainer = host.querySelector('.slot-container')!; - const elementToRemove = slotContainer.children[slotContainer.children.length - 1]; + const slot = host.querySelector('.slot-container slot')!; + const elementToRemove = slot.children[slot.children.length - 1]; host.removeChild(elementToRemove); }); @@ -52,8 +52,8 @@ describe('remove-child-patch', () => { await waitForExist('remove-child-patch.hydrated'); const host = document.querySelector('remove-child-patch')!; document.querySelector('#remove-child-button')!.addEventListener('click', () => { - const slotContainer = host.querySelector('.slot-container')!; - const elementToRemove = slotContainer.children[slotContainer.children.length - 1]; + const slot = host.querySelector('.slot-container slot')!; + const elementToRemove = slot.children[slot.children.length - 1]; host.removeChild(elementToRemove); }); diff --git a/test/runtime/src/components/scoped/scoped-add-remove-classes/readme.md b/test/runtime/src/components/scoped/scoped-add-remove-classes/readme.md index ee3af524c60..02f3b190b19 100644 --- a/test/runtime/src/components/scoped/scoped-add-remove-classes/readme.md +++ b/test/runtime/src/components/scoped/scoped-add-remove-classes/readme.md @@ -7,10 +7,10 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| --------------- | --------- | ----------- | ---------- | ----------- | -| `items` | -- | | `Item[]` | `undefined` | -| `selectedItems` | -- | | `number[]` | `undefined` | +| Property | Attribute | Description | Type | Default | +| --------------- | --------- | ----------- | ----------------------------------------------------- | ----------- | +| `items` | -- | | `{ id: number; label: string; selected: boolean; }[]` | `undefined` | +| `selectedItems` | -- | | `number[]` | `undefined` | ---------------------------------------------- diff --git a/test/runtime/src/components/scoped/scoped-basic/scoped-basic.spec.tsx b/test/runtime/src/components/scoped/scoped-basic/scoped-basic.spec.tsx index e320c6bbfde..5627d768df7 100644 --- a/test/runtime/src/components/scoped/scoped-basic/scoped-basic.spec.tsx +++ b/test/runtime/src/components/scoped/scoped-basic/scoped-basic.spec.tsx @@ -24,9 +24,12 @@ describe('scoped-basic', () => { expect(getComputedStyle(scopedSpan).color).toBe('rgb(255, 0, 0)'); const scopedP = scopedEl.querySelector('p')!; - // p element should have scoped class and slot class + // p element should have scoped class expect(scopedP.classList.toString()).toContain('sc-scoped-basic'); - expect(scopedP.classList.toString()).toContain('sc-scoped-basic-s'); + + // the slot element inside p should have the scoped slot class + const slotEl = scopedP.querySelector('slot')!; + expect(slotEl.classList.toString()).toContain('sc-scoped-basic-s'); const slottedSpan = scopedEl.querySelector('p span')!; // slotted span should have the parent root's scoped class diff --git a/test/runtime/src/components/slots/scoped-slot-append-and-prepend/scoped-slot-append-and-prepend.spec.tsx b/test/runtime/src/components/slots/scoped-slot-append-and-prepend/scoped-slot-append-and-prepend.spec.tsx index e976af44918..348f5a72c51 100644 --- a/test/runtime/src/components/slots/scoped-slot-append-and-prepend/scoped-slot-append-and-prepend.spec.tsx +++ b/test/runtime/src/components/slots/scoped-slot-append-and-prepend/scoped-slot-append-and-prepend.spec.tsx @@ -11,18 +11,20 @@ describe('scoped-slot-append-and-prepend', () => { await waitForExist('scoped-slot-append-and-prepend.hydrated'); const host = root; const parentDiv = host.querySelector('#parentDiv')! as HTMLDivElement; + // slotted content lives inside which is a child of #parentDiv + const slot = parentDiv.querySelector('slot')!; expect(host).toBeDefined(); expect(parentDiv).toBeDefined(); - expect(parentDiv.children.length).toBe(1); - expect(parentDiv.children[0].textContent).toBe('My initial slotted content.'); + expect(slot.children.length).toBe(1); + expect(slot.children[0].textContent).toBe('My initial slotted content.'); const el = document.createElement('p'); el.innerText = 'The new slotted content.'; host.append(el); - expect(parentDiv.children.length).toBe(2); - expect(parentDiv.children[1].textContent).toBe('The new slotted content.'); + expect(slot.children.length).toBe(2); + expect(slot.children[1].textContent).toBe('The new slotted content.'); }); }); @@ -36,18 +38,19 @@ describe('scoped-slot-append-and-prepend', () => { const host = document.querySelector('scoped-slot-append-and-prepend')!; const parentDiv = host.querySelector('#parentDiv')! as HTMLDivElement; + const slot = parentDiv.querySelector('slot')!; expect(host).toBeDefined(); expect(parentDiv).toBeDefined(); - expect(parentDiv.children.length).toBe(1); - expect(parentDiv.children[0].textContent).toBe('My initial slotted content.'); + expect(slot.children.length).toBe(1); + expect(slot.children[0].textContent).toBe('My initial slotted content.'); const el = document.createElement('p'); el.innerText = 'The new slotted content.'; host.appendChild(el); - expect(parentDiv.children.length).toBe(2); - expect(parentDiv.children[1].textContent).toBe('The new slotted content.'); + expect(slot.children.length).toBe(2); + expect(slot.children[1].textContent).toBe('The new slotted content.'); }); }); @@ -61,18 +64,19 @@ describe('scoped-slot-append-and-prepend', () => { const host = document.querySelector('scoped-slot-append-and-prepend')!; const parentDiv = host.querySelector('#parentDiv')! as HTMLDivElement; + const slot = parentDiv.querySelector('slot')!; expect(host).toBeDefined(); expect(parentDiv).toBeDefined(); - expect(parentDiv.children.length).toBe(1); - expect(parentDiv.children[0].textContent).toBe('My initial slotted content.'); + expect(slot.children.length).toBe(1); + expect(slot.children[0].textContent).toBe('My initial slotted content.'); const el = document.createElement('p'); el.innerText = 'The new slotted content.'; host.prepend(el); - expect(parentDiv.children.length).toBe(2); - expect(parentDiv.children[0].textContent).toBe('The new slotted content.'); + expect(slot.children.length).toBe(2); + expect(slot.children[0].textContent).toBe('The new slotted content.'); }); }); }); diff --git a/test/runtime/src/components/slots/scoped-slot-in-slot/scoped-slot-in-slot.spec.tsx b/test/runtime/src/components/slots/scoped-slot-in-slot/scoped-slot-in-slot.spec.tsx index af04de14b56..9f99645e13e 100644 --- a/test/runtime/src/components/slots/scoped-slot-in-slot/scoped-slot-in-slot.spec.tsx +++ b/test/runtime/src/components/slots/scoped-slot-in-slot/scoped-slot-in-slot.spec.tsx @@ -18,13 +18,13 @@ describe('scoped-slot-in-slot', () => { expect(parent.firstElementChild!.tagName).toBe('LABEL'); // Ensure the label slot content made it through - const span = parent.firstElementChild!.firstElementChild!; + const span = parent.querySelector('label span[slot="label"]')!; expect(span).toBeDefined(); expect(span.tagName).toBe('SPAN'); expect(span.textContent).toBe('Label text'); - // Ensure the message slot content made it through - expect(parent.lastElementChild!.tagName).toBe('SPAN'); + // Ensure the message slot content made it through (slot[name=message] is lastElementChild) + expect(parent.lastElementChild!.tagName).toBe('SLOT'); expect(parent.lastElementChild!.textContent).toBe('Message text'); // Check the child content @@ -32,7 +32,8 @@ describe('scoped-slot-in-slot', () => { expect(child).toBeDefined(); // Ensure the suffix slot content made it through - expect(child.firstElementChild!.firstElementChild!.tagName).toBe('SPAN'); - expect(child.firstElementChild!.firstElementChild!.textContent).toBe('Suffix text'); + const suffixSpan = child.querySelector('div span[slot="suffix"]')!; + expect(suffixSpan.tagName).toBe('SPAN'); + expect(suffixSpan.textContent).toBe('Suffix text'); }); }); diff --git a/test/runtime/src/components/slots/scoped-slot-insertbefore/scoped-slot-insertbefore.spec.tsx b/test/runtime/src/components/slots/scoped-slot-insertbefore/scoped-slot-insertbefore.spec.tsx index 7b91714edb3..8b3f49ce014 100644 --- a/test/runtime/src/components/slots/scoped-slot-insertbefore/scoped-slot-insertbefore.spec.tsx +++ b/test/runtime/src/components/slots/scoped-slot-insertbefore/scoped-slot-insertbefore.spec.tsx @@ -13,9 +13,10 @@ describe('testing a `scoped="true"` component `insertBefore` method', () => { const endSlot = host.querySelector('#parentDiv .end-slot')! as HTMLDivElement; const defaultSlot = host.querySelector('#parentDiv .default-slot')! as HTMLDivElement; + // label div + element expect(defaultSlot.children.length).toBe(2); - expect(startSlot.children.length).toBe(1); - expect(endSlot.children.length).toBe(1); + expect(startSlot.children.length).toBe(2); + expect(endSlot.children.length).toBe(2); const el1 = document.createElement('p'); const el2 = document.createElement('p'); @@ -28,9 +29,10 @@ describe('testing a `scoped="true"` component `insertBefore` method', () => { host.insertBefore(el2, el1); host.insertBefore(el3, el2); - expect(defaultSlot.children.length).toBe(5); - expect(startSlot.children.length).toBe(1); - expect(endSlot.children.length).toBe(1); + // wrapper children are always label div + (slotted content lives inside ) + expect(defaultSlot.children.length).toBe(2); + expect(startSlot.children.length).toBe(2); + expect(endSlot.children.length).toBe(2); expect(defaultSlot.textContent).toBe( `Default slot is here:My initial slotted content.Content 3. Content 2. Content 1. `, ); @@ -49,8 +51,8 @@ describe('testing a `scoped="true"` component `insertBefore` method', () => { const defaultSlot = host.querySelector('#parentDiv .default-slot')! as HTMLDivElement; expect(defaultSlot.children.length).toBe(2); - expect(startSlot.children.length).toBe(1); - expect(endSlot.children.length).toBe(1); + expect(startSlot.children.length).toBe(2); + expect(endSlot.children.length).toBe(2); const el1 = document.createElement('p'); const el2 = document.createElement('p'); @@ -65,7 +67,7 @@ describe('testing a `scoped="true"` component `insertBefore` method', () => { host.insertBefore(el2, el1); host.insertBefore(el3, el2); - expect(defaultSlot.children.length).toBe(3); + expect(defaultSlot.children.length).toBe(2); expect(startSlot.children.length).toBe(2); expect(endSlot.children.length).toBe(2); expect(host.textContent).toBe(`My initial slotted content.Content 1. Content 2. Content 3. `); @@ -84,8 +86,8 @@ describe('testing a `scoped="true"` component `insertBefore` method', () => { const defaultSlot = host.querySelector('#parentDiv .default-slot')! as HTMLDivElement; expect(defaultSlot.children.length).toBe(2); - expect(startSlot.children.length).toBe(1); - expect(endSlot.children.length).toBe(1); + expect(startSlot.children.length).toBe(2); + expect(endSlot.children.length).toBe(2); const el1 = document.createElement('p'); const el2 = document.createElement('p'); @@ -100,7 +102,7 @@ describe('testing a `scoped="true"` component `insertBefore` method', () => { expect(host.lastElementChild.textContent).toBe('Content 1. '); expect(defaultSlot.children.length).toBe(2); - expect(startSlot.children.length).toBe(1); - expect(endSlot.children.length).toBe(1); + expect(startSlot.children.length).toBe(2); + expect(endSlot.children.length).toBe(2); }); }); diff --git a/test/runtime/src/components/slots/scoped-slot-slotted-parentnode/scoped-slot-slotted-parentnode.spec.tsx b/test/runtime/src/components/slots/scoped-slot-slotted-parentnode/scoped-slot-slotted-parentnode.spec.tsx index e84eb00cacb..8c4b0417a9c 100644 --- a/test/runtime/src/components/slots/scoped-slot-slotted-parentnode/scoped-slot-slotted-parentnode.spec.tsx +++ b/test/runtime/src/components/slots/scoped-slot-slotted-parentnode/scoped-slot-slotted-parentnode.spec.tsx @@ -26,11 +26,12 @@ describe('checks slotted node parentNode', () => { ); await waitForExist('cmp-slotted-parentnode.hydrated'); + // is the real DOM parent of slotted nodes (it is internal to the component) expect( (document.querySelector('cmp-slotted-parentnode')!.children[0] as any).__parentNode.tagName, - ).toBe('LABEL'); + ).toBe('SLOT'); expect( (document.querySelector('cmp-slotted-parentnode')!.childNodes[0] as any).__parentNode.tagName, - ).toBe('LABEL'); + ).toBe('SLOT'); }); }); diff --git a/test/runtime/src/components/slots/scoped-slot-text-with-sibling/scoped-slot-text-with-sibling.spec.tsx b/test/runtime/src/components/slots/scoped-slot-text-with-sibling/scoped-slot-text-with-sibling.spec.tsx index 50b24bb1299..4a1ff072df7 100644 --- a/test/runtime/src/components/slots/scoped-slot-text-with-sibling/scoped-slot-text-with-sibling.spec.tsx +++ b/test/runtime/src/components/slots/scoped-slot-text-with-sibling/scoped-slot-text-with-sibling.spec.tsx @@ -28,15 +28,14 @@ describe('scoped-slot-text-with-sibling', () => { root.textContent = 'New text for label structure testing'; const label = root.querySelector('label')!; /** - * Expect three child nodes in the label - * - a content reference text node - * - the slotted text node - * - the non-slotted text + * Expect two child nodes in the label: + * - the element (contains slotted text) + * - the non-slotted
*/ expect(label).toBeTruthy(); - expect(label.childNodes.length).toBe(3); + expect(label.childNodes.length).toBe(2); expect((label.childNodes[0] as any)['s-cr']).toBeDefined(); - expect(label.childNodes[1].textContent).toBe('New text for label structure testing'); - expect(label.childNodes[2].textContent).toBe('Non-slotted text'); + expect(label.childNodes[0].textContent).toBe('New text for label structure testing'); + expect(label.childNodes[1].textContent).toBe('Non-slotted text'); }); }); diff --git a/test/runtime/src/components/slots/scoped-slot-text/scoped-slot-text.spec.tsx b/test/runtime/src/components/slots/scoped-slot-text/scoped-slot-text.spec.tsx index 2ac9012c02a..ed6d3800068 100644 --- a/test/runtime/src/components/slots/scoped-slot-text/scoped-slot-text.spec.tsx +++ b/test/runtime/src/components/slots/scoped-slot-text/scoped-slot-text.spec.tsx @@ -17,13 +17,12 @@ describe('scoped-slot-text', () => { const label = root.querySelector('label')!; /** - * Expect two child nodes in the label - * - a content reference text node - * - the slotted text node + * Expect one child node in the label: the element. + * Slotted text now lives physically inside . */ expect(label).toBeTruthy(); - expect(label.childNodes.length).toBe(2); + expect(label.childNodes.length).toBe(1); expect((label.childNodes[0] as any)['s-cr']).toBeDefined(); - expect(label.childNodes[1].textContent).toBe('New text for label structure testing'); + expect(label.childNodes[0].textContent).toBe('New text for label structure testing'); }); }); diff --git a/test/runtime/src/components/slots/slot-array-basic/slot-array-basic.spec.tsx b/test/runtime/src/components/slots/slot-array-basic/slot-array-basic.spec.tsx index 5e40af6e5d0..51fc9f2fb7d 100644 --- a/test/runtime/src/components/slots/slot-array-basic/slot-array-basic.spec.tsx +++ b/test/runtime/src/components/slots/slot-array-basic/slot-array-basic.spec.tsx @@ -25,45 +25,46 @@ describe('slot array basic', () => { ); await waitForExist('.results1.hydrated'); + + // is now a real DOM element — direct children are header, slot, footer let children = document.querySelectorAll('.results1 > *'); - expect(children.length).toBe(2); + expect(children.length).toBe(3); expect(children[0].tagName.toLowerCase()).toBe('header'); expect(children[0].textContent).toBe('Header'); - expect(children[1].tagName.toLowerCase()).toBe('footer'); - expect(children[1].textContent).toBe('Footer'); + expect(children[1].tagName.toLowerCase()).toBe('slot'); + expect(children[2].tagName.toLowerCase()).toBe('footer'); + expect(children[2].textContent).toBe('Footer'); + // slotted content lives inside children = document.querySelectorAll('.results2 > *'); expect(children.length).toBe(3); expect(children[0].tagName.toLowerCase()).toBe('header'); - expect(children[0].textContent).toBe('Header'); - expect(children[1].tagName.toLowerCase()).toBe('content-top'); - expect(children[1].textContent).toBe('Content'); + expect(children[1].tagName.toLowerCase()).toBe('slot'); + expect(children[1].children[0].tagName.toLowerCase()).toBe('content-top'); + expect(children[1].children[0].textContent).toBe('Content'); expect(children[2].tagName.toLowerCase()).toBe('footer'); - expect(children[2].textContent).toBe('Footer'); children = document.querySelectorAll('.results3 > *'); - expect(children.length).toBe(4); + expect(children.length).toBe(3); expect(children[0].tagName.toLowerCase()).toBe('header'); - expect(children[0].textContent).toBe('Header'); - expect(children[1].tagName.toLowerCase()).toBe('content-top'); - expect(children[1].textContent).toBe('Content Top'); - expect(children[2].tagName.toLowerCase()).toBe('content-bottom'); - expect(children[2].textContent).toBe('Content Bottom'); - expect(children[3].tagName.toLowerCase()).toBe('footer'); - expect(children[3].textContent).toBe('Footer'); + expect(children[1].tagName.toLowerCase()).toBe('slot'); + expect(children[1].children[0].tagName.toLowerCase()).toBe('content-top'); + expect(children[1].children[0].textContent).toBe('Content Top'); + expect(children[1].children[1].tagName.toLowerCase()).toBe('content-bottom'); + expect(children[1].children[1].textContent).toBe('Content Bottom'); + expect(children[2].tagName.toLowerCase()).toBe('footer'); children = document.querySelectorAll('.results4 > *'); - expect(children.length).toBe(5); + expect(children.length).toBe(3); expect(children[0].tagName.toLowerCase()).toBe('header'); - expect(children[0].textContent).toBe('Header'); - expect(children[1].tagName.toLowerCase()).toBe('content-top'); - expect(children[1].textContent).toBe('Content Top'); - expect(children[2].tagName.toLowerCase()).toBe('content-middle'); - expect(children[2].textContent).toBe('Content Middle'); - expect(children[3].tagName.toLowerCase()).toBe('content-bottom'); - expect(children[3].textContent).toBe('Content Bottom'); - expect(children[4].tagName.toLowerCase()).toBe('footer'); - expect(children[4].textContent).toBe('Footer'); + expect(children[1].tagName.toLowerCase()).toBe('slot'); + expect(children[1].children[0].tagName.toLowerCase()).toBe('content-top'); + expect(children[1].children[0].textContent).toBe('Content Top'); + expect(children[1].children[1].tagName.toLowerCase()).toBe('content-middle'); + expect(children[1].children[1].textContent).toBe('Content Middle'); + expect(children[1].children[2].tagName.toLowerCase()).toBe('content-bottom'); + expect(children[1].children[2].textContent).toBe('Content Bottom'); + expect(children[2].tagName.toLowerCase()).toBe('footer'); expect(document.querySelector('[hidden]')).toBeNull(); }); diff --git a/test/runtime/src/components/slots/slot-fallback-with-forwarded-slot/slot-fallback-with-forwarded-slot.spec.tsx b/test/runtime/src/components/slots/slot-fallback-with-forwarded-slot/slot-fallback-with-forwarded-slot.spec.tsx index 4d90b6cfdcb..4b98b5bd620 100644 --- a/test/runtime/src/components/slots/slot-fallback-with-forwarded-slot/slot-fallback-with-forwarded-slot.spec.tsx +++ b/test/runtime/src/components/slots/slot-fallback-with-forwarded-slot/slot-fallback-with-forwarded-slot.spec.tsx @@ -11,8 +11,7 @@ describe('slot-fallback-with-forwarded-slot', () => { const fb = cmp.querySelector('slot-fb') as HTMLElement; expect(fb).toHaveTextContent('Slot fallback via property'); - expect(fb.getAttribute('hidden')).toBeNull(); - expect(fb.hidden).toBe(false); + expect(fb).toBeVisible(); // Add slotted content dynamically const p = document.createElement('p'); @@ -22,8 +21,7 @@ describe('slot-fallback-with-forwarded-slot', () => { await waitForChanges(); expect(cmp).toHaveTextContent('Slot content via slot'); - expect(fb.getAttribute('hidden')).toBe(''); - expect(fb.hidden).toBe(true); + expect(fb).not.toBeVisible(); }); it('should hide slot-fb elements when slotted content exists', async () => { @@ -39,14 +37,12 @@ describe('slot-fallback-with-forwarded-slot', () => { expect(cmp).toHaveTextContent('Slot content via slot'); expect(fb).toHaveTextContent('Slot fallback via property'); - expect(fb.getAttribute('hidden')).toBe(''); - expect(fb.hidden).toBe(true); + expect(fb).not.toBeVisible(); // Remove slotted content cmp.removeChild(cmp.childNodes[0]); await waitForChanges(); - expect(fb.getAttribute('hidden')).toBeNull(); - expect(fb.hidden).toBe(false); + expect(fb).toBeVisible(); }); }); diff --git a/test/runtime/src/components/slots/slot-fallback/slot-fallback.spec.tsx b/test/runtime/src/components/slots/slot-fallback/slot-fallback.spec.tsx index 7cb22608501..d0e8664fb1b 100644 --- a/test/runtime/src/components/slots/slot-fallback/slot-fallback.spec.tsx +++ b/test/runtime/src/components/slots/slot-fallback/slot-fallback.spec.tsx @@ -5,48 +5,40 @@ describe('slot-fallback', () => { const { waitForChanges } = await render(); // show fallback content - expect( - document.querySelector('.results1 slot-fb[name="start"]:not([hidden])')!.textContent, - ).toBe('slot start fallback 0'); - expect(document.querySelector('.results1 section slot-fb:not([hidden])')!.textContent).toBe( - 'slot default fallback 0', - ); - expect( - document.querySelector('.results1 article span slot-fb[name="end"]:not([hidden])')! - .textContent, - ).toBe('slot end fallback 0'); + const fbStart = () => document.querySelector('.results1 slot-fb[name="start"]')!; + const fbDefault = () => document.querySelector('.results1 section slot-fb')!; + const fbEnd = () => + document.querySelector('.results1 article span slot-fb[name="end"]')!; + + expect(fbStart().textContent).toBe('slot start fallback 0'); + expect(fbStart()).toBeVisible(); + expect(fbDefault().textContent).toBe('slot default fallback 0'); + expect(fbDefault()).toBeVisible(); + expect(fbEnd().textContent).toBe('slot end fallback 0'); + expect(fbEnd()).toBeVisible(); // update fallback content (document.querySelector('button.change-fallback-content') as HTMLButtonElement).click(); await waitForChanges(); - expect( - document.querySelector('.results1 slot-fb[name="start"]:not([hidden])')!.textContent, - ).toBe('slot start fallback 1'); - expect(document.querySelector('.results1 section slot-fb:not([hidden])')!.textContent).toBe( - 'slot default fallback 1', - ); - expect( - document.querySelector('.results1 article span slot-fb[name="end"]:not([hidden])')! - .textContent, - ).toBe('slot end fallback 1'); + expect(fbStart().textContent).toBe('slot start fallback 1'); + expect(fbStart()).toBeVisible(); + expect(fbDefault().textContent).toBe('slot default fallback 1'); + expect(fbDefault()).toBeVisible(); + expect(fbEnd().textContent).toBe('slot end fallback 1'); + expect(fbEnd()).toBeVisible(); // set light dom instead and hide fallback content (document.querySelector('button.change-light-dom') as HTMLButtonElement).click(); await waitForChanges(); // fallback content hidden but still the same - expect( - document.body.querySelector('.results1 slot-fb[name="start"][hidden]')!.textContent!.trim(), - ).toBe('slot start fallback 1'); - expect( - document.body.querySelector('.results1 section slot-fb[hidden]')!.textContent!.trim(), - ).toBe('slot default fallback 1'); - expect( - document.body - .querySelector('.results1 article span slot-fb[name="end"][hidden]')! - .textContent!.trim(), - ).toBe('slot end fallback 1'); + expect(fbStart().textContent!.trim()).toBe('slot start fallback 1'); + expect(fbStart()).not.toBeVisible(); + expect(fbDefault().textContent!.trim()).toBe('slot default fallback 1'); + expect(fbDefault()).not.toBeVisible(); + expect(fbEnd().textContent!.trim()).toBe('slot end fallback 1'); + expect(fbEnd()).not.toBeVisible(); // light dom content rendered expect(document.querySelector('.results1 content-start')!.textContent).toBe( @@ -63,18 +55,13 @@ describe('slot-fallback', () => { (document.querySelector('button.change-slot-content') as HTMLButtonElement).click(); await waitForChanges(); - // fallback content hidden and updated content - expect( - document.querySelector('.results1 slot-fb[name="start"][hidden]')!.textContent!.trim(), - ).toBe('slot start fallback 2'); - expect(document.querySelector('.results1 section slot-fb[hidden]')!.textContent!.trim()).toBe( - 'slot default fallback 2', - ); - expect( - document - .querySelector('.results1 article span slot-fb[name="end"][hidden]')! - .textContent!.trim(), - ).toBe('slot end fallback 2'); + // fallback content hidden and updated + expect(fbStart().textContent!.trim()).toBe('slot start fallback 2'); + expect(fbStart()).not.toBeVisible(); + expect(fbDefault().textContent!.trim()).toBe('slot default fallback 2'); + expect(fbDefault()).not.toBeVisible(); + expect(fbEnd().textContent!.trim()).toBe('slot end fallback 2'); + expect(fbEnd()).not.toBeVisible(); // light dom content updated expect(document.querySelector('.results1 content-start')!.textContent).toBe( @@ -91,17 +78,13 @@ describe('slot-fallback', () => { (document.querySelector('button.change-light-dom') as HTMLButtonElement).click(); await waitForChanges(); - // fallback content should not be hidden - expect( - document.querySelector('.results1 slot-fb[name="start"]:not([hidden])')!.textContent, - ).toBe('slot start fallback 2'); - expect(document.querySelector('.results1 section slot-fb:not([hidden])')!.textContent).toBe( - 'slot default fallback 2', - ); - expect( - document.querySelector('.results1 article span slot-fb[name="end"]:not([hidden])')! - .textContent, - ).toBe('slot end fallback 2'); + // fallback content should be visible again + expect(fbStart().textContent).toBe('slot start fallback 2'); + expect(fbStart()).toBeVisible(); + expect(fbDefault().textContent).toBe('slot default fallback 2'); + expect(fbDefault()).toBeVisible(); + expect(fbEnd().textContent).toBe('slot end fallback 2'); + expect(fbEnd()).toBeVisible(); // light dom content should not exist expect(document.querySelector('.results1 content-start')).toBeNull(); diff --git a/test/runtime/src/components/slots/slot-hide-content/slot-hide-content.spec.tsx b/test/runtime/src/components/slots/slot-hide-content/slot-hide-content.spec.tsx index eddd93512b2..94ab91df78a 100644 --- a/test/runtime/src/components/slots/slot-hide-content/slot-hide-content.spec.tsx +++ b/test/runtime/src/components/slots/slot-hide-content/slot-hide-content.spec.tsx @@ -8,8 +8,6 @@ describe('slot-hide-content', () => {

Hello

, ); - await waitForExist('slot-hide-content-scoped.hydrated'); - const host = root; const slottedContent = host.querySelector('#slotted-1')!; @@ -21,7 +19,8 @@ describe('slot-hide-content', () => { await waitForChanges(); expect(slottedContent.hasAttribute('hidden')).toBe(false); - expect(slottedContent.parentElement!.classList).toContain('slot-wrapper'); + // slottedContent is inside which is inside .slot-wrapper + expect(slottedContent.closest('.slot-wrapper')).not.toBeNull(); }); }); }); diff --git a/test/runtime/src/components/slots/slot-html/slot-html.spec.tsx b/test/runtime/src/components/slots/slot-html/slot-html.spec.tsx index 897cae3c5c9..f031a904f79 100644 --- a/test/runtime/src/components/slots/slot-html/slot-html.spec.tsx +++ b/test/runtime/src/components/slots/slot-html/slot-html.spec.tsx @@ -112,7 +112,8 @@ describe('slot-html', () => { expect(results5SlotStartChildren[0].textContent).toBe('start slot 1'); expect(results5SlotStartChildren[1].textContent).toBe('start slot 2'); - expect(document.querySelector('.results5 div')!.childNodes[3].textContent!.trim()).toBe( + // default text node is now inside at childNodes[2] + expect(document.querySelector('.results5 div')!.childNodes[2].textContent!.trim()).toBe( 'default text node', ); @@ -148,36 +149,53 @@ describe('slot-html', () => { expect(results9DefaultSlotChildren[0].textContent).toBe('default slot 1'); expect(results9DefaultSlotChildren[1].textContent).toBe('default slot 2'); - const results10Children = document.querySelector('.results10 div')!.childNodes; - expect(results10Children[3].textContent!.trim()).toBe('default slot 1'); - expect(results10Children[4].textContent!.trim()).toBe('default slot 2'); - expect(results10Children[5].textContent!.trim()).toBe('default slot text node'); + // results10: div > [hr, article, slot(default), section] + // default slot content is inside at childNodes[2] + const results10DefaultSlot = document.querySelector('.results10 div')!.childNodes[2]; + expect(results10DefaultSlot.childNodes[0].textContent!.trim()).toBe('default slot 1'); + expect(results10DefaultSlot.childNodes[1].textContent!.trim()).toBe('default slot 2'); + expect(results10DefaultSlot.childNodes[2].textContent!.trim()).toBe('default slot text node'); + // results11/12: div > [hr, article, slot(default), section] + // article > span > slot[name=start] > content + // section > slot[name=end] > content const results11 = document.querySelector('.results11 div')!; - expect(results11.childNodes[1].childNodes[0].childNodes[1].textContent!.trim()).toBe( - 'start slot 1', + expect( + results11.childNodes[1].childNodes[0].childNodes[0].childNodes[0].textContent!.trim(), + ).toBe('start slot 1'); + expect( + results11.childNodes[1].childNodes[0].childNodes[0].childNodes[1].textContent!.trim(), + ).toBe('start slot 2'); + expect(results11.childNodes[2].childNodes[0].textContent!.trim()).toBe('default slot 1'); + expect(results11.childNodes[2].childNodes[1].textContent!.trim()).toBe('default slot 2'); + expect(results11.childNodes[2].childNodes[2].textContent!.trim()).toBe( + 'default slot text node', ); - expect(results11.childNodes[1].childNodes[0].childNodes[2].textContent!.trim()).toBe( - 'start slot 2', + expect(results11.childNodes[3].childNodes[0].childNodes[0].textContent!.trim()).toBe( + 'end slot 1', + ); + expect(results11.childNodes[3].childNodes[0].childNodes[1].textContent!.trim()).toBe( + 'end slot 2', ); - expect(results11.childNodes[3].textContent!.trim()).toBe('default slot 1'); - expect(results11.childNodes[4].textContent!.trim()).toBe('default slot 2'); - expect(results11.childNodes[5].textContent!.trim()).toBe('default slot text node'); - expect(results11.childNodes[6].childNodes[1].textContent!.trim()).toBe('end slot 1'); - expect(results11.childNodes[6].childNodes[2].textContent!.trim()).toBe('end slot 2'); const results12 = document.querySelector('.results12 div')!; - expect(results12.childNodes[1].childNodes[0].childNodes[1].textContent!.trim()).toBe( - 'start slot 1', + expect( + results12.childNodes[1].childNodes[0].childNodes[0].childNodes[0].textContent!.trim(), + ).toBe('start slot 1'); + expect( + results12.childNodes[1].childNodes[0].childNodes[0].childNodes[1].textContent!.trim(), + ).toBe('start slot 2'); + expect(results12.childNodes[2].childNodes[0].textContent!.trim()).toBe( + 'default slot text node', + ); + expect(results12.childNodes[2].childNodes[1].textContent!.trim()).toBe('default slot 1'); + expect(results12.childNodes[2].childNodes[2].textContent!.trim()).toBe('default slot 2'); + expect(results12.childNodes[3].childNodes[0].childNodes[0].textContent!.trim()).toBe( + 'end slot 1', ); - expect(results12.childNodes[1].childNodes[0].childNodes[2].textContent!.trim()).toBe( - 'start slot 2', + expect(results12.childNodes[3].childNodes[0].childNodes[1].textContent!.trim()).toBe( + 'end slot 2', ); - expect(results12.childNodes[3].textContent!.trim()).toBe('default slot text node'); - expect(results12.childNodes[4].textContent!.trim()).toBe('default slot 1'); - expect(results12.childNodes[5].textContent!.trim()).toBe('default slot 2'); - expect(results12.childNodes[6].childNodes[1].textContent!.trim()).toBe('end slot 1'); - expect(results12.childNodes[6].childNodes[2].textContent!.trim()).toBe('end slot 2'); expect(document.querySelector('[hidden]')).toBeNull(); }); diff --git a/test/runtime/src/components/slots/slot-nested-default-order/slot-nested-default-order.spec.tsx b/test/runtime/src/components/slots/slot-nested-default-order/slot-nested-default-order.spec.tsx index 198e80a461a..14e0e9027a6 100644 --- a/test/runtime/src/components/slots/slot-nested-default-order/slot-nested-default-order.spec.tsx +++ b/test/runtime/src/components/slots/slot-nested-default-order/slot-nested-default-order.spec.tsx @@ -12,11 +12,13 @@ describe('slot-nested-default-order', () => { 'slot-nested-default-order-parent slot-nested-default-order-child > *', ); + // div + slot (slot now exists as a real DOM element) expect(childCmps).toHaveLength(2); expect(childCmps[0].tagName.toLowerCase()).toBe('div'); expect(childCmps[0]).toHaveTextContent('State: true'); - expect(childCmps[1].tagName.toLowerCase()).toBe('p'); + expect(childCmps[1].tagName.toLowerCase()).toBe('slot'); + //

is physically inside expect(childCmps[1]).toHaveTextContent('Hello'); }); }); diff --git a/test/runtime/src/components/slots/slot-reorder/slot-reorder.spec.tsx b/test/runtime/src/components/slots/slot-reorder/slot-reorder.spec.tsx index 9e68bc1fd73..1588326e984 100644 --- a/test/runtime/src/components/slots/slot-reorder/slot-reorder.spec.tsx +++ b/test/runtime/src/components/slots/slot-reorder/slot-reorder.spec.tsx @@ -4,110 +4,129 @@ describe('slot-reorder', () => { it('renders and reorders slots correctly', async () => { const { root, waitForChanges } = await render(); + /** + * In the new slot model each is a real DOM element followed immediately + * by its sibling. In "ordered" state the render order is: + * slot(default), slot-fb(default), slot(slot-a), slot-fb(slot-a), slot(slot-b), slot-fb(slot-b) + * indices: 0 1 2 3 4 5 + */ function ordered() { + // results1 — no slotted content, all slot-fbs visible let r = root.querySelector('.results1 div')!; - expect(r.children[0].textContent!.trim()).toBe('fallback default'); - expect(r.children[0].hasAttribute('hidden')).toBe(false); - expect(r.children[0].getAttribute('name')).toBe(null); - expect(r.children[1].textContent!.trim()).toBe('fallback slot-a'); - expect(r.children[1].hasAttribute('hidden')).toBe(false); - expect(r.children[1].getAttribute('name')).toBe('slot-a'); - expect(r.children[2].textContent!.trim()).toBe('fallback slot-b'); - expect(r.children[2].hasAttribute('hidden')).toBe(false); - expect(r.children[2].getAttribute('name')).toBe('slot-b'); + expect(r.children[1].textContent!.trim()).toBe('fallback default'); + expect(r.children[1] as HTMLElement).toBeVisible(); + expect(r.children[1].getAttribute('name')).toBeNull(); + expect(r.children[3].textContent!.trim()).toBe('fallback slot-a'); + expect(r.children[3] as HTMLElement).toBeVisible(); + expect(r.children[3].getAttribute('name')).toBe('slot-a'); + expect(r.children[5].textContent!.trim()).toBe('fallback slot-b'); + expect(r.children[5] as HTMLElement).toBeVisible(); + expect(r.children[5].getAttribute('name')).toBe('slot-b'); + // results2 — default slot has content r = root.querySelector('.results2 div')!; - expect(r.children[0].textContent!.trim()).toBe('fallback default'); - expect(r.children[0].hasAttribute('hidden')).toBe(true); - expect(r.children[0].getAttribute('name')).toBe(null); - expect(r.children[1].textContent!.trim()).toBe('default content'); - expect(r.children[2].textContent!.trim()).toBe('fallback slot-a'); - expect(r.children[2].hasAttribute('hidden')).toBe(false); - expect(r.children[2].getAttribute('name')).toBe('slot-a'); - expect(r.children[3].textContent!.trim()).toBe('fallback slot-b'); - expect(r.children[3].hasAttribute('hidden')).toBe(false); - expect(r.children[3].getAttribute('name')).toBe('slot-b'); + expect(r.children[0].textContent!.trim()).toBe('default content'); // slot + expect(r.children[1].textContent!.trim()).toBe('fallback default'); // slot-fb hidden + expect(r.children[1] as HTMLElement).not.toBeVisible(); + expect(r.children[1].getAttribute('name')).toBeNull(); + expect(r.children[3].textContent!.trim()).toBe('fallback slot-a'); + expect(r.children[3] as HTMLElement).toBeVisible(); + expect(r.children[3].getAttribute('name')).toBe('slot-a'); + expect(r.children[5].textContent!.trim()).toBe('fallback slot-b'); + expect(r.children[5] as HTMLElement).toBeVisible(); + expect(r.children[5].getAttribute('name')).toBe('slot-b'); + // results3 — all slots have content r = root.querySelector('.results3 div')!; - expect(r.children[0].textContent!.trim()).toBe('fallback default'); - expect(r.children[0].hasAttribute('hidden')).toBe(true); - expect(r.children[0].getAttribute('name')).toBe(null); - expect(r.children[1].textContent!.trim()).toBe('default content'); - expect(r.children[2].textContent!.trim()).toBe('fallback slot-a'); - expect(r.children[2].hasAttribute('hidden')).toBe(true); - expect(r.children[2].getAttribute('name')).toBe('slot-a'); - expect(r.children[3].textContent!.trim()).toBe('slot-a content'); - expect(r.children[4].textContent!.trim()).toBe('fallback slot-b'); - expect(r.children[4].hasAttribute('hidden')).toBe(true); - expect(r.children[4].getAttribute('name')).toBe('slot-b'); - expect(r.children[5].textContent!.trim()).toBe('slot-b content'); + expect(r.children[0].textContent!.trim()).toBe('default content'); + expect(r.children[1].textContent!.trim()).toBe('fallback default'); + expect(r.children[1] as HTMLElement).not.toBeVisible(); + expect(r.children[1].getAttribute('name')).toBeNull(); + expect(r.children[2].textContent!.trim()).toBe('slot-a content'); + expect(r.children[3].textContent!.trim()).toBe('fallback slot-a'); + expect(r.children[3] as HTMLElement).not.toBeVisible(); + expect(r.children[3].getAttribute('name')).toBe('slot-a'); + expect(r.children[4].textContent!.trim()).toBe('slot-b content'); + expect(r.children[5].textContent!.trim()).toBe('fallback slot-b'); + expect(r.children[5] as HTMLElement).not.toBeVisible(); + expect(r.children[5].getAttribute('name')).toBe('slot-b'); + // results4 — same content as results3, different source order r = root.querySelector('.results4 div')!; - expect(r.children[0].textContent!.trim()).toBe('fallback default'); - expect(r.children[0].hasAttribute('hidden')).toBe(true); - expect(r.children[0].getAttribute('name')).toBe(null); - expect(r.children[1].textContent!.trim()).toBe('default content'); - expect(r.children[2].textContent!.trim()).toBe('fallback slot-a'); - expect(r.children[2].hasAttribute('hidden')).toBe(true); - expect(r.children[2].getAttribute('name')).toBe('slot-a'); - expect(r.children[3].textContent!.trim()).toBe('slot-a content'); - expect(r.children[4].textContent!.trim()).toBe('fallback slot-b'); - expect(r.children[4].hasAttribute('hidden')).toBe(true); - expect(r.children[4].getAttribute('name')).toBe('slot-b'); - expect(r.children[5].textContent!.trim()).toBe('slot-b content'); + expect(r.children[0].textContent!.trim()).toBe('default content'); + expect(r.children[1].textContent!.trim()).toBe('fallback default'); + expect(r.children[1] as HTMLElement).not.toBeVisible(); + expect(r.children[1].getAttribute('name')).toBeNull(); + expect(r.children[2].textContent!.trim()).toBe('slot-a content'); + expect(r.children[3].textContent!.trim()).toBe('fallback slot-a'); + expect(r.children[3] as HTMLElement).not.toBeVisible(); + expect(r.children[3].getAttribute('name')).toBe('slot-a'); + expect(r.children[4].textContent!.trim()).toBe('slot-b content'); + expect(r.children[5].textContent!.trim()).toBe('fallback slot-b'); + expect(r.children[5] as HTMLElement).not.toBeVisible(); + expect(r.children[5].getAttribute('name')).toBe('slot-b'); } + /** + * Reordered state render order: + * slot(slot-b), slot-fb(slot-b), slot(default), slot-fb(default), slot(slot-a), slot-fb(slot-a) + * indices: 0 1 2 3 4 5 + */ function reordered() { + // results1 — no content, all slot-fbs visible let r = root.querySelector('.results1 div')!; - expect(r.children[0].textContent!.trim()).toBe('fallback slot-b'); - expect(r.children[0].hasAttribute('hidden')).toBe(false); - expect(r.children[0].getAttribute('name')).toBe('slot-b'); - expect(r.children[1].textContent!.trim()).toBe('fallback default'); - expect(r.children[1].hasAttribute('hidden')).toBe(false); - expect(r.children[1].getAttribute('name')).toBe(null); - expect(r.children[2].textContent!.trim()).toBe('fallback slot-a'); - expect(r.children[2].hasAttribute('hidden')).toBe(false); - expect(r.children[2].getAttribute('name')).toBe('slot-a'); + expect(r.children[1].textContent!.trim()).toBe('fallback slot-b'); + expect(r.children[1] as HTMLElement).toBeVisible(); + expect(r.children[1].getAttribute('name')).toBe('slot-b'); + expect(r.children[3].textContent!.trim()).toBe('fallback default'); + expect(r.children[3] as HTMLElement).toBeVisible(); + expect(r.children[3].getAttribute('name')).toBeNull(); + expect(r.children[5].textContent!.trim()).toBe('fallback slot-a'); + expect(r.children[5] as HTMLElement).toBeVisible(); + expect(r.children[5].getAttribute('name')).toBe('slot-a'); + // results2 — default slot has content r = root.querySelector('.results2 div')!; - expect(r.children[0].textContent!.trim()).toBe('fallback slot-b'); - expect(r.children[0].hasAttribute('hidden')).toBe(false); - expect(r.children[0].getAttribute('name')).toBe('slot-b'); - expect(r.children[1].textContent!.trim()).toBe('fallback default'); - expect(r.children[1].hasAttribute('hidden')).toBe(true); - expect(r.children[1].getAttribute('name')).toBe(null); - expect(r.children[2].textContent!.trim()).toBe('default content'); - expect(r.children[3].textContent!.trim()).toBe('fallback slot-a'); - expect(r.children[3].hasAttribute('hidden')).toBe(false); - expect(r.children[3].getAttribute('name')).toBe('slot-a'); + expect(r.children[1].textContent!.trim()).toBe('fallback slot-b'); + expect(r.children[1] as HTMLElement).toBeVisible(); + expect(r.children[1].getAttribute('name')).toBe('slot-b'); + expect(r.children[2].textContent!.trim()).toBe('default content'); // slot + expect(r.children[3].textContent!.trim()).toBe('fallback default'); + expect(r.children[3] as HTMLElement).not.toBeVisible(); + expect(r.children[3].getAttribute('name')).toBeNull(); + expect(r.children[5].textContent!.trim()).toBe('fallback slot-a'); + expect(r.children[5] as HTMLElement).toBeVisible(); + expect(r.children[5].getAttribute('name')).toBe('slot-a'); + // results3 — all slots have content r = root.querySelector('.results3 div')!; - expect(r.children[0].textContent!.trim()).toBe('fallback slot-b'); - expect(r.children[0].hasAttribute('hidden')).toBe(true); - expect(r.children[0].getAttribute('name')).toBe('slot-b'); - expect(r.children[1].textContent!.trim()).toBe('slot-b content'); - expect(r.children[2].textContent!.trim()).toBe('fallback default'); - expect(r.children[2].hasAttribute('hidden')).toBe(true); - expect(r.children[2].getAttribute('name')).toBe(null); - expect(r.children[3].textContent!.trim()).toBe('default content'); - expect(r.children[4].textContent!.trim()).toBe('fallback slot-a'); - expect(r.children[4].hasAttribute('hidden')).toBe(true); - expect(r.children[4].getAttribute('name')).toBe('slot-a'); - expect(r.children[5].textContent!.trim()).toBe('slot-a content'); + expect(r.children[0].textContent!.trim()).toBe('slot-b content'); + expect(r.children[1].textContent!.trim()).toBe('fallback slot-b'); + expect(r.children[1] as HTMLElement).not.toBeVisible(); + expect(r.children[1].getAttribute('name')).toBe('slot-b'); + expect(r.children[2].textContent!.trim()).toBe('default content'); + expect(r.children[3].textContent!.trim()).toBe('fallback default'); + expect(r.children[3] as HTMLElement).not.toBeVisible(); + expect(r.children[3].getAttribute('name')).toBeNull(); + expect(r.children[4].textContent!.trim()).toBe('slot-a content'); + expect(r.children[5].textContent!.trim()).toBe('fallback slot-a'); + expect(r.children[5] as HTMLElement).not.toBeVisible(); + expect(r.children[5].getAttribute('name')).toBe('slot-a'); + // results4 — same content as results3 r = root.querySelector('.results4 div')!; - expect(r.children[0].textContent!.trim()).toBe('fallback slot-b'); - expect(r.children[0].hasAttribute('hidden')).toBe(true); - expect(r.children[0].getAttribute('name')).toBe('slot-b'); - expect(r.children[1].textContent!.trim()).toBe('slot-b content'); - expect(r.children[2].textContent!.trim()).toBe('fallback default'); - expect(r.children[2].hasAttribute('hidden')).toBe(true); - expect(r.children[2].getAttribute('name')).toBe(null); - expect(r.children[3].textContent!.trim()).toBe('default content'); - expect(r.children[4].textContent!.trim()).toBe('fallback slot-a'); - expect(r.children[4].hasAttribute('hidden')).toBe(true); - expect(r.children[4].getAttribute('name')).toBe('slot-a'); - expect(r.children[5].textContent!.trim()).toBe('slot-a content'); + expect(r.children[0].textContent!.trim()).toBe('slot-b content'); + expect(r.children[1].textContent!.trim()).toBe('fallback slot-b'); + expect(r.children[1] as HTMLElement).not.toBeVisible(); + expect(r.children[1].getAttribute('name')).toBe('slot-b'); + expect(r.children[2].textContent!.trim()).toBe('default content'); + expect(r.children[3].textContent!.trim()).toBe('fallback default'); + expect(r.children[3] as HTMLElement).not.toBeVisible(); + expect(r.children[3].getAttribute('name')).toBeNull(); + expect(r.children[4].textContent!.trim()).toBe('slot-a content'); + expect(r.children[5].textContent!.trim()).toBe('fallback slot-a'); + expect(r.children[5] as HTMLElement).not.toBeVisible(); + expect(r.children[5].getAttribute('name')).toBe('slot-a'); } // Initial state diff --git a/test/runtime/src/components/slots/slot-replace-wrapper/slot-replace-wrapper.spec.tsx b/test/runtime/src/components/slots/slot-replace-wrapper/slot-replace-wrapper.spec.tsx index 8c8ed0899df..56f7922b6ae 100644 --- a/test/runtime/src/components/slots/slot-replace-wrapper/slot-replace-wrapper.spec.tsx +++ b/test/runtime/src/components/slots/slot-replace-wrapper/slot-replace-wrapper.spec.tsx @@ -20,7 +20,8 @@ describe('slot-replace-wrapper', () => { const result = root.querySelector('.results2 a')!; expect(result.textContent!.trim()).toBe('B'); - expect(result.children[0].children[0].textContent!.trim()).toBe('B'); + // a > slot[name=start](empty) | span > slot(default) > content-end + expect(result.children[1].children[0].children[0].textContent!.trim()).toBe('B'); expect(root.querySelector('[hidden]')).toBeNull(); }); @@ -31,7 +32,8 @@ describe('slot-replace-wrapper', () => { const result = root.querySelector('.results3 a')!; expect(result.textContent!.trim()).toBe('C'); - expect(result.children[0].children[0].children[0].textContent!.trim()).toBe('C'); + // a > slot[name=start](empty) | span > slot(default)(empty) | span > slot[name=end] > content-end + expect(result.children[1].children[1].children[0].children[0].textContent!.trim()).toBe('C'); expect(root.querySelector('[hidden]')).toBeNull(); }); diff --git a/test/runtime/src/components/slots/slot-scoped-list/slot-scoped-list.spec.tsx b/test/runtime/src/components/slots/slot-scoped-list/slot-scoped-list.spec.tsx index 4ab277f8743..8cdddff4e97 100644 --- a/test/runtime/src/components/slots/slot-scoped-list/slot-scoped-list.spec.tsx +++ b/test/runtime/src/components/slots/slot-scoped-list/slot-scoped-list.spec.tsx @@ -10,12 +10,13 @@ describe('slot-scoped-list', () => { expect(button).toBeTruthy(); expect(list).toBeTruthy(); - expect(list!.querySelectorAll('.list-wrapper > div').length).toBe(0); + // divs live inside which is inside .list-wrapper + expect(list!.querySelectorAll('.list-wrapper slot > div').length).toBe(0); button!.click(); await waitForChanges(); - expect(list!.querySelectorAll('.list-wrapper > div').length).toBe(4); + expect(list!.querySelectorAll('.list-wrapper slot > div').length).toBe(4); expect(root.querySelector('[hidden]')).toBeNull(); }); diff --git a/test/runtime/src/components/slots/slot-shadow-list/slot-shadow-list.spec.tsx b/test/runtime/src/components/slots/slot-shadow-list/slot-shadow-list.spec.tsx index 224f72f5889..bf1ea4f090d 100644 --- a/test/runtime/src/components/slots/slot-shadow-list/slot-shadow-list.spec.tsx +++ b/test/runtime/src/components/slots/slot-shadow-list/slot-shadow-list.spec.tsx @@ -10,14 +10,14 @@ describe('slot-shadow-list', () => { expect(button).toBeTruthy(); expect(list).toBeTruthy(); - // Query shadow DOM for list items - let items = list!.shadowRoot!.querySelectorAll('.list-wrapper > div'); + // divs live inside which is inside .list-wrapper + let items = list!.shadowRoot!.querySelectorAll('.list-wrapper slot > div'); expect(items.length).toBe(0); button!.click(); await waitForChanges(); - items = list!.shadowRoot!.querySelectorAll('.list-wrapper > div'); + items = list!.shadowRoot!.querySelectorAll('.list-wrapper slot > div'); expect(items.length).toBe(4); expect(root.querySelector('[hidden]')).toBeNull(); diff --git a/test/runtime/stencil.config.ts b/test/runtime/stencil.config.ts index 5ddf759cb47..a21fb6c1cf0 100644 --- a/test/runtime/stencil.config.ts +++ b/test/runtime/stencil.config.ts @@ -18,7 +18,6 @@ export const config: Config = { type: 'standalone', dir: 'dist/custom-elements', customElementsExportBehavior: 'auto-define-custom-elements', - externalRuntime: false, skipInDev: false, }, ], diff --git a/test/runtime/vitest-setup-custom-elements.ts b/test/runtime/vitest-setup-custom-elements.ts index a9afb5a08fa..a094d97e6dd 100644 --- a/test/runtime/vitest-setup-custom-elements.ts +++ b/test/runtime/vitest-setup-custom-elements.ts @@ -1,5 +1,5 @@ /// -import './hydrated.css'; +import './dist/assets/stencil-hydrate.css'; import * as ce from './dist/custom-elements/index.js'; ce.setNonce('test-csp-nonce'); From 318c70869f981b1f9cc841ac03b93987f2c0171b Mon Sep 17 00:00:00 2001 From: John Jenkins Date: Wed, 20 May 2026 00:45:01 +0100 Subject: [PATCH 2/3] chore: --- packages/core/src/runtime/dom-extras.ts | 5 +- .../core/src/runtime/slot-polyfill-utils.ts | 62 ++------ packages/core/src/runtime/vdom/vdom-render.ts | 136 +++++++----------- .../kitchen-sink/test-bundle-size.js | 2 +- ...r-a-component-with-closed-shadow-DOM-1.txt | 18 +-- ...w-DOM-renders-slot-content-correctly-1.txt | 24 ++-- ...oes-not-render-the-shadow-root-twice-1.txt | 9 +- 7 files changed, 94 insertions(+), 162 deletions(-) diff --git a/packages/core/src/runtime/dom-extras.ts b/packages/core/src/runtime/dom-extras.ts index 14535070eda..a39225dd38e 100644 --- a/packages/core/src/runtime/dom-extras.ts +++ b/packages/core/src/runtime/dom-extras.ts @@ -5,7 +5,7 @@ import { addSlotRelocateNode, dispatchSlotChangeEvent, findSlotFromSlottedNode, - getHostSlotNodes, + getHostSlotNode, getSlotName, getSlottedChildNodes, } from './slot-polyfill-utils'; @@ -146,8 +146,7 @@ export const patchSlotPrepend = (HostElementPrototype: HTMLElement) => { newChild = this.ownerDocument.createTextNode(newChild) as unknown as d.RenderNode; } const slotName = (newChild['s-sn'] = getSlotName(newChild)) || ''; - const childNodes = internalCall(this, 'childNodes'); - const slotNode = getHostSlotNodes(childNodes, this.tagName, slotName)[0]; + const slotNode = getHostSlotNode(this, slotName); if (slotNode) { addSlotRelocateNode(newChild, slotNode, true); internalCall(slotNode, 'prepend')(newChild); diff --git a/packages/core/src/runtime/slot-polyfill-utils.ts b/packages/core/src/runtime/slot-polyfill-utils.ts index 98d60e78c4a..fcbde3d01dc 100644 --- a/packages/core/src/runtime/slot-polyfill-utils.ts +++ b/packages/core/src/runtime/slot-polyfill-utils.ts @@ -2,7 +2,6 @@ import { BUILD } from 'virtual:app-data'; import type * as d from '@stencil/core'; import { internalCall } from './dom-extras'; -import { NODE_TYPE } from './runtime-constants'; /** * Get's the child nodes of a component that are actually slotted. @@ -26,34 +25,19 @@ export const getSlottedChildNodes = (childNodes: NodeListOf): d.Patch }; /** - * Recursively searches a series of child nodes for slot node/s, optionally with a provided slot name. - * @param childNodes the nodes to search for a slot with a specific name. Should be an element's root nodes. - * @param hostName the host name of the slot to match on. - * @param slotName the name of the slot to match on. - * @returns a reference to the slot node that matches the provided name, `null` otherwise + * Finds a slot element within a host element using native DOM query. + * @param host the host element to search within + * @param slotName the name of the slot to find, or undefined to get all slots + * @returns the matching slot node, or null */ -export function getHostSlotNodes( - childNodes: NodeListOf, - hostName?: string, - slotName?: string, -) { - let i = 0; - let slottedNodes: d.RenderNode[] = []; - let childNode: d.RenderNode; - - for (; i < childNodes.length; i++) { - childNode = childNodes[i] as any; - if ( - childNode['s-sr'] && - (!hostName || childNode['s-hn'] === hostName) && - (slotName === undefined || getSlotName(childNode) === slotName) - ) { - slottedNodes.push(childNode); - if (typeof slotName !== 'undefined') return slottedNodes; +export function getHostSlotNode(host: Element, slotName?: string): d.RenderNode | null { + for (const slot of (host as Element).querySelectorAll('slot') as NodeListOf) { + if (slot['s-sr'] && slot['s-hn'] === (host as HTMLElement).tagName && + (slotName === undefined || slot['s-sn'] === slotName)) { + return slot; } - slottedNodes = [...slottedNodes, ...getHostSlotNodes(childNode.childNodes, hostName, slotName)]; } - return slottedNodes; + return null; } /** @@ -64,26 +48,9 @@ export function getHostSlotNodes( * @returns whether the node is located in the slot or not */ export const isNodeLocatedInSlot = (nodeToRelocate: d.RenderNode, slotName: string): boolean => { - if (nodeToRelocate.nodeType === NODE_TYPE.ElementNode) { - // A forwarding slot (a rendered inside another component's children) - // matches by its own slot name rather than a `slot` attribute. - if (nodeToRelocate['s-sr'] && nodeToRelocate['s-sn'] === slotName) { - return true; - } - if (nodeToRelocate.getAttribute('slot') === null && slotName === '') { - // if the node doesn't have a slot attribute, and the slot we're checking - // is not a named slot, then we assume the node should be within the slot - return true; - } - if (nodeToRelocate.getAttribute('slot') === slotName) { - return true; - } - return false; - } - if (nodeToRelocate['s-sn'] === slotName) { - return true; - } - return slotName === ''; + // getSlotName uses cached s-sn when available, falling back to getAttribute('slot') + const nodeName = getSlotName(nodeToRelocate); + return nodeName !== undefined ? nodeName === slotName : slotName === ''; }; /** @@ -208,7 +175,6 @@ export function findSlotFromSlottedNode(slottedNode: d.PatchedSlotNode, parentHo if (!parentHost) return { slotNode: null, slotName: '' }; const slotName = (slottedNode['s-sn'] = getSlotName(slottedNode) || ''); - const childNodes = internalCall(parentHost, 'childNodes'); - const slotNode = getHostSlotNodes(childNodes, parentHost.tagName, slotName)[0]; + const slotNode = getHostSlotNode(parentHost, slotName); return { slotNode, slotName }; } diff --git a/packages/core/src/runtime/vdom/vdom-render.ts b/packages/core/src/runtime/vdom/vdom-render.ts index db9686809ed..576b40b4645 100644 --- a/packages/core/src/runtime/vdom/vdom-render.ts +++ b/packages/core/src/runtime/vdom/vdom-render.ts @@ -461,12 +461,6 @@ const updateChildren = ( // // In this situation we need to patch `newEndVnode` onto `oldStartVnode` // and move the DOM element for `oldStartVnode`. - if ( - BUILD.slotRelocation && - (oldStartVnode.$tag$ === 'slot' || newEndVnode.$tag$ === 'slot') - ) { - putBackInOriginalLocation(oldStartVnode.$elm$.parentNode, false); - } patch(oldStartVnode, newEndVnode, isInitialRender); // We need to move the element for `oldStartVnode` into a position which // will be appropriate for `newEndVnode`. For this we can use @@ -504,12 +498,6 @@ const updateChildren = ( // (which will handle updating any changed attributes, reconciling their // children etc) but we also need to move the DOM node to which // `oldEndVnode` corresponds. - if ( - BUILD.slotRelocation && - (oldStartVnode.$tag$ === 'slot' || newEndVnode.$tag$ === 'slot') - ) { - putBackInOriginalLocation(oldEndVnode.$elm$.parentNode, false); - } patch(oldEndVnode, newStartVnode, isInitialRender); // We've already checked above if `oldStartVnode` and `newStartVnode` are // the same node, so since we're here we know that they are not. Thus we @@ -762,94 +750,68 @@ const relocateNodes: RelocateNodeData[] = []; * @param elm a render node whose child nodes need to be relocated */ const markSlotContentForRelocation = (elm: d.RenderNode) => { - // tslint:disable-next-line: prefer-const let node: d.RenderNode; let hostContentNodes: NodeList; let j: number; - const children = elm.__childNodes || elm.childNodes; - for (const childNode of children as unknown as d.RenderNode[]) { - // we need to find child nodes which are slot references so we can then try - // to match them up with nodes that need to be relocated - if (childNode['s-sr'] && (node = childNode['s-cr']) && node.parentNode) { - // first get the content reference comment node ('s-cr'), then we get - // its parent, which is where all the host content is now - hostContentNodes = - (node.parentNode as d.RenderNode).__childNodes || node.parentNode.childNodes; - const slotName = childNode['s-sn']; - - // iterate through all the nodes under the location where the host was - // originally rendered - forward order so appendChild preserves source order - for (j = 0; j < hostContentNodes.length; j++) { - node = hostContentNodes[j] as d.RenderNode; - - // check that the node is not a content reference node or a node - // reference and then check that the host name does not match that of - // childNode. - // In addition, check that the slot either has not already been relocated, or - // that its current location's host is not childNode's host. This is essentially - // a check so that we don't try to relocate (and then hide) a node that is already - // where it should be. - if ( - !node['s-cn'] && - !node['s-nr'] && - node['s-hn'] !== childNode['s-hn'] && - (!node['s-sh'] || node['s-sh'] !== childNode['s-hn']) - ) { - // if `node` is located in the slot that `childNode` refers to (via the - // `'s-sn'` property) then we need to relocate it from it's current spot - // (under the host element parent) to the right slot location - if (isNodeLocatedInSlot(node, slotName)) { - // it's possible we've already decided to relocate this node - let relocateNodeData = relocateNodes.find((r) => r.$nodeToRelocate$ === node); - - // ensure that the slot-name attr is correct - node['s-sn'] = node['s-sn'] || slotName; - - if (relocateNodeData) { - relocateNodeData.$nodeToRelocate$['s-sh'] = childNode['s-hn']; - // we marked this node for relocation previously but didn't find - // out the slot reference node to which it needs to be relocated - // so write it down now! - relocateNodeData.$slotRefNode$ = childNode; - } else { - node['s-sh'] = childNode['s-hn']; - // add to our list of nodes to relocate - relocateNodes.push({ - $slotRefNode$: childNode, - $nodeToRelocate$: node, - }); - } + // is a real element now — querySelectorAll replaces the old recursive walk. + // Process ALL slots in the subtree (not just this host's) so parent re-renders + // correctly relocate lightDOM into nested child component slots. + for (const childNode of (elm as Element).querySelectorAll('slot') as NodeListOf) { + if (!childNode['s-sr']) continue; + node = childNode['s-cr']; + if (!node?.parentNode) continue; - if (node['s-sr']) { - relocateNodes.map((relocateNode) => { - if (isNodeLocatedInSlot(relocateNode.$nodeToRelocate$, node['s-sn'])) { - relocateNodeData = relocateNodes.find((r) => r.$nodeToRelocate$ === node); + // get the host root where lightDOM content lives + hostContentNodes = (node.parentNode as d.RenderNode).__childNodes || node.parentNode.childNodes; + const slotName = childNode['s-sn']; - if (relocateNodeData && !relocateNode.$slotRefNode$) { - relocateNode.$slotRefNode$ = relocateNodeData.$slotRefNode$; - } - } - }); - } - } else if (!relocateNodes.some((r) => r.$nodeToRelocate$ === node)) { - // the node is not found within the slot (`childNode`) that we're - // currently looking at, so we stick it into `relocateNodes` to - // handle later. If we never find a home for this element then - // we'll need to hide it + // forward order so appendChild preserves source order + for (j = 0; j < hostContentNodes.length; j++) { + node = hostContentNodes[j] as d.RenderNode; + + // skip the content-ref comment itself, s-ol forwarding anchors, and nodes + // already correctly slotted by this host + if ( + !node['s-cn'] && + !node['s-nr'] && + node['s-hn'] !== childNode['s-hn'] && + (!node['s-sh'] || node['s-sh'] !== childNode['s-hn']) + ) { + if (isNodeLocatedInSlot(node, slotName)) { + let relocateNodeData = relocateNodes.find((r) => r.$nodeToRelocate$ === node); + + node['s-sn'] = node['s-sn'] || slotName; + + if (relocateNodeData) { + relocateNodeData.$nodeToRelocate$['s-sh'] = childNode['s-hn']; + relocateNodeData.$slotRefNode$ = childNode; + } else { + node['s-sh'] = childNode['s-hn']; relocateNodes.push({ + $slotRefNode$: childNode, $nodeToRelocate$: node, }); } + + if (node['s-sr']) { + relocateNodes.map((relocateNode) => { + if (isNodeLocatedInSlot(relocateNode.$nodeToRelocate$, node['s-sn'])) { + relocateNodeData = relocateNodes.find((r) => r.$nodeToRelocate$ === node); + + if (relocateNodeData && !relocateNode.$slotRefNode$) { + relocateNode.$slotRefNode$ = relocateNodeData.$slotRefNode$; + } + } + }); + } + } else if (!relocateNodes.some((r) => r.$nodeToRelocate$ === node)) { + relocateNodes.push({ + $nodeToRelocate$: node, + }); } } } - - // if we're dealing with any type of element (capable of itself being a - // slot reference or containing one) then we recur - if (childNode.nodeType === NODE_TYPE.ElementNode) { - markSlotContentForRelocation(childNode); - } } }; diff --git a/test/build/build-size/kitchen-sink/test-bundle-size.js b/test/build/build-size/kitchen-sink/test-bundle-size.js index 355815bd89c..5506569513f 100644 --- a/test/build/build-size/kitchen-sink/test-bundle-size.js +++ b/test/build/build-size/kitchen-sink/test-bundle-size.js @@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const distDir = path.join(__dirname, 'dist', 'loader-bundle', 'bundlesize-kitchen-sink'); -const maxBundleSize = 26 * 1024; // 26KB in bytes (~26KB non-gzipped, ~9KB gzipped) +const maxBundleSize = 25 * 1024; // 25KB in bytes (~25KB non-gzipped, ~9KB gzipped) console.log('\nChecking bundle size (kitchen-sink)...'); diff --git a/test/ssr/test/__snapshots__/render-to-string.e2e.tsrenderToString-API-closed-shadow-DOM-can-render-a-component-with-closed-shadow-DOM-1.txt b/test/ssr/test/__snapshots__/render-to-string.e2e.tsrenderToString-API-closed-shadow-DOM-can-render-a-component-with-closed-shadow-DOM-1.txt index c2fadd7ab96..a23f821c7e2 100644 --- a/test/ssr/test/__snapshots__/render-to-string.e2e.tsrenderToString-API-closed-shadow-DOM-can-render-a-component-with-closed-shadow-DOM-1.txt +++ b/test/ssr/test/__snapshots__/render-to-string.e2e.tsrenderToString-API-closed-shadow-DOM-can-render-a-component-with-closed-shadow-DOM-1.txt @@ -6,13 +6,9 @@ Closed Shadow DOM Content

- - + + + + + Fallback slot content + + \ No newline at end of file diff --git a/test/ssr/test/__snapshots__/render-to-string.e2e.tsrenderToString-API-closed-shadow-DOM-closed-shadow-DOM-renders-slot-content-correctly-1.txt b/test/ssr/test/__snapshots__/render-to-string.e2e.tsrenderToString-API-closed-shadow-DOM-closed-shadow-DOM-renders-slot-content-correctly-1.txt index a7c1fc60c70..23561188162 100644 --- a/test/ssr/test/__snapshots__/render-to-string.e2e.tsrenderToString-API-closed-shadow-DOM-closed-shadow-DOM-renders-slot-content-correctly-1.txt +++ b/test/ssr/test/__snapshots__/render-to-string.e2e.tsrenderToString-API-closed-shadow-DOM-closed-shadow-DOM-renders-slot-content-correctly-1.txt @@ -7,13 +7,9 @@ Closed Shadow DOM Content
- - - - Custom slotted content - + + + Custom slotted content + + + + + Fallback slot content + + \ No newline at end of file diff --git a/test/ssr/test/__snapshots__/render-to-string.e2e.tsrenderToString-API-does-not-render-the-shadow-root-twice-1.txt b/test/ssr/test/__snapshots__/render-to-string.e2e.tsrenderToString-API-does-not-render-the-shadow-root-twice-1.txt index 63a466bb82e..aa46cdef734 100644 --- a/test/ssr/test/__snapshots__/render-to-string.e2e.tsrenderToString-API-does-not-render-the-shadow-root-twice-1.txt +++ b/test/ssr/test/__snapshots__/render-to-string.e2e.tsrenderToString-API-does-not-render-the-shadow-root-twice-1.txt @@ -1,15 +1,16 @@