diff --git a/src/declarations/stencil-private.ts b/src/declarations/stencil-private.ts index 7c2d1bd3d6e..dbe578a169d 100644 --- a/src/declarations/stencil-private.ts +++ b/src/declarations/stencil-private.ts @@ -1147,6 +1147,10 @@ export interface HostElement extends HTMLElement { */ ['s-hmr']?: (versionId: string) => void; + /** + * A list of nested nested hydration promises that + * must be resolved for the top, ancestor component to be fully hydrated + */ ['s-p']?: Promise[]; componentOnReady?: () => Promise; @@ -1392,9 +1396,10 @@ export interface RenderNode extends HostElement { ['s-cn']?: boolean; /** - * Is a slot reference node: - * This is a node that represents where a slot - * was originally located. + * Is a `slot` node when `shadow: false` (or `scoped: true`). + * + * This is a node (either empty text-node or `` element) + * that represents where a `` is located in the original JSX. */ ['s-sr']?: boolean; diff --git a/src/mock-doc/element.ts b/src/mock-doc/element.ts index 48db9403132..5a57f15c115 100644 --- a/src/mock-doc/element.ts +++ b/src/mock-doc/element.ts @@ -52,6 +52,9 @@ export function createElement(ownerDocument: any, tagName: string): any { case 'ul': return new MockUListElement(ownerDocument); + + case 'slot-fb': + return new MockHTMLElement(ownerDocument, tagName); } if (ownerDocument != null && tagName.includes('-')) { diff --git a/src/runtime/client-hydrate.ts b/src/runtime/client-hydrate.ts index 304ee905982..f8cf819ac88 100644 --- a/src/runtime/client-hydrate.ts +++ b/src/runtime/client-hydrate.ts @@ -1,8 +1,9 @@ import { BUILD } from '@app-data'; import { doc, plt } from '@platform'; +import { CMP_FLAGS } from '@utils'; import type * as d from '../declarations'; -import { addSlotRelocateNode, patchNextPrev } from './dom-extras'; +import { patchNextPrev } from './dom-extras'; import { createTime } from './profile'; import { COMMENT_NODE_ID, @@ -13,7 +14,9 @@ import { ORG_LOCATION_ID, SLOT_NODE_ID, TEXT_NODE_ID, + VNODE_FLAGS, } from './runtime-constants'; +import { addSlotRelocateNode } from './slot-polyfill-utils'; import { newVNode } from './vdom/h'; /** @@ -50,6 +53,17 @@ export const initializeClientHydrate = ( const vnode: d.VNode = newVNode(tagName, null); vnode.$elm$ = hostElm; + let scopeId: string; + if (BUILD.scoped) { + const cmpMeta = hostRef.$cmpMeta$; + if (cmpMeta && cmpMeta.$flags$ & CMP_FLAGS.needsScopedEncapsulation && hostElm['s-sc']) { + scopeId = hostElm['s-sc']; + hostElm.classList.add(scopeId + '-h'); + } else if (hostElm['s-sc']) { + delete hostElm['s-sc']; + } + } + if (!plt.$orgLocNodes$) { // This is the first pass over of this whole document; // does a scrape to construct a 'bare-bones' tree of what elements we have and where content has been moved from @@ -94,20 +108,36 @@ export const initializeClientHydrate = ( } } + if (childRenderNode.$tag$ === 'slot') { + 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; + } + } + if (orgLocationNode && orgLocationNode.isConnected) { if (shadowRoot && orgLocationNode['s-en'] === '') { // if this node is within a shadowDOM, with an original location home // we're safe to move it now orgLocationNode.parentNode.insertBefore(node, orgLocationNode.nextSibling); } - // Remove original location / slot reference comment now regardless: - // 1) Stops SSR frameworks complaining about mismatches - // 2) is un-required for non-shadow, slotted nodes as we'll add all the meta nodes we need when we deal with *all* slotted nodes ↓↓↓ + // Remove original location / slot reference comment now. + // we'll handle it via `addSlotRelocateNode` later orgLocationNode.parentNode.removeChild(orgLocationNode); if (!shadowRoot) { // Add the Original Order of this node. - // We'll use it later to make sure slotted nodes get added in the correct order + // We'll use it to make sure slotted nodes get added in the correct order node['s-oo'] = parseInt(childRenderNode.$nodeId$); } } @@ -116,14 +146,15 @@ export const initializeClientHydrate = ( } const hosts: d.HostElement[] = []; - let snIndex = 0; const snLen = slottedNodes.length; + let snIndex = 0; let slotGroup: SlottedNodes; let snGroupIdx: number; let snGroupLen: number; let slottedItem: SlottedNodes[0]; - // Loops through all the slotted nodes we found while stepping through this component + // 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]; @@ -157,8 +188,7 @@ export const initializeClientHydrate = ( } 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) - const hostChildren = (hostEle as any).__childNodes || hostEle.childNodes; - slottedItem.slot['s-cr'] = hostChildren[0] as d.RenderNode; + slottedItem.slot['s-cr'] = ((hostEle as any).__childNodes || hostEle.childNodes)[0]; } // Create our 'Original Location' node addSlotRelocateNode(slottedItem.node, slottedItem.slot, false, slottedItem.node['s-oo']); @@ -176,6 +206,13 @@ export const initializeClientHydrate = ( } } + 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'); + }); + } + if (BUILD.shadowDom && shadowRoot) { // Add all the root nodes in the shadowDOM (a root node can have a whole nested DOM tree) let rnIdex = 0; @@ -247,9 +284,9 @@ const clientHydrate = ( $index$: childIdSplt[3], $tag$: node.tagName.toLowerCase(), $elm$: node, - // If we don't add the initial classes to the VNode, the first `vdom-render.ts` reconciliation will fail: - // client side changes before componentDidLoad will be ignored, `set-accessor.ts` will just take the element's initial classes - $attrs$: { class: node.className }, + // If we don't add the initial classes to the VNode, the first `vdom-render.ts` patch + // won't try to reconcile them. Classes set on the node will be blown away. + $attrs$: { class: node.className || '' }, }); childRenderNodes.push(childVNode); @@ -260,6 +297,13 @@ const clientHydrate = ( parentVNode.$children$ = []; } + if (BUILD.scoped && scopeId) { + // Host is `scoped: true` - add that flag to the child. + // It's used in 'set-accessor.ts' to make sure our scoped class is present + node['s-si'] = scopeId; + childVNode.$attrs$.class += ' ' + scopeId; + } + // 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') { @@ -276,6 +320,12 @@ const clientHydrate = ( 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'); @@ -285,10 +335,6 @@ const clientHydrate = ( parentVNode.$children$[childVNode.$index$ as any] = childVNode; } - // Host is `scoped: true` - add that flag to the child. - // It's used in 'set-accessor.ts' to make sure our scoped class is present - if (scopeId) node['s-si'] = scopeId; - // This is now the new parent VNode for all the next child checks parentVNode = childVNode; @@ -390,10 +436,10 @@ const clientHydrate = ( if (childNodeType === SLOT_NODE_ID) { // Comment refers to a slot node: // `${SLOT_NODE_ID}.${hostId}.${nodeId}.${depth}.${index}.${slotName}`; - childVNode.$tag$ = 'slot'; // Add the slot name - const slotName = (node['s-sn'] = childVNode.$name$ = childIdSplt[5] || ''); + const slotName = (node['s-sn'] = childIdSplt[5] || ''); + // add the `` node to the VNode tree and prepare any slotted any child nodes addSlot( slotName, @@ -502,6 +548,8 @@ function addSlot( 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 @@ -604,4 +652,5 @@ interface RenderNodeData extends d.VNode { $nodeId$: string; $depth$: string; $index$: string; + $elm$: d.RenderNode; } diff --git a/src/runtime/connected-callback.ts b/src/runtime/connected-callback.ts index 9ae5077d982..87544e1e0ef 100644 --- a/src/runtime/connected-callback.ts +++ b/src/runtime/connected-callback.ts @@ -134,5 +134,5 @@ const setContentReference = (elm: d.HostElement) => { BUILD.isDebug ? `content-ref (host=${elm.localName})` : '', ) as any); contentRefElm['s-cn'] = true; - insertBefore(elm, contentRefElm, elm.firstChild); + insertBefore(elm, contentRefElm, elm.firstChild as d.RenderNode); }; diff --git a/src/runtime/dom-extras.ts b/src/runtime/dom-extras.ts index 9a4a2da907e..ea840f2fe8f 100644 --- a/src/runtime/dom-extras.ts +++ b/src/runtime/dom-extras.ts @@ -4,7 +4,15 @@ import { HOST_FLAGS } from '@utils/constants'; import type * as d from '../declarations'; import { PLATFORM_FLAGS } from './runtime-constants'; -import { insertBefore, updateFallbackSlotVisibility } from './vdom/vdom-render'; +import { + addSlotRelocateNode, + getHostSlotChildNodes, + getHostSlotNodes, + getSlotName, + getSlottedChildNodes, + updateFallbackSlotVisibility, +} from './slot-polyfill-utils'; +import { insertBefore } from './vdom/vdom-render'; export const patchPseudoShadowDom = (hostElementPrototype: HTMLElement) => { patchCloneNode(hostElementPrototype); @@ -79,13 +87,13 @@ export const patchSlotAppendChild = (HostElementPrototype: any) => { HostElementPrototype.__appendChild = HostElementPrototype.appendChild; HostElementPrototype.appendChild = function (this: d.RenderNode, newChild: d.RenderNode) { const slotName = (newChild['s-sn'] = getSlotName(newChild)); - const slotNode = getHostSlotNode((this as any).__childNodes || this.childNodes, slotName, this.tagName); + const slotNode = getHostSlotNodes((this as any).__childNodes || this.childNodes, this.tagName, slotName)[0]; if (slotNode) { addSlotRelocateNode(newChild, slotNode); const slotChildNodes = getHostSlotChildNodes(slotNode, slotName); const appendAfter = slotChildNodes[slotChildNodes.length - 1]; - const insertedNode = insertBefore(appendAfter.parentNode, newChild, appendAfter.nextSibling); + const insertedNode = insertBefore(appendAfter.parentNode, newChild, appendAfter.nextSibling as d.RenderNode); // Check if there is fallback content that should be hidden updateFallbackSlotVisibility(this); @@ -109,7 +117,7 @@ const patchSlotRemoveChild = (ElementPrototype: any) => { 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 = getHostSlotNode(childNodes, toRemove['s-sn'], this.tagName); + 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 @@ -137,12 +145,12 @@ export const patchSlotPrepend = (HostElementPrototype: HTMLElement) => { } const slotName = (newChild['s-sn'] = getSlotName(newChild)); const childNodes = (this as any).__childNodes || this.childNodes; - const slotNode = getHostSlotNode(childNodes, slotName, this.tagName); + const slotNode = getHostSlotNodes(childNodes, this.tagName, slotName)[0]; if (slotNode) { addSlotRelocateNode(newChild, slotNode, true); const slotChildNodes = getHostSlotChildNodes(slotNode, slotName); const appendAfter = slotChildNodes[0]; - return insertBefore(appendAfter.parentNode, newChild, appendAfter.nextSibling); + return insertBefore(appendAfter.parentNode, newChild, appendAfter.nextSibling as d.RenderNode); } if (newChild.nodeType === 1 && !!newChild.getAttribute('slot')) { @@ -473,113 +481,3 @@ function patchHostOriginalAccessor( } if (accessor) Object.defineProperty(node, '__' + accessorName, accessor); } - -/** - * Creates an empty text node to act as a forwarding address to a slotted node: - * 1) When non-shadow components re-render, they need a place to temporarily put 'lightDOM' elements. - * 2) Patched dom methods and accessors use this node to calculate what 'lightDOM' nodes are in the host. - * - * @param newChild a node that's going to be added to the component - * @param slotNode the slot node that the node will be added to - * @param prepend move the slotted location node to the beginning of the host - * @param position an ordered position to add the ref node which mirrors the lightDom nodes' order. Used during SSR hydration - * (the order of the slot location nodes determines the order of the slotted nodes in our patched accessors) - */ -export const addSlotRelocateNode = ( - newChild: d.RenderNode, - slotNode: d.RenderNode, - prepend?: boolean, - position?: number, -) => { - let slottedNodeLocation: d.RenderNode; - // does newChild already have a slot location node? - if (newChild['s-ol'] && newChild['s-ol'].isConnected) { - slottedNodeLocation = newChild['s-ol']; - } else { - slottedNodeLocation = document.createTextNode('') as any; - slottedNodeLocation['s-nr'] = newChild; - } - - if (!slotNode['s-cr'] || !slotNode['s-cr'].parentNode) return; - - const parent = slotNode['s-cr'].parentNode as any; - const appendMethod = prepend ? parent.__prepend || parent.prepend : parent.__appendChild || parent.appendChild; - - if (typeof position !== 'undefined') { - if (BUILD.hydrateClientSide) { - slottedNodeLocation['s-oo'] = position; - const childNodes = (parent.__childNodes || parent.childNodes) as NodeListOf; - const slotRelocateNodes: d.RenderNode[] = [slottedNodeLocation]; - childNodes.forEach((n) => { - if (n['s-nr']) slotRelocateNodes.push(n); - }); - - slotRelocateNodes.sort((a, b) => { - if (!a['s-oo'] || a['s-oo'] < b['s-oo']) return -1; - else if (!b['s-oo'] || b['s-oo'] < a['s-oo']) return 1; - return 0; - }); - slotRelocateNodes.forEach((n) => appendMethod.call(parent, n)); - } - } else { - appendMethod.call(parent, slottedNodeLocation); - } - - newChild['s-ol'] = slottedNodeLocation; - newChild['s-sh'] = slotNode['s-hn']; -}; - -/** - * Get's the child nodes of a component that are actually slotted. - * This is only required until all patches are unified - * either under 'experimentalSlotFixes' or on by default - * - * @param childNodes all 'internal' child nodes of the component - * @returns An array of slotted reference nodes. - */ -const getSlottedChildNodes = (childNodes: NodeListOf) => { - const result = []; - for (let i = 0; i < childNodes.length; i++) { - const slottedNode = childNodes[i]['s-nr']; - if (slottedNode && slottedNode.isConnected) { - result.push(slottedNode); - } - } - return result; -}; - -const getSlotName = (node: d.RenderNode) => - node['s-sn'] || (node.nodeType === 1 && (node as Element).getAttribute('slot')) || ''; - -/** - * Recursively searches a series of child nodes for a slot with the provided name. - * - * @param childNodes the nodes to search for a slot with a specific name. - * @param slotName the name of the slot to match on. - * @param hostName the host name of the slot to match on. - * @returns a reference to the slot node that matches the provided name, `null` otherwise - */ -const getHostSlotNode = (childNodes: NodeListOf, slotName: string, hostName: string) => { - let i = 0; - let childNode: d.RenderNode; - - for (; i < childNodes.length; i++) { - childNode = childNodes[i] as any; - if (childNode['s-sr'] && childNode['s-sn'] === slotName && childNode['s-hn'] === hostName) { - return childNode; - } - childNode = getHostSlotNode(childNode.childNodes, slotName, hostName); - if (childNode) { - return childNode; - } - } - return null; -}; - -const getHostSlotChildNodes = (n: d.RenderNode, slotName: string) => { - const childNodes: d.RenderNode[] = [n]; - while ((n = n.nextSibling as any) && (n as d.RenderNode)['s-sn'] === slotName) { - childNodes.push(n as any); - } - return childNodes; -}; diff --git a/src/runtime/slot-polyfill-utils.ts b/src/runtime/slot-polyfill-utils.ts new file mode 100644 index 00000000000..16b89da6c6c --- /dev/null +++ b/src/runtime/slot-polyfill-utils.ts @@ -0,0 +1,185 @@ +import { BUILD } from '@app-data'; + +import type * as d from '../declarations'; +import { NODE_TYPE } from './runtime-constants'; + +/** + * Adjust the `.hidden` property as-needed on any nodes in a DOM subtree which + * are slot fallbacks 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: d.RenderNode[] = elm.__childNodes || (elm.childNodes as any); + + // 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 (getHostSlotChildNodes(slotNode, slotNode['s-sn'], false)?.length) { + // has slotted nodes, hide fallback + slotNode.hidden = true; + } else { + // no slotted nodes + slotNode.hidden = false; + } + } + }); + } + for (const childNode of childNodes) { + if (childNode.nodeType === NODE_TYPE.ElementNode && (childNode.__childNodes || childNode.childNodes).length) { + // keep drilling down + updateFallbackSlotVisibility(childNode); + } + } +}; + +/** + * Get's the child nodes of a component that are actually slotted. + * This is only required until all patches are unified + * either under 'experimentalSlotFixes' or on by default + * @param childNodes all 'internal' child nodes of the component + * @returns An array of slotted reference nodes. + */ +export const getSlottedChildNodes = (childNodes: NodeListOf) => { + const result = []; + for (let i = 0; i < childNodes.length; i++) { + const slottedNode = childNodes[i]['s-nr']; + if (slottedNode && slottedNode.isConnected) { + result.push(slottedNode); + } + } + return result; +}; + +/** + * 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 + */ +export const 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'] && childNode['s-hn'] === hostName && (!slotName || childNode['s-sn'] === slotName)) { + slottedNodes.push(childNode); + if (typeof slotName !== 'undefined') return slottedNodes; + } + slottedNodes = [...slottedNodes, ...getHostSlotNodes(childNode.childNodes, hostName, slotName)]; + } + return slottedNodes; +}; + +/** + * Get slotted child nodes of a slot node + * @param node - 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 slotted child nodes of the slot node + */ +export const getHostSlotChildNodes = (node: d.RenderNode, slotName: string, includeSlot = true) => { + const childNodes: d.RenderNode[] = []; + if ((includeSlot && node['s-sr']) || !node['s-sr']) childNodes.push(node as any); + + while ((node = node.nextSibling as any) && (node as d.RenderNode)['s-sn'] === slotName) { + childNodes.push(node as any); + } + return childNodes; +}; + +/** + * Check whether a node is located in a given named slot. + * + * @param nodeToRelocate the node of interest + * @param slotName the slot name to check + * @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) { + 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 === ''; +}; + +/** + * Creates an empty text node to act as a forwarding address to a slotted node: + * 1) When non-shadow components re-render, they need a place to temporarily put 'lightDOM' elements. + * 2) Patched dom methods and accessors use this node to calculate what 'lightDOM' nodes are in the host. + * + * @param newChild a node that's going to be added to the component + * @param slotNode the slot node that the node will be added to + * @param prepend move the slotted location node to the beginning of the host + * @param position an ordered position to add the ref node which mirrors the lightDom nodes' order. Used during SSR hydration + * (the order of the slot location nodes determines the order of the slotted nodes in our patched accessors) + */ +export const addSlotRelocateNode = ( + newChild: d.RenderNode, + slotNode: d.RenderNode, + prepend?: boolean, + position?: number, +) => { + let slottedNodeLocation: d.RenderNode; + // does newChild already have a slot location node? + if (newChild['s-ol'] && newChild['s-ol'].isConnected) { + slottedNodeLocation = newChild['s-ol']; + } else { + slottedNodeLocation = document.createTextNode('') as any; + slottedNodeLocation['s-nr'] = newChild; + } + + if (!slotNode['s-cr'] || !slotNode['s-cr'].parentNode) return; + + const parent = slotNode['s-cr'].parentNode as any; + const appendMethod = prepend ? parent.__prepend || parent.prepend : parent.__appendChild || parent.appendChild; + + if (typeof position !== 'undefined') { + if (BUILD.hydrateClientSide) { + slottedNodeLocation['s-oo'] = position; + const childNodes = (parent.__childNodes || parent.childNodes) as NodeListOf; + const slotRelocateNodes: d.RenderNode[] = [slottedNodeLocation]; + childNodes.forEach((n) => { + if (n['s-nr']) slotRelocateNodes.push(n); + }); + + slotRelocateNodes.sort((a, b) => { + if (!a['s-oo'] || a['s-oo'] < b['s-oo']) return -1; + else if (!b['s-oo'] || b['s-oo'] < a['s-oo']) return 1; + return 0; + }); + slotRelocateNodes.forEach((n) => appendMethod.call(parent, n)); + } + } else { + appendMethod.call(parent, slottedNodeLocation); + } + + newChild['s-ol'] = slottedNodeLocation; + newChild['s-sh'] = slotNode['s-hn']; +}; + +export const getSlotName = (node: d.RenderNode) => + node['s-sn'] || (node.nodeType === 1 && (node as Element).getAttribute('slot')) || ''; diff --git a/src/runtime/styles.ts b/src/runtime/styles.ts index 03bc7dadba2..3b497c9d00f 100644 --- a/src/runtime/styles.ts +++ b/src/runtime/styles.ts @@ -75,7 +75,7 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet // This is only happening on native shadow-dom, do not needs CSS var shim styleElm.innerHTML = style; } else { - styleElm = doc.createElement('style'); + styleElm = document.querySelector(`[${HYDRATED_STYLE_ID}="${scopeId}"]`) || doc.createElement('style'); styleElm.innerHTML = style; // Apply CSP nonce to the style tag if it exists @@ -197,10 +197,6 @@ export const attachStyles = (hostRef: d.HostRef) => { // DOM WRITE!! elm['s-sc'] = scopeId; elm.classList.add(scopeId + '-h'); - - if (BUILD.scoped && flags & CMP_FLAGS.scopedCssEncapsulation) { - elm.classList.add(scopeId + '-s'); - } } endAttachStyles(); }; diff --git a/src/runtime/test/hydrate-no-encapsulation.spec.tsx b/src/runtime/test/hydrate-no-encapsulation.spec.tsx index c1e24987423..6f8b3033733 100644 --- a/src/runtime/test/hydrate-no-encapsulation.spec.tsx +++ b/src/runtime/test/hydrate-no-encapsulation.spec.tsx @@ -211,6 +211,7 @@ describe('hydrate no encapsulation', () => { + light-dom
@@ -270,6 +271,7 @@ describe('hydrate no encapsulation', () => { + light-dom
@@ -331,6 +333,7 @@ describe('hydrate no encapsulation', () => {
+ light-dom
@@ -392,6 +395,7 @@ describe('hydrate no encapsulation', () => {
+ light-dom
@@ -476,6 +480,7 @@ describe('hydrate no encapsulation', () => {
top light-dom
+ middle light-dom
diff --git a/src/runtime/test/hydrate-scoped.spec.tsx b/src/runtime/test/hydrate-scoped.spec.tsx index f8d23ed42fe..d7b0150cdd0 100644 --- a/src/runtime/test/hydrate-scoped.spec.tsx +++ b/src/runtime/test/hydrate-scoped.spec.tsx @@ -45,6 +45,7 @@ describe('hydrate scoped', () => {
+ 88mph
@@ -93,9 +94,10 @@ describe('hydrate scoped', () => { expect(clientHydrated.root['s-cr']['s-cn']).toBe(true); expect(clientHydrated.root).toEqualHtml(` - +
+ 88mph
@@ -139,12 +141,61 @@ describe('hydrate scoped', () => { expect(clientHydrated.root['s-cr']['s-cn']).toBe(true); expect(clientHydrated.root).toEqualHtml(` - + -

+

Hello

`); }); + + it('adds a scoped-slot class to the slot parent element', async () => { + @Component({ tag: 'cmp-a', scoped: true }) + class CmpA { + render() { + return ( + +
+

+ +

+
+
+ ); + } + } + // @ts-ignore + const serverHydrated = await newSpecPage({ + components: [CmpA], + html: ``, + hydrateServerSide: true, + }); + expect(serverHydrated.root).toEqualHtml(` + + +
+

+ +

+
+
`); + + const clientHydrated = await newSpecPage({ + components: [CmpA], + html: serverHydrated.root.outerHTML, + hydrateClientSide: true, + }); + expect(clientHydrated.root.querySelector('p').className).toBe('hi sc-cmp-a-s sc-cmp-a'); + + expect(clientHydrated.root).toEqualHtml(` + + +
+

+ +

+
+
`); + }); }); diff --git a/src/runtime/test/hydrate-shadow-child.spec.tsx b/src/runtime/test/hydrate-shadow-child.spec.tsx index b779f8db511..e25c8db5197 100644 --- a/src/runtime/test/hydrate-shadow-child.spec.tsx +++ b/src/runtime/test/hydrate-shadow-child.spec.tsx @@ -502,6 +502,7 @@ describe('hydrate, shadow child', () => { expect(clientHydrated.root).toEqualHtml(` +
diff --git a/src/runtime/test/hydrate-shadow-parent.spec.tsx b/src/runtime/test/hydrate-shadow-parent.spec.tsx index 9c059237ebb..bf32114c011 100644 --- a/src/runtime/test/hydrate-shadow-parent.spec.tsx +++ b/src/runtime/test/hydrate-shadow-parent.spec.tsx @@ -270,6 +270,7 @@ describe('hydrate, shadow parent', () => { + cmp-a-light-dom @@ -498,6 +499,7 @@ describe('hydrate, shadow parent', () => { + cmp-a-light-dom @@ -507,6 +509,7 @@ describe('hydrate, shadow parent', () => { + cmp-a-light-dom diff --git a/src/runtime/test/hydrate-slot-fallback.spec.tsx b/src/runtime/test/hydrate-slot-fallback.spec.tsx index 7a3afd8b68a..1c079fa50a8 100644 --- a/src/runtime/test/hydrate-slot-fallback.spec.tsx +++ b/src/runtime/test/hydrate-slot-fallback.spec.tsx @@ -48,7 +48,7 @@ describe('hydrate, slot fallback', () => { }); expect(clientHydrated.root).toEqualHtml(` - +
@@ -164,6 +164,10 @@ describe('hydrate, slot fallback', () => {
+
@@ -177,10 +181,6 @@ describe('hydrate, slot fallback', () => {

-

Non slot based content @@ -196,13 +196,16 @@ describe('hydrate, slot fallback', () => { }); expect(clientHydrated.root.outerHTML).toEqualHtml(` - +

- + +
- + Fallback content child - should not be hidden

@@ -210,9 +213,6 @@ describe('hydrate, slot fallback', () => {

-

Non slot based content

@@ -262,6 +262,10 @@ describe('hydrate, slot fallback', () => {
+
@@ -275,10 +279,6 @@ describe('hydrate, slot fallback', () => {

-

Non slot based content @@ -374,6 +374,14 @@ describe('hydrate, slot fallback', () => {

+ +

slotted item 1

@@ -383,14 +391,6 @@ describe('hydrate, slot fallback', () => {

slotted item 3

- -
@@ -404,7 +404,7 @@ describe('hydrate, slot fallback', () => { }); expect(clientHydrated.root).toEqualHtml(` - +
@@ -432,4 +432,91 @@ describe('hydrate, slot fallback', () => { `); }); + + it('does not hide slot fallback text when a `scoped: true` component forwards the slot to nested `shadow: true`', async () => { + @Component({ + tag: 'cmp-a', + scoped: true, + }) + class CmpA { + render() { + return ( +
+ + Fallback content parent - should not be hidden + +
+ ); + } + } + + @Component({ + tag: 'cmp-b', + shadow: true, + }) + class CmpB { + render() { + return ( +
+ Fallback content child - should be hidden +
+ ); + } + } + + const serverHydrated = await newSpecPage({ + components: [CmpA, CmpB], + html: ` + + `, + hydrateServerSide: true, + }); + expect(serverHydrated.root).toEqualHtml(` + + +
+ + + +
+ + + + Fallback content parent - should not be hidden + +
+
+
+
+ `); + + const clientHydrated = await newSpecPage({ + components: [CmpA, CmpB], + html: serverHydrated.root.outerHTML, + hydrateClientSide: true, + }); + + expect(clientHydrated.root).toEqualHtml(` + + +
+ + +
+ + Fallback content child - should be hidden + +
+
+ + Fallback content parent - should not be hidden + +
+
+
+ `); + }); }); diff --git a/src/runtime/test/hydrate-slotted-content-order.spec.tsx b/src/runtime/test/hydrate-slotted-content-order.spec.tsx index 77dfa36a39f..409e5f5e1ae 100644 --- a/src/runtime/test/hydrate-slotted-content-order.spec.tsx +++ b/src/runtime/test/hydrate-slotted-content-order.spec.tsx @@ -340,9 +340,10 @@ describe("hydrated components' slotted node order", () => { patchPseudoShadowDom(clientHydrated.root); expect(clientHydrated.root.outerHTML).toEqualHtml(` - +
+

slotted item 1

diff --git a/src/runtime/test/render-vdom.spec.tsx b/src/runtime/test/render-vdom.spec.tsx index 8a6c1f68bac..a72d34af264 100644 --- a/src/runtime/test/render-vdom.spec.tsx +++ b/src/runtime/test/render-vdom.spec.tsx @@ -855,7 +855,7 @@ describe('render-vdom', () => { includeAnnotations: true, }); expect(root).toEqualHtml(` - + `); @@ -865,7 +865,7 @@ describe('render-vdom', () => { await waitForChanges(); expect(root).toEqualHtml(` - + `); diff --git a/src/runtime/test/scoped.spec.tsx b/src/runtime/test/scoped.spec.tsx index 531b66f48de..d619d3c7703 100644 --- a/src/runtime/test/scoped.spec.tsx +++ b/src/runtime/test/scoped.spec.tsx @@ -1,4 +1,4 @@ -import { Component, h } from '@stencil/core'; +import { Component, h, Prop } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; describe('scoped', () => { @@ -39,11 +39,11 @@ describe('scoped', () => { }); expect(page.root).toEqualHtml(` - - + + -
- +
+ Hola
@@ -51,4 +51,56 @@ describe('scoped', () => { `); }); + + it('should remove the scoped slot class when the slot is removed', async () => { + @Component({ + tag: 'cmp-b', + styles: ':host { color: inherit }', + scoped: true, + }) + class CmpB { + @Prop() slot = true; + + render() { + return ( +
+ {this.slot ? ( +
+ +
+ ) : ( +
+ )} +
+ ); + } + } + const page = await newSpecPage({ + components: [CmpB], + includeAnnotations: true, + html: `hello`, + }); + + expect(page.root).toEqualHtml(` + + +
+
hello
+
+
+ `); + + page.root.slot = false; + await page.waitForChanges(); + await page.waitForChanges(); + + expect(page.root).toEqualHtml(` + + +
+
+
+
+ `); + }); }); diff --git a/src/runtime/vdom/set-accessor.ts b/src/runtime/vdom/set-accessor.ts index bcfa304f334..3b6bd79d97a 100644 --- a/src/runtime/vdom/set-accessor.ts +++ b/src/runtime/vdom/set-accessor.ts @@ -44,14 +44,23 @@ export const setAccessor = ( if (BUILD.vdomClass && memberName === 'class') { const classList = elm.classList; const oldClasses = parseClassList(oldValue); - const newClasses = parseClassList(newValue); - // for `scoped: true` components, new nodes after initial hydration - // from SSR don't have the slotted class added. Let's add that now - if (elm['s-si'] && newClasses.indexOf(elm['s-si']) < 0) { + let newClasses = parseClassList(newValue); + + if (elm['s-si']) { + // for `scoped: true` components, new nodes after initial hydration + // from SSR don't have the slotted class added. Let's add that now newClasses.push(elm['s-si']); + oldClasses.forEach((c) => { + if (c.startsWith(elm['s-si'])) newClasses.push(c); + }); + newClasses = [...new Set(newClasses)]; + + classList.add(...newClasses); + delete elm['s-si']; + } else { + classList.remove(...oldClasses.filter((c) => c && !newClasses.includes(c))); + classList.add(...newClasses.filter((c) => c && !oldClasses.includes(c))); } - classList.remove(...oldClasses.filter((c) => c && !newClasses.includes(c))); - classList.add(...newClasses.filter((c) => c && !oldClasses.includes(c))); } else if (BUILD.vdomStyle && memberName === 'style') { // update style attribute, css properties and values if (BUILD.updatable) { diff --git a/src/runtime/vdom/test/scoped-slot.spec.tsx b/src/runtime/vdom/test/scoped-slot.spec.tsx index 9b14332d597..a0eb453b78f 100644 --- a/src/runtime/vdom/test/scoped-slot.spec.tsx +++ b/src/runtime/vdom/test/scoped-slot.spec.tsx @@ -796,8 +796,8 @@ describe('scoped slot', () => { html: `Content`, }); - expect(root.firstElementChild.children[1].nodeName).toBe('SLOT-FB'); - expect(root.firstElementChild.children[1]).toHaveAttribute('hidden'); + expect(root.firstElementChild.children[0].nodeName).toBe('SLOT-FB'); + expect(root.firstElementChild.children[0]).toHaveAttribute('hidden'); }); it("should hide the slot's fallback content for a non-shadow component when slot content passed in", async () => { @@ -818,7 +818,7 @@ describe('scoped slot', () => { html: `Content`, }); - expect(root.firstElementChild.children[1].nodeName).toBe('SLOT-FB'); - expect(root.firstElementChild.children[1]).toHaveAttribute('hidden'); + expect(root.firstElementChild.children[0].nodeName).toBe('SLOT-FB'); + expect(root.firstElementChild.children[0]).toHaveAttribute('hidden'); }); }); diff --git a/src/runtime/vdom/test/vdom-render.spec.tsx b/src/runtime/vdom/test/vdom-render.spec.tsx index c4d5c06c904..a783511c4c7 100644 --- a/src/runtime/vdom/test/vdom-render.spec.tsx +++ b/src/runtime/vdom/test/vdom-render.spec.tsx @@ -12,28 +12,33 @@ describe('isSameVnode', () => { $key$: '1', $elm$: { nodeType: 9 }, }; + const vnode3: any = { + $tag$: 'slot', + $key$: '1', + $name$: 'my-slot', + $elm$: { nodeType: 9 }, + }; + const vnode4: any = { + $tag$: 'slot', + $name$: 'my-slot', + $elm$: { nodeType: 9 }, + }; expect(isSameVnode(vnode1, vnode2)).toBe(true); + expect(isSameVnode(vnode3, vnode4)).toBe(true); }); - it('should return false in case of hyration', () => { + it('should add key to old node (e.g. via hydration) on init', () => { const vnode1: any = { - $tag$: 'slot', - $key$: '1', + $tag$: 'div', $elm$: { nodeType: 9 }, - $nodeId$: 1, }; const vnode2: any = { - $tag$: 'slot', + $tag$: 'div', $key$: '1', $elm$: { nodeType: 9 }, }; - const vnode3: any = { - $tag$: 'slot', - $key$: '1', - $elm$: { nodeType: 8 }, - $nodeId$: 2, - }; - expect(isSameVnode(vnode1, vnode2, true)).toBe(false); - expect(isSameVnode(vnode3, vnode2, true)).toBe(true); + expect(isSameVnode(vnode1, vnode2)).toBe(false); + expect(isSameVnode(vnode1, vnode2, true)).toBe(true); + expect(vnode1.$key$).toBe('1'); }); }); diff --git a/src/runtime/vdom/vdom-annotations.ts b/src/runtime/vdom/vdom-annotations.ts index 4f69cf9bae8..34d3ec3b45f 100644 --- a/src/runtime/vdom/vdom-annotations.ts +++ b/src/runtime/vdom/vdom-annotations.ts @@ -66,7 +66,7 @@ export const insertVdomAnnotations = (doc: Document, staticComponents: string[]) } const commentBeforeTextNode = doc.createComment(childId); commentBeforeTextNode.nodeValue = `${TEXT_NODE_ID}.${childId}`; - insertBefore(nodeRef.parentNode, commentBeforeTextNode, nodeRef); + insertBefore(nodeRef.parentNode, commentBeforeTextNode as any, nodeRef); } else if (nodeRef.nodeType === NODE_TYPE.CommentNode) { const commentBeforeTextNode = doc.createComment(childId); commentBeforeTextNode.nodeValue = `${COMMENT_NODE_ID}.${childId}`; @@ -240,7 +240,7 @@ const insertChildVNodeAnnotations = ( const textNodeId = `${TEXT_NODE_ID}.${childId}`; const commentBeforeTextNode = doc.createComment(textNodeId); - insertBefore(parentNode, commentBeforeTextNode, childElm); + insertBefore(parentNode, commentBeforeTextNode as any, childElm); } } else if (childElm.nodeType === NODE_TYPE.CommentNode) { if (childElm['s-sr']) { diff --git a/src/runtime/vdom/vdom-render.ts b/src/runtime/vdom/vdom-render.ts index 5f637ec5ab2..81b508b38bd 100644 --- a/src/runtime/vdom/vdom-render.ts +++ b/src/runtime/vdom/vdom-render.ts @@ -12,6 +12,7 @@ import { CMP_FLAGS, HTML_NS, isDef, SVG_NS } from '@utils'; import type * as d from '../../declarations'; import { NODE_TYPE, PLATFORM_FLAGS, VNODE_FLAGS } from '../runtime-constants'; +import { isNodeLocatedInSlot, updateFallbackSlotVisibility } from '../slot-polyfill-utils'; import { h, isHost, newVNode } from './h'; import { updateElement } from './update-element'; @@ -30,10 +31,9 @@ let isSvgMode = false; * @param newParentVNode the parent VNode from the current render * @param childIndex the index of the VNode, in the _new_ parent node's * children, for which we will create a new DOM node - * @param parentElm the parent DOM node which our new node will be a child of * @returns the newly created node */ -const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: number, parentElm: d.RenderNode) => { +const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: number) => { // tslint:disable-next-line: prefer-const const newVNode = newParentVNode.$children$[childIndex]; let i = 0; @@ -46,11 +46,6 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: checkSlotRelocate = true; if (newVNode.$tag$ === 'slot') { - if (scopeId) { - // scoped css needs to add its scoped id to the parent element - parentElm.classList.add(scopeId + '-s'); - } - newVNode.$flags$ |= newVNode.$children$ ? // slot element has fallback content // still create an element that "mocks" the slot element @@ -105,26 +100,15 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: updateElement(null, newVNode, isSvgMode); } - /** - * walk up the DOM tree and check if we are in a shadow root because if we are within - * a shadow root DOM we don't need to attach scoped class names to the element - */ - const rootNode = elm.getRootNode() as HTMLElement; - const isElementWithinShadowRoot = !rootNode.querySelector('body'); - if (!isElementWithinShadowRoot && BUILD.scoped && isDef(scopeId) && elm['s-si'] !== scopeId) { - // if there is a scopeId and this is the initial render - // then let's add the scopeId as a css class + if (BUILD.scoped && isDef(scopeId) && elm['s-si'] !== scopeId) { + // if this element is `scoped: true` all internal + // children required the scope id class for styling elm.classList.add((elm['s-si'] = scopeId)); } - - if (BUILD.scoped) { - updateElementScopeIds(elm as d.RenderNode, parentElm as d.RenderNode); - } - if (newVNode.$children$) { for (i = 0; i < newVNode.$children$.length; ++i) { // create the node - childNode = createElm(oldParentVNode, newVNode, i, elm); + childNode = createElm(oldParentVNode, newVNode, i); // return node could have been null if (childNode) { @@ -175,6 +159,9 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: putBackInOriginalLocation(oldParentVNode.$elm$, false); } } + if (BUILD.scoped) { + addRemoveSlotScopedClass(contentRef, elm, newParentVNode.$elm$, oldParentVNode?.$elm$); + } } } @@ -221,6 +208,12 @@ const relocateToHostRoot = (parentElm: Element) => { plt.$flags$ &= ~PLATFORM_FLAGS.isTmpDisconnected; }; +/** + * Puts `` nodes and any slotted nodes back to their original location (wherever they were before being slotted). + * + * @param parentElm - The parent element of the nodes to relocate. + * @param recursive - Whether or not to relocate nodes in child nodes as well. + */ const putBackInOriginalLocation = (parentElm: d.RenderNode, recursive: boolean) => { plt.$flags$ |= PLATFORM_FLAGS.isTmpDisconnected; const oldSlotChildNodes: ChildNode[] = Array.from(parentElm.__childNodes || parentElm.childNodes); @@ -238,7 +231,7 @@ const putBackInOriginalLocation = (parentElm: d.RenderNode, recursive: boolean) const childNode = oldSlotChildNodes[i] as any; if (childNode['s-hn'] !== hostTagName && childNode['s-ol']) { // and relocate it back to it's original location - insertBefore(parentReferenceNode(childNode), childNode, referenceNode(childNode)); + insertBefore(referenceNode(childNode).parentNode, childNode, referenceNode(childNode)); // remove the old original location comment entirely // later on the patch function will know what to do @@ -291,10 +284,10 @@ const addVnodes = ( for (; startIdx <= endIdx; ++startIdx) { if (vnodes[startIdx]) { - childNode = createElm(null, parentVNode, startIdx, parentElm); + childNode = createElm(null, parentVNode, startIdx); if (childNode) { vnodes[startIdx].$elm$ = childNode as any; - insertBefore(containerElm, childNode, BUILD.slotRelocation ? referenceNode(before) : before); + insertBefore(containerElm, childNode as d.RenderNode, BUILD.slotRelocation ? referenceNode(before) : before); } } } @@ -548,7 +541,7 @@ const updateChildren = ( if (elmToMove.$tag$ !== newStartVnode.$tag$) { // the tag doesn't match so we'll need a new DOM element - node = createElm(oldCh && oldCh[newStartIdx], newVNode, idxInOld, parentElm); + node = createElm(oldCh && oldCh[newStartIdx], newVNode, idxInOld); } else { patch(elmToMove, newStartVnode, isInitialRender); // invalidate the matching old node so that we won't try to update it @@ -563,16 +556,20 @@ const updateChildren = ( // the key of the first new child OR the build is not using `key` // attributes at all. In either case we need to create a new element // for the new node. - node = createElm(oldCh && oldCh[newStartIdx], newVNode, newStartIdx, parentElm); + node = createElm(oldCh && oldCh[newStartIdx], newVNode, newStartIdx); newStartVnode = newCh[++newStartIdx]; } if (node) { // if we created a new node then handle inserting it to the DOM if (BUILD.slotRelocation) { - insertBefore(parentReferenceNode(oldStartVnode.$elm$), node, referenceNode(oldStartVnode.$elm$)); + insertBefore( + referenceNode(oldStartVnode.$elm$).parentNode, + node as d.RenderNode, + referenceNode(oldStartVnode.$elm$), + ); } else { - insertBefore(oldStartVnode.$elm$.parentNode, node, oldStartVnode.$elm$); + insertBefore(oldStartVnode.$elm$.parentNode, node as d.RenderNode, oldStartVnode.$elm$); } } } @@ -620,19 +617,6 @@ export const isSameVnode = (leftVNode: d.VNode, rightVNode: d.VNode, isInitialRe // need to have the same element tag, and same key to be the same if (leftVNode.$tag$ === rightVNode.$tag$) { if (BUILD.slotRelocation && leftVNode.$tag$ === 'slot') { - // We are not considering the same node if: - if ( - // The component gets hydrated and no VDOM has been initialized. - // Here the comparison can't happen as $name$ property is not set for `leftNode`. - '$nodeId$' in leftVNode && - isInitialRender && - // `leftNode` is not from type HTMLComment which would cause many - // hydration comments to be removed - leftVNode.$elm$.nodeType !== 8 - ) { - return false; - } - return leftVNode.$name$ === rightVNode.$name$; } // this will be set if JSX tags in the build have `key` attrs set on them @@ -643,20 +627,27 @@ export const isSameVnode = (leftVNode: d.VNode, rightVNode: d.VNode, isInitialRe if (BUILD.vdomKey && !isInitialRender) { return leftVNode.$key$ === rightVNode.$key$; } + // if we're comparing the same node and it's the initial render, + // let's set the $key$ property to the rightVNode so we don't cause re-renders + if (isInitialRender && !leftVNode.$key$ && rightVNode.$key$) { + leftVNode.$key$ = rightVNode.$key$; + } return true; } return false; }; -const referenceNode = (node: d.RenderNode) => { - // this node was relocated to a new location in the dom - // because of some other component's slot - // but we still have an html comment in place of where - // it's original location was according to it's original vdom - return (node && node['s-ol']) || node; -}; - -const parentReferenceNode = (node: d.RenderNode) => (node['s-ol'] ? node['s-ol'] : node).parentNode; +/** + * Returns the reference node (a comment which represents the + * original location of a node in the vdom - before it was moved to its slot) + * of a given node. + * + * (slot nodes can be relocated to a new location in the dom because of + * some other component's slot) + * @param node the node to find the original location reference node for + * @returns reference node + */ +const referenceNode = (node: d.RenderNode) => (node && node['s-ol']) || node; /** * Handle reconciling an outdated VNode with a new one which corresponds to @@ -731,72 +722,6 @@ export const patch = (oldVNode: d.VNode, newVNode: d.VNode, isInitialRender = fa } }; -/** - * Adjust the `.hidden` property as-needed on any nodes in a DOM subtree which - * are slot fallbacks 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: d.RenderNode[] = elm.__childNodes || (elm.childNodes as any); - - for (const childNode of childNodes) { - if (childNode.nodeType === NODE_TYPE.ElementNode) { - if (childNode['s-sr']) { - // this is a slot fallback node - - // get the slot name for this slot reference node - const slotName = childNode['s-sn']; - - // by default always show a fallback slot node - // then hide it if there are other slots in the light dom - childNode.hidden = false; - - // we need to check all of its sibling nodes in order to see if - // `childNode` should be hidden - for (const siblingNode of childNodes) { - // Don't check the node against itself - if (siblingNode !== childNode) { - if (siblingNode['s-hn'] !== childNode['s-hn'] || slotName !== '') { - // this sibling node is from a different component OR is a named - // fallback slot node - if ( - (siblingNode.nodeType === NODE_TYPE.ElementNode && - (slotName === siblingNode.getAttribute('slot') || slotName === siblingNode['s-sn'])) || - (siblingNode.nodeType === NODE_TYPE.TextNode && slotName === siblingNode['s-sn']) - ) { - childNode.hidden = true; - break; - } - } else if (slotName === siblingNode['s-sn']) { - // this is a default fallback slot node - // any element or text node (with content) - // should hide the default fallback slot node - if ( - siblingNode.nodeType === NODE_TYPE.ElementNode || - (siblingNode.nodeType === NODE_TYPE.TextNode && siblingNode.textContent.trim() !== '') - ) { - childNode.hidden = true; - break; - } - } - } - } - } - - // keep drilling down - updateFallbackSlotVisibility(childNode); - } - } -}; - /** * Component-global information about nodes which are either currently being * relocated or will be shortly. @@ -905,31 +830,6 @@ const markSlotContentForRelocation = (elm: d.RenderNode) => { } }; -/** - * Check whether a node is located in a given named slot. - * - * @param nodeToRelocate the node of interest - * @param slotName the slot name to check - * @returns whether the node is located in the slot or not - */ -const isNodeLocatedInSlot = (nodeToRelocate: d.RenderNode, slotName: string): boolean => { - if (nodeToRelocate.nodeType === NODE_TYPE.ElementNode) { - 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 === ''; -}; - /** * 'Nullify' any VDom `ref` callbacks on a VDom node or its children by calling * them with `null`. This signals that the DOM element corresponding to the VDom @@ -953,58 +853,66 @@ export const nullifyVNodeRefs = (vNode: d.VNode) => { * @param reference anchor element * @returns inserted node */ -export const insertBefore = (parent: Node, newNode: Node, reference?: Node): Node => { - const inserted = parent?.insertBefore(newNode, reference); - - if (BUILD.scoped) { - updateElementScopeIds(newNode as d.RenderNode, parent as d.RenderNode); +export const insertBefore = (parent: Node, newNode: d.RenderNode, reference?: d.RenderNode): Node => { + if (BUILD.scoped && typeof newNode['s-sn'] === 'string' && !!newNode['s-sr'] && !!newNode['s-cr']) { + addRemoveSlotScopedClass(newNode['s-cr'], newNode, parent as d.RenderNode, newNode.parentElement); } - + const inserted = parent?.insertBefore(newNode, reference); return inserted; }; -const findScopeIds = (element: d.RenderNode): string[] => { - const scopeIds: string[] = []; - if (element) { - scopeIds.push( - ...(element['s-scs'] || []), - element['s-si'], - element['s-sc'], - ...findScopeIds(element.parentElement), - ); - } - return scopeIds; -}; - /** - * To be able to style the deep nested scoped component from the parent components, - * all the scope ids of its parents need to be added to the child node since sass compiler - * adds scope id to the nested selectors during compilation phase + * 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. * - * @param element an element to be updated - * @param parent a parent element that scope id is retrieved - * @param iterateChildNodes iterate child nodes + * @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 */ -const updateElementScopeIds = (element: d.RenderNode, parent: d.RenderNode, iterateChildNodes = false) => { - if (element && parent && element.nodeType === NODE_TYPE.ElementNode) { - const scopeIds = new Set(findScopeIds(parent).filter(Boolean)); - if (scopeIds.size) { - element.classList?.add(...(element['s-scs'] = Array.from(scopeIds))); - - if (element['s-ol'] || iterateChildNodes) { - /** - * If the element has an original location, this means element is relocated. - * So, we need to notify the child nodes to update their new scope ids since - * the DOM structure is changed. - */ - for (const childNode of Array.from(element.__childNodes || element.childNodes)) { - updateElementScopeIds(childNode as d.RenderNode, element, true); +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 + let scopeId: string; + if ( + reference && + typeof slotNode['s-sn'] === 'string' && + !!slotNode['s-sr'] && + reference.parentNode && + (reference.parentNode as d.RenderNode)['s-sc'] && + (scopeId = 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(scopeId + '-s'); + + if (oldParent && oldParent.classList.contains(scopeId + '-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(scopeId + '-s'); } } -}; - +} /** * Information about nodes to be relocated in order to support * `` elements in scoped (i.e. non-shadow DOM) components @@ -1152,8 +1060,8 @@ render() { // // TODO(STENCIL-914): Remove `experimentalSlotFixes` check if ( - !BUILD.experimentalSlotFixes || - (insertBeforeNode && insertBeforeNode.nodeType === NODE_TYPE.ElementNode) + !BUILD.hydrateServerSide && + (!BUILD.experimentalSlotFixes || (insertBeforeNode && insertBeforeNode.nodeType === NODE_TYPE.ElementNode)) ) { let orgLocationNode = nodeToRelocate['s-ol']?.previousSibling as d.RenderNode | null; while (orgLocationNode) { @@ -1202,7 +1110,7 @@ render() { // 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) { + if (nodeToRelocate.nodeType === NODE_TYPE.ElementNode && nodeToRelocate.tagName !== 'SLOT-FB') { nodeToRelocate.hidden = nodeToRelocate['s-ih'] ?? false; } } diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 233da9b0b72..e4cac488a14 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,4 +1,4 @@ -export const isDef = (v: any) => v != null; +export const isDef = (v: any) => v != null && v !== undefined; /** * Convert a string from PascalCase to dash-case diff --git a/test/end-to-end/src/components.d.ts b/test/end-to-end/src/components.d.ts index 800def9c64e..3e3a594daea 100644 --- a/test/end-to-end/src/components.d.ts +++ b/test/end-to-end/src/components.d.ts @@ -112,6 +112,8 @@ export namespace Components { } interface NonShadowForwardedSlot { } + interface NonShadowMultiSlots { + } interface NonShadowWrapper { } interface PathAliasCmp { @@ -393,6 +395,12 @@ declare global { prototype: HTMLNonShadowForwardedSlotElement; new (): HTMLNonShadowForwardedSlotElement; }; + interface HTMLNonShadowMultiSlotsElement extends Components.NonShadowMultiSlots, HTMLStencilElement { + } + var HTMLNonShadowMultiSlotsElement: { + prototype: HTMLNonShadowMultiSlotsElement; + new (): HTMLNonShadowMultiSlotsElement; + }; interface HTMLNonShadowWrapperElement extends Components.NonShadowWrapper, HTMLStencilElement { } var HTMLNonShadowWrapperElement: { @@ -510,6 +518,7 @@ declare global { "nested-scope-cmp": HTMLNestedScopeCmpElement; "non-shadow-child": HTMLNonShadowChildElement; "non-shadow-forwarded-slot": HTMLNonShadowForwardedSlotElement; + "non-shadow-multi-slots": HTMLNonShadowMultiSlotsElement; "non-shadow-wrapper": HTMLNonShadowWrapperElement; "path-alias-cmp": HTMLPathAliasCmpElement; "prerender-cmp": HTMLPrerenderCmpElement; @@ -605,6 +614,8 @@ declare namespace LocalJSX { } interface NonShadowForwardedSlot { } + interface NonShadowMultiSlots { + } interface NonShadowWrapper { } interface PathAliasCmp { @@ -679,6 +690,7 @@ declare namespace LocalJSX { "nested-scope-cmp": NestedScopeCmp; "non-shadow-child": NonShadowChild; "non-shadow-forwarded-slot": NonShadowForwardedSlot; + "non-shadow-multi-slots": NonShadowMultiSlots; "non-shadow-wrapper": NonShadowWrapper; "path-alias-cmp": PathAliasCmp; "prerender-cmp": PrerenderCmp; @@ -733,6 +745,7 @@ declare module "@stencil/core" { "nested-scope-cmp": LocalJSX.NestedScopeCmp & JSXBase.HTMLAttributes; "non-shadow-child": LocalJSX.NonShadowChild & JSXBase.HTMLAttributes; "non-shadow-forwarded-slot": LocalJSX.NonShadowForwardedSlot & JSXBase.HTMLAttributes; + "non-shadow-multi-slots": LocalJSX.NonShadowMultiSlots & JSXBase.HTMLAttributes; "non-shadow-wrapper": LocalJSX.NonShadowWrapper & JSXBase.HTMLAttributes; "path-alias-cmp": LocalJSX.PathAliasCmp & JSXBase.HTMLAttributes; "prerender-cmp": LocalJSX.PrerenderCmp & JSXBase.HTMLAttributes; diff --git a/test/end-to-end/src/declarative-shadow-dom/test.e2e.ts b/test/end-to-end/src/declarative-shadow-dom/test.e2e.ts index 70e02dc4733..8db88ae0266 100644 --- a/test/end-to-end/src/declarative-shadow-dom/test.e2e.ts +++ b/test/end-to-end/src/declarative-shadow-dom/test.e2e.ts @@ -350,12 +350,12 @@ describe('renderToString', () => { .sc-nested-scope-cmp-h{color:green}:host{display:inline-block}
- +
- +
diff --git a/test/end-to-end/src/scoped-hydration/non-shadow-multi-slots.tsx b/test/end-to-end/src/scoped-hydration/non-shadow-multi-slots.tsx new file mode 100644 index 00000000000..1ff791879b5 --- /dev/null +++ b/test/end-to-end/src/scoped-hydration/non-shadow-multi-slots.tsx @@ -0,0 +1,18 @@ +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'non-shadow-multi-slots', + scoped: true, +}) +export class NonShadowMultiSlots { + render() { + return [ +
Internal: BEFORE DEFAULT SLOT
, + , +
Internal: AFTER DEFAULT SLOT
, +
Internal: BEFORE SECOND SLOT
, + Second slot fallback text, +
Internal: AFTER SECOND SLOT
, + ]; + } +} diff --git a/test/end-to-end/src/scoped-hydration/scoped-hydration.e2e.ts b/test/end-to-end/src/scoped-hydration/scoped-hydration.e2e.ts index 5a830656a67..5f26e7b5f6e 100644 --- a/test/end-to-end/src/scoped-hydration/scoped-hydration.e2e.ts +++ b/test/end-to-end/src/scoped-hydration/scoped-hydration.e2e.ts @@ -19,6 +19,35 @@ describe('`scoped: true` hydration checks', () => { renderToString = mod.renderToString; }); + it('does not add multiple style tags', async () => { + const { html } = await renderToString( + ` + + `, + ); + const page = await newE2EPage({ html, url: 'https://stencil.com' }); + const styles = await page.findAll('style'); + expect(styles.length).toBe(3); + expect(styles[0].textContent).toContain(`.sc-non-shadow-child-h`); + expect(styles[1].textContent).not.toContain(`.sc-non-shadow-child-h`); + expect(styles[2].textContent).not.toContain(`.sc-non-shadow-child-h`); + }); + + it('maintains order of multiple slots', async () => { + const { html } = await renderToString( + ` + +

Default slot element

+

Second slot element

+
+ `, + ); + const page = await newE2EPage({ html, url: 'https://stencil.com' }); + const { internal } = await getElementOrder(page, 'non-shadow-multi-slots'); + expect(internal.length).toBe(7); + expect(internal).toEqual(['DIV', 'P', 'DIV', 'DIV', 'SLOT-FB', 'P', 'DIV']); + }); + it('shows fallback slot when no content is slotted', async () => { const { html } = await renderToString( ` diff --git a/test/wdio/package-lock.json b/test/wdio/package-lock.json index 0497280932b..2aedb85100f 100644 --- a/test/wdio/package-lock.json +++ b/test/wdio/package-lock.json @@ -25,7 +25,7 @@ }, "../..": { "name": "@stencil/core", - "version": "4.18.1", + "version": "4.23.0", "dev": true, "license": "MIT", "bin": { @@ -36,8 +36,8 @@ "@rollup/plugin-commonjs": "21.1.0", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-node-resolve": "9.0.0", - "@rollup/plugin-replace": "5.0.5", - "@rollup/pluginutils": "5.1.0", + "@rollup/plugin-replace": "5.0.7", + "@rollup/pluginutils": "5.1.3", "@types/eslint": "^8.4.6", "@types/exit": "^0.1.31", "@types/fs-extra": "^11.0.0", @@ -56,7 +56,7 @@ "@yarnpkg/lockfile": "^1.1.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.19", - "conventional-changelog-cli": "^4.0.0", + "conventional-changelog-cli": "^5.0.0", "cspell": "^8.0.0", "dts-bundle-generator": "~9.5.0", "esbuild": "^0.21.0", @@ -64,13 +64,13 @@ "eslint": "^8.23.1", "eslint-config-prettier": "^9.0.0", "eslint-plugin-jest": "^28.0.0", - "eslint-plugin-jsdoc": "^48.0.0", + "eslint-plugin-jsdoc": "^50.0.0", "eslint-plugin-simple-import-sort": "^12.0.0", "eslint-plugin-wdio": "^8.24.12", - "execa": "8.0.1", + "execa": "9.3.0", "exit": "^0.1.2", "fs-extra": "^11.0.0", - "glob": "10.3.15", + "glob": "10.4.1", "graceful-fs": "~4.2.6", "jest": "^27.4.5", "jest-cli": "^27.4.5", @@ -87,15 +87,17 @@ "parse5": "7.1.2", "pixelmatch": "5.3.0", "postcss": "^8.2.8", - "prettier": "3.2.4", + "prettier": "3.3.1", "prompts": "2.4.2", "puppeteer": "^21.0.0", + "rimraf": "^6.0.1", "rollup": "2.56.3", "semver": "^7.3.7", - "terser": "5.31.0", - "typescript": "~5.4.0", + "terser": "5.31.1", + "tsx": "^4.10.3", + "typescript": "~5.5.4", "webpack": "^5.75.0", - "ws": "8.17.0" + "ws": "8.17.1" }, "engines": { "node": ">=16.0.0", diff --git a/test/wdio/scoped-basic/cmp.test.tsx b/test/wdio/scoped-basic/cmp.test.tsx index 925f72eea34..f0d620bf3f2 100644 --- a/test/wdio/scoped-basic/cmp.test.tsx +++ b/test/wdio/scoped-basic/cmp.test.tsx @@ -13,7 +13,6 @@ describe('scoped-basic', function () { await doc.waitForStable(); await expect(doc).toHaveElementClass(expect.stringContaining('sc-scoped-basic-root-md-h')); - await expect(doc).toHaveElementClass(expect.stringContaining('sc-scoped-basic-root-md-s')); await expect(doc).toHaveElementClass(expect.stringContaining('hydrated')); const scopedEl = await $('scoped-basic'); @@ -21,7 +20,6 @@ describe('scoped-basic', function () { await expect(scopedEl).toHaveElementClass(expect.stringContaining('sc-scoped-basic-root-md')); await expect(scopedEl).toHaveElementClass(expect.stringContaining('sc-scoped-basic-h')); - await expect(scopedEl).toHaveElementClass(expect.stringContaining('sc-scoped-basic-s')); await expect(scopedEl).toHaveElementClass(expect.stringContaining('hydrated')); await expect(scopedEl).toHaveStyle({ @@ -29,7 +27,7 @@ describe('scoped-basic', function () { color: browser.isChromium ? 'rgba(128,128,128,1)' : 'rgb(128,128,128)', }); - const scopedDiv = await $('scoped-basic div'); + const scopedDiv = await $('scoped-basic span'); await expect(scopedDiv).toHaveElementClass(expect.stringContaining('sc-scoped-basic')); await expect(scopedDiv).toHaveStyle({ color: browser.isChromium ? 'rgba(255,0,0,1)' : browser.isFirefox ? 'rgb(255,0,0)' : 'rgb(255, 0, 0)', diff --git a/test/wdio/scoped-basic/cmp.tsx b/test/wdio/scoped-basic/cmp.tsx index 51a55115f25..15f33fcbbad 100644 --- a/test/wdio/scoped-basic/cmp.tsx +++ b/test/wdio/scoped-basic/cmp.tsx @@ -9,7 +9,7 @@ import { Component, h } from '@stencil/core'; color: grey; } - div { + span { color: red; } @@ -22,7 +22,7 @@ import { Component, h } from '@stencil/core'; export class ScopedBasic { render() { return [ -
scoped
, + scoped,

, diff --git a/test/wdio/scoped-id-in-nested-classname/cmp-level-1.scss b/test/wdio/scoped-id-in-nested-classname/cmp-level-1.scss index d1fe7df9355..857ea5e9f3e 100644 --- a/test/wdio/scoped-id-in-nested-classname/cmp-level-1.scss +++ b/test/wdio/scoped-id-in-nested-classname/cmp-level-1.scss @@ -1,5 +1,8 @@ :host { cmp-level-2 { + ::slotted(#test-element) { + color: blue; + } cmp-level-3 { padding: 32px; diff --git a/test/wdio/scoped-id-in-nested-classname/cmp.test.tsx b/test/wdio/scoped-id-in-nested-classname/cmp.test.tsx index 4791d99ca8a..d93e02ce99d 100644 --- a/test/wdio/scoped-id-in-nested-classname/cmp.test.tsx +++ b/test/wdio/scoped-id-in-nested-classname/cmp.test.tsx @@ -6,17 +6,18 @@ describe('scope-id-in-nested-classname', function () { render({ template: () => , }); - await expect($('cmp-level-3')).toHaveElementClass('sc-cmp-level-1'); + await $('cmp-level-3').waitForStable(); + await expect($('cmp-level-3')).toHaveElementClass('sc-cmp-level-2'); await expect($('cmp-level-3')).toHaveElementClass('sc-cmp-level-2'); const padding = await $('cmp-level-3').getCSSProperty('padding'); - await expect(padding.parsed.value).toBe(32); + await expect(padding.parsed.value).toBe(8); const fontWeight = await $('cmp-level-3').getCSSProperty('font-weight'); await expect(fontWeight.parsed.value).toBe(800); }); - it('should have root scope id in the user provided nested element as classname', async () => { + it('should not have root scope id in slotted / user provided nested element as classname', async () => { render({ template: () => ( @@ -24,13 +25,13 @@ describe('scope-id-in-nested-classname', function () { ), }); - await expect($('#test-element')).toHaveElementClass('sc-cmp-level-1'); - await expect($('#test-element')).toHaveElementClass('sc-cmp-level-2'); + await expect($('#test-element')).not.toHaveElementClass('sc-cmp-level-1'); + await expect($('#test-element')).not.toHaveElementClass('sc-cmp-level-2'); const padding = await $('#test-element').getCSSProperty('padding'); - await expect(padding.parsed.value).toBe(24); + await expect(padding.parsed.value).not.toBe(24); const fontWeight = await $('#test-element').getCSSProperty('font-weight'); - await expect(fontWeight.parsed.value).toBe(600); + await expect(fontWeight.parsed.value).not.toBe(600); }); }); diff --git a/test/wdio/scoped-slot-children/cmp.test.tsx b/test/wdio/scoped-slot-children/cmp.test.tsx index 2e17cc7f07e..58c318e24f6 100644 --- a/test/wdio/scoped-slot-children/cmp.test.tsx +++ b/test/wdio/scoped-slot-children/cmp.test.tsx @@ -14,7 +14,7 @@ describe('scoped-slot-children', function () { a default slot, slotted element
a second slot, slotted element - nested element in the second slot + nested element in the second slot
), @@ -29,25 +29,21 @@ describe('scoped-slot-children', function () { (document.querySelector('scoped-slot-children') as any).__childNodes as NodeListOf; expect(nodeOrEleContent(childNodes()[0])).toBe(`Some default slot, slotted text`); - expect(nodeOrEleContent(childNodes()[1])).toBe( - `a default slot, slotted element`, - ); + expect(nodeOrEleContent(childNodes()[1])).toBe(`a default slot, slotted element`); expect(nodeOrEleContent(childNodes()[2])).toBe( - `
a second slot, slotted element nested element in the second slot
`, + `
a second slot, slotted element nested element in the second slot
`, ); expect(nodeOrEleContent(innerChildNodes()[4])).toBe(`

internal text 1

`); childNodes()[0].remove(); - expect(nodeOrEleContent(childNodes()[0])).toBe( - `a default slot, slotted element`, - ); + expect(nodeOrEleContent(childNodes()[0])).toBe(`a default slot, slotted element`); expect(nodeOrEleContent(innerChildNodes()[4])).toBe(`

internal text 1

`); childNodes()[0].remove(); expect(nodeOrEleContent(childNodes()[0])).toBe( - `
a second slot, slotted element nested element in the second slot
`, + `
a second slot, slotted element nested element in the second slot
`, ); expect(nodeOrEleContent(innerChildNodes()[4])).toBe(`

internal text 1

`); @@ -65,11 +61,9 @@ describe('scoped-slot-children', function () { const innerChildren = () => (document.querySelector('scoped-slot-children') as any).__children as NodeListOf; - expect(nodeOrEleContent(children()[0])).toBe( - `a default slot, slotted element`, - ); + expect(nodeOrEleContent(children()[0])).toBe(`a default slot, slotted element`); expect(nodeOrEleContent(children()[1])).toBe( - `
a second slot, slotted element nested element in the second slot
`, + `
a second slot, slotted element nested element in the second slot
`, ); expect(nodeOrEleContent(children()[2])).toBe(undefined); @@ -77,7 +71,7 @@ describe('scoped-slot-children', function () { children()[0].remove(); expect(nodeOrEleContent(children()[0])).toBe( - `
a second slot, slotted element nested element in the second slot
`, + `
a second slot, slotted element nested element in the second slot
`, ); expect(nodeOrEleContent(innerChildren()[0])).toBe(`

internal text 1

`); @@ -94,12 +88,12 @@ describe('scoped-slot-children', function () { document.querySelector('scoped-slot-children').firstChild.remove(); expect(nodeOrEleContent(document.querySelector('scoped-slot-children').firstChild)).toBe( - `a default slot, slotted element`, + `a default slot, slotted element`, ); document.querySelector('scoped-slot-children').firstChild.remove(); expect(nodeOrEleContent(document.querySelector('scoped-slot-children').firstChild)).toBe( - `
a second slot, slotted element nested element in the second slot
`, + `
a second slot, slotted element nested element in the second slot
`, ); document.querySelector('scoped-slot-children').firstChild.remove(); @@ -109,12 +103,12 @@ describe('scoped-slot-children', function () { it('patches `lastChild` to return only the last slotted node', async () => { await $('scoped-slot-children').waitForStable(); expect(nodeOrEleContent(document.querySelector('scoped-slot-children').lastChild)).toBe( - `
a second slot, slotted element nested element in the second slot
`, + `
a second slot, slotted element nested element in the second slot
`, ); document.querySelector('scoped-slot-children').lastChild.remove(); expect(nodeOrEleContent(document.querySelector('scoped-slot-children').lastChild)).toBe( - `a default slot, slotted element`, + `a default slot, slotted element`, ); document.querySelector('scoped-slot-children').lastChild.remove();