diff --git a/src/runtime/connected-callback.ts b/src/runtime/connected-callback.ts index 947a2ee5f07..e83db09d5f1 100644 --- a/src/runtime/connected-callback.ts +++ b/src/runtime/connected-callback.ts @@ -9,6 +9,7 @@ import { createTime } from './profile'; import { HYDRATE_ID, NODE_TYPE, PLATFORM_FLAGS } from './runtime-constants'; import { addStyle } from './styles'; import { attachToAncestor } from './update-component'; +import { insertBefore } from './vdom/vdom-render'; export const connectedCallback = (elm: d.HostElement) => { if ((plt.$flags$ & PLATFORM_FLAGS.isTmpDisconnected) === 0) { @@ -128,5 +129,5 @@ const setContentReference = (elm: d.HostElement) => { BUILD.isDebug ? `content-ref (host=${elm.localName})` : '', ) as any); contentRefElm['s-cn'] = true; - elm.insertBefore(contentRefElm, elm.firstChild); + insertBefore(elm, contentRefElm, elm.firstChild); }; diff --git a/src/runtime/dom-extras.ts b/src/runtime/dom-extras.ts index f069392903e..a2edcccd34b 100644 --- a/src/runtime/dom-extras.ts +++ b/src/runtime/dom-extras.ts @@ -4,7 +4,7 @@ import { CMP_FLAGS, HOST_FLAGS, NODE_TYPES } from '@utils/constants'; import type * as d from '../declarations'; import { PLATFORM_FLAGS } from './runtime-constants'; -import { updateFallbackSlotVisibility } from './vdom/vdom-render'; +import { insertBefore, updateFallbackSlotVisibility } from './vdom/vdom-render'; export const patchPseudoShadowDom = ( hostElementPrototype: HTMLElement, @@ -47,6 +47,7 @@ export const patchCloneNode = (HostElementPrototype: HTMLElement) => { 's-nr', 's-si', 's-rf', + 's-rsc', ]; for (; i < srcNode.childNodes.length; i++) { @@ -84,7 +85,7 @@ export const patchSlotAppendChild = (HostElementPrototype: any) => { if (slotNode) { const slotChildNodes = getHostSlotChildNodes(slotNode, slotName); const appendAfter = slotChildNodes[slotChildNodes.length - 1]; - const insertedNode = appendAfter.parentNode.insertBefore(newChild, appendAfter.nextSibling); + const insertedNode = insertBefore(appendAfter.parentNode, newChild, appendAfter.nextSibling); // Check if there is fallback content that should be hidden updateFallbackSlotVisibility(this); @@ -149,7 +150,7 @@ export const patchSlotPrepend = (HostElementPrototype: HTMLElement) => { const slotChildNodes = getHostSlotChildNodes(slotNode, slotName); const appendAfter = slotChildNodes[0]; - return appendAfter.parentNode.insertBefore(newChild, appendAfter.nextSibling); + return insertBefore(appendAfter.parentNode, newChild, appendAfter.nextSibling); } if (newChild.nodeType === 1 && !!newChild.getAttribute('slot')) { @@ -309,7 +310,7 @@ export const patchTextContent = (hostElementPrototype: HTMLElement): void => { if (node['s-sn'] === '') { const textNode = this.ownerDocument.createTextNode(value); textNode['s-sn'] = ''; - node.parentElement.insertBefore(textNode, node.nextSibling); + insertBefore(node.parentElement, textNode, node.nextSibling); } else { node.remove(); } @@ -352,7 +353,7 @@ export const patchTextContent = (hostElementPrototype: HTMLElement): void => { this.__textContent = value; const contentRefElm = this['s-cr']; if (contentRefElm) { - this.insertBefore(contentRefElm, this.firstChild); + insertBefore(this, contentRefElm, this.firstChild); } } }, diff --git a/src/runtime/test/shadow.spec.tsx b/src/runtime/test/shadow.spec.tsx index 856427d8684..6cffdea5655 100644 --- a/src/runtime/test/shadow.spec.tsx +++ b/src/runtime/test/shadow.spec.tsx @@ -87,14 +87,14 @@ describe('shadow', () => {
- + Start Text
- + End
diff --git a/src/runtime/vdom/vdom-annotations.ts b/src/runtime/vdom/vdom-annotations.ts index 09541d7ae8f..431d41f8d62 100644 --- a/src/runtime/vdom/vdom-annotations.ts +++ b/src/runtime/vdom/vdom-annotations.ts @@ -10,6 +10,7 @@ import { SLOT_NODE_ID, TEXT_NODE_ID, } from '../runtime-constants'; +import { insertBefore } from './vdom-render'; /** * Updates the DOM generated on the server with annotations such as node attributes and @@ -58,7 +59,7 @@ export const insertVdomAnnotations = (doc: Document, staticComponents: string[]) } const commentBeforeTextNode = doc.createComment(childId); commentBeforeTextNode.nodeValue = `${TEXT_NODE_ID}.${childId}`; - nodeRef.parentNode?.insertBefore(commentBeforeTextNode, nodeRef); + insertBefore(nodeRef.parentNode, commentBeforeTextNode, nodeRef); } } @@ -220,7 +221,7 @@ const insertChildVNodeAnnotations = ( const textNodeId = `${TEXT_NODE_ID}.${childId}`; const commentBeforeTextNode = doc.createComment(textNodeId); - parentNode?.insertBefore(commentBeforeTextNode, childElm); + insertBefore(parentNode, commentBeforeTextNode, childElm); } } else if (childElm.nodeType === NODE_TYPE.CommentNode) { if (childElm['s-sr']) { diff --git a/src/runtime/vdom/vdom-render.ts b/src/runtime/vdom/vdom-render.ts index 559e0ba91c7..8217a29661e 100644 --- a/src/runtime/vdom/vdom-render.ts +++ b/src/runtime/vdom/vdom-render.ts @@ -111,19 +111,6 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: elm.classList.add((elm['s-si'] = scopeId)); } - if (BUILD.scoped) { - // to be able to style the deep nested scoped component from the root component, - // root component's scope id needs to be added to the child nodes since sass compiler - // adds scope id to the nested selectors during compilation phase - const rootScopeId = - newParentVNode.$elm$?.['s-rsc'] || newParentVNode.$elm$?.['s-si'] || newParentVNode.$elm$?.['s-sc']; - - if (rootScopeId) { - elm['s-rsc'] = rootScopeId; - !elm.classList.contains(rootScopeId) && elm.classList.add(rootScopeId); - } - } - if (newVNode.$children$) { for (i = 0; i < newVNode.$children$.length; ++i) { // create the node @@ -206,7 +193,7 @@ const relocateToHostRoot = (parentElm: Element) => { for (const childNode of contentRefNode ? childNodeArray.reverse() : childNodeArray) { // Only relocate nodes that were slotted in if (childNode['s-sh'] != null) { - host.insertBefore(childNode, contentRefNode ?? null); + insertBefore(host, childNode, contentRefNode ?? null); // Reset so we can correctly move the node around again. childNode['s-sh'] = undefined; @@ -237,7 +224,7 @@ const putBackInOriginalLocation = (parentElm: d.RenderNode, recursive: boolean) const childNode = oldSlotChildNodes[i] as any; if (childNode['s-hn'] !== hostTagName && childNode['s-ol']) { // and relocate it back to it's original location - parentReferenceNode(childNode).insertBefore(childNode, referenceNode(childNode)); + insertBefore(parentReferenceNode(childNode), childNode, referenceNode(childNode)); // remove the old original location comment entirely // later on the patch function will know what to do @@ -293,7 +280,7 @@ const addVnodes = ( childNode = createElm(null, parentVNode, startIdx, parentElm); if (childNode) { vnodes[startIdx].$elm$ = childNode as any; - containerElm.insertBefore(childNode, BUILD.slotRelocation ? referenceNode(before) : before); + insertBefore(containerElm, childNode, BUILD.slotRelocation ? referenceNode(before) : before); } } } @@ -490,7 +477,7 @@ const updateChildren = ( // `parentElm`. Luckily, `Node.nextSibling` will return `null` if there // aren't any siblings, and passing `null` to `Node.insertBefore` will // append it to the children of the parent element. - parentElm.insertBefore(oldStartVnode.$elm$, oldEndVnode.$elm$.nextSibling as any); + insertBefore(parentElm, oldStartVnode.$elm$, oldEndVnode.$elm$.nextSibling as any); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; } else if (isSameVnode(oldEndVnode, newStartVnode, isInitialRender)) { @@ -518,7 +505,7 @@ const updateChildren = ( // can move the element for `oldEndVnode` _before_ the element for // `oldStartVnode`, leaving `oldStartVnode` to be reconciled in the // future. - parentElm.insertBefore(oldEndVnode.$elm$, oldStartVnode.$elm$); + insertBefore(parentElm, oldEndVnode.$elm$, oldStartVnode.$elm$); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { @@ -569,9 +556,9 @@ const updateChildren = ( if (node) { // if we created a new node then handle inserting it to the DOM if (BUILD.slotRelocation) { - parentReferenceNode(oldStartVnode.$elm$).insertBefore(node, referenceNode(oldStartVnode.$elm$)); + insertBefore(parentReferenceNode(oldStartVnode.$elm$), node, referenceNode(oldStartVnode.$elm$)); } else { - oldStartVnode.$elm$.parentNode.insertBefore(node, oldStartVnode.$elm$); + insertBefore(oldStartVnode.$elm$.parentNode, node, oldStartVnode.$elm$); } } } @@ -924,6 +911,52 @@ export const nullifyVNodeRefs = (vNode: d.VNode) => { } }; +/** + * Inserts a node before a reference node as a child of a specified parent node. + * Additionally, adds parent element's scope id as class to the new node. + * + * @param parent parent node + * @param newNode element to be inserted + * @param reference anchor element + * @returns inserted node + */ +export const insertBefore = (parent: Node, newNode: Node, reference?: Node): Node => { + const inserted = parent?.insertBefore(newNode, reference); + + if (BUILD.scoped) { + setParentScopeIdAsClassName(newNode as d.RenderNode, parent as d.RenderNode); + } + + return inserted; +}; + +const findParentScopeId = (element: d.RenderNode): string | undefined => { + return element + ? element['s-rsc'] || element['s-si'] || element['s-sc'] || findParentScopeId(element.parentElement) + : undefined; +}; + +/** + * to be able to style the deep nested scoped component from the root component, + * root component's scope id needs to be added to the child nodes since sass compiler + * adds scope id to the nested selectors during compilation phase + * + * @param element an element to be updated + * @param parent a parent element that scope id is retrieved + */ +export const setParentScopeIdAsClassName = (element: d.RenderNode, parent: d.RenderNode) => { + if (element && parent) { + const oldRootScopeId = element['s-rsc']; + const newRootScopeId = findParentScopeId(parent); + + oldRootScopeId && element.classList?.contains(oldRootScopeId) && element.classList.remove(oldRootScopeId); + if (newRootScopeId) { + element['s-rsc'] = newRootScopeId; + !element.classList?.contains(newRootScopeId) && element.classList?.add(newRootScopeId); + } + } +}; + /** * Information about nodes to be relocated in order to support * `` elements in scoped (i.e. non-shadow DOM) components @@ -1045,7 +1078,7 @@ render() { : (doc.createTextNode('') as any); orgLocationNode['s-nr'] = nodeToRelocate; - nodeToRelocate.parentNode.insertBefore((nodeToRelocate['s-ol'] = orgLocationNode), nodeToRelocate); + insertBefore(nodeToRelocate.parentNode, (nodeToRelocate['s-ol'] = orgLocationNode), nodeToRelocate); } } @@ -1115,7 +1148,7 @@ render() { // If we get to this point and `insertBeforeNode` is `null`, that means // we're just going to append the node as the last child of the parent. Passing // `null` as the second arg here will trigger that behavior. - parentNodeRef.insertBefore(nodeToRelocate, insertBeforeNode); + insertBefore(parentNodeRef, nodeToRelocate, insertBeforeNode); // Reset the `hidden` value back to what it was defined as originally // This solves a problem where a `slot` is dynamically rendered and `hidden` may have diff --git a/test/wdio/scoped-id-in-nested-classname/cmp-level-1.scss b/test/wdio/scoped-id-in-nested-classname/cmp-level-1.scss index 7164e48b3cd..d1fe7df9355 100644 --- a/test/wdio/scoped-id-in-nested-classname/cmp-level-1.scss +++ b/test/wdio/scoped-id-in-nested-classname/cmp-level-1.scss @@ -2,6 +2,10 @@ cmp-level-2 { cmp-level-3 { padding: 32px; + + #test-element { + padding: 24px; + } } } } diff --git a/test/wdio/scoped-id-in-nested-classname/cmp-level-1.tsx b/test/wdio/scoped-id-in-nested-classname/cmp-level-1.tsx index 7455647a059..71139176592 100644 --- a/test/wdio/scoped-id-in-nested-classname/cmp-level-1.tsx +++ b/test/wdio/scoped-id-in-nested-classname/cmp-level-1.tsx @@ -8,6 +8,10 @@ import { Component, h } from '@stencil/core'; }) export class CmpLevel1 { render() { - return ; + return ( + + + + ); } } diff --git a/test/wdio/scoped-id-in-nested-classname/cmp.test.tsx b/test/wdio/scoped-id-in-nested-classname/cmp.test.tsx index 39cfa1e3c8a..49657d7f55b 100644 --- a/test/wdio/scoped-id-in-nested-classname/cmp.test.tsx +++ b/test/wdio/scoped-id-in-nested-classname/cmp.test.tsx @@ -2,16 +2,27 @@ import { h } from '@stencil/core'; import { render } from '@wdio/browser-runner/stencil'; describe('scope-id-in-nested-classname', function () { - beforeEach(() => { + it('should have root scope id in the nested element as classname', async () => { render({ template: () => , }); - }); - - it('should have root scope id in the nested element as classname', async () => { await expect($('cmp-level-3')).toHaveElementClass('sc-cmp-level-1'); const appliedCss = await (await $('cmp-level-3')).getCSSProperty('padding'); await expect(appliedCss.parsed.value).toBe(32); }); + + it('should have root scope id in the user provided nested element as classname', async () => { + render({ + template: () => ( + + Test + + ), + }); + await expect($('#test-element')).toHaveElementClass('sc-cmp-level-1'); + + const appliedCss = await (await $('#test-element')).getCSSProperty('padding'); + await expect(appliedCss.parsed.value).toBe(24); + }); });