diff --git a/src/declarations/stencil-private.ts b/src/declarations/stencil-private.ts index 137beec3f8a..2835cebac34 100644 --- a/src/declarations/stencil-private.ts +++ b/src/declarations/stencil-private.ts @@ -1121,11 +1121,11 @@ export interface HostElement extends HTMLElement { ['s-sc']?: string; /** - * Root Scope Id - * The scope id of the root component when using scoped css encapsulation + * Scope Ids + * All the possible scope ids of this component when using scoped css encapsulation * or using shadow dom but the browser doesn't support it */ - ['s-rsc']?: string; + ['s-scs']?: string[]; /** * Hot Module Replacement, dev mode only diff --git a/src/runtime/dom-extras.ts b/src/runtime/dom-extras.ts index a2edcccd34b..acbc0d0ed3c 100644 --- a/src/runtime/dom-extras.ts +++ b/src/runtime/dom-extras.ts @@ -47,7 +47,7 @@ export const patchCloneNode = (HostElementPrototype: HTMLElement) => { 's-nr', 's-si', 's-rf', - 's-rsc', + 's-scs', ]; for (; i < srcNode.childNodes.length; i++) { diff --git a/src/runtime/test/scoped.spec.tsx b/src/runtime/test/scoped.spec.tsx index 890a255123a..531b66f48de 100644 --- a/src/runtime/test/scoped.spec.tsx +++ b/src/runtime/test/scoped.spec.tsx @@ -43,7 +43,7 @@ describe('scoped', () => {
- + Hola
diff --git a/src/runtime/vdom/vdom-render.ts b/src/runtime/vdom/vdom-render.ts index 8217a29661e..d00c8ab7454 100644 --- a/src/runtime/vdom/vdom-render.ts +++ b/src/runtime/vdom/vdom-render.ts @@ -111,6 +111,10 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: elm.classList.add((elm['s-si'] = scopeId)); } + if (BUILD.scoped) { + updateElementScopeIds(elm as d.RenderNode, parentElm as d.RenderNode); + } + if (newVNode.$children$) { for (i = 0; i < newVNode.$children$.length; ++i) { // create the node @@ -913,7 +917,7 @@ 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. + * Additionally, adds parent elements' scope ids as class names to the new node. * * @param parent parent node * @param newNode element to be inserted @@ -924,35 +928,50 @@ export const insertBefore = (parent: Node, newNode: Node, reference?: Node): Nod const inserted = parent?.insertBefore(newNode, reference); if (BUILD.scoped) { - setParentScopeIdAsClassName(newNode as d.RenderNode, parent as d.RenderNode); + updateElementScopeIds(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; +const findScopeIds = (element: d.RenderNode): string[] => { + const scopeIds: string[] = []; + if (element) { + scopeIds.push( + ...(element['s-scs'] || []), + element['s-si'], + element['s-sc'], + ...findScopeIds(element.parentElement), + ); + } + return scopeIds; }; /** - * to be able to style the deep nested scoped component from the root component, - * root component's scope id needs to be added to the child nodes since sass compiler + * To be able to style the deep nested scoped component from the parent components, + * all the scope ids of its parents need to be added to the child node since sass compiler * adds scope id to the nested selectors during compilation phase * * @param element an element to be updated * @param parent a parent element that scope id is retrieved + * @param iterateChildNodes iterate child nodes */ -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); +const updateElementScopeIds = (element: d.RenderNode, parent: d.RenderNode, iterateChildNodes = false) => { + if (element && parent && element.nodeType === NODE_TYPE.ElementNode) { + const scopeIds = new Set(findScopeIds(parent).filter(Boolean)); + if (scopeIds.size) { + element.classList?.add(...(element['s-scs'] = [...scopeIds])); + + if (element['s-ol'] || iterateChildNodes) { + /** + * If the element has an original location, this means element is relocated. + * So, we need to notify the child nodes to update their new scope ids since + * the DOM structure is changed. + */ + for (const childNode of Array.from(element.childNodes)) { + updateElementScopeIds(childNode as d.RenderNode, element, true); + } + } } } }; diff --git a/test/wdio/scoped-id-in-nested-classname/cmp-level-2.scss b/test/wdio/scoped-id-in-nested-classname/cmp-level-2.scss new file mode 100644 index 00000000000..930681f81bb --- /dev/null +++ b/test/wdio/scoped-id-in-nested-classname/cmp-level-2.scss @@ -0,0 +1,9 @@ +:host { + cmp-level-3 { + font-weight: 800; + + #test-element { + font-weight: 600; + } + } +} diff --git a/test/wdio/scoped-id-in-nested-classname/cmp-level-2.tsx b/test/wdio/scoped-id-in-nested-classname/cmp-level-2.tsx index ca967ac5705..44ed759fa25 100644 --- a/test/wdio/scoped-id-in-nested-classname/cmp-level-2.tsx +++ b/test/wdio/scoped-id-in-nested-classname/cmp-level-2.tsx @@ -2,6 +2,7 @@ import { Component, h } from '@stencil/core'; @Component({ tag: 'cmp-level-2', + styleUrl: 'cmp-level-2.scss', shadow: false, scoped: true, }) 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 49657d7f55b..4791d99ca8a 100644 --- a/test/wdio/scoped-id-in-nested-classname/cmp.test.tsx +++ b/test/wdio/scoped-id-in-nested-classname/cmp.test.tsx @@ -7,9 +7,13 @@ describe('scope-id-in-nested-classname', function () { template: () => , }); await expect($('cmp-level-3')).toHaveElementClass('sc-cmp-level-1'); + await expect($('cmp-level-3')).toHaveElementClass('sc-cmp-level-2'); - const appliedCss = await (await $('cmp-level-3')).getCSSProperty('padding'); - await expect(appliedCss.parsed.value).toBe(32); + const padding = await $('cmp-level-3').getCSSProperty('padding'); + await expect(padding.parsed.value).toBe(32); + + const fontWeight = await $('cmp-level-3').getCSSProperty('font-weight'); + await expect(fontWeight.parsed.value).toBe(800); }); it('should have root scope id in the user provided nested element as classname', async () => { @@ -21,8 +25,12 @@ describe('scope-id-in-nested-classname', function () { ), }); await expect($('#test-element')).toHaveElementClass('sc-cmp-level-1'); + await expect($('#test-element')).toHaveElementClass('sc-cmp-level-2'); + + const padding = await $('#test-element').getCSSProperty('padding'); + await expect(padding.parsed.value).toBe(24); - const appliedCss = await (await $('#test-element')).getCSSProperty('padding'); - await expect(appliedCss.parsed.value).toBe(24); + const fontWeight = await $('#test-element').getCSSProperty('font-weight'); + await expect(fontWeight.parsed.value).toBe(600); }); });