From b45ab4fb8e5e78cf150495e5bf53c0c9d7f87ba3 Mon Sep 17 00:00:00 2001 From: John Jenkins Date: Mon, 10 Feb 2025 10:30:07 +0000 Subject: [PATCH 01/10] chore: wip.. pretty much there? --- src/runtime/dom-extras.ts | 51 +++++++++++++++++---------------- src/runtime/vdom/vdom-render.ts | 11 ++++--- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/runtime/dom-extras.ts b/src/runtime/dom-extras.ts index 06da86742f2..02df7bfd926 100644 --- a/src/runtime/dom-extras.ts +++ b/src/runtime/dom-extras.ts @@ -90,6 +90,7 @@ export const patchCloneNode = (HostElementPrototype: HTMLElement) => { */ 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 = getHostSlotNodes((this as any).__childNodes || this.childNodes, this.tagName, slotName)[0]; @@ -99,13 +100,9 @@ export const patchSlotAppendChild = (HostElementPrototype: any) => { const slotChildNodes = getHostSlotChildNodes(slotNode, slotName); const appendAfter = slotChildNodes[slotChildNodes.length - 1]; - const parent = intrnlCall(appendAfter, 'parentNode') as d.RenderNode; - let insertedNode: d.RenderNode; - if (parent.__insertBefore) { - insertedNode = parent.__insertBefore(newChild, appendAfter.nextSibling); - } else { - insertedNode = parent.insertBefore(newChild, appendAfter.nextSibling); - } + const parent = internalCall(appendAfter, 'parentNode') as d.RenderNode; + let insertedNode: d.RenderNode = internalCall(parent, 'insertBefore')(newChild, appendAfter.nextSibling); + dispatchSlotChangeEvent(slotNode); // Check if there is fallback content that should be hidden updateFallbackSlotVisibility(this); @@ -156,19 +153,18 @@ export const patchSlotPrepend = (HostElementPrototype: HTMLElement) => { newChild = this.ownerDocument.createTextNode(newChild) as unknown as d.RenderNode; } const slotName = (newChild['s-sn'] = getSlotName(newChild)); - const childNodes = (this as any).__childNodes || this.childNodes; + const childNodes = internalCall(this, 'childNodes'); const slotNode = getHostSlotNodes(childNodes, this.tagName, slotName)[0]; if (slotNode) { addSlotRelocateNode(newChild, slotNode, true); + const slotChildNodes = getHostSlotChildNodes(slotNode, slotName); const appendAfter = slotChildNodes[0]; - const parent = intrnlCall(appendAfter, 'parentNode') as d.RenderNode; - - if (parent.__insertBefore) { - return parent.__insertBefore(newChild, intrnlCall(appendAfter, 'nextSibling')); - } else { - return parent.insertBefore(newChild, intrnlCall(appendAfter, 'nextSibling')); - } + + const parent = internalCall(appendAfter, 'parentNode') as d.RenderNode; + internalCall(parent, 'insertBefore')(newChild, internalCall(appendAfter, 'nextSibling')); + + dispatchSlotChangeEvent(slotNode); } if (newChild.nodeType === 1 && !!newChild.getAttribute('slot')) { @@ -286,13 +282,10 @@ const patchInsertBefore = (HostElementPrototype: HTMLElement) => { // current child ('slot before' node) is 'in' the same slot addSlotRelocateNode(newChild, slotNode); - const parent = intrnlCall(currentChild, 'parentNode') as d.RenderNode; - if (parent.__insertBefore) { - // the parent is a patched component, so we need to use the internal method - parent.__insertBefore(newChild, currentChild); - } else { - parent.insertBefore(newChild, currentChild); - } + const parent = internalCall(currentChild, 'parentNode') as d.RenderNode; + internalCall(parent, 'insertBefore')(newChild, currentChild); + + dispatchSlotChangeEvent(slotNode); } return; } @@ -402,6 +395,11 @@ export const patchChildSlotNodes = (elm: HTMLElement) => { }); }; +function dispatchSlotChangeEvent(elm: d.RenderNode) { + console.log('dispatchSlotChangeEvent'); + elm.dispatchEvent(new CustomEvent('slotchange', { bubbles: false, cancelable: false, composed: false })); +} + /// SLOTTED NODES /// /** @@ -580,10 +578,13 @@ function patchHostOriginalAccessor( * * @returns the original accessor or method of the node */ -function intrnlCall(node: T, method: P): T[P] { +function internalCall(node: T, method: P): T[P] { if ('__' + method in node) { - return node[('__' + method) as keyof d.RenderNode] as T[P]; + const toReturn = node[('__' + method) as keyof d.RenderNode] as T[P]; + if (typeof toReturn !== 'function') return toReturn; + return toReturn.bind(node) as T[P];; } else { - return node[method]; + if (typeof node[method] !== 'function') return node[method]; + return (node[method]).bind(node); } } diff --git a/src/runtime/vdom/vdom-render.ts b/src/runtime/vdom/vdom-render.ts index a21cae838d1..70f437db0da 100644 --- a/src/runtime/vdom/vdom-render.ts +++ b/src/runtime/vdom/vdom-render.ts @@ -680,12 +680,11 @@ export const patch = (oldVNode: d.VNode, newVNode: d.VNode, isInitialRender = fa newVNode.$elm$['s-sn'] = newVNode.$name$ || ''; relocateToHostRoot(newVNode.$elm$.parentElement); } - } else { - // 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); - } + } + // 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) { From 08a3fbc4ca369e7a79f91ac3d7031e6ade23ec73 Mon Sep 17 00:00:00 2001 From: John Jenkins Date: Mon, 10 Feb 2025 23:12:38 +0000 Subject: [PATCH 02/10] feat(runtime): add `assignedNodes()` and `assignedElements()` to polyfilled slot elements --- src/runtime/slot-polyfill-utils.ts | 35 +++++++++++++++++++++++++++++- src/runtime/vdom/set-accessor.ts | 7 ++++-- src/runtime/vdom/vdom-render.ts | 3 ++- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/runtime/slot-polyfill-utils.ts b/src/runtime/slot-polyfill-utils.ts index 275f5d92fdd..17c0b7e9708 100644 --- a/src/runtime/slot-polyfill-utils.ts +++ b/src/runtime/slot-polyfill-utils.ts @@ -5,7 +5,7 @@ import { NODE_TYPE } from './runtime-constants'; /** * Adjust the `.hidden` property as-needed on any nodes in a DOM subtree which - * are slot fallbacks nodes - `...` + * are slot fallback nodes - `...` * * A slot fallback node should be visible by default. Then, it should be * conditionally hidden if: @@ -191,3 +191,36 @@ export const addSlotRelocateNode = ( export const getSlotName = (node: d.PatchedSlotNode) => node['s-sn'] || (node.nodeType === 1 && (node as Element).getAttribute('slot')) || ''; + +/** + * Add `assignedElements` and `assignedNodes` methods on a fake slot node + * + * @param node - slot node to patch + * @returns + */ +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 }) { + let toReturn = getHostSlotChildNodes(this, this['s-sn'], true); + if (elementsOnly) toReturn = toReturn.filter((n) => n.nodeType === NODE_TYPE.ElementNode); + + if (!opts?.flatten) return toReturn.filter(n => !n['s-sr']); + + return toReturn.reduce( + (acc, node) => { + if (node['s-sr']) { + if (elementsOnly) acc.push(...(node as any).assignedElements(opts)); + else acc.push(...(node as any).assignedNodes(opts)); + } else { + acc.push(node); + } + return acc; + }, + [] as d.RenderNode[], + ); + }).bind(node); + + (node as any).assignedElements = assignedFactory(true); + (node as any).assignedNodes = assignedFactory(false); +} \ No newline at end of file diff --git a/src/runtime/vdom/set-accessor.ts b/src/runtime/vdom/set-accessor.ts index 1b64c805fda..76ff0a9d76a 100644 --- a/src/runtime/vdom/set-accessor.ts +++ b/src/runtime/vdom/set-accessor.ts @@ -12,7 +12,7 @@ import { isMemberInElement, plt, win } from '@platform'; import { isComplexType } from '@utils'; import type * as d from '../../declarations'; -import { VNODE_FLAGS, XLINK_NS } from '../runtime-constants'; +import { NODE_TYPE, VNODE_FLAGS, XLINK_NS } from '../runtime-constants'; /** * When running a VDom render set properties present on a VDom node onto the @@ -189,7 +189,10 @@ export const setAccessor = ( elm.removeAttribute(memberName); } } - } else if ((!isProp || flags & VNODE_FLAGS.isHost || isSvg) && !isComplex) { + } else if ( + (!isProp || flags & VNODE_FLAGS.isHost || isSvg) && + !isComplex && elm.nodeType === NODE_TYPE.ElementNode + ) { newValue = newValue === true ? '' : newValue; if (BUILD.vdomXlink && xlink) { elm.setAttributeNS(XLINK_NS, memberName, newValue); diff --git a/src/runtime/vdom/vdom-render.ts b/src/runtime/vdom/vdom-render.ts index 70f437db0da..6570798a140 100644 --- a/src/runtime/vdom/vdom-render.ts +++ b/src/runtime/vdom/vdom-render.ts @@ -13,7 +13,7 @@ import { CMP_FLAGS, HTML_NS, isDef, NODE_TYPES, SVG_NS } from '@utils'; import type * as d from '../../declarations'; import { patchParentNode } from '../dom-extras'; import { NODE_TYPE, PLATFORM_FLAGS, VNODE_FLAGS } from '../runtime-constants'; -import { isNodeLocatedInSlot, updateFallbackSlotVisibility } from '../slot-polyfill-utils'; +import { isNodeLocatedInSlot, patchSlotNode, updateFallbackSlotVisibility } from '../slot-polyfill-utils'; import { h, isHost, newVNode } from './h'; import { updateElement } from './update-element'; @@ -162,6 +162,7 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: } if (BUILD.scoped) { addRemoveSlotScopedClass(contentRef, elm, newParentVNode.$elm$, oldParentVNode?.$elm$); + patchSlotNode(elm); } } } From 074cb5f4a07f0ae5e36205004b62e0d279276281 Mon Sep 17 00:00:00 2001 From: John Jenkins Date: Mon, 10 Feb 2025 23:27:42 +0000 Subject: [PATCH 03/10] chore: tidy --- src/runtime/dom-extras.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/runtime/dom-extras.ts b/src/runtime/dom-extras.ts index 02df7bfd926..03278497cbe 100644 --- a/src/runtime/dom-extras.ts +++ b/src/runtime/dom-extras.ts @@ -395,8 +395,12 @@ export const patchChildSlotNodes = (elm: HTMLElement) => { }); }; +/** + * Dispatches a `slotchange` event on a fake `` node. + * + * @param elm the slot node to dispatch the event from + */ function dispatchSlotChangeEvent(elm: d.RenderNode) { - console.log('dispatchSlotChangeEvent'); elm.dispatchEvent(new CustomEvent('slotchange', { bubbles: false, cancelable: false, composed: false })); } From a0963c11eafe48da8cc5e9e5a6c3cb4ca1827f09 Mon Sep 17 00:00:00 2001 From: John Jenkins Date: Tue, 11 Feb 2025 15:18:16 +0000 Subject: [PATCH 04/10] chore: add to client-hydration --- src/runtime/client-hydrate.ts | 3 ++- src/runtime/vdom/vdom-render.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/runtime/client-hydrate.ts b/src/runtime/client-hydrate.ts index 3077044acda..c9437420e48 100644 --- a/src/runtime/client-hydrate.ts +++ b/src/runtime/client-hydrate.ts @@ -16,7 +16,7 @@ import { TEXT_NODE_ID, VNODE_FLAGS, } from './runtime-constants'; -import { addSlotRelocateNode } from './slot-polyfill-utils'; +import { addSlotRelocateNode, patchSlotNode } from './slot-polyfill-utils'; import { newVNode } from './vdom/h'; /** @@ -586,6 +586,7 @@ function addSlot( // 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) diff --git a/src/runtime/vdom/vdom-render.ts b/src/runtime/vdom/vdom-render.ts index 6570798a140..4932bc28269 100644 --- a/src/runtime/vdom/vdom-render.ts +++ b/src/runtime/vdom/vdom-render.ts @@ -147,6 +147,9 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: // 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$) { @@ -162,7 +165,6 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: } if (BUILD.scoped) { addRemoveSlotScopedClass(contentRef, elm, newParentVNode.$elm$, oldParentVNode?.$elm$); - patchSlotNode(elm); } } } From 5ca47fa2adf8d226fc8d016abd9ca27c8272f841 Mon Sep 17 00:00:00 2001 From: John Jenkins Date: Fri, 14 Feb 2025 01:16:29 +0000 Subject: [PATCH 05/10] chore: pretty much there --- src/runtime/dom-extras.ts | 33 ++---- src/runtime/slot-polyfill-utils.ts | 108 +++++++++++------- src/runtime/vdom/vdom-render.ts | 27 ++++- .../scoped-slot-assigned-methods/cmp.test.tsx | 98 ++++++++++++++++ .../wdio/scoped-slot-assigned-methods/cmp.tsx | 39 +++++++ test/wdio/scoped-slot-slotchange/cmp-wrap.tsx | 19 +++ test/wdio/scoped-slot-slotchange/cmp.test.tsx | 24 ++++ test/wdio/scoped-slot-slotchange/cmp.tsx | 21 ++++ 8 files changed, 302 insertions(+), 67 deletions(-) create mode 100644 test/wdio/scoped-slot-assigned-methods/cmp.test.tsx create mode 100644 test/wdio/scoped-slot-assigned-methods/cmp.tsx create mode 100644 test/wdio/scoped-slot-slotchange/cmp-wrap.tsx create mode 100644 test/wdio/scoped-slot-slotchange/cmp.test.tsx create mode 100644 test/wdio/scoped-slot-slotchange/cmp.tsx diff --git a/src/runtime/dom-extras.ts b/src/runtime/dom-extras.ts index 03278497cbe..f67ec75dde1 100644 --- a/src/runtime/dom-extras.ts +++ b/src/runtime/dom-extras.ts @@ -4,6 +4,8 @@ import { supportsShadow } from '@platform'; import type * as d from '../declarations'; import { addSlotRelocateNode, + dispatchSlotChangeEvent, + findSlotFromSlottedNode, getHostSlotChildNodes, getHostSlotNodes, getSlotName, @@ -92,8 +94,7 @@ 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 = getHostSlotNodes((this as any).__childNodes || this.childNodes, this.tagName, slotName)[0]; + const { slotName, slotNode } = findSlotFromSlottedNode(newChild, this); if (slotNode) { addSlotRelocateNode(newChild, slotNode); @@ -157,13 +158,13 @@ export const patchSlotPrepend = (HostElementPrototype: HTMLElement) => { const slotNode = getHostSlotNodes(childNodes, this.tagName, slotName)[0]; if (slotNode) { addSlotRelocateNode(newChild, slotNode, true); - + const slotChildNodes = getHostSlotChildNodes(slotNode, slotName); const appendAfter = slotChildNodes[0]; - + const parent = internalCall(appendAfter, 'parentNode') as d.RenderNode; internalCall(parent, 'insertBefore')(newChild, internalCall(appendAfter, 'nextSibling')); - + dispatchSlotChangeEvent(slotNode); } @@ -259,8 +260,7 @@ const patchInsertBefore = (HostElementPrototype: HTMLElement) => { newChild: T, currentChild: d.RenderNode | null, ) { - const slotName = (newChild['s-sn'] = getSlotName(newChild)); - const slotNode = getHostSlotNodes(this.__childNodes, this.tagName, slotName)[0]; + const { slotName, slotNode } = findSlotFromSlottedNode(newChild, this); const slottedNodes = this.__childNodes ? this.childNodes : getSlottedChildNodes(this.childNodes); if (slotNode) { @@ -285,7 +285,7 @@ const patchInsertBefore = (HostElementPrototype: HTMLElement) => { const parent = internalCall(currentChild, 'parentNode') as d.RenderNode; internalCall(parent, 'insertBefore')(newChild, currentChild); - dispatchSlotChangeEvent(slotNode); + dispatchSlotChangeEvent(slotNode); } return; } @@ -395,15 +395,6 @@ export const patchChildSlotNodes = (elm: HTMLElement) => { }); }; -/** - * Dispatches a `slotchange` event on a fake `` node. - * - * @param elm the slot node to dispatch the event from - */ -function dispatchSlotChangeEvent(elm: d.RenderNode) { - elm.dispatchEvent(new CustomEvent('slotchange', { bubbles: false, cancelable: false, composed: false })); -} - /// SLOTTED NODES /// /** @@ -419,7 +410,7 @@ function dispatchSlotChangeEvent(elm: d.RenderNode) { * @param node the slotted node to be patched */ export const patchSlottedNode = (node: Node) => { - if (!node || (node as any).__nextSibling || !globalThis.Node) return; + if (!node || (node as any).__nextSibling !== undefined || !globalThis.Node) return; patchNextSibling(node); patchPreviousSibling(node); @@ -582,13 +573,13 @@ function patchHostOriginalAccessor( * * @returns the original accessor or method of the node */ -function internalCall(node: T, method: P): T[P] { +export function internalCall(node: T, method: P): T[P] { if ('__' + method in node) { const toReturn = node[('__' + method) as keyof d.RenderNode] as T[P]; if (typeof toReturn !== 'function') return toReturn; - return toReturn.bind(node) as T[P];; + return toReturn.bind(node) as T[P]; } else { if (typeof node[method] !== 'function') return node[method]; - return (node[method]).bind(node); + return node[method].bind(node) as T[P]; } } diff --git a/src/runtime/slot-polyfill-utils.ts b/src/runtime/slot-polyfill-utils.ts index 17c0b7e9708..46db2bd57d3 100644 --- a/src/runtime/slot-polyfill-utils.ts +++ b/src/runtime/slot-polyfill-utils.ts @@ -2,6 +2,7 @@ import { BUILD } from '@app-data'; import type * as d from '../declarations'; import { NODE_TYPE } from './runtime-constants'; +import { internalCall } from './dom-extras'; /** * Adjust the `.hidden` property as-needed on any nodes in a DOM subtree which @@ -17,7 +18,7 @@ import { NODE_TYPE } from './runtime-constants'; * @param elm the element of interest */ export const updateFallbackSlotVisibility = (elm: d.RenderNode) => { - const childNodes: d.RenderNode[] = elm.__childNodes || (elm.childNodes as any); + const childNodes = internalCall(elm, 'childNodes'); // is this is a stencil component? if (elm.tagName && elm.tagName.includes('-') && elm['s-cr'] && elm.tagName !== 'SLOT-FB') { @@ -25,7 +26,7 @@ export const updateFallbackSlotVisibility = (elm: d.RenderNode) => { 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) { + if (getHostSlotChildNodes(slotNode, getSlotName(slotNode), false)?.length) { // has slotted nodes, hide fallback slotNode.hidden = true; } else { @@ -35,8 +36,11 @@ export const updateFallbackSlotVisibility = (elm: d.RenderNode) => { } }); } - for (const childNode of childNodes) { - if (childNode.nodeType === NODE_TYPE.ElementNode && (childNode.__childNodes || childNode.childNodes).length) { + + 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); } @@ -54,7 +58,7 @@ export const updateFallbackSlotVisibility = (elm: d.RenderNode) => { * @returns An array of slotted reference nodes. */ export const getSlottedChildNodes = (childNodes: NodeListOf): d.PatchedSlotNode[] => { - const result = []; + const result: d.PatchedSlotNode[] = []; for (let i = 0; i < childNodes.length; i++) { const slottedNode = ((childNodes[i] as d.RenderNode)['s-nr'] as d.PatchedSlotNode) || undefined; if (slottedNode && slottedNode.isConnected) { @@ -71,7 +75,7 @@ export const getSlottedChildNodes = (childNodes: NodeListOf): d.Patch * @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 function getHostSlotNodes(childNodes: NodeListOf, hostName: string, slotName?: string) { +export function getHostSlotNodes(childNodes: NodeListOf, hostName?: string, slotName?: string) { let i = 0; let slottedNodes: d.RenderNode[] = []; let childNode: d.RenderNode; @@ -80,8 +84,8 @@ export function getHostSlotNodes(childNodes: NodeListOf, hostName: st childNode = childNodes[i] as any; if ( childNode['s-sr'] && - childNode['s-hn'] === hostName && - (slotName === undefined || childNode['s-sn'] === slotName) + (!hostName || childNode['s-hn'] === hostName) && + (slotName === undefined || getSlotName(childNode) === slotName) ) { slottedNodes.push(childNode); if (typeof slotName !== 'undefined') return slottedNodes; @@ -92,18 +96,28 @@ export function getHostSlotNodes(childNodes: NodeListOf, hostName: st } /** - * Get slotted child nodes of a slot node - * @param node - the slot node to get the child nodes from + * Get all slotted child 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 slotted child nodes of the slot node */ -export const getHostSlotChildNodes = (node: d.RenderNode, slotName: string, includeSlot = true) => { +export const getHostSlotChildNodes = (slot: d.RenderNode, slotName: string, includeSlot = true, flatten?: boolean) => { const childNodes: d.RenderNode[] = []; - if ((includeSlot && node['s-sr']) || !node['s-sr']) childNodes.push(node as any); + if ((includeSlot && slot['s-sr']) || !slot['s-sr']) childNodes.push(slot as any); + let node = slot; - while ((node = node.nextSibling as any) && (node as d.RenderNode)['s-sn'] === slotName) { - childNodes.push(node as any); + while ((node = node.nextSibling as any)) { + if (getSlotName(node) === slotName) childNodes.push(node as any); + } + if (flatten && slot.nodeType === NODE_TYPE.ElementNode && !slot.hidden && slot.childNodes.length) { + (slot.childNodes as NodeListOf).forEach((nestedNode) => { + if (nestedNode['s-sr'] && !nestedNode.hidden) { + childNodes.push(...getHostSlotChildNodes(nestedNode, getSlotName(nestedNode), false, true)); + } else { + childNodes.push(nestedNode); + } + }); } return childNodes; }; @@ -163,19 +177,19 @@ export const addSlotRelocateNode = ( 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; + const appendMethod = prepend ? internalCall(parent, 'prepend') : internalCall(parent, 'appendChild'); if (typeof position !== 'undefined') { if (BUILD.hydrateClientSide) { slottedNodeLocation['s-oo'] = position; - const childNodes = (parent.__childNodes || parent.childNodes) as NodeListOf; + const childNodes = internalCall(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; + if (!a['s-oo'] || a['s-oo'] < (b['s-oo'] || 0)) return -1; else if (!b['s-oo'] || b['s-oo'] < a['s-oo']) return 1; return 0; }); @@ -190,37 +204,51 @@ export const addSlotRelocateNode = ( }; export const getSlotName = (node: d.PatchedSlotNode) => - node['s-sn'] || (node.nodeType === 1 && (node as Element).getAttribute('slot')) || ''; + typeof node['s-sn'] === 'string' + ? node['s-sn'] || (node.nodeType === 1 && (node as Element).getAttribute('slot')) || '' + : undefined; /** * Add `assignedElements` and `assignedNodes` methods on a fake slot node - * + * * @param node - slot node to patch - * @returns + * @returns */ 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 }) { - let toReturn = getHostSlotChildNodes(this, this['s-sn'], true); - if (elementsOnly) toReturn = toReturn.filter((n) => n.nodeType === NODE_TYPE.ElementNode); - - if (!opts?.flatten) return toReturn.filter(n => !n['s-sr']); - - return toReturn.reduce( - (acc, node) => { - if (node['s-sr']) { - if (elementsOnly) acc.push(...(node as any).assignedElements(opts)); - else acc.push(...(node as any).assignedNodes(opts)); - } else { - acc.push(node); - } - return acc; - }, - [] as d.RenderNode[], - ); - }).bind(node); + const assignedFactory = (elementsOnly: boolean) => + function (opts?: { flatten: boolean }) { + let toReturn = getHostSlotChildNodes(this, this['s-sn'], false, opts?.flatten); + if (elementsOnly) return toReturn.filter((n) => n.nodeType === NODE_TYPE.ElementNode); + return toReturn; + }.bind(node); (node as any).assignedElements = assignedFactory(true); (node as any).assignedNodes = assignedFactory(false); -} \ No newline at end of file +} + +/** + * Dispatches a `slotchange` event on a fake `` node. + * + * @param elm the slot node to dispatch the event from + */ +export function dispatchSlotChangeEvent(elm: d.RenderNode) { + elm.dispatchEvent(new CustomEvent('slotchange', { bubbles: false, cancelable: false, composed: false })); +} + +/** + * Find the slot node that a slotted node belongs to + * + * @param slottedNode + * @returns the slot node and slot name + */ +export function findSlotFromSlottedNode(slottedNode: d.PatchedSlotNode, parentHost?: HTMLElement) { + parentHost = parentHost || slottedNode['s-ol']?.parentElement; + 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]; + return { slotNode, slotName }; +} diff --git a/src/runtime/vdom/vdom-render.ts b/src/runtime/vdom/vdom-render.ts index 4932bc28269..b319f2061f5 100644 --- a/src/runtime/vdom/vdom-render.ts +++ b/src/runtime/vdom/vdom-render.ts @@ -13,7 +13,13 @@ import { CMP_FLAGS, HTML_NS, isDef, NODE_TYPES, SVG_NS } from '@utils'; import type * as d from '../../declarations'; import { patchParentNode } from '../dom-extras'; import { NODE_TYPE, PLATFORM_FLAGS, VNODE_FLAGS } from '../runtime-constants'; -import { isNodeLocatedInSlot, patchSlotNode, updateFallbackSlotVisibility } from '../slot-polyfill-utils'; +import { + dispatchSlotChangeEvent, + findSlotFromSlottedNode, + isNodeLocatedInSlot, + patchSlotNode, + updateFallbackSlotVisibility, +} from '../slot-polyfill-utils'; import { h, isHost, newVNode } from './h'; import { updateElement } from './update-element'; @@ -73,6 +79,10 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: // create a slot reference node elm = newVNode.$elm$ = BUILD.isDebug || BUILD.hydrateServerSide ? slotReferenceDebugNode(newVNode) : (doc.createTextNode('') as any); + // add css classes, attrs, props, listeners, etc. + if (BUILD.vdomAttribute) { + updateElement(null, newVNode, isSvgMode); + } } else { if (BUILD.svg && !isSvgMode) { isSvgMode = newVNode.$tag$ === 'svg'; @@ -147,7 +157,7 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: // remember the ref callback function elm['s-rf'] = newVNode.$attrs$?.ref; - // give this node `assignedElements` and `assignedNodes` methods + // give this node `assignedElements` and `assignedNodes` methods patchSlotNode(elm); // check if we've got an old vnode for this slot @@ -683,7 +693,7 @@ export const patch = (oldVNode: d.VNode, newVNode: d.VNode, isInitialRender = fa newVNode.$elm$['s-sn'] = newVNode.$name$ || ''; relocateToHostRoot(newVNode.$elm$.parentElement); } - } + } // 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. @@ -871,7 +881,13 @@ export const insertBefore = ( patchParentNode(newNode); } // potentially use the patched insertBefore method. This will correctly slot the new node - return parent.insertBefore(newNode, reference); + parent.insertBefore(newNode, reference); + + // if we find a corresponding slot node, dispatch a slotchange event now + const { slotNode } = findSlotFromSlottedNode(newNode); + if (slotNode) dispatchSlotChangeEvent(slotNode); + + return newNode; } if (BUILD.experimentalSlotFixes && (parent as d.RenderNode).__insertBefore) { @@ -1138,8 +1154,7 @@ render() { } } } - - nodeToRelocate && typeof slotRefNode['s-rf'] === 'function' && slotRefNode['s-rf'](nodeToRelocate); + nodeToRelocate && typeof slotRefNode['s-rf'] === 'function' && slotRefNode['s-rf'](slotRefNode); } else { // this node doesn't have a slot home to go to, so let's hide it if (nodeToRelocate.nodeType === NODE_TYPE.ElementNode) { diff --git a/test/wdio/scoped-slot-assigned-methods/cmp.test.tsx b/test/wdio/scoped-slot-assigned-methods/cmp.test.tsx new file mode 100644 index 00000000000..000d5e2656f --- /dev/null +++ b/test/wdio/scoped-slot-assigned-methods/cmp.test.tsx @@ -0,0 +1,98 @@ +import { h, Fragment } from '@stencil/core'; +import { render } from '@wdio/browser-runner/stencil'; +import { $, expect } from '@wdio/globals'; + +describe('scoped-slot-assigned-methods', () => { + beforeEach(async () => { + // @ts-expect-error - no components array? + render({ + template: () => ( + + <> +

My initial slotted content.

+ Plain text +
Plain slot content.
+ +
+ ), + }); + await $('scoped-slot-assigned-methods div').waitForExist(); + }); + + it('tests assignedElements method on a ``', async () => { + const component: any = document.querySelector('scoped-slot-assigned-methods'); + expect(component.getSlotAssignedElements).toBeDefined(); + + let nodes = await component.getSlotAssignedElements(); + expect(nodes).toBeDefined(); + expect(nodes.length).toBe(1); + expect(nodes[0].outerHTML).toBe('

My initial slotted content.

'); + component.removeChild(nodes[0]); + + expect(await component.getSlotAssignedElements()).toHaveLength(0); + nodes = await component.getSlotAssignedElements({ flatten: true }); + expect(nodes).toHaveLength(0); + + const div = document.createElement('div'); + div.slot = 'nested-slot'; + div.textContent = 'Nested slotted content'; + component.appendChild(div); + expect(await component.getSlotAssignedElements()).toHaveLength(0); + + nodes = await component.getSlotAssignedElements({ flatten: true }); + expect(nodes).toHaveLength(1); + expect(nodes[0].outerHTML).toBe('
Nested slotted content
'); + }); + + it('tests assignedNodes method on a ``', async () => { + const component: any = document.querySelector('scoped-slot-assigned-methods'); + expect(component.getSlotAssignedNodes).toBeDefined(); + + let nodes = await component.getSlotAssignedNodes(); + expect(nodes).toBeDefined(); + expect(nodes.length).toBe(2); + expect(nodes[0].outerHTML).toBe('

My initial slotted content.

'); + component.removeChild(nodes[0]); + + nodes = await component.getSlotAssignedNodes(); + expect(nodes).toHaveLength(1); + expect(nodes[0].nodeValue).toBe('Plain text'); + component.removeChild(nodes[0]); + expect(await component.getSlotAssignedNodes()).toHaveLength(0); + + nodes = await component.getSlotAssignedNodes({ flatten: true }); + expect(nodes).toHaveLength(1); + expect(nodes[0].nodeValue).toBe('Fallback content'); + + const div = document.createElement('div'); + div.slot = 'nested-slot'; + div.textContent = 'Nested slotted content'; + component.appendChild(div); + expect(await component.getSlotAssignedNodes()).toHaveLength(0); + + nodes = await component.getSlotAssignedNodes({ flatten: true }); + expect(nodes).toHaveLength(1); + expect(nodes[0].outerHTML).toBe('
Nested slotted content
'); + }); + + it('tests assignedElements / assignedNodes method on a plain slot (a text / comment node)', async () => { + const component: any = document.querySelector('scoped-slot-assigned-methods'); + expect(component.getSlotAssignedElements).toBeDefined(); + + let eles = await component.getSlotAssignedElements(undefined, true); + let nodes = await component.getSlotAssignedNodes(undefined, true); + expect(eles).toBeDefined(); + expect(nodes).toBeDefined(); + expect(nodes.length).toBe(1); + expect(eles.length).toBe(1); + expect(nodes[0].outerHTML).toBe('
Plain slot content.
'); + expect(eles[0].outerHTML).toBe('
Plain slot content.
'); + component.removeChild(nodes[0]); + + expect(await component.getSlotAssignedElements(undefined, true)).toHaveLength(0); + expect(await component.getSlotAssignedNodes(undefined, true)).toHaveLength(0); + nodes = await component.getSlotAssignedElements({ flatten: true }, true); + + expect(nodes).toHaveLength(0); + }); +}); diff --git a/test/wdio/scoped-slot-assigned-methods/cmp.tsx b/test/wdio/scoped-slot-assigned-methods/cmp.tsx new file mode 100644 index 00000000000..b2ed1ea8663 --- /dev/null +++ b/test/wdio/scoped-slot-assigned-methods/cmp.tsx @@ -0,0 +1,39 @@ +import { Component, h, Method } from '@stencil/core'; + +@Component({ + tag: 'scoped-slot-assigned-methods', + scoped: true, +}) +export class ScopedSlotAssignedMethods { + private fbSlot: HTMLSlotElement; + private plainSlot: HTMLSlotElement; + + @Method() + async getSlotAssignedElements(opts?: { flatten: boolean }, getPlainSlot = false) { + if (getPlainSlot) { + console.log('plain slot', this.plainSlot); + return this.plainSlot.assignedElements(opts); + } + return this.fbSlot.assignedElements(opts); + } + + @Method() + async getSlotAssignedNodes(opts?: { flatten: boolean }, getPlainSlot = false) { + if (getPlainSlot) { + console.log('plain slot', this.plainSlot); + return this.plainSlot.assignedNodes(opts); + } + return this.fbSlot.assignedNodes(opts); + } + + render() { + return ( +
+ (this.fbSlot = s)}> + Fallback content + + (this.plainSlot = s)} /> +
+ ); + } +} diff --git a/test/wdio/scoped-slot-slotchange/cmp-wrap.tsx b/test/wdio/scoped-slot-slotchange/cmp-wrap.tsx new file mode 100644 index 00000000000..570cce96c14 --- /dev/null +++ b/test/wdio/scoped-slot-slotchange/cmp-wrap.tsx @@ -0,0 +1,19 @@ +import { Component, h, Prop } from '@stencil/core'; + +@Component({ + tag: 'scoped-slot-slotchange-wrap', + scoped: true, +}) +export class ScopedSlotChangeWrap { + @Prop() swapSlotContent: boolean = false; + + render() { + return ( +
+ + {this.swapSlotContent ?
Swapped slotted content
: Initial slotted content} +
+
+ ); + } +} diff --git a/test/wdio/scoped-slot-slotchange/cmp.test.tsx b/test/wdio/scoped-slot-slotchange/cmp.test.tsx new file mode 100644 index 00000000000..a23c087b60e --- /dev/null +++ b/test/wdio/scoped-slot-slotchange/cmp.test.tsx @@ -0,0 +1,24 @@ +import { h, Fragment } from '@stencil/core'; +import { render } from '@wdio/browser-runner/stencil'; +import { $, expect } from '@wdio/globals'; + +describe('scoped-slot-slotchange', () => { + beforeEach(async () => { + // @ts-expect-error - no components array? + render({ + template: () => ( + +

My initial slotted content.

+
+ ), + }); + await $('scoped-slot-slotchange-wrap div').waitForExist(); + }); + + it('checks that internal, stencil content changes fire slotchange events', async () => { + const slotChangeEle: any = document.querySelector('scoped-slot-slotchange'); + expect(slotChangeEle).toBeDefined(); + expect(slotChangeEle.slotEventCatch).toBeDefined(); + expect(slotChangeEle.slotEventCatch).toStrictEqual([]); + }); +}); diff --git a/test/wdio/scoped-slot-slotchange/cmp.tsx b/test/wdio/scoped-slot-slotchange/cmp.tsx new file mode 100644 index 00000000000..736cbb20443 --- /dev/null +++ b/test/wdio/scoped-slot-slotchange/cmp.tsx @@ -0,0 +1,21 @@ +import { Component, h, Prop } from '@stencil/core'; + +@Component({ + tag: 'scoped-slot-slotchange', + scoped: true, +}) +export class ScopedSlotChange { + @Prop({ mutable: true }) slotEventCatch: { event: Event; assignedNodes: Node[] }[] = []; + + private handleSlotchange = (e) => { + this.slotEventCatch.push({ event: e, assignedNodes: e.target.assignedNodes({ flatten: true }) }); + }; + + render() { + return ( +
+ +
+ ); + } +} From e1dfc1f2f4a781d700c1daa92489860cae2621ff Mon Sep 17 00:00:00 2001 From: John Jenkins Date: Fri, 14 Feb 2025 23:19:47 +0000 Subject: [PATCH 06/10] chore: more tests --- src/runtime/dom-extras.ts | 2 +- src/runtime/slot-polyfill-utils.ts | 3 +- src/runtime/vdom/set-accessor.ts | 5 +- test/wdio/scoped-slot-slotchange/cmp-wrap.tsx | 2 +- test/wdio/scoped-slot-slotchange/cmp.test.tsx | 65 +++++++++++++++++-- test/wdio/scoped-slot-slotchange/cmp.tsx | 3 + 6 files changed, 68 insertions(+), 12 deletions(-) diff --git a/src/runtime/dom-extras.ts b/src/runtime/dom-extras.ts index f67ec75dde1..0a326c9f96d 100644 --- a/src/runtime/dom-extras.ts +++ b/src/runtime/dom-extras.ts @@ -92,7 +92,7 @@ export const patchCloneNode = (HostElementPrototype: HTMLElement) => { */ export const patchSlotAppendChild = (HostElementPrototype: any) => { HostElementPrototype.__appendChild = HostElementPrototype.appendChild; - + HostElementPrototype.appendChild = function (this: d.RenderNode, newChild: d.RenderNode) { const { slotName, slotNode } = findSlotFromSlottedNode(newChild, this); if (slotNode) { diff --git a/src/runtime/slot-polyfill-utils.ts b/src/runtime/slot-polyfill-utils.ts index 46db2bd57d3..17799f7b3fb 100644 --- a/src/runtime/slot-polyfill-utils.ts +++ b/src/runtime/slot-polyfill-utils.ts @@ -245,9 +245,10 @@ export function dispatchSlotChangeEvent(elm: d.RenderNode) { */ export function findSlotFromSlottedNode(slottedNode: d.PatchedSlotNode, parentHost?: HTMLElement) { parentHost = parentHost || slottedNode['s-ol']?.parentElement; + if (!parentHost) return { slotNode: null, slotName: '' }; - const slotName = (slottedNode['s-sn'] = getSlotName(slottedNode)); + const slotName = (slottedNode['s-sn'] = getSlotName(slottedNode) || ''); const childNodes = internalCall(parentHost, 'childNodes'); const slotNode = getHostSlotNodes(childNodes, parentHost.tagName, slotName)[0]; return { slotNode, slotName }; diff --git a/src/runtime/vdom/set-accessor.ts b/src/runtime/vdom/set-accessor.ts index 76ff0a9d76a..53b7e755ee0 100644 --- a/src/runtime/vdom/set-accessor.ts +++ b/src/runtime/vdom/set-accessor.ts @@ -190,8 +190,9 @@ export const setAccessor = ( } } } else if ( - (!isProp || flags & VNODE_FLAGS.isHost || isSvg) && - !isComplex && elm.nodeType === NODE_TYPE.ElementNode + (!isProp || flags & VNODE_FLAGS.isHost || isSvg) && + !isComplex && + elm.nodeType === NODE_TYPE.ElementNode ) { newValue = newValue === true ? '' : newValue; if (BUILD.vdomXlink && xlink) { diff --git a/test/wdio/scoped-slot-slotchange/cmp-wrap.tsx b/test/wdio/scoped-slot-slotchange/cmp-wrap.tsx index 570cce96c14..8c0367c4913 100644 --- a/test/wdio/scoped-slot-slotchange/cmp-wrap.tsx +++ b/test/wdio/scoped-slot-slotchange/cmp-wrap.tsx @@ -11,7 +11,7 @@ export class ScopedSlotChangeWrap { return (
- {this.swapSlotContent ?
Swapped slotted content
: Initial slotted content} + {this.swapSlotContent ?
Swapped slotted content
:

Initial slotted content

}
); diff --git a/test/wdio/scoped-slot-slotchange/cmp.test.tsx b/test/wdio/scoped-slot-slotchange/cmp.test.tsx index a23c087b60e..ae5f17f0e4b 100644 --- a/test/wdio/scoped-slot-slotchange/cmp.test.tsx +++ b/test/wdio/scoped-slot-slotchange/cmp.test.tsx @@ -1,9 +1,11 @@ -import { h, Fragment } from '@stencil/core'; -import { render } from '@wdio/browser-runner/stencil'; +/// + +import { h } from '@stencil/core'; +import { render, waitForChanges } from '@wdio/browser-runner/stencil'; import { $, expect } from '@wdio/globals'; describe('scoped-slot-slotchange', () => { - beforeEach(async () => { + it('checks that internal, stencil content changes fire slotchange events', async () => { // @ts-expect-error - no components array? render({ template: () => ( @@ -13,12 +15,61 @@ describe('scoped-slot-slotchange', () => { ), }); await $('scoped-slot-slotchange-wrap div').waitForExist(); + + const slotChangeEle: any = document.querySelector('scoped-slot-slotchange'); + await expect(slotChangeEle).toBeDefined(); + await expect(slotChangeEle.slotEventCatch).toBeDefined(); + await expect(slotChangeEle.slotEventCatch).toHaveLength(1); + await expect(slotChangeEle.slotEventCatch[0]).toMatchObject({ event: { type: 'slotchange' } }); + await expect(slotChangeEle.slotEventCatch[0].assignedNodes[0].outerHTML).toMatch( + `

Initial slotted content

`, + ); + + document.querySelector('scoped-slot-slotchange-wrap').setAttribute('swap-slot-content', 'true'); + await waitForChanges(); + await expect(slotChangeEle.slotEventCatch).toHaveLength(2); + await expect(slotChangeEle.slotEventCatch[1]).toMatchObject({ event: { type: 'slotchange' } }); + await expect(slotChangeEle.slotEventCatch[1].assignedNodes[0].outerHTML).toMatch( + '
Swapped slotted content
', + ); }); - it('checks that internal, stencil content changes fire slotchange events', async () => { + it('checks that external, browser content changes fire slotchange events', async () => { + // @ts-expect-error - no components array? + render({ + template: () => , + }); + await $('scoped-slot-slotchange div').waitForExist(); const slotChangeEle: any = document.querySelector('scoped-slot-slotchange'); - expect(slotChangeEle).toBeDefined(); - expect(slotChangeEle.slotEventCatch).toBeDefined(); - expect(slotChangeEle.slotEventCatch).toStrictEqual([]); + + await expect(slotChangeEle).toBeDefined(); + await expect(slotChangeEle.slotEventCatch).toHaveLength(0); + + const p = document.createElement('p'); + p.innerHTML = 'Append child content'; + slotChangeEle.appendChild(p); + await waitForChanges(); + + await expect(slotChangeEle.slotEventCatch).toHaveLength(1); + await expect(slotChangeEle.slotEventCatch[0]).toMatchObject({ event: { type: 'slotchange' } }); + await expect(slotChangeEle.slotEventCatch[0].event.target.name).toBeFalsy(); + await expect(slotChangeEle.slotEventCatch[0].assignedNodes[0].outerHTML).toMatch(`

Append child content

`); + + p.slot = 'fallback-slot'; + slotChangeEle.appendChild(p); + await waitForChanges(); + + await expect(slotChangeEle.slotEventCatch).toHaveLength(2); + await expect(slotChangeEle.slotEventCatch[1]).toMatchObject({ event: { type: 'slotchange' } }); + await expect(slotChangeEle.slotEventCatch[1].event.target.getAttribute('name')).toBe('fallback-slot'); + + const div = document.createElement('div'); + div.innerHTML = 'InsertBefore content'; + slotChangeEle.insertBefore(div, null); + await waitForChanges(); + + await expect(slotChangeEle.slotEventCatch).toHaveLength(3); + await expect(slotChangeEle.slotEventCatch[2]).toMatchObject({ event: { type: 'slotchange' } }); + await expect(slotChangeEle.slotEventCatch[2].assignedNodes[0].outerHTML).toMatch(`
InsertBefore content
`); }); }); diff --git a/test/wdio/scoped-slot-slotchange/cmp.tsx b/test/wdio/scoped-slot-slotchange/cmp.tsx index 736cbb20443..89790c30221 100644 --- a/test/wdio/scoped-slot-slotchange/cmp.tsx +++ b/test/wdio/scoped-slot-slotchange/cmp.tsx @@ -15,6 +15,9 @@ export class ScopedSlotChange { return (
+ + Slot with fallback +
); } From 8288d1461363345bf30dfad2cabef08aada0320c Mon Sep 17 00:00:00 2001 From: John Jenkins Date: Fri, 14 Feb 2025 23:28:28 +0000 Subject: [PATCH 07/10] chore: formatting / linting --- src/runtime/dom-extras.ts | 2 +- src/runtime/slot-polyfill-utils.ts | 10 ++++++---- test/wdio/scoped-slot-assigned-methods/cmp.test.tsx | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/runtime/dom-extras.ts b/src/runtime/dom-extras.ts index 0a326c9f96d..34dcbc3f8e9 100644 --- a/src/runtime/dom-extras.ts +++ b/src/runtime/dom-extras.ts @@ -102,7 +102,7 @@ export const patchSlotAppendChild = (HostElementPrototype: any) => { const appendAfter = slotChildNodes[slotChildNodes.length - 1]; const parent = internalCall(appendAfter, 'parentNode') as d.RenderNode; - let insertedNode: d.RenderNode = internalCall(parent, 'insertBefore')(newChild, appendAfter.nextSibling); + const insertedNode: d.RenderNode = internalCall(parent, 'insertBefore')(newChild, appendAfter.nextSibling); dispatchSlotChangeEvent(slotNode); // Check if there is fallback content that should be hidden diff --git a/src/runtime/slot-polyfill-utils.ts b/src/runtime/slot-polyfill-utils.ts index 17799f7b3fb..b49a40f1268 100644 --- a/src/runtime/slot-polyfill-utils.ts +++ b/src/runtime/slot-polyfill-utils.ts @@ -1,8 +1,8 @@ import { BUILD } from '@app-data'; import type * as d from '../declarations'; -import { NODE_TYPE } from './runtime-constants'; 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 @@ -100,6 +100,8 @@ export function getHostSlotNodes(childNodes: NodeListOf, hostName?: s * @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 + * @param flatten - recursively get slotted nodes of child slot nodes + * (https://developer.mozilla.org/en-US/docs/Web/API/HTMLSlotElement/assignedNodes#flatten) * @returns slotted child nodes of the slot node */ export const getHostSlotChildNodes = (slot: d.RenderNode, slotName: string, includeSlot = true, flatten?: boolean) => { @@ -212,14 +214,13 @@ export const getSlotName = (node: d.PatchedSlotNode) => * Add `assignedElements` and `assignedNodes` methods on a fake slot node * * @param node - slot node to patch - * @returns */ 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 }) { - let toReturn = getHostSlotChildNodes(this, this['s-sn'], false, opts?.flatten); + const toReturn = getHostSlotChildNodes(this, this['s-sn'], false, opts?.flatten); if (elementsOnly) return toReturn.filter((n) => n.nodeType === NODE_TYPE.ElementNode); return toReturn; }.bind(node); @@ -240,7 +241,8 @@ export function dispatchSlotChangeEvent(elm: d.RenderNode) { /** * Find the slot node that a slotted node belongs to * - * @param slottedNode + * @param slottedNode - the slotted node to find the slot for + * @param parentHost - the parent host element of the slotted node * @returns the slot node and slot name */ export function findSlotFromSlottedNode(slottedNode: d.PatchedSlotNode, parentHost?: HTMLElement) { diff --git a/test/wdio/scoped-slot-assigned-methods/cmp.test.tsx b/test/wdio/scoped-slot-assigned-methods/cmp.test.tsx index 000d5e2656f..5806dd0bfc2 100644 --- a/test/wdio/scoped-slot-assigned-methods/cmp.test.tsx +++ b/test/wdio/scoped-slot-assigned-methods/cmp.test.tsx @@ -1,4 +1,4 @@ -import { h, Fragment } from '@stencil/core'; +import { Fragment, h } from '@stencil/core'; import { render } from '@wdio/browser-runner/stencil'; import { $, expect } from '@wdio/globals'; @@ -79,7 +79,7 @@ describe('scoped-slot-assigned-methods', () => { const component: any = document.querySelector('scoped-slot-assigned-methods'); expect(component.getSlotAssignedElements).toBeDefined(); - let eles = await component.getSlotAssignedElements(undefined, true); + const eles = await component.getSlotAssignedElements(undefined, true); let nodes = await component.getSlotAssignedNodes(undefined, true); expect(eles).toBeDefined(); expect(nodes).toBeDefined(); From 9f99f9cedfa0af743b4d04796d0890c893788e8c Mon Sep 17 00:00:00 2001 From: John Jenkins Date: Sat, 15 Feb 2025 00:00:04 +0000 Subject: [PATCH 08/10] chore: update tests --- src/runtime/dom-extras.ts | 9 ++++----- test/wdio/slot-ref/cmp.test.tsx | 23 ----------------------- test/wdio/slot-ref/cmp.tsx | 24 ------------------------ 3 files changed, 4 insertions(+), 52 deletions(-) delete mode 100644 test/wdio/slot-ref/cmp.test.tsx delete mode 100644 test/wdio/slot-ref/cmp.tsx diff --git a/src/runtime/dom-extras.ts b/src/runtime/dom-extras.ts index 34dcbc3f8e9..941878e61c4 100644 --- a/src/runtime/dom-extras.ts +++ b/src/runtime/dom-extras.ts @@ -153,19 +153,18 @@ export const patchSlotPrepend = (HostElementPrototype: HTMLElement) => { if (typeof newChild === 'string') { newChild = this.ownerDocument.createTextNode(newChild) as unknown as d.RenderNode; } - const slotName = (newChild['s-sn'] = getSlotName(newChild)); + const slotName = (newChild['s-sn'] = getSlotName(newChild)) || ''; const childNodes = internalCall(this, 'childNodes'); const slotNode = getHostSlotNodes(childNodes, this.tagName, slotName)[0]; if (slotNode) { addSlotRelocateNode(newChild, slotNode, true); - const slotChildNodes = getHostSlotChildNodes(slotNode, slotName); const appendAfter = slotChildNodes[0]; const parent = internalCall(appendAfter, 'parentNode') as d.RenderNode; - internalCall(parent, 'insertBefore')(newChild, internalCall(appendAfter, 'nextSibling')); - + const toReturn = internalCall(parent, 'insertBefore')(newChild, internalCall(appendAfter, 'nextSibling')); dispatchSlotChangeEvent(slotNode); + return toReturn; } if (newChild.nodeType === 1 && !!newChild.getAttribute('slot')) { @@ -292,7 +291,7 @@ const patchInsertBefore = (HostElementPrototype: HTMLElement) => { }); if (found) return newChild; } - return (this as d.RenderNode).__insertBefore(newChild, currentChild); + return (this as any).__insertBefore(newChild, currentChild); }; }; diff --git a/test/wdio/slot-ref/cmp.test.tsx b/test/wdio/slot-ref/cmp.test.tsx deleted file mode 100644 index 58580c093fd..00000000000 --- a/test/wdio/slot-ref/cmp.test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { h } from '@stencil/core'; -import { render } from '@wdio/browser-runner/stencil'; - -describe('slot-ref', () => { - beforeEach(async () => { - render({ - template: () => ( - - - Hello World! - - - ), - }); - - await $('slot-ref').waitForExist(); - }); - - it('ref callback of slot is called', async () => { - await expect($('slot-ref')).toHaveAttribute('data-ref-id', 'slotted-element-id'); - await expect($('slot-ref')).toHaveAttribute('data-ref-tagname', 'SPAN'); - }); -}); diff --git a/test/wdio/slot-ref/cmp.tsx b/test/wdio/slot-ref/cmp.tsx deleted file mode 100644 index 4a1f61aabc5..00000000000 --- a/test/wdio/slot-ref/cmp.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Component, Element, h, Host } from '@stencil/core'; - -@Component({ - tag: 'slot-ref', - shadow: false, - scoped: true, -}) -export class SlotRef { - @Element() hostElement: HTMLElement; - - render() { - return ( - - { - this.hostElement.setAttribute('data-ref-id', el!.id); - this.hostElement.setAttribute('data-ref-tagname', el!.tagName); - }} - /> - - ); - } -} From 02e9c0b8a725c1b84ab3891f5a3d793649ae6040 Mon Sep 17 00:00:00 2001 From: John Jenkins Date: Sun, 16 Feb 2025 02:17:45 +0000 Subject: [PATCH 09/10] chore: fixup tests --- src/declarations/stencil-private.ts | 2 +- src/runtime/dom-extras.ts | 6 +- src/runtime/slot-polyfill-utils.ts | 102 +++++++++++------- .../wdio/scoped-slot-assigned-methods/cmp.tsx | 2 - test/wdio/scoped-slot-slotchange/cmp.test.tsx | 10 +- 5 files changed, 73 insertions(+), 49 deletions(-) diff --git a/src/declarations/stencil-private.ts b/src/declarations/stencil-private.ts index 0dfb7b9995d..6ff17da01e9 100644 --- a/src/declarations/stencil-private.ts +++ b/src/declarations/stencil-private.ts @@ -1440,7 +1440,7 @@ export interface RenderNode extends HostElement { /** * Node reference: - * This is a reference for a original location node + * This is a reference from an original location node * back to the node that's been moved around. */ ['s-nr']?: PatchedSlotNode | RenderNode; diff --git a/src/runtime/dom-extras.ts b/src/runtime/dom-extras.ts index 1dff57b378e..8120553b653 100644 --- a/src/runtime/dom-extras.ts +++ b/src/runtime/dom-extras.ts @@ -6,8 +6,8 @@ import { addSlotRelocateNode, dispatchSlotChangeEvent, findSlotFromSlottedNode, - getHostSlotChildNodes, getHostSlotNodes, + getSlotChildSiblings, getSlotName, getSlottedChildNodes, updateFallbackSlotVisibility, @@ -98,7 +98,7 @@ export const patchSlotAppendChild = (HostElementPrototype: any) => { if (slotNode) { addSlotRelocateNode(newChild, slotNode); - const slotChildNodes = getHostSlotChildNodes(slotNode, slotName); + const slotChildNodes = getSlotChildSiblings(slotNode, slotName); const appendAfter = slotChildNodes[slotChildNodes.length - 1]; const parent = internalCall(appendAfter, 'parentNode') as d.RenderNode; @@ -158,7 +158,7 @@ export const patchSlotPrepend = (HostElementPrototype: HTMLElement) => { const slotNode = getHostSlotNodes(childNodes, this.tagName, slotName)[0]; if (slotNode) { addSlotRelocateNode(newChild, slotNode, true); - const slotChildNodes = getHostSlotChildNodes(slotNode, slotName); + const slotChildNodes = getSlotChildSiblings(slotNode, slotName); const appendAfter = slotChildNodes[0]; const parent = internalCall(appendAfter, 'parentNode') as d.RenderNode; diff --git a/src/runtime/slot-polyfill-utils.ts b/src/runtime/slot-polyfill-utils.ts index b49a40f1268..f1ca5eda6b3 100644 --- a/src/runtime/slot-polyfill-utils.ts +++ b/src/runtime/slot-polyfill-utils.ts @@ -26,7 +26,7 @@ export const updateFallbackSlotVisibility = (elm: d.RenderNode) => { 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, getSlotName(slotNode), false)?.length) { + if (getSlotChildSiblings(slotNode, getSlotName(slotNode), false)?.length) { // has slotted nodes, hide fallback slotNode.hidden = true; } else { @@ -96,15 +96,13 @@ export function getHostSlotNodes(childNodes: NodeListOf, hostName?: s } /** - * Get all slotted child nodes of a slot node + * 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 - * @param flatten - recursively get slotted nodes of child slot nodes - * (https://developer.mozilla.org/en-US/docs/Web/API/HTMLSlotElement/assignedNodes#flatten) - * @returns slotted child nodes of the slot node + * @returns child nodes of the slot node */ -export const getHostSlotChildNodes = (slot: d.RenderNode, slotName: string, includeSlot = true, flatten?: boolean) => { +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; @@ -112,15 +110,6 @@ export const getHostSlotChildNodes = (slot: d.RenderNode, slotName: string, incl while ((node = node.nextSibling as any)) { if (getSlotName(node) === slotName) childNodes.push(node as any); } - if (flatten && slot.nodeType === NODE_TYPE.ElementNode && !slot.hidden && slot.childNodes.length) { - (slot.childNodes as NodeListOf).forEach((nestedNode) => { - if (nestedNode['s-sr'] && !nestedNode.hidden) { - childNodes.push(...getHostSlotChildNodes(nestedNode, getSlotName(nestedNode), false, true)); - } else { - childNodes.push(nestedNode); - } - }); - } return childNodes; }; @@ -166,37 +155,35 @@ export const addSlotRelocateNode = ( 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; + // newChild already has a slot location node + return; } + const slottedNodeLocation = document.createTextNode('') as any; + slottedNodeLocation['s-nr'] = newChild; + + // if there's no content reference node, or parentNode we can't do anything if (!slotNode['s-cr'] || !slotNode['s-cr'].parentNode) return; const parent = slotNode['s-cr'].parentNode as any; const appendMethod = prepend ? internalCall(parent, 'prepend') : internalCall(parent, 'appendChild'); - if (typeof position !== 'undefined') { - if (BUILD.hydrateClientSide) { - slottedNodeLocation['s-oo'] = position; - const childNodes = internalCall(parent, 'childNodes') as NodeListOf; - const slotRelocateNodes: d.RenderNode[] = [slottedNodeLocation]; - childNodes.forEach((n) => { - if (n['s-nr']) slotRelocateNodes.push(n); - }); + if (BUILD.hydrateClientSide && typeof position !== 'undefined') { + slottedNodeLocation['s-oo'] = position; + const childNodes = internalCall(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'] || 0)) return -1; - else if (!b['s-oo'] || b['s-oo'] < a['s-oo']) return 1; - return 0; - }); - slotRelocateNodes.forEach((n) => appendMethod.call(parent, n)); - } + slotRelocateNodes.sort((a, b) => { + if (!a['s-oo'] || a['s-oo'] < (b['s-oo'] || 0)) 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); } @@ -207,8 +194,8 @@ export const addSlotRelocateNode = ( export const getSlotName = (node: d.PatchedSlotNode) => typeof node['s-sn'] === 'string' - ? node['s-sn'] || (node.nodeType === 1 && (node as Element).getAttribute('slot')) || '' - : undefined; + ? node['s-sn'] + : (node.nodeType === 1 && (node as Element).getAttribute('slot')) || undefined; /** * Add `assignedElements` and `assignedNodes` methods on a fake slot node @@ -220,7 +207,44 @@ export function patchSlotNode(node: d.RenderNode) { const assignedFactory = (elementsOnly: boolean) => function (opts?: { flatten: boolean }) { - const toReturn = getHostSlotChildNodes(this, this['s-sn'], false, opts?.flatten); + const toReturn: d.RenderNode[] = []; + const slotNamesToFind = [this['s-sn']]; + + if ((this['s-sn'], opts?.flatten && this.nodeType === NODE_TYPE.ElementNode && this.childNodes.length)) { + // if we're flattening, we need to find all nested nodes + const getNestedSlotNames = (slot: d.RenderNode) => { + (slot.childNodes as NodeListOf).forEach((nestedNode) => { + if (nestedNode['s-sr']) { + // found a slot node. Let's add it to the list of slot names to find + slotNamesToFind.push(getSlotName(nestedNode)); + if (nestedNode.nodeType === NODE_TYPE.ElementNode && !nestedNode.hidden) { + // this 'slot' is also a `` so we need to drill down recursively + getNestedSlotNames(nestedNode); + } + } else { + toReturn.push(nestedNode as d.RenderNode); + } + }); + }; + getNestedSlotNames(this); + } + + 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 (slotNamesToFind.includes(getSlotName(n))) { + if (toReturn.includes(n)) { + // if the node is already in the list, remove it; + // it may be in the wrong order + toReturn.splice(toReturn.indexOf(n), 1); + } + toReturn.push(n); + } + }); + if (elementsOnly) return toReturn.filter((n) => n.nodeType === NODE_TYPE.ElementNode); return toReturn; }.bind(node); diff --git a/test/wdio/scoped-slot-assigned-methods/cmp.tsx b/test/wdio/scoped-slot-assigned-methods/cmp.tsx index b2ed1ea8663..ae7b910a097 100644 --- a/test/wdio/scoped-slot-assigned-methods/cmp.tsx +++ b/test/wdio/scoped-slot-assigned-methods/cmp.tsx @@ -11,7 +11,6 @@ export class ScopedSlotAssignedMethods { @Method() async getSlotAssignedElements(opts?: { flatten: boolean }, getPlainSlot = false) { if (getPlainSlot) { - console.log('plain slot', this.plainSlot); return this.plainSlot.assignedElements(opts); } return this.fbSlot.assignedElements(opts); @@ -20,7 +19,6 @@ export class ScopedSlotAssignedMethods { @Method() async getSlotAssignedNodes(opts?: { flatten: boolean }, getPlainSlot = false) { if (getPlainSlot) { - console.log('plain slot', this.plainSlot); return this.plainSlot.assignedNodes(opts); } return this.fbSlot.assignedNodes(opts); diff --git a/test/wdio/scoped-slot-slotchange/cmp.test.tsx b/test/wdio/scoped-slot-slotchange/cmp.test.tsx index ae5f17f0e4b..b67b992c38a 100644 --- a/test/wdio/scoped-slot-slotchange/cmp.test.tsx +++ b/test/wdio/scoped-slot-slotchange/cmp.test.tsx @@ -55,8 +55,10 @@ describe('scoped-slot-slotchange', () => { await expect(slotChangeEle.slotEventCatch[0].event.target.name).toBeFalsy(); await expect(slotChangeEle.slotEventCatch[0].assignedNodes[0].outerHTML).toMatch(`

Append child content

`); - p.slot = 'fallback-slot'; - slotChangeEle.appendChild(p); + const p2 = document.createElement('p'); + p2.innerHTML = 'Fallback content'; + p2.slot = 'fallback-slot'; + slotChangeEle.appendChild(p2); await waitForChanges(); await expect(slotChangeEle.slotEventCatch).toHaveLength(2); @@ -68,8 +70,8 @@ describe('scoped-slot-slotchange', () => { slotChangeEle.insertBefore(div, null); await waitForChanges(); - await expect(slotChangeEle.slotEventCatch).toHaveLength(3); + await expect(slotChangeEle.slotEventCatch).toHaveLength(4); await expect(slotChangeEle.slotEventCatch[2]).toMatchObject({ event: { type: 'slotchange' } }); - await expect(slotChangeEle.slotEventCatch[2].assignedNodes[0].outerHTML).toMatch(`
InsertBefore content
`); + await expect(slotChangeEle.slotEventCatch[2].assignedNodes[1].outerHTML).toMatch(`
InsertBefore content
`); }); }); From 92fc04e87fa186774625d88035d05874892830dc Mon Sep 17 00:00:00 2001 From: John Jenkins Date: Tue, 18 Feb 2025 22:18:24 +0000 Subject: [PATCH 10/10] chore: remove `{deepn: true}` --- src/runtime/slot-polyfill-utils.ts | 37 ++++++------------- .../scoped-slot-assigned-methods/cmp.test.tsx | 33 ++++++++++++++--- test/wdio/scoped-slot-slotchange/cmp.tsx | 2 +- 3 files changed, 39 insertions(+), 33 deletions(-) diff --git a/src/runtime/slot-polyfill-utils.ts b/src/runtime/slot-polyfill-utils.ts index f1ca5eda6b3..9c7264e330c 100644 --- a/src/runtime/slot-polyfill-utils.ts +++ b/src/runtime/slot-polyfill-utils.ts @@ -155,7 +155,6 @@ export const addSlotRelocateNode = ( prepend?: boolean, position?: number, ) => { - if (newChild['s-ol'] && newChild['s-ol'].isConnected) { // newChild already has a slot location node return; @@ -208,25 +207,14 @@ export function patchSlotNode(node: d.RenderNode) { const assignedFactory = (elementsOnly: boolean) => function (opts?: { flatten: boolean }) { const toReturn: d.RenderNode[] = []; - const slotNamesToFind = [this['s-sn']]; + const slotName = this['s-sn']; - if ((this['s-sn'], opts?.flatten && this.nodeType === NODE_TYPE.ElementNode && this.childNodes.length)) { - // if we're flattening, we need to find all nested nodes - const getNestedSlotNames = (slot: d.RenderNode) => { - (slot.childNodes as NodeListOf).forEach((nestedNode) => { - if (nestedNode['s-sr']) { - // found a slot node. Let's add it to the list of slot names to find - slotNamesToFind.push(getSlotName(nestedNode)); - if (nestedNode.nodeType === NODE_TYPE.ElementNode && !nestedNode.hidden) { - // this 'slot' is also a `` so we need to drill down recursively - getNestedSlotNames(nestedNode); - } - } else { - toReturn.push(nestedNode as d.RenderNode); - } - }); - }; - getNestedSlotNames(this); + if (opts?.flatten) { + console.error(` + Flattening is not supported for Stencil non-shadow slots. + You can use \`.childNodes\` to nested slot fallback content. + If you have a particular use case, please open an issue on the Stencil repo. + `); } const parent = this['s-cr'].parentElement as d.RenderNode; @@ -235,17 +223,14 @@ export function patchSlotNode(node: d.RenderNode) { (slottedNodes as d.RenderNode[]).forEach((n) => { // find all the nodes assigned to slots we care about - if (slotNamesToFind.includes(getSlotName(n))) { - if (toReturn.includes(n)) { - // if the node is already in the list, remove it; - // it may be in the wrong order - toReturn.splice(toReturn.indexOf(n), 1); - } + if (slotName === getSlotName(n)) { toReturn.push(n); } }); - if (elementsOnly) return toReturn.filter((n) => n.nodeType === NODE_TYPE.ElementNode); + if (elementsOnly) { + return toReturn.filter((n) => n.nodeType === NODE_TYPE.ElementNode); + } return toReturn; }.bind(node); diff --git a/test/wdio/scoped-slot-assigned-methods/cmp.test.tsx b/test/wdio/scoped-slot-assigned-methods/cmp.test.tsx index 5806dd0bfc2..c175b36646e 100644 --- a/test/wdio/scoped-slot-assigned-methods/cmp.test.tsx +++ b/test/wdio/scoped-slot-assigned-methods/cmp.test.tsx @@ -3,6 +3,8 @@ import { render } from '@wdio/browser-runner/stencil'; import { $, expect } from '@wdio/globals'; describe('scoped-slot-assigned-methods', () => { + let originalConsoleError: typeof console.error; + beforeEach(async () => { // @ts-expect-error - no components array? render({ @@ -19,7 +21,18 @@ describe('scoped-slot-assigned-methods', () => { await $('scoped-slot-assigned-methods div').waitForExist(); }); + before(async () => { + originalConsoleError = console.error; + }); + + after(() => { + console.error = originalConsoleError; + }); + it('tests assignedElements method on a ``', async () => { + const errorLogs: string[] = []; + console.error = (message) => errorLogs.push(message); + const component: any = document.querySelector('scoped-slot-assigned-methods'); expect(component.getSlotAssignedElements).toBeDefined(); @@ -32,6 +45,7 @@ describe('scoped-slot-assigned-methods', () => { expect(await component.getSlotAssignedElements()).toHaveLength(0); nodes = await component.getSlotAssignedElements({ flatten: true }); expect(nodes).toHaveLength(0); + expect(errorLogs.length).toEqual(1); const div = document.createElement('div'); div.slot = 'nested-slot'; @@ -40,11 +54,14 @@ describe('scoped-slot-assigned-methods', () => { expect(await component.getSlotAssignedElements()).toHaveLength(0); nodes = await component.getSlotAssignedElements({ flatten: true }); - expect(nodes).toHaveLength(1); - expect(nodes[0].outerHTML).toBe('
Nested slotted content
'); + expect(nodes).toHaveLength(0); + expect(errorLogs.length).toEqual(2); }); it('tests assignedNodes method on a ``', async () => { + const errorLogs: string[] = []; + console.error = (message) => errorLogs.push(message); + const component: any = document.querySelector('scoped-slot-assigned-methods'); expect(component.getSlotAssignedNodes).toBeDefined(); @@ -61,8 +78,8 @@ describe('scoped-slot-assigned-methods', () => { expect(await component.getSlotAssignedNodes()).toHaveLength(0); nodes = await component.getSlotAssignedNodes({ flatten: true }); - expect(nodes).toHaveLength(1); - expect(nodes[0].nodeValue).toBe('Fallback content'); + expect(nodes).toHaveLength(0); + expect(errorLogs.length).toEqual(1); const div = document.createElement('div'); div.slot = 'nested-slot'; @@ -71,11 +88,14 @@ describe('scoped-slot-assigned-methods', () => { expect(await component.getSlotAssignedNodes()).toHaveLength(0); nodes = await component.getSlotAssignedNodes({ flatten: true }); - expect(nodes).toHaveLength(1); - expect(nodes[0].outerHTML).toBe('
Nested slotted content
'); + expect(nodes).toHaveLength(0); + expect(errorLogs.length).toEqual(2); }); it('tests assignedElements / assignedNodes method on a plain slot (a text / comment node)', async () => { + const errorLogs: string[] = []; + console.error = (message) => errorLogs.push(message); + const component: any = document.querySelector('scoped-slot-assigned-methods'); expect(component.getSlotAssignedElements).toBeDefined(); @@ -94,5 +114,6 @@ describe('scoped-slot-assigned-methods', () => { nodes = await component.getSlotAssignedElements({ flatten: true }, true); expect(nodes).toHaveLength(0); + expect(errorLogs.length).toEqual(1); }); }); diff --git a/test/wdio/scoped-slot-slotchange/cmp.tsx b/test/wdio/scoped-slot-slotchange/cmp.tsx index 89790c30221..c014b280191 100644 --- a/test/wdio/scoped-slot-slotchange/cmp.tsx +++ b/test/wdio/scoped-slot-slotchange/cmp.tsx @@ -8,7 +8,7 @@ export class ScopedSlotChange { @Prop({ mutable: true }) slotEventCatch: { event: Event; assignedNodes: Node[] }[] = []; private handleSlotchange = (e) => { - this.slotEventCatch.push({ event: e, assignedNodes: e.target.assignedNodes({ flatten: true }) }); + this.slotEventCatch.push({ event: e, assignedNodes: e.target.assignedNodes() }); }; render() {