diff --git a/src/runtime/styles.ts b/src/runtime/styles.ts index a060206b73b..5b3c0b51531 100644 --- a/src/runtime/styles.ts +++ b/src/runtime/styles.ts @@ -47,13 +47,7 @@ export const registerStyle = (scopeId: string, cssText: string, allowCS: boolean * @param mode an optional current mode * @returns the scope ID for the component of interest */ -export const addStyle = ( - styleContainerNode: Element | Document | ShadowRoot, - cmpMeta: d.ComponentRuntimeMeta, - mode?: string, -) => { - const styleContainerDocument = styleContainerNode as Document; - const styleContainerShadowRoot = styleContainerNode as ShadowRoot; +export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMeta, mode?: string) => { const scopeId = getScopeId(cmpMeta, mode); const style = styles.get(scopeId); @@ -66,7 +60,7 @@ export const addStyle = ( if (style) { if (typeof style === 'string') { - styleContainerNode = styleContainerDocument.head || (styleContainerNode as HTMLElement); + styleContainerNode = styleContainerNode.head || (styleContainerNode as HTMLElement); let appliedStyles = rootAppliedStyles.get(styleContainerNode); let styleElm; if (!appliedStyles) { @@ -75,7 +69,7 @@ export const addStyle = ( if (!appliedStyles.has(scopeId)) { if ( BUILD.hydrateClientSide && - styleContainerShadowRoot.host && + styleContainerNode.host && (styleElm = styleContainerNode.querySelector(`[${HYDRATED_STYLE_ID}="${scopeId}"]`)) ) { // This is only happening on native shadow-dom, do not needs CSS var shim @@ -106,8 +100,8 @@ export const addStyle = ( appliedStyles.add(scopeId); } } - } else if (BUILD.constructableCSS && !styleContainerDocument.adoptedStyleSheets.includes(style)) { - styleContainerDocument.adoptedStyleSheets = [...styleContainerDocument.adoptedStyleSheets, style]; + } else if (BUILD.constructableCSS && !styleContainerNode.adoptedStyleSheets.includes(style)) { + styleContainerNode.adoptedStyleSheets = [...styleContainerNode.adoptedStyleSheets, style]; } } return scopeId; diff --git a/test/end-to-end/src/components.d.ts b/test/end-to-end/src/components.d.ts index 6472c8baeb3..57cf1d21e50 100644 --- a/test/end-to-end/src/components.d.ts +++ b/test/end-to-end/src/components.d.ts @@ -108,6 +108,16 @@ export namespace Components { */ "mode"?: any; } + interface ScopedCarDetail { + "car": CarData; + } + /** + * Component that helps display a list of cars + */ + interface ScopedCarList { + "cars": CarData[]; + "selected": CarData; + } interface SlotCmp { } interface SlotCmpContainer { @@ -130,6 +140,10 @@ export interface EventCmpCustomEvent extends CustomEvent { detail: T; target: HTMLEventCmpElement; } +export interface ScopedCarListCustomEvent extends CustomEvent { + detail: T; + target: HTMLScopedCarListElement; +} declare global { interface HTMLAnotherCarDetailElement extends Components.AnotherCarDetail, HTMLStencilElement { } @@ -322,6 +336,32 @@ declare global { prototype: HTMLPropCmpElement; new (): HTMLPropCmpElement; }; + interface HTMLScopedCarDetailElement extends Components.ScopedCarDetail, HTMLStencilElement { + } + var HTMLScopedCarDetailElement: { + prototype: HTMLScopedCarDetailElement; + new (): HTMLScopedCarDetailElement; + }; + interface HTMLScopedCarListElementEventMap { + "carSelected": CarData; + } + /** + * Component that helps display a list of cars + */ + interface HTMLScopedCarListElement extends Components.ScopedCarList, HTMLStencilElement { + addEventListener(type: K, listener: (this: HTMLScopedCarListElement, ev: ScopedCarListCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLScopedCarListElement, ev: ScopedCarListCustomEvent) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; + } + var HTMLScopedCarListElement: { + prototype: HTMLScopedCarListElement; + new (): HTMLScopedCarListElement; + }; interface HTMLSlotCmpElement extends Components.SlotCmp, HTMLStencilElement { } var HTMLSlotCmpElement: { @@ -372,6 +412,8 @@ declare global { "path-alias-cmp": HTMLPathAliasCmpElement; "prerender-cmp": HTMLPrerenderCmpElement; "prop-cmp": HTMLPropCmpElement; + "scoped-car-detail": HTMLScopedCarDetailElement; + "scoped-car-list": HTMLScopedCarListElement; "slot-cmp": HTMLSlotCmpElement; "slot-cmp-container": HTMLSlotCmpContainerElement; "slot-parent-cmp": HTMLSlotParentCmpElement; @@ -455,6 +497,17 @@ declare namespace LocalJSX { */ "mode"?: any; } + interface ScopedCarDetail { + "car"?: CarData; + } + /** + * Component that helps display a list of cars + */ + interface ScopedCarList { + "cars"?: CarData[]; + "onCarSelected"?: (event: ScopedCarListCustomEvent) => void; + "selected"?: CarData; + } interface SlotCmp { } interface SlotCmpContainer { @@ -490,6 +543,8 @@ declare namespace LocalJSX { "path-alias-cmp": PathAliasCmp; "prerender-cmp": PrerenderCmp; "prop-cmp": PropCmp; + "scoped-car-detail": ScopedCarDetail; + "scoped-car-list": ScopedCarList; "slot-cmp": SlotCmp; "slot-cmp-container": SlotCmpContainer; "slot-parent-cmp": SlotParentCmp; @@ -531,6 +586,11 @@ declare module "@stencil/core" { "path-alias-cmp": LocalJSX.PathAliasCmp & JSXBase.HTMLAttributes; "prerender-cmp": LocalJSX.PrerenderCmp & JSXBase.HTMLAttributes; "prop-cmp": LocalJSX.PropCmp & JSXBase.HTMLAttributes; + "scoped-car-detail": LocalJSX.ScopedCarDetail & JSXBase.HTMLAttributes; + /** + * Component that helps display a list of cars + */ + "scoped-car-list": LocalJSX.ScopedCarList & JSXBase.HTMLAttributes; "slot-cmp": LocalJSX.SlotCmp & JSXBase.HTMLAttributes; "slot-cmp-container": LocalJSX.SlotCmpContainer & JSXBase.HTMLAttributes; "slot-parent-cmp": LocalJSX.SlotParentCmp & JSXBase.HTMLAttributes; diff --git a/test/end-to-end/src/declarative-shadow-dom/__snapshots__/test.e2e.ts.snap b/test/end-to-end/src/declarative-shadow-dom/__snapshots__/test.e2e.ts.snap index 50c0177c75d..eb10e186db3 100644 --- a/test/end-to-end/src/declarative-shadow-dom/__snapshots__/test.e2e.ts.snap +++ b/test/end-to-end/src/declarative-shadow-dom/__snapshots__/test.e2e.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`renderToString can render a scoped component within a shadow component 1`] = `""`; +exports[`renderToString can render a scoped component within a shadow component 1`] = `""`; exports[`renderToString can render nested components 1`] = `""`; diff --git a/test/end-to-end/src/declarative-shadow-dom/readme.md b/test/end-to-end/src/declarative-shadow-dom/readme.md index 763587e9cbb..c9cd4edb487 100644 --- a/test/end-to-end/src/declarative-shadow-dom/readme.md +++ b/test/end-to-end/src/declarative-shadow-dom/readme.md @@ -5,6 +5,52 @@ +## Overview + +Component that helps display a list of cars + +## Properties + +| Property | Attribute | Description | Type | Default | +| ---------- | --------- | ----------- | ----------- | ----------- | +| `cars` | -- | | `CarData[]` | `undefined` | +| `selected` | -- | | `CarData` | `undefined` | + + +## Events + +| Event | Description | Type | +| ------------- | ----------- | ---------------------- | +| `carSelected` | | `CustomEvent` | + + +## Slots + +| Slot | Description | +| ---------- | -------------------------------- | +| `"header"` | The slot for the header content. | + + +## Shadow Parts + +| Part | Description | +| ------- | ------------------------------------------- | +| `"car"` | The shadow part to target to style the car. | + + +## Dependencies + +### Depends on + +- [another-car-detail](.) + +### Graph +```mermaid +graph TD; + scoped-car-list --> another-car-detail + style scoped-car-list fill:#f9f,stroke:#333,stroke-width:4px +``` + ---------------------------------------------- *Built with [StencilJS](https://stenciljs.com/)* diff --git a/test/end-to-end/src/declarative-shadow-dom/scoped-car-detail.tsx b/test/end-to-end/src/declarative-shadow-dom/scoped-car-detail.tsx new file mode 100644 index 00000000000..1480f434a7b --- /dev/null +++ b/test/end-to-end/src/declarative-shadow-dom/scoped-car-detail.tsx @@ -0,0 +1,24 @@ +import { Component, h, Prop } from '@stencil/core'; + +import { CarData } from '../car-list/car-data'; + +@Component({ + tag: 'scoped-car-detail', + styleUrl: 'another-car-detail.css', + scoped: true, +}) +export class CarDetail { + @Prop() car: CarData; + + render() { + if (!this.car) { + return null; + } + + return ( +
+ {this.car.year} {this.car.make} {this.car.model} +
+ ); + } +} diff --git a/test/end-to-end/src/declarative-shadow-dom/scoped-car-list.tsx b/test/end-to-end/src/declarative-shadow-dom/scoped-car-list.tsx new file mode 100644 index 00000000000..fbd38b4ef9f --- /dev/null +++ b/test/end-to-end/src/declarative-shadow-dom/scoped-car-list.tsx @@ -0,0 +1,46 @@ +import { Component, Event, EventEmitter, h, Prop } from '@stencil/core'; + +import { CarData } from '../car-list/car-data'; + +/** + * Component that helps display a list of cars + * @slot header - The slot for the header content. + * @part car - The shadow part to target to style the car. + */ +@Component({ + tag: 'scoped-car-list', + styleUrl: 'another-car-list.css', + scoped: true, +}) +export class CarList { + @Prop() cars: CarData[]; + @Prop({ mutable: true }) selected: CarData; + @Event() carSelected: EventEmitter; + + componentWillLoad() { + return new Promise((resolve) => setTimeout(resolve, 20)); + } + + selectCar(car: CarData) { + this.selected = car; + this.carSelected.emit(car); + } + + render() { + if (!Array.isArray(this.cars)) { + return null; + } + + return ( +
    + {this.cars.map((car) => { + return ( +
  • + +
  • + ); + })} +
+ ); + } +} diff --git a/test/end-to-end/src/declarative-shadow-dom/test.e2e.ts b/test/end-to-end/src/declarative-shadow-dom/test.e2e.ts index 9ac74aea725..bdea1ef7eff 100644 --- a/test/end-to-end/src/declarative-shadow-dom/test.e2e.ts +++ b/test/end-to-end/src/declarative-shadow-dom/test.e2e.ts @@ -141,18 +141,18 @@ describe('renderToString', () => { }); expect(html).toMatchSnapshot(); expect(html).toContain( - '
2024 VW Vento
', + '
2024 VW Vento
', ); expect(html).toContain( - '
2023 VW Beetle
', + '
2023 VW Beetle
', ); }); it('can render a scoped component within a shadow component (sync)', async () => { const input = ``; const expectedResults = [ - '
2024 VW Vento
', - '
2023 VW Beetle
', + '
2024 VW Vento
', + '
2023 VW Beetle
', ] as const; const opts = { serializeShadowRoot: true, diff --git a/test/end-to-end/src/miscellaneous/renderToString.e2e.ts b/test/end-to-end/src/miscellaneous/renderToString.e2e.ts new file mode 100644 index 00000000000..b50b36e8b7b --- /dev/null +++ b/test/end-to-end/src/miscellaneous/renderToString.e2e.ts @@ -0,0 +1,84 @@ +import { CarData } from '../car-list/car-data'; + +const vento = new CarData('VW', 'Vento', 2024); +const beetle = new CarData('VW', 'Beetle', 2023); + +// @ts-ignore may not be existing when project hasn't been built +type HydrateModule = typeof import('../../hydrate'); +let renderToString: HydrateModule['renderToString']; + +describe('renderToString', () => { + beforeAll(async () => { + // @ts-ignore may not be existing when project hasn't been built + const mod = await import('../../hydrate'); + renderToString = mod.renderToString; + }); + + it('allows to hydrate whole HTML page', async () => { + const { html } = await renderToString( + ` + + + + + +
+
+ +
+
+ + + + `, + { fullDocument: true, serializeShadowRoot: false }, + ); + /** + * starts with a DocType and HTML tag + */ + expect(html.startsWith(' ', + ); + }); + + it('allows to hydrate whole HTML page with using a scoped component', async () => { + const { html } = await renderToString( + ` + + + + + +
+
+ +
+
+ + + + `, + { fullDocument: true, serializeShadowRoot: false }, + ); + /** + * starts with a DocType and HTML tag + */ + expect(html.startsWith('
{ .map((c) => c.slice(0, c.indexOf('{'))) .find((c) => c.includes('app-root')); expect(classSelector).toBe( - 'another-car-detail,another-car-list,app-root,build-data,car-detail,car-list,cmp-a,cmp-b,cmp-c,cmp-dsd,cmp-server-vs-client,dom-api,dom-interaction,dom-visible,element-cmp,empty-cmp,empty-cmp-shadow,env-data,event-cmp,import-assets,listen-cmp,method-cmp,path-alias-cmp,prerender-cmp,prop-cmp,slot-cmp,slot-cmp-container,slot-parent-cmp,state-cmp', + 'another-car-detail,another-car-list,app-root,build-data,car-detail,car-list,cmp-a,cmp-b,cmp-c,cmp-dsd,cmp-server-vs-client,dom-api,dom-interaction,dom-visible,element-cmp,empty-cmp,empty-cmp-shadow,env-data,event-cmp,import-assets,listen-cmp,method-cmp,path-alias-cmp,prerender-cmp,prop-cmp,scoped-car-detail,scoped-car-list,slot-cmp,slot-cmp-container,slot-parent-cmp,state-cmp', ); }); });