Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(runtime): add root scope id to the user provided nested children as classname #5750

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/runtime/connected-callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
};
11 changes: 6 additions & 5 deletions src/runtime/dom-extras.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -47,6 +47,7 @@ export const patchCloneNode = (HostElementPrototype: HTMLElement) => {
's-nr',
's-si',
's-rf',
's-rsc',
];

for (; i < srcNode.childNodes.length; i++) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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')) {
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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);
}
}
},
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/test/shadow.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,14 @@ describe('shadow', () => {
<cmp-a class="hydrated sc-cmp-a-h">
<!---->
<div class="sc-cmp-a sc-cmp-a-s">
<span slot=\"start\">
<span class="sc-cmp-a" slot=\"start\">
Start
</span>
<span class='sc-cmp-a sc-cmp-a-s'>
Text
</span>
<div class='end sc-cmp-a sc-cmp-a-s'>
<span slot=\"end\">
<span class="sc-cmp-a" slot=\"end\">
End
</span>
</div>
Expand Down
5 changes: 3 additions & 2 deletions src/runtime/vdom/vdom-annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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']) {
Expand Down
77 changes: 55 additions & 22 deletions src/runtime/vdom/vdom-render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}
}
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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$);
}
}
}
Expand Down Expand Up @@ -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
* `<slot>` elements in scoped (i.e. non-shadow DOM) components
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions test/wdio/scoped-id-in-nested-classname/cmp-level-1.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
cmp-level-2 {
cmp-level-3 {
padding: 32px;

#test-element {
padding: 24px;
}
}
}
}
6 changes: 5 additions & 1 deletion test/wdio/scoped-id-in-nested-classname/cmp-level-1.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { Component, h } from '@stencil/core';
})
export class CmpLevel1 {
render() {
return <cmp-level-2></cmp-level-2>;
return (
<cmp-level-2>
<slot />
</cmp-level-2>
);
}
}
19 changes: 15 additions & 4 deletions test/wdio/scoped-id-in-nested-classname/cmp.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => <cmp-level-1></cmp-level-1>,
});
});

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: () => (
<cmp-level-1>
<span id="test-element">Test</span>
</cmp-level-1>
),
});
await expect($('#test-element')).toHaveElementClass('sc-cmp-level-1');

const appliedCss = await (await $('#test-element')).getCSSProperty('padding');
await expect(appliedCss.parsed.value).toBe(24);
});
});