diff --git a/src/declarations/stencil-private.ts b/src/declarations/stencil-private.ts index dbe578a169d..d1a785ac1d5 100644 --- a/src/declarations/stencil-private.ts +++ b/src/declarations/stencil-private.ts @@ -1404,7 +1404,7 @@ export interface RenderNode extends HostElement { ['s-sr']?: boolean; /** - * Slot name + * Slot name of either the slot itself or the slotted node */ ['s-sn']?: string; @@ -1441,7 +1441,7 @@ export interface RenderNode extends HostElement { * This is a reference for a original location node * back to the node that's been moved around. */ - ['s-nr']?: RenderNode; + ['s-nr']?: PatchedSlotNode | RenderNode; /** * Original Order: @@ -1526,6 +1526,13 @@ export interface RenderNode extends HostElement { */ __appendChild?: (newChild: T) => T; + /** + * On a `scoped: true` component + * with `experimentalSlotFixes` flag enabled, + * gives access to the original `insertBefore` method + */ + __insertBefore?: (node: T, child: Node | null) => T; + /** * On a `scoped: true` component * with `experimentalSlotFixes` flag enabled, @@ -1534,6 +1541,77 @@ export interface RenderNode extends HostElement { __removeChild?: (child: T) => T; } +export interface PatchedSlotNode extends Node { + /** + * Slot name + */ + ['s-sn']?: string; + + /** + * Original Location Reference: + * A reference pointing to the comment + * which represents the original location + * before it was moved to its slot. + */ + ['s-ol']?: RenderNode; + + /** + * Slot host tag name: + * This is the tag name of the element where this node + * has been moved to during slot relocation. + * + * This allows us to check if the node has been moved and prevent + * us from thinking a node _should_ be moved when it may already be in + * its final destination. + * + * This value is set to `undefined` whenever the node is put back into its original location. + */ + ['s-sh']?: string; + + /** + * 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; + + /** + * On a `scoped: true` component + * with `experimentalSlotFixes` flag enabled, + * returns the actual `parentNode` of the component + */ + __parentNode?: RenderNode; + + /** + * On a `scoped: true` component + * with `experimentalSlotFixes` flag enabled, + * returns the actual `nextSibling` of the component + */ + __nextSibling?: RenderNode; + + /** + * On a `scoped: true` component + * with `experimentalSlotFixes` flag enabled, + * returns the actual `previousSibling` of the component + */ + __previousSibling?: RenderNode; + + /** + * On a `scoped: true` component + * with `experimentalSlotFixes` flag enabled, + * returns the actual `nextElementSibling` of the component + */ + __nextElementSibling?: RenderNode; + + /** + * On a `scoped: true` component + * with `experimentalSlotFixes` flag enabled, + * returns the actual `nextElementSibling` of the component + */ + __previousElementSibling?: RenderNode; +} + export type LazyBundlesRuntimeData = LazyBundleRuntimeData[]; export type LazyBundleRuntimeData = [ diff --git a/src/mock-doc/node.ts b/src/mock-doc/node.ts index 6c25b8c55c1..bbab00ed33d 100644 --- a/src/mock-doc/node.ts +++ b/src/mock-doc/node.ts @@ -182,7 +182,7 @@ export class MockNode { remove() { if (this.parentNode != null) { - this.parentNode.removeChild(this); + (this as any).__parentNode ? (this as any).__parentNode.removeChild(this) : this.parentNode.removeChild(this); } } diff --git a/src/runtime/client-hydrate.ts b/src/runtime/client-hydrate.ts index f8cf819ac88..f63deaff8a8 100644 --- a/src/runtime/client-hydrate.ts +++ b/src/runtime/client-hydrate.ts @@ -3,7 +3,7 @@ import { doc, plt } from '@platform'; import { CMP_FLAGS } from '@utils'; import type * as d from '../declarations'; -import { patchNextPrev } from './dom-extras'; +import { patchSlottedNode } from './dom-extras'; import { createTime } from './profile'; import { COMMENT_NODE_ID, @@ -195,7 +195,7 @@ export const initializeClientHydrate = ( if (BUILD.experimentalSlotFixes) { // patch this node for accessors like `nextSibling` (et al) - patchNextPrev(slottedItem.node); + patchSlottedNode(slottedItem.node); } } diff --git a/src/runtime/dom-extras.ts b/src/runtime/dom-extras.ts index ea840f2fe8f..06da86742f2 100644 --- a/src/runtime/dom-extras.ts +++ b/src/runtime/dom-extras.ts @@ -1,9 +1,7 @@ import { BUILD } from '@app-data'; -import { getHostRef, plt, supportsShadow } from '@platform'; -import { HOST_FLAGS } from '@utils/constants'; +import { supportsShadow } from '@platform'; import type * as d from '../declarations'; -import { PLATFORM_FLAGS } from './runtime-constants'; import { addSlotRelocateNode, getHostSlotChildNodes, @@ -12,7 +10,8 @@ import { getSlottedChildNodes, updateFallbackSlotVisibility, } from './slot-polyfill-utils'; -import { insertBefore } from './vdom/vdom-render'; + +/// HOST ELEMENTS /// export const patchPseudoShadowDom = (hostElementPrototype: HTMLElement) => { patchCloneNode(hostElementPrototype); @@ -22,11 +21,17 @@ export const patchPseudoShadowDom = (hostElementPrototype: HTMLElement) => { patchSlotInsertAdjacentElement(hostElementPrototype); patchSlotInsertAdjacentHTML(hostElementPrototype); patchSlotInsertAdjacentText(hostElementPrototype); + patchInsertBefore(hostElementPrototype); patchTextContent(hostElementPrototype); patchChildSlotNodes(hostElementPrototype); patchSlotRemoveChild(hostElementPrototype); }; +/** + * Patches the `cloneNode` method on a `scoped` Stencil component. + * + * @param HostElementPrototype The Stencil component to be patched + */ export const patchCloneNode = (HostElementPrototype: HTMLElement) => { const orgCloneNode = HostElementPrototype.cloneNode; @@ -93,7 +98,14 @@ export const patchSlotAppendChild = (HostElementPrototype: any) => { const slotChildNodes = getHostSlotChildNodes(slotNode, slotName); const appendAfter = slotChildNodes[slotChildNodes.length - 1]; - const insertedNode = insertBefore(appendAfter.parentNode, newChild, appendAfter.nextSibling as d.RenderNode); + + 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); + } // Check if there is fallback content that should be hidden updateFallbackSlotVisibility(this); @@ -150,7 +162,13 @@ export const patchSlotPrepend = (HostElementPrototype: HTMLElement) => { addSlotRelocateNode(newChild, slotNode, true); const slotChildNodes = getHostSlotChildNodes(slotNode, slotName); const appendAfter = slotChildNodes[0]; - return insertBefore(appendAfter.parentNode, newChild, appendAfter.nextSibling as d.RenderNode); + 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')); + } } if (newChild.nodeType === 1 && !!newChild.getAttribute('slot')) { @@ -223,6 +241,68 @@ export const patchSlotInsertAdjacentText = (HostElementPrototype: HTMLElement) = }; }; +/** + * Patches the `insertBefore` of a non-shadow component. + * + * The *current* node to insert before may not be in the root of our component + * (e.g. if it's 'slotted' it appears in the root, but isn't really) + * + * This tries to find where the *current* node lives within the component and insert the new node before it + * *If* the new node is in the same slot as the *current* node. Otherwise the new node is appended to it's 'slot' + * + * @param HostElementPrototype the custom element prototype to patch + */ +const patchInsertBefore = (HostElementPrototype: HTMLElement) => { + const eleProto: d.RenderNode = HostElementPrototype; + if (eleProto.__insertBefore) return; + + eleProto.__insertBefore = HostElementPrototype.insertBefore; + + HostElementPrototype.insertBefore = function ( + this: d.RenderNode, + newChild: T, + currentChild: d.RenderNode | null, + ) { + const slotName = (newChild['s-sn'] = getSlotName(newChild)); + const slotNode = getHostSlotNodes(this.__childNodes, this.tagName, slotName)[0]; + const slottedNodes = this.__childNodes ? this.childNodes : getSlottedChildNodes(this.childNodes); + + if (slotNode) { + let found = false; + + slottedNodes.forEach((childNode) => { + if (childNode === currentChild || currentChild === null) { + // we found the node to insert before in our list of 'lightDOM' / slotted nodes + found = true; + + if (currentChild === null || slotName !== currentChild['s-sn']) { + // new child is not in the same slot as 'slot before' node + // so let's use the patched appendChild method. This will correctly slot the node + this.appendChild(newChild); + return; + } + + if (slotName === currentChild['s-sn']) { + // 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); + } + } + return; + } + }); + if (found) return newChild; + } + return (this as d.RenderNode).__insertBefore(newChild, currentChild); + }; +}; + /** * Patches the `insertAdjacentElement` method for a slotted node inside a scoped component. Specifically, * we only need to patch the behavior for the specific `beforeend` and `afterbegin` positions so the element @@ -253,7 +333,7 @@ export const patchSlotInsertAdjacentElement = (HostElementPrototype: HTMLElement }; /** - * Patches the text content of an unnamed slotted node inside a scoped component + * Patches the `textContent` of an unnamed slotted node inside a scoped component * * @param hostElementPrototype the `Element` to be patched */ @@ -315,17 +395,9 @@ export const patchChildSlotNodes = (elm: HTMLElement) => { patchHostOriginalAccessor('childNodes', elm); Object.defineProperty(elm, 'childNodes', { get() { - if ( - !plt.$flags$ || - !getHostRef(this)?.$flags$ || - ((plt.$flags$ & PLATFORM_FLAGS.isTmpDisconnected) === 0 && getHostRef(this)?.$flags$ & HOST_FLAGS.hasRendered) - ) { - const result = new FakeNodeList(); - const nodes = getSlottedChildNodes(this.__childNodes); - result.push(...nodes); - return result; - } - return FakeNodeList.from(this.__childNodes); + const result = new FakeNodeList(); + result.push(...getSlottedChildNodes(this.__childNodes)); + return result; }, }); }; @@ -344,11 +416,12 @@ export const patchChildSlotNodes = (elm: HTMLElement) => { * * @param node the slotted node to be patched */ -export const patchNextPrev = (node: Node) => { +export const patchSlottedNode = (node: Node) => { if (!node || (node as any).__nextSibling || !globalThis.Node) return; patchNextSibling(node); patchPreviousSibling(node); + patchParentNode(node); if (node.nodeType === Node.ELEMENT_NODE) { patchNextElementSibling(node as Element); @@ -360,7 +433,6 @@ export const patchNextPrev = (node: Node) => { * Patches the `nextSibling` accessor of a non-shadow slotted node * * @param node the slotted node to be patched - * Required during during testing / mock environnement. */ const patchNextSibling = (node: Node) => { // already been patched? return @@ -383,7 +455,6 @@ const patchNextSibling = (node: Node) => { * Patches the `nextElementSibling` accessor of a non-shadow slotted node * * @param element the slotted element node to be patched - * Required during during testing / mock environnement. */ const patchNextElementSibling = (element: Element) => { if (!element || (element as any).__nextElementSibling) return; @@ -405,7 +476,6 @@ const patchNextElementSibling = (element: Element) => { * Patches the `previousSibling` accessor of a non-shadow slotted node * * @param node the slotted node to be patched - * Required during during testing / mock environnement. */ const patchPreviousSibling = (node: Node) => { if (!node || (node as any).__previousSibling) return; @@ -427,7 +497,6 @@ const patchPreviousSibling = (node: Node) => { * Patches the `previousElementSibling` accessor of a non-shadow slotted node * * @param element the slotted element node to be patched - * Required during during testing / mock environnement. */ const patchPreviousElementSibling = (element: Element) => { if (!element || (element as any).__previousElementSibling) return; @@ -446,6 +515,26 @@ const patchPreviousElementSibling = (element: Element) => { }); }; +/** + * Patches the `parentNode` accessor of a non-shadow slotted node + * + * @param node the slotted node to be patched + */ +export const patchParentNode = (node: Node) => { + if (!node || (node as any).__parentNode) return; + + patchHostOriginalAccessor('parentNode', node); + Object.defineProperty(node, 'parentNode', { + get: function () { + return this['s-ol']?.parentNode || this.__parentNode; + }, + set: function (value) { + // mock-doc sets parentNode? + this.__parentNode = value; + }, + }); +}; + /// UTILS /// const validElementPatches = ['children', 'nextElementSibling', 'previousElementSibling'] as const; @@ -456,6 +545,7 @@ const validNodesPatches = [ 'nextSibling', 'previousSibling', 'textContent', + 'parentNode', ] as const; /** @@ -481,3 +571,19 @@ function patchHostOriginalAccessor( } if (accessor) Object.defineProperty(node, '__' + accessorName, accessor); } + +/** + * Get the original / internal accessor or method of a node or element. + * + * @param node - the node to get the accessor from + * @param method - the name of the accessor to get + * + * @returns the original accessor or method of the node + */ +function intrnlCall(node: T, method: P): T[P] { + if ('__' + method in node) { + return node[('__' + method) as keyof d.RenderNode] as T[P]; + } else { + return node[method]; + } +} diff --git a/src/runtime/slot-polyfill-utils.ts b/src/runtime/slot-polyfill-utils.ts index 16b89da6c6c..275f5d92fdd 100644 --- a/src/runtime/slot-polyfill-utils.ts +++ b/src/runtime/slot-polyfill-utils.ts @@ -45,15 +45,18 @@ export const updateFallbackSlotVisibility = (elm: d.RenderNode) => { /** * Get's the child nodes of a component that are actually slotted. - * This is only required until all patches are unified + * It does this by using root nodes of a component; for each slotted node there is a + * corresponding slot location node which points to the slotted node (via `['s-nr']`). + * + * This is only required until all patches are unified / switched on all the time (then we can rely on `childNodes`) * 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) => { +export const getSlottedChildNodes = (childNodes: NodeListOf): d.PatchedSlotNode[] => { const result = []; for (let i = 0; i < childNodes.length; i++) { - const slottedNode = childNodes[i]['s-nr']; + const slottedNode = ((childNodes[i] as d.RenderNode)['s-nr'] as d.PatchedSlotNode) || undefined; if (slottedNode && slottedNode.isConnected) { result.push(slottedNode); } @@ -68,21 +71,25 @@ export const getSlottedChildNodes = (childNodes: NodeListOf) => { * @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) => { +export function getHostSlotNodes(childNodes: NodeListOf, hostName: string, slotName?: string) { let i = 0; let slottedNodes: d.RenderNode[] = []; let childNode: d.RenderNode; for (; i < childNodes.length; i++) { childNode = childNodes[i] as any; - if (childNode['s-sr'] && childNode['s-hn'] === hostName && (!slotName || childNode['s-sn'] === slotName)) { + if ( + childNode['s-sr'] && + childNode['s-hn'] === hostName && + (slotName === undefined || 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 @@ -138,12 +145,13 @@ export const isNodeLocatedInSlot = (nodeToRelocate: d.RenderNode, slotName: stri * (the order of the slot location nodes determines the order of the slotted nodes in our patched accessors) */ export const addSlotRelocateNode = ( - newChild: d.RenderNode, + newChild: d.PatchedSlotNode, 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']; @@ -181,5 +189,5 @@ export const addSlotRelocateNode = ( newChild['s-sh'] = slotNode['s-hn']; }; -export const getSlotName = (node: d.RenderNode) => +export const getSlotName = (node: d.PatchedSlotNode) => node['s-sn'] || (node.nodeType === 1 && (node as Element).getAttribute('slot')) || ''; diff --git a/src/runtime/test/dom-extras.spec.tsx b/src/runtime/test/dom-extras.spec.tsx index 5d689d58c37..9422201d8e7 100644 --- a/src/runtime/test/dom-extras.spec.tsx +++ b/src/runtime/test/dom-extras.spec.tsx @@ -1,13 +1,13 @@ import { Component, h, Host } from '@stencil/core'; import { newSpecPage, SpecPage } from '@stencil/core/testing'; -import { patchNextPrev, patchPseudoShadowDom } from '../../runtime/dom-extras'; +import { patchPseudoShadowDom, patchSlottedNode } from '../../runtime/dom-extras'; describe('dom-extras - patches for non-shadow dom methods and accessors', () => { let specPage: SpecPage; const nodeOrEleContent = (node: Node | Element) => { - return (node as Element)?.outerHTML || node?.nodeValue.trim(); + return (node as Element)?.outerHTML || node?.nodeValue?.trim(); }; beforeEach(async () => { @@ -102,7 +102,7 @@ describe('dom-extras - patches for non-shadow dom methods and accessors', () => }); it('patches nextSibling / previousSibling accessors of slotted nodes', async () => { - specPage.root.childNodes.forEach((node: Node) => patchNextPrev(node)); + specPage.root.childNodes.forEach((node: Node) => patchSlottedNode(node)); expect(nodeOrEleContent(specPage.root.firstChild)).toBe('Some default slot, slotted text'); expect(nodeOrEleContent(specPage.root.firstChild.nextSibling)).toBe('a default slot, slotted element'); expect(nodeOrEleContent(specPage.root.firstChild.nextSibling.nextSibling)).toBe(``); @@ -122,7 +122,7 @@ describe('dom-extras - patches for non-shadow dom methods and accessors', () => }); it('patches nextElementSibling / previousElementSibling accessors of slotted nodes', async () => { - specPage.root.childNodes.forEach((node: Node) => patchNextPrev(node)); + specPage.root.childNodes.forEach((node: Node) => patchSlottedNode(node)); expect(nodeOrEleContent(specPage.root.children[0].nextElementSibling)).toBe( '
a second slot, slotted element nested element in the second slot
', ); @@ -130,4 +130,14 @@ describe('dom-extras - patches for non-shadow dom methods and accessors', () => 'a default slot, slotted element', ); }); + + it('patches parentNode of slotted nodes', async () => { + specPage.root.childNodes.forEach((node: Node) => patchSlottedNode(node)); + expect(specPage.root.children[0].parentNode.tagName).toBe('CMP-A'); + expect(specPage.root.children[1].parentNode.tagName).toBe('CMP-A'); + expect(specPage.root.childNodes[0].parentNode.tagName).toBe('CMP-A'); + expect(specPage.root.childNodes[1].parentNode.tagName).toBe('CMP-A'); + expect(specPage.root.children[0].__parentNode.tagName).toBe('DIV'); + expect(specPage.root.childNodes[0].__parentNode.tagName).toBe('DIV'); + }); }); diff --git a/src/runtime/vdom/util.ts b/src/runtime/vdom/util.ts index 1d6600d354a..4d79aeea074 100755 --- a/src/runtime/vdom/util.ts +++ b/src/runtime/vdom/util.ts @@ -19,7 +19,7 @@ export function toVNode(node: Node): d.VNode | null { const vnode: d.VNode = newVNode(node.nodeName.toLowerCase(), null); vnode.$elm$ = node; - const childNodes = node.childNodes; + const childNodes = (node as any).__childNodes || node.childNodes; let childVnode: d.VNode; for (let i = 0, l = childNodes.length; i < l; i++) { diff --git a/src/runtime/vdom/vdom-annotations.ts b/src/runtime/vdom/vdom-annotations.ts index 34d3ec3b45f..b432f7c3737 100644 --- a/src/runtime/vdom/vdom-annotations.ts +++ b/src/runtime/vdom/vdom-annotations.ts @@ -38,7 +38,7 @@ export const insertVdomAnnotations = (doc: Document, staticComponents: string[]) orgLocationNodes.forEach((orgLocationNode) => { if (orgLocationNode != null && orgLocationNode['s-nr']) { - const nodeRef = orgLocationNode['s-nr']; + const nodeRef = orgLocationNode['s-nr'] as d.RenderNode; let hostId = nodeRef['s-host-id']; let nodeId = nodeRef['s-node-id']; diff --git a/src/runtime/vdom/vdom-render.ts b/src/runtime/vdom/vdom-render.ts index 81b508b38bd..179c8457e11 100644 --- a/src/runtime/vdom/vdom-render.ts +++ b/src/runtime/vdom/vdom-render.ts @@ -8,9 +8,10 @@ */ import { BUILD } from '@app-data'; import { consoleDevError, doc, plt, supportsShadow } from '@platform'; -import { CMP_FLAGS, HTML_NS, isDef, SVG_NS } from '@utils'; +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 { h, isHost, newVNode } from './h'; @@ -853,12 +854,29 @@ export const nullifyVNodeRefs = (vNode: d.VNode) => { * @param reference anchor element * @returns inserted node */ -export const insertBefore = (parent: Node, newNode: d.RenderNode, reference?: d.RenderNode): Node => { +export const insertBefore = ( + parent: Node, + newNode: d.RenderNode, + reference?: d.RenderNode | d.PatchedSlotNode, +): Node => { if (BUILD.scoped && typeof newNode['s-sn'] === 'string' && !!newNode['s-sr'] && !!newNode['s-cr']) { + // this is a slot node addRemoveSlotScopedClass(newNode['s-cr'], newNode, parent as d.RenderNode, newNode.parentElement); + } else if (BUILD.experimentalSlotFixes && typeof newNode['s-sn'] === 'string') { + // this is a slotted node. + if (parent.getRootNode().nodeType !== NODE_TYPES.DOCUMENT_FRAGMENT_NODE) { + // we don't need to patch this node if it's nested in a shadow root + patchParentNode(newNode); + } + // potentially use the patched insertBefore method. This will correctly slot the new node + return parent.insertBefore(newNode, reference); + } + + if (BUILD.experimentalSlotFixes && (parent as d.RenderNode).__insertBefore) { + return (parent as d.RenderNode).__insertBefore(newNode, reference) as d.RenderNode; + } else { + return parent?.insertBefore(newNode, reference) as d.RenderNode; } - const inserted = parent?.insertBefore(newNode, reference); - return inserted; }; /** @@ -1065,9 +1083,13 @@ render() { ) { let orgLocationNode = nodeToRelocate['s-ol']?.previousSibling as d.RenderNode | null; while (orgLocationNode) { - let refNode = orgLocationNode['s-nr'] ?? null; + let refNode = (orgLocationNode['s-nr'] as d.RenderNode) ?? null; - if (refNode && refNode['s-sn'] === nodeToRelocate['s-sn'] && parentNodeRef === refNode.parentNode) { + if ( + refNode && + refNode['s-sn'] === nodeToRelocate['s-sn'] && + parentNodeRef === ((refNode as d.PatchedSlotNode).__parentNode || refNode.parentNode) + ) { refNode = refNode.nextSibling as d.RenderNode | null; // If the refNode is the same node to be relocated or another element's slot reference, keep searching to find the @@ -1086,10 +1108,9 @@ render() { } } - if ( - (!insertBeforeNode && parentNodeRef !== nodeToRelocate.parentNode) || - nodeToRelocate.nextSibling !== insertBeforeNode - ) { + const parent = (nodeToRelocate as d.PatchedSlotNode).__parentNode || nodeToRelocate.parentNode; + const nextSibling = (nodeToRelocate as d.PatchedSlotNode).__nextSibling || nodeToRelocate.nextSibling; + if ((!insertBeforeNode && parentNodeRef !== parent) || nextSibling !== insertBeforeNode) { // we've checked that it's worth while to relocate // since that the node to relocate // has a different next sibling or parent relocated diff --git a/test/wdio/scoped-slot-insertbefore/cmp.test.tsx b/test/wdio/scoped-slot-insertbefore/cmp.test.tsx new file mode 100644 index 00000000000..93665aa3889 --- /dev/null +++ b/test/wdio/scoped-slot-insertbefore/cmp.test.tsx @@ -0,0 +1,97 @@ +import { Fragment, h } from '@stencil/core'; +import { render } from '@wdio/browser-runner/stencil'; + +describe('testing a `scoped="true"` component `insertBefore` method', () => { + let host: HTMLScopedSlotInsertBeforeElement; + let defaultSlot: HTMLDivElement; + let startSlot: HTMLDivElement; + let endSlot: HTMLDivElement; + + beforeEach(async () => { + render({ + template: () => ( + <> + +

My initial slotted content.

+
+ + ), + }); + + await $('#parentDiv').waitForExist(); + host = document.querySelector('scoped-slot-insertbefore'); + startSlot = host.querySelector('#parentDiv .start-slot'); + endSlot = host.querySelector('#parentDiv .end-slot'); + defaultSlot = host.querySelector('#parentDiv .default-slot'); + }); + + it('slots nodes in the correct order when they have the same slot', async () => { + expect(defaultSlot.children.length).toBe(2); + expect(startSlot.children.length).toBe(1); + expect(endSlot.children.length).toBe(1); + + const el1 = document.createElement('p'); + const el2 = document.createElement('p'); + const el3 = document.createElement('p'); + el1.innerText = 'Content 1. '; + el2.innerText = 'Content 2. '; + el3.innerText = 'Content 3. '; + + host.insertBefore(el1, null); + host.insertBefore(el2, el1); + host.insertBefore(el3, el2); + + expect(defaultSlot.children.length).toBe(5); + expect(startSlot.children.length).toBe(1); + expect(endSlot.children.length).toBe(1); + expect(defaultSlot.textContent).toBe( + `Default slot is here:My initial slotted content.Content 3. Content 2. Content 1. `, + ); + }); + + it('slots nodes in the correct slot despite the insertion order', async () => { + expect(defaultSlot.children.length).toBe(2); + expect(startSlot.children.length).toBe(1); + expect(endSlot.children.length).toBe(1); + + const el1 = document.createElement('p'); + const el2 = document.createElement('p'); + const el3 = document.createElement('p'); + el1.innerText = 'Content 1. '; + el1.slot = 'start-slot'; + el2.innerText = 'Content 2. '; + el2.slot = 'end-slot'; + el3.innerText = 'Content 3. '; + + host.insertBefore(el1, null); + host.insertBefore(el2, el1); + host.insertBefore(el3, el2); + + expect(defaultSlot.children.length).toBe(3); + expect(startSlot.children.length).toBe(2); + expect(endSlot.children.length).toBe(2); + expect(host.textContent).toBe(`My initial slotted content.Content 1. Content 2. Content 3. `); + }); + + it('can still use original `insertBefore` method', async () => { + expect(defaultSlot.children.length).toBe(2); + expect(startSlot.children.length).toBe(1); + expect(endSlot.children.length).toBe(1); + + const el1 = document.createElement('p'); + const el2 = document.createElement('p'); + el1.innerText = 'Content 1. '; + el2.innerText = 'Content 2. '; + el2.slot = 'end-slot'; + + host.__insertBefore(el1, null); + host.__insertBefore(el2, host.querySelector('#parentDiv')); + + expect(host.firstElementChild.textContent).toBe('Content 2. '); + expect(host.lastElementChild.textContent).toBe('Content 1. '); + + expect(defaultSlot.children.length).toBe(2); + expect(startSlot.children.length).toBe(1); + expect(endSlot.children.length).toBe(1); + }); +}); diff --git a/test/wdio/scoped-slot-insertbefore/cmp.tsx b/test/wdio/scoped-slot-insertbefore/cmp.tsx new file mode 100644 index 00000000000..46fa99d2a81 --- /dev/null +++ b/test/wdio/scoped-slot-insertbefore/cmp.tsx @@ -0,0 +1,28 @@ +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'scoped-slot-insertbefore', + scoped: true, +}) +export class ScopedSlotInsertBefore { + render() { + return ( +
+
+
Start slot is here:
+ +
+ +
+
Default slot is here:
+ +
+ +
+
End slot is here:
+ +
+
+ ); + } +} diff --git a/test/wdio/scoped-slot-slotted-parentnode/cmp.test.tsx b/test/wdio/scoped-slot-slotted-parentnode/cmp.test.tsx new file mode 100644 index 00000000000..f366279cd88 --- /dev/null +++ b/test/wdio/scoped-slot-slotted-parentnode/cmp.test.tsx @@ -0,0 +1,29 @@ +import { h } from '@stencil/core'; +import { render } from '@wdio/browser-runner/stencil'; + +describe('checks slotted node parentNode', () => { + beforeEach(async () => { + render({ + template: () => ( + + A text node
An element
+
+ ), + }); + await $('cmp-slotted-parentnode label').waitForExist(); + }); + + it('slotted nodes and elements `parentNode` do not return component internals', async () => { + expect((document.querySelector('cmp-slotted-parentnode').children[0].parentNode as Element).tagName).toBe( + 'CMP-SLOTTED-PARENTNODE', + ); + expect((document.querySelector('cmp-slotted-parentnode').childNodes[0].parentNode as Element).tagName).toBe( + 'CMP-SLOTTED-PARENTNODE', + ); + }); + + it('slotted nodes and elements `__parentNode` return component internals', async () => { + expect((document.querySelector('cmp-slotted-parentnode').children[0] as any).__parentNode.tagName).toBe('LABEL'); + expect((document.querySelector('cmp-slotted-parentnode').childNodes[0] as any).__parentNode.tagName).toBe('LABEL'); + }); +}); diff --git a/test/wdio/scoped-slot-slotted-parentnode/cmp.tsx b/test/wdio/scoped-slot-slotted-parentnode/cmp.tsx new file mode 100644 index 00000000000..d3047883a54 --- /dev/null +++ b/test/wdio/scoped-slot-slotted-parentnode/cmp.tsx @@ -0,0 +1,17 @@ +import { Component, h, Host } from '@stencil/core'; + +@Component({ + tag: 'cmp-slotted-parentnode', + scoped: true, +}) +export class CmpSlottedParentnode { + render() { + return ( + + + + ); + } +}