diff --git a/src/compiler/bundle/bundle-output.ts b/src/compiler/bundle/bundle-output.ts index 08ae331e952..01c0f5ecbce 100644 --- a/src/compiler/bundle/bundle-output.ts +++ b/src/compiler/bundle/bundle-output.ts @@ -121,7 +121,7 @@ export const getRollupOptions = ( userIndexPlugin(config, compilerCtx), typescriptPlugin(compilerCtx, bundleOpts, config), extFormatPlugin(config), - extTransformsPlugin(config, compilerCtx, buildCtx, bundleOpts), + extTransformsPlugin(config, compilerCtx, buildCtx), workerPlugin(config, compilerCtx, buildCtx, bundleOpts.platform, !!bundleOpts.inlineWorkers), serverPlugin(config, bundleOpts.platform), ...beforePlugins, diff --git a/src/compiler/bundle/ext-transforms-plugin.ts b/src/compiler/bundle/ext-transforms-plugin.ts index 314587f5cb7..9a8108f3ff9 100644 --- a/src/compiler/bundle/ext-transforms-plugin.ts +++ b/src/compiler/bundle/ext-transforms-plugin.ts @@ -5,7 +5,6 @@ import type * as d from '../../declarations'; import { runPluginTransformsEsmImports } from '../plugin/plugin'; import { getScopeId } from '../style/scope-css'; import { parseImportPath } from '../transformers/stencil-import-path'; -import type { BundleOptions } from './bundle-interface'; /** * This keeps a map of all the component styles we've seen already so we can create @@ -33,14 +32,12 @@ const allCmpStyles = new Map(); * @param config a user-supplied configuration * @param compilerCtx the current compiler context * @param buildCtx the current build context - * @param bundleOpts bundle options for Rollup * @returns a Rollup plugin which carries out the necessary work */ export const extTransformsPlugin = ( config: d.ValidatedConfig, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx, - bundleOpts: BundleOptions, ): Plugin => { return { name: 'extTransformsPlugin', @@ -94,24 +91,6 @@ export const extTransformsPlugin = ( const pluginTransforms = await runPluginTransformsEsmImports(config, compilerCtx, buildCtx, code, filePath); - // We need to check whether the current build is a dev-mode watch build w/ HMR enabled in - // order to know how we'll want to set `commentOriginalSelector` (below). If we are doing - // a hydrate build we need to set this to `true` because commenting-out selectors is what - // gives us support for scoped CSS w/ hydrated components (we don't support shadow DOM and - // styling via that route for them). However, we don't want to comment selectors in dev - // mode when using HMR in the browser, since there we _do_ support putting stylesheets into - // the shadow DOM and commenting out e.g. the `:host` selector in those stylesheets will - // break components' CSS when an HMR update is sent to the browser. - // - // See https://github.com/ionic-team/stencil/issues/3461 for details - const isDevWatchHMRBuild = - config.flags.watch && - config.flags.dev && - config.flags.serve && - (config.devServer?.reloadStrategy ?? null) === 'hmr'; - const commentOriginalSelector = - bundleOpts.platform === 'hydrate' && data.encapsulation === 'shadow' && !isDevWatchHMRBuild; - if (data.tag) { cmp = buildCtx.components.find((c) => c.tagName === data.tag); const moduleFile = cmp && compilerCtx.moduleMap.get(cmp.sourceFilePath); @@ -147,7 +126,6 @@ export const extTransformsPlugin = ( tag: data.tag, encapsulation: data.encapsulation, mode: data.mode, - commentOriginalSelector, sourceMap: config.sourceMap, minify: config.minifyCss, autoprefixer: config.autoprefixCss, @@ -214,6 +192,7 @@ export const extTransformsPlugin = ( * as it is not connected to a component. */ cssTransformResults.styleText; + buildCtx.stylesUpdated.push({ styleTag: data.tag, styleMode: data.mode, diff --git a/src/compiler/bundle/test/ext-transforms-plugin.spec.ts b/src/compiler/bundle/test/ext-transforms-plugin.spec.ts index 5335f949834..984ee2d7a80 100644 --- a/src/compiler/bundle/test/ext-transforms-plugin.spec.ts +++ b/src/compiler/bundle/test/ext-transforms-plugin.spec.ts @@ -63,7 +63,7 @@ describe('extTransformsPlugin', () => { const writeFileSpy = jest.spyOn(compilerCtx.fs, 'writeFile'); return { - plugin: extTransformsPlugin(config, compilerCtx, buildCtx, bundleOpts), + plugin: extTransformsPlugin(config, compilerCtx, buildCtx), config, compilerCtx, buildCtx, @@ -101,41 +101,5 @@ describe('extTransformsPlugin', () => { expect(css).toBe(':host { text: pink; }'); }); - - describe('passing `commentOriginalSelector` to `transformCssToEsm`', () => { - it.each([ - [false, 'tag=my-component&encapsulation=scoped'], - [true, 'tag=my-component&encapsulation=shadow'], - [false, 'tag=my-component'], - ])('should pass true if %p and hydrate', async (expectation, queryParams) => { - const { plugin, transformCssToEsmSpy } = setup({ platform: 'hydrate' }); - // @ts-ignore the Rollup plugins expect to be called in a Rollup context - await plugin.transform('asdf', `/some/stubbed/path/foo.css?${queryParams}`); - expect(transformCssToEsmSpy.mock.calls[0][0].commentOriginalSelector).toBe(expectation); - }); - - it('should pass false if shadow, hydrate, but using HMR in dev watch mode', async () => { - const { plugin, transformCssToEsmSpy, config } = setup({ platform: 'hydrate' }); - - config.flags.watch = true; - config.flags.dev = true; - config.flags.serve = true; - config.devServer = { reloadStrategy: 'hmr' }; - - // @ts-ignore the Rollup plugins expect to be called in a Rollup context - await plugin.transform('asdf', '/some/stubbed/path/foo.css?tag=my-component&encapsulation=shadow'); - expect(transformCssToEsmSpy.mock.calls[0][0].commentOriginalSelector).toBe(false); - }); - - it.each(['tag=my-component&encapsulation=scoped', 'tag=my-component&encapsulation=shadow', 'tag=my-component'])( - 'should pass false if %p without hydrate', - async (queryParams) => { - const { plugin, transformCssToEsmSpy } = setup(); - // @ts-ignore the Rollup plugins expect to be called in a Rollup context - await plugin.transform('asdf', `/some/stubbed/path/foo.css?${queryParams}`); - expect(transformCssToEsmSpy.mock.calls[0][0].commentOriginalSelector).toBe(false); - }, - ); - }); }); }); diff --git a/src/compiler/config/transpile-options.ts b/src/compiler/config/transpile-options.ts index dba0e845f7d..20896e6c931 100644 --- a/src/compiler/config/transpile-options.ts +++ b/src/compiler/config/transpile-options.ts @@ -166,7 +166,6 @@ export const getTranspileCssConfig = ( encapsulation: importData && importData.encapsulation, mode: importData && importData.mode, sourceMap: compileOpts.sourceMap !== false, - commentOriginalSelector: false, minify: false, autoprefixer: false, module: compileOpts.module, diff --git a/src/compiler/style/css-to-esm.ts b/src/compiler/style/css-to-esm.ts index c78d723308f..67c7519f9d6 100644 --- a/src/compiler/style/css-to-esm.ts +++ b/src/compiler/style/css-to-esm.ts @@ -113,11 +113,9 @@ const transformCssToEsmModule = (input: d.TransformCssToEsmInput): d.TransformCs try { const varNames = new Set([results.defaultVarName]); - if (isString(input.tag)) { - if (input.encapsulation === 'scoped' || (input.encapsulation === 'shadow' && input.commentOriginalSelector)) { - const scopeId = getScopeId(input.tag, input.mode); - results.styleText = scopeCss(results.styleText, scopeId, !!input.commentOriginalSelector); - } + if (isString(input.tag) && input.encapsulation === 'scoped') { + const scopeId = getScopeId(input.tag, input.mode); + results.styleText = scopeCss(results.styleText, scopeId); } const cssImports = getCssToEsmImports(varNames, results.styleText, input.file, input.mode); diff --git a/src/compiler/transformers/add-static-style.ts b/src/compiler/transformers/add-static-style.ts index 8c879b2ca7b..74efe1257de 100644 --- a/src/compiler/transformers/add-static-style.ts +++ b/src/compiler/transformers/add-static-style.ts @@ -16,14 +16,12 @@ import { createStaticGetter, getExternalStyles } from './transform-utils'; * @param classMembers a class to existing members of a class. **this parameter will be mutated** rather than returning * a cloned version * @param cmp the metadata associated with the component being evaluated - * @param commentOriginalSelector if `true`, add a comment with the original CSS selector to the style. */ export const addStaticStyleGetterWithinClass = ( classMembers: ts.ClassElement[], cmp: d.ComponentCompilerMeta, - commentOriginalSelector: boolean, ): void => { - const styleLiteral = getStyleLiteral(cmp, commentOriginalSelector); + const styleLiteral = getStyleLiteral(cmp); if (styleLiteral) { classMembers.push(createStaticGetter('style', styleLiteral)); } @@ -41,7 +39,7 @@ export const addStaticStyleGetterWithinClass = ( * @param cmp the metadata associated with the component being evaluated */ export const addStaticStylePropertyToClass = (styleStatements: ts.Statement[], cmp: d.ComponentCompilerMeta): void => { - const styleLiteral = getStyleLiteral(cmp, false); + const styleLiteral = getStyleLiteral(cmp); if (styleLiteral) { const statement = ts.factory.createExpressionStatement( ts.factory.createAssignment( @@ -53,24 +51,20 @@ export const addStaticStylePropertyToClass = (styleStatements: ts.Statement[], c } }; -const getStyleLiteral = (cmp: d.ComponentCompilerMeta, commentOriginalSelector: boolean) => { +const getStyleLiteral = (cmp: d.ComponentCompilerMeta) => { if (Array.isArray(cmp.styles) && cmp.styles.length > 0) { if (cmp.styles.length > 1 || (cmp.styles.length === 1 && cmp.styles[0].modeName !== DEFAULT_STYLE_MODE)) { // multiple style modes - return getMultipleModeStyle(cmp, cmp.styles, commentOriginalSelector); + return getMultipleModeStyle(cmp, cmp.styles); } else { // single style - return getSingleStyle(cmp, cmp.styles[0], commentOriginalSelector); + return getSingleStyle(cmp, cmp.styles[0]); } } return null; }; -const getMultipleModeStyle = ( - cmp: d.ComponentCompilerMeta, - styles: d.StyleCompiler[], - commentOriginalSelector: boolean, -) => { +const getMultipleModeStyle = (cmp: d.ComponentCompilerMeta, styles: d.StyleCompiler[]) => { const styleModes: ts.ObjectLiteralElementLike[] = []; styles.forEach((style) => { @@ -83,7 +77,7 @@ const getMultipleModeStyle = ( if (typeof style.styleStr === 'string') { // inline the style string // static get style() { return { ios: "string" }; } - const styleLiteral = createStyleLiteral(cmp, style, commentOriginalSelector); + const styleLiteral = createStyleLiteral(cmp, style); const propStr = ts.factory.createPropertyAssignment(style.modeName, styleLiteral); styleModes.push(propStr); } else if (Array.isArray(style.externalStyles) && style.externalStyles.length > 0) { @@ -106,7 +100,7 @@ const getMultipleModeStyle = ( return ts.factory.createObjectLiteralExpression(styleModes, true); }; -const getSingleStyle = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler, commentOriginalSelector: boolean) => { +const getSingleStyle = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler) => { /** * the order of these if statements must match with * - {@link src/compiler/transformers/component-native/native-static-style.ts#addSingleStyleGetter} @@ -116,7 +110,7 @@ const getSingleStyle = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler, co if (typeof style.styleStr === 'string') { // inline the style string // static get style() { return "string"; } - return createStyleLiteral(cmp, style, commentOriginalSelector); + return createStyleLiteral(cmp, style); } if (Array.isArray(style.externalStyles) && style.externalStyles.length > 0) { @@ -136,11 +130,11 @@ const getSingleStyle = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler, co return null; }; -const createStyleLiteral = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler, commentOriginalSelector: boolean) => { - if (cmp.encapsulation === 'scoped' || (commentOriginalSelector && cmp.encapsulation === 'shadow')) { +const createStyleLiteral = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler) => { + if (cmp.encapsulation === 'scoped') { // scope the css first const scopeId = getScopeId(cmp.tagName, style.modeName); - return ts.factory.createStringLiteral(scopeCss(style.styleStr, scopeId, commentOriginalSelector)); + return ts.factory.createStringLiteral(scopeCss(style.styleStr, scopeId)); } return ts.factory.createStringLiteral(style.styleStr); diff --git a/src/compiler/transformers/component-hydrate/hydrate-runtime-cmp-meta.ts b/src/compiler/transformers/component-hydrate/hydrate-runtime-cmp-meta.ts index 9c10684a8d7..f1b8cf406a0 100644 --- a/src/compiler/transformers/component-hydrate/hydrate-runtime-cmp-meta.ts +++ b/src/compiler/transformers/component-hydrate/hydrate-runtime-cmp-meta.ts @@ -21,8 +21,7 @@ export const addHydrateRuntimeCmpMeta = (classMembers: ts.ClassElement[], cmp: d cmpMeta.$flags$ |= CMP_FLAGS.needsShadowDomShim; } const staticMember = createStaticGetter('cmpMeta', convertValueToLiteral(cmpMeta)); - const commentOriginalSelector = cmp.encapsulation === 'shadow'; - addStaticStyleGetterWithinClass(classMembers, cmp, commentOriginalSelector); + addStaticStyleGetterWithinClass(classMembers, cmp); classMembers.push(staticMember); }; diff --git a/src/compiler/transformers/component-native/native-static-style.ts b/src/compiler/transformers/component-native/native-static-style.ts index c5ea1f3eca1..3accfc4b2cf 100644 --- a/src/compiler/transformers/component-native/native-static-style.ts +++ b/src/compiler/transformers/component-native/native-static-style.ts @@ -96,7 +96,7 @@ const createStyleLiteral = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler if (cmp.encapsulation === 'scoped') { // scope the css first const scopeId = getScopeId(cmp.tagName, style.modeName); - return ts.factory.createStringLiteral(scopeCss(style.styleStr, scopeId, false)); + return ts.factory.createStringLiteral(scopeCss(style.styleStr, scopeId)); } return ts.factory.createStringLiteral(style.styleStr); diff --git a/src/declarations/stencil-private.ts b/src/declarations/stencil-private.ts index 1fc3f2d5a07..5fa107ee001 100644 --- a/src/declarations/stencil-private.ts +++ b/src/declarations/stencil-private.ts @@ -1938,7 +1938,6 @@ export interface TransformCssToEsmInput { * is not shared by multiple fields, nor is it a composite of multiple modes). */ mode?: string; - commentOriginalSelector?: boolean; sourceMap?: boolean; minify?: boolean; docs?: boolean; diff --git a/src/hydrate/platform/hydrate-app.ts b/src/hydrate/platform/hydrate-app.ts index f24c9a29e90..82be52843f6 100644 --- a/src/hydrate/platform/hydrate-app.ts +++ b/src/hydrate/platform/hydrate-app.ts @@ -91,7 +91,7 @@ export function hydrateApp( registerHost(elm, Cstr.cmpMeta); // proxy the host element with the component's metadata - proxyHostElement(elm, Cstr.cmpMeta, opts); + proxyHostElement(elm, Cstr.cmpMeta); } } } diff --git a/src/hydrate/platform/proxy-host-element.ts b/src/hydrate/platform/proxy-host-element.ts index 06a7853d3b0..53daf1f3c42 100644 --- a/src/hydrate/platform/proxy-host-element.ts +++ b/src/hydrate/platform/proxy-host-element.ts @@ -5,11 +5,7 @@ import { CMP_FLAGS, MEMBER_FLAGS } from '@utils'; import type * as d from '../../declarations'; -export function proxyHostElement( - elm: d.HostElement, - cmpMeta: d.ComponentRuntimeMeta, - opts: d.HydrateFactoryOptions, -): void { +export function proxyHostElement(elm: d.HostElement, cmpMeta: d.ComponentRuntimeMeta): void { if (typeof elm.componentOnReady !== 'function') { elm.componentOnReady = componentOnReady; } @@ -26,14 +22,8 @@ export function proxyHostElement( mode: 'open', delegatesFocus: !!(cmpMeta.$flags$ & CMP_FLAGS.shadowDelegatesFocus), }); - } else if (opts.serializeShadowRoot) { - elm.attachShadow({ mode: 'open' }); } else { - /** - * For hydration users may want to render the shadow component as scoped - * component, so we need to assign the element as shadowRoot. - */ - (elm as any).shadowRoot = elm; + elm.attachShadow({ mode: 'open' }); } } diff --git a/src/hydrate/runner/inspect-element.ts b/src/hydrate/runner/inspect-element.ts index ec9250d5fd7..d9f43406b0a 100644 --- a/src/hydrate/runner/inspect-element.ts +++ b/src/hydrate/runner/inspect-element.ts @@ -1,7 +1,7 @@ import type * as d from '../../declarations'; export function inspectElement(results: d.HydrateResults, elm: Element, depth: number) { - const children = elm.children; + const children = [...Array.from(elm.children), ...Array.from(elm.shadowRoot ? elm.shadowRoot.children : [])]; for (let i = 0, ii = children.length; i < ii; i++) { const childElm = children[i]; diff --git a/src/hydrate/runner/render.ts b/src/hydrate/runner/render.ts index 7860e722e7c..1254e4b315e 100644 --- a/src/hydrate/runner/render.ts +++ b/src/hydrate/runner/render.ts @@ -47,7 +47,7 @@ export function renderToString( /** * Defines whether we render the shadow root as a declarative shadow root or as scoped shadow root. */ - opts.serializeShadowRoot = Boolean(opts.serializeShadowRoot); + opts.serializeShadowRoot = typeof opts.serializeShadowRoot === 'boolean' ? opts.serializeShadowRoot : true; /** * Make sure we wait for components to be hydrated. */ diff --git a/src/mock-doc/serialize-node.ts b/src/mock-doc/serialize-node.ts index 61f51c13e77..23540a6a316 100644 --- a/src/mock-doc/serialize-node.ts +++ b/src/mock-doc/serialize-node.ts @@ -29,7 +29,7 @@ function normalizeSerializationOptions(opts: Partial removeBooleanAttributeQuotes: typeof opts.removeBooleanAttributeQuotes !== 'boolean' ? false : opts.removeBooleanAttributeQuotes, removeHtmlComments: typeof opts.removeHtmlComments !== 'boolean' ? false : opts.removeHtmlComments, - serializeShadowRoot: typeof opts.serializeShadowRoot !== 'boolean' ? false : opts.serializeShadowRoot, + serializeShadowRoot: typeof opts.serializeShadowRoot !== 'boolean' ? true : opts.serializeShadowRoot, fullDocument: typeof opts.fullDocument !== 'boolean' ? true : opts.fullDocument, } as const; } @@ -229,7 +229,7 @@ function* streamToHtml( if (EMPTY_ELEMENTS.has(tagName) === false) { const shadowRoot = (node as HTMLElement).shadowRoot; - if (opts.serializeShadowRoot && shadowRoot != null) { + if (shadowRoot != null && opts.serializeShadowRoot) { output.indent = output.indent + (opts.indentSpaces ?? 0); yield* streamToHtml(shadowRoot, opts, output); diff --git a/src/runtime/bootstrap-lazy.ts b/src/runtime/bootstrap-lazy.ts index 17e08b9f7b1..af49b5387d1 100644 --- a/src/runtime/bootstrap-lazy.ts +++ b/src/runtime/bootstrap-lazy.ts @@ -16,8 +16,7 @@ import { import { hmrStart } from './hmr-component'; import { createTime, installDevTools } from './profile'; import { proxyComponent } from './proxy-component'; -import { HYDRATED_CSS, HYDRATED_STYLE_ID, PLATFORM_FLAGS, PROXY_FLAGS, SLOT_FB_CSS } from './runtime-constants'; -import { convertScopedToShadow, registerStyle } from './styles'; +import { HYDRATED_CSS, PLATFORM_FLAGS, PROXY_FLAGS, SLOT_FB_CSS } from './runtime-constants'; import { appDidLoad } from './update-component'; export { setNonce } from '@platform'; @@ -35,10 +34,8 @@ export const bootstrapLazy = (lazyBundles: d.LazyBundlesRuntimeData, options: d. const metaCharset = /*@__PURE__*/ head.querySelector('meta[charset]'); const dataStyles = /*@__PURE__*/ doc.createElement('style'); const deferredConnectedCallbacks: { connectedCallback: () => void }[] = []; - const styles = /*@__PURE__*/ doc.querySelectorAll(`[${HYDRATED_STYLE_ID}]`); let appLoadFallback: any; let isBootstrapping = true; - let i = 0; Object.assign(plt, options); plt.$resourcesUrl$ = new URL(options.resourcesUrl || './', doc.baseURI).href; @@ -52,11 +49,6 @@ export const bootstrapLazy = (lazyBundles: d.LazyBundlesRuntimeData, options: d. // async queue. This will improve the first input delay plt.$flags$ |= PLATFORM_FLAGS.appLoaded; } - if (BUILD.hydrateClientSide && BUILD.shadowDom) { - for (; i < styles.length; i++) { - registerStyle(styles[i].getAttribute(HYDRATED_STYLE_ID), convertScopedToShadow(styles[i].innerHTML), true); - } - } let hasSlotRelocation = false; lazyBundles.map((lazyBundle) => { diff --git a/src/runtime/initialize-component.ts b/src/runtime/initialize-component.ts index 47b3c1f1691..b0075a7cd9f 100644 --- a/src/runtime/initialize-component.ts +++ b/src/runtime/initialize-component.ts @@ -162,7 +162,7 @@ export const initializeComponent = async ( BUILD.shadowDomShim && cmpMeta.$flags$ & CMP_FLAGS.needsShadowDomShim ) { - style = await import('@utils/shadow-css').then((m) => m.scopeCss(style, scopeId, false)); + style = await import('@utils/shadow-css').then((m) => m.scopeCss(style, scopeId)); } registerStyle(scopeId, style, !!(cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation)); diff --git a/src/runtime/styles.ts b/src/runtime/styles.ts index 5b3c0b51531..316ea26b926 100644 --- a/src/runtime/styles.ts +++ b/src/runtime/styles.ts @@ -84,11 +84,28 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet styleElm.setAttribute('nonce', nonce); } - if (BUILD.hydrateServerSide || BUILD.hotModuleReplacement) { + if ( + (BUILD.hydrateServerSide || BUILD.hotModuleReplacement) && + cmpMeta.$flags$ & CMP_FLAGS.scopedCssEncapsulation + ) { styleElm.setAttribute(HYDRATED_STYLE_ID, scopeId); } - styleContainerNode.insertBefore(styleElm, styleContainerNode.querySelector('link')); + /** + * only attach style tag to section if: + */ + const injectStyle = + /** + * we render a scoped component + */ + !(cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) || + /** + * we are using shadow dom and render the style tag within the shadowRoot + */ + (cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation && styleContainerNode.nodeName !== 'HEAD'); + if (injectStyle) { + styleContainerNode.insertBefore(styleElm, styleContainerNode.querySelector('link')); + } } // Add styles for `slot-fb` elements if we're using slots outside the Shadow DOM @@ -124,7 +141,12 @@ export const attachStyles = (hostRef: d.HostRef) => { hostRef.$modeName$, ); - if ((BUILD.shadowDom || BUILD.scoped) && BUILD.cssAnnotations && flags & CMP_FLAGS.needsScopedEncapsulation) { + if ( + (BUILD.shadowDom || BUILD.scoped) && + BUILD.cssAnnotations && + flags & CMP_FLAGS.needsScopedEncapsulation && + flags & CMP_FLAGS.scopedCssEncapsulation + ) { // only required when we're NOT using native shadow dom (slot) // or this browser doesn't support native shadow dom // and this host element was NOT created with SSR @@ -152,34 +174,6 @@ export const attachStyles = (hostRef: d.HostRef) => { export const getScopeId = (cmp: d.ComponentRuntimeMeta, mode?: string) => 'sc-' + (BUILD.mode && mode && cmp.$flags$ & CMP_FLAGS.hasMode ? cmp.$tagName$ + '-' + mode : cmp.$tagName$); -/** - * Convert a 'scoped' CSS string to one appropriate for use in the shadow DOM. - * - * Given a 'scoped' CSS string that looks like this: - * - * ``` - * /*!@div*\/div.class-name { display: flex }; - * ``` - * - * Convert it to a 'shadow' appropriate string, like so: - * - * ``` - * /*!@div*\/div.class-name { display: flex } - * ─┬─ ────────┬──────── - * │ │ - * │ ┌─────────────────┘ - * ▼ ▼ - * div{ display: flex } - * ``` - * - * Note that forward-slashes in the above are escaped so they don't end the - * comment. - * - * @param css a CSS string to convert - * @returns the converted string - */ -export const convertScopedToShadow = (css: string) => css.replace(/\/\*!@([^\/]+)\*\/[^\{]+\{/g, '$1{'); - declare global { export interface CSSStyleSheet { replaceSync(cssText: string): void; diff --git a/src/runtime/test/shadow.spec.tsx b/src/runtime/test/shadow.spec.tsx index 6cffdea5655..4b9fa10a47d 100644 --- a/src/runtime/test/shadow.spec.tsx +++ b/src/runtime/test/shadow.spec.tsx @@ -84,17 +84,17 @@ describe('shadow', () => { }); const expected = ` - + -
- +
+ Start - + Text -
- +
+ End
diff --git a/src/runtime/vdom/vdom-render.ts b/src/runtime/vdom/vdom-render.ts index aada2d1d680..1ed1d1c92dd 100644 --- a/src/runtime/vdom/vdom-render.ts +++ b/src/runtime/vdom/vdom-render.ts @@ -105,7 +105,13 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: updateElement(null, newVNode, isSvgMode); } - if ((BUILD.shadowDom || BUILD.scoped) && isDef(scopeId) && elm['s-si'] !== scopeId) { + /** + * walk up the DOM tree and check if we are in a shadow root because if we are within + * a shadow root DOM we don't need to attach scoped class names to the element + */ + const rootNode = elm.getRootNode() as HTMLElement; + const isElementWithinShadowRoot = !rootNode.querySelector('body'); + if (!isElementWithinShadowRoot && BUILD.scoped && isDef(scopeId) && elm['s-si'] !== scopeId) { // if there is a scopeId and this is the initial render // then let's add the scopeId as a css class elm.classList.add((elm['s-si'] = scopeId)); diff --git a/src/utils/shadow-css.ts b/src/utils/shadow-css.ts index f2a600bd12c..eaa31f7e694 100644 --- a/src/utils/shadow-css.ts +++ b/src/utils/shadow-css.ts @@ -425,13 +425,7 @@ const scopeSelector = (selector: string, scopeSelectorText: string, hostSelector .join(', '); }; -const scopeSelectors = ( - cssText: string, - scopeSelectorText: string, - hostSelector: string, - slotSelector: string, - commentOriginalSelector: boolean, -) => { +const scopeSelectors = (cssText: string, scopeSelectorText: string, hostSelector: string, slotSelector: string) => { return processRules(cssText, (rule: CssRule) => { let selector = rule.selector; let content = rule.content; @@ -443,7 +437,7 @@ const scopeSelectors = ( rule.selector.startsWith('@page') || rule.selector.startsWith('@document') ) { - content = scopeSelectors(rule.content, scopeSelectorText, hostSelector, slotSelector, commentOriginalSelector); + content = scopeSelectors(rule.content, scopeSelectorText, hostSelector, slotSelector); } const cssRule: CssRule = { @@ -454,13 +448,7 @@ const scopeSelectors = ( }); }; -const scopeCssText = ( - cssText: string, - scopeId: string, - hostScopeId: string, - slotScopeId: string, - commentOriginalSelector: boolean, -) => { +const scopeCssText = (cssText: string, scopeId: string, hostScopeId: string, slotScopeId: string) => { cssText = insertPolyfillHostInCssText(cssText); cssText = convertColonHost(cssText); cssText = convertColonHostContext(cssText); @@ -470,7 +458,7 @@ const scopeCssText = ( cssText = convertShadowDOMSelectors(cssText); if (scopeId) { - cssText = scopeSelectors(cssText, scopeId, hostScopeId, slotScopeId, commentOriginalSelector); + cssText = scopeSelectors(cssText, scopeId, hostScopeId, slotScopeId); } cssText = replaceShadowCssHost(cssText, hostScopeId); @@ -499,53 +487,16 @@ const replaceShadowCssHost = (cssText: string, hostScopeId: string) => { return cssText.replace(/-shadowcsshost-no-combinator/g, `.${hostScopeId}`); }; -export const scopeCss = (cssText: string, scopeId: string, commentOriginalSelector: boolean) => { +export const scopeCss = (cssText: string, scopeId: string) => { const hostScopeId = scopeId + '-h'; const slotScopeId = scopeId + '-s'; const commentsWithHash = extractCommentsWithHash(cssText); cssText = stripComments(cssText); - const orgSelectors: { - placeholder: string; - comment: string; - }[] = []; - - if (commentOriginalSelector) { - const processCommentedSelector = (rule: CssRule) => { - const placeholder = `/*!@___${orgSelectors.length}___*/`; - const comment = `/*!@${rule.selector}*/`; - - orgSelectors.push({ placeholder, comment }); - rule.selector = placeholder + rule.selector; - return rule; - }; - - cssText = processRules(cssText, (rule) => { - if (rule.selector[0] !== '@') { - return processCommentedSelector(rule); - } else if ( - rule.selector.startsWith('@media') || - rule.selector.startsWith('@supports') || - rule.selector.startsWith('@page') || - rule.selector.startsWith('@document') - ) { - rule.content = processRules(rule.content, processCommentedSelector); - return rule; - } - return rule; - }); - } - - const scoped = scopeCssText(cssText, scopeId, hostScopeId, slotScopeId, commentOriginalSelector); + const scoped = scopeCssText(cssText, scopeId, hostScopeId, slotScopeId); cssText = [scoped.cssText, ...commentsWithHash].join('\n'); - if (commentOriginalSelector) { - orgSelectors.forEach(({ placeholder, comment }) => { - cssText = cssText.replace(placeholder, comment); - }); - } - scoped.slottedSelectors.forEach((slottedSelector) => { const regex = new RegExp(escapeRegExpSpecialCharacters(slottedSelector.orgSelector), 'g'); cssText = cssText.replace(regex, slottedSelector.updatedSelector); diff --git a/src/utils/test/scope-css.spec.ts b/src/utils/test/scope-css.spec.ts index 1804411c06a..a6e9dff6dad 100644 --- a/src/utils/test/scope-css.spec.ts +++ b/src/utils/test/scope-css.spec.ts @@ -11,12 +11,11 @@ * found in the LICENSE file at https://angular.io/license */ -import { convertScopedToShadow } from '../../runtime/styles'; import { scopeCss } from '../shadow-css'; describe('ShadowCss', function () { - function s(cssText: string, scopeId: string, commentOriginalSelector = false) { - const shim = scopeCss(cssText, scopeId, commentOriginalSelector); + function s(cssText: string, scopeId: string) { + const shim = scopeCss(cssText, scopeId); const nlRegexp = /\n/g; return normalizeCSS(shim.replace(nlRegexp, '')); @@ -26,21 +25,6 @@ describe('ShadowCss', function () { expect(s('', 'a')).toEqual(''); }); - it('should handle empty string, commented org selector', () => { - expect(s('', 'a', true)).toEqual(''); - }); - - it('div', () => { - const r = s('div {}', 'sc-ion-tag', true); - expect(r).toEqual('/*!@div*/div.sc-ion-tag {}'); - }); - - it('should add an attribute to every rule, commented org selector', () => { - const css = 'one {color: red;}two {color: red;}'; - const expected = '/*!@one*/one.a {color:red;}/*!@two*/two.a {color:red;}'; - expect(s(css, 'a', true)).toEqual(expected); - }); - it('should add an attribute to every rule', () => { const css = 'one {color: red;}two {color: red;}'; const expected = 'one.a {color:red;}two.a {color:red;}'; @@ -71,24 +55,12 @@ describe('ShadowCss', function () { expect(s(css, 'a')).toEqual(expected); }); - it('should handle media rules, commentOriginalSelector', () => { - const css = '@media screen and (max-width:800px, max-height:100%) {div {font-size:50px;}}'; - const expected = '@media screen and (max-width:800px, max-height:100%) {/*!@div*/div.a {font-size:50px;}}'; - expect(s(css, 'a', true)).toEqual(expected); - }); - it('should handle page rules', () => { const css = '@page {div {font-size:50px;}}'; const expected = '@page {div.a {font-size:50px;}}'; expect(s(css, 'a')).toEqual(expected); }); - it('should handle page rules, commentOriginalSelector', () => { - const css = '@page {div {font-size:50px;}}'; - const expected = '@page {/*!@div*/div.a {font-size:50px;}}'; - expect(s(css, 'a', true)).toEqual(expected); - }); - it('should handle document rules', () => { const css = '@document url(http://www.w3.org/) {div {font-size:50px;}}'; const expected = '@document url(http://www.w3.org/) {div.a {font-size:50px;}}'; @@ -113,11 +85,6 @@ describe('ShadowCss', function () { expect(s(css, 'a')).toEqual(css); }); - it('should handle keyframes rules, commentOriginalSelector', () => { - const css = '@keyframes foo {0% {transform:translate(-50%) scaleX(0);}}'; - expect(s(css, 'a', true)).toEqual(css); - }); - it('should handle -webkit-keyframes rules', () => { const css = '@-webkit-keyframes foo {0% {-webkit-transform:translate(-50%) scaleX(0);}}'; expect(s(css, 'a')).toEqual(css); @@ -153,10 +120,6 @@ describe('ShadowCss', function () { }); describe(':host', () => { - it('should handle no context, commentOriginalSelector', () => { - expect(s(':host {}', 'a', true)).toEqual('/*!@:host*/.a-h {}'); - }); - it('should handle no context', () => { expect(s(':host {}', 'a')).toEqual('.a-h {}'); }); @@ -174,11 +137,6 @@ describe('ShadowCss', function () { expect(s(':host([a=b]) {}', 'a')).toEqual('[a="b"].a-h {}'); }); - it('should handle multiple tag selectors, commenting the original selector', () => { - expect(s(':host(ul,li) {}', 'a', true)).toEqual('/*!@:host(ul,li)*/ul.a-h, li.a-h {}'); - expect(s(':host(ul,li) > .z {}', 'a', true)).toEqual('/*!@:host(ul,li) > .z*/ul.a-h > .z.a, li.a-h > .z.a {}'); - }); - it('should handle multiple tag selectors', () => { expect(s(':host(ul,li) {}', 'a')).toEqual('ul.a-h, li.a-h {}'); expect(s(':host(ul,li) > .z {}', 'a')).toEqual('ul.a-h > .z.a, li.a-h > .z.a {}'); @@ -193,10 +151,6 @@ describe('ShadowCss', function () { expect(s(':host([a="b"],[c=d]) {}', 'a')).toEqual('[a="b"].a-h, [c="d"].a-h {}'); }); - it('should handle multiple attribute selectors, commentOriginalSelector', () => { - expect(s(':host([a="b"],[c=d]) {}', 'a', true)).toEqual('/*!@:host([a="b"],[c=d])*/[a="b"].a-h, [c="d"].a-h {}'); - }); - it('should handle pseudo selectors', () => { expect(s(':host(:before) {}', 'a')).toEqual('.a-h:before {}'); expect(s(':host:before {}', 'a')).toEqual('.a-h:before {}'); @@ -215,13 +169,6 @@ describe('ShadowCss', function () { }); describe(':host-context', () => { - it('should handle tag selector, commentOriginalSelector', () => { - expect(s(':host-context(div) {}', 'a', true)).toEqual('/*!@:host-context(div)*/div.a-h, div .a-h {}'); - expect(s(':host-context(ul) > .y {}', 'a', true)).toEqual( - '/*!@:host-context(ul) > .y*/ul.a-h > .y.a, ul .a-h > .y.a {}', - ); - }); - it('should handle tag selector', () => { expect(s(':host-context(div) {}', 'a')).toEqual('div.a-h, div .a-h {}'); expect(s(':host-context(ul) > .y {}', 'a')).toEqual('ul.a-h > .y.a, ul .a-h > .y.a {}'); @@ -247,11 +194,6 @@ describe('ShadowCss', function () { }); describe('::slotted', () => { - it('should handle *, commentOriginalSelector', () => { - const r = s('::slotted(*) {}', 'sc-ion-tag', true); - expect(r).toEqual('/*!@::slotted(*)*/.sc-ion-tag-s > * {}'); - }); - it('should handle *', () => { const r = s('::slotted(*) {}', 'sc-ion-tag'); expect(r).toEqual('.sc-ion-tag-s > * {}'); @@ -306,23 +248,11 @@ describe('ShadowCss', function () { expect(r).toEqual('.sc-ion-tag-s > * {}, .sc-ion-tag-s > * {}, .sc-ion-tag-s > * {}'); }); - it('same selectors, commentOriginalSelector', () => { - const r = s('::slotted(*) {}, ::slotted(*) {}, ::slotted(*) {}', 'sc-ion-tag', true); - expect(r).toEqual( - '/*!@::slotted(*)*/.sc-ion-tag-s > * {}/*!@, ::slotted(*)*/.sc-ion-tag, .sc-ion-tag-s > * {}/*!@, ::slotted(*)*/.sc-ion-tag, .sc-ion-tag-s > * {}', - ); - }); - it('should combine parent selector when comma', () => { const r = s('.a .b, .c ::slotted(*) {}', 'sc-ion-tag'); expect(r).toEqual('.a.sc-ion-tag .b.sc-ion-tag, .c.sc-ion-tag-s > *, .c .sc-ion-tag-s > * {}'); }); - it('should handle multiple selector, commentOriginalSelector', () => { - const r = s('::slotted(ul), ::slotted(li) {}', 'sc-ion-tag', true); - expect(r).toEqual('/*!@::slotted(ul), ::slotted(li)*/.sc-ion-tag-s > ul, .sc-ion-tag-s > li {}'); - }); - it('should not replace the selector in a `@supports` rule', () => { expect(s('@supports selector(::slotted(*)) {::slotted(*) {color: red; }}', 'sc-cmp')).toEqual( '@supports selector(::slotted(*)) {.sc-cmp-s > * {color:red;}}', @@ -330,38 +260,6 @@ describe('ShadowCss', function () { }); }); - describe('convertScopedToShadow', () => { - it('media query', () => { - const input = `@media screen and (max-width:800px, max-height:100%) {/*!@div*/div.a {font-size:50px;}}`; - const expected = `@media screen and (max-width:800px, max-height:100%) {div{font-size:50px;}}`; - expect(convertScopedToShadow(input)).toBe(expected); - }); - - it('div', () => { - const input = `/*!@div*/div.sc-ion-tag {}`; - const expected = `div{}`; - expect(convertScopedToShadow(input)).toBe(expected); - }); - - it('new lines', () => { - const input = `/*!@div*/div.sc-ion-tag \n\n\n \t{}`; - const expected = `div{}`; - expect(convertScopedToShadow(input)).toBe(expected); - }); - - it(':host', () => { - const input = `/*!@:host*/.a-h {}`; - const expected = `:host{}`; - expect(convertScopedToShadow(input)).toBe(expected); - }); - - it('::slotted', () => { - const input = `/*!@::slotted(ul), ::slotted(li)*/.sc-ion-tag-s > ul, .sc-ion-tag-s > li {}`; - const expected = `::slotted(ul), ::slotted(li){}`; - expect(convertScopedToShadow(input)).toBe(expected); - }); - }); - it('should handle ::shadow', () => { const css = s('x::shadow > y {}', 'a'); expect(css).toEqual('x.a > y.a {}'); diff --git a/test/end-to-end/src/components.d.ts b/test/end-to-end/src/components.d.ts index 5f7107efecb..64e705faa31 100644 --- a/test/end-to-end/src/components.d.ts +++ b/test/end-to-end/src/components.d.ts @@ -43,6 +43,8 @@ export namespace Components { } interface CmpServerVsClient { } + interface CmpWithSlot { + } interface DomApi { } interface DomInteraction { @@ -241,6 +243,12 @@ declare global { prototype: HTMLCmpServerVsClientElement; new (): HTMLCmpServerVsClientElement; }; + interface HTMLCmpWithSlotElement extends Components.CmpWithSlot, HTMLStencilElement { + } + var HTMLCmpWithSlotElement: { + prototype: HTMLCmpWithSlotElement; + new (): HTMLCmpWithSlotElement; + }; interface HTMLDomApiElement extends Components.DomApi, HTMLStencilElement { } var HTMLDomApiElement: { @@ -406,6 +414,7 @@ declare global { "cmp-c": HTMLCmpCElement; "cmp-dsd": HTMLCmpDsdElement; "cmp-server-vs-client": HTMLCmpServerVsClientElement; + "cmp-with-slot": HTMLCmpWithSlotElement; "dom-api": HTMLDomApiElement; "dom-interaction": HTMLDomInteractionElement; "dom-visible": HTMLDomVisibleElement; @@ -467,6 +476,8 @@ declare namespace LocalJSX { } interface CmpServerVsClient { } + interface CmpWithSlot { + } interface DomApi { } interface DomInteraction { @@ -540,6 +551,7 @@ declare namespace LocalJSX { "cmp-c": CmpC; "cmp-dsd": CmpDsd; "cmp-server-vs-client": CmpServerVsClient; + "cmp-with-slot": CmpWithSlot; "dom-api": DomApi; "dom-interaction": DomInteraction; "dom-visible": DomVisible; @@ -584,6 +596,7 @@ declare module "@stencil/core" { "cmp-c": LocalJSX.CmpC & JSXBase.HTMLAttributes; "cmp-dsd": LocalJSX.CmpDsd & JSXBase.HTMLAttributes; "cmp-server-vs-client": LocalJSX.CmpServerVsClient & JSXBase.HTMLAttributes; + "cmp-with-slot": LocalJSX.CmpWithSlot & JSXBase.HTMLAttributes; "dom-api": LocalJSX.DomApi & JSXBase.HTMLAttributes; "dom-interaction": LocalJSX.DomInteraction & JSXBase.HTMLAttributes; "dom-visible": LocalJSX.DomVisible & 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 eb10e186db3..6470810e7bd 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,11 +1,85 @@ // 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`] = `""`; +exports[`renderToString can render a simple shadow component 1`] = ` +" + + +" +`; -exports[`renderToString renders scoped component 1`] = `""`; +exports[`renderToString can render nested components 1`] = ` +" + + +" +`; -exports[`renderToString supports passing props to components 1`] = `""`; +exports[`renderToString supports passing props to components 1`] = ` +" + + +" +`; -exports[`renderToString supports passing props to components with a simple object 1`] = `""`; +exports[`renderToString supports passing props to components with a simple object 1`] = ` +" + + +" +`; diff --git a/test/end-to-end/src/declarative-shadow-dom/cmp-dsd.css b/test/end-to-end/src/declarative-shadow-dom/cmp-dsd.css new file mode 100644 index 00000000000..d76dfbfeb09 --- /dev/null +++ b/test/end-to-end/src/declarative-shadow-dom/cmp-dsd.css @@ -0,0 +1,11 @@ +:host { + display: block; + border: 1px solid black; + padding: 10px; +} + +:host button { + background-color: lightblue; + border: 1px solid blue; + padding: 5px; +} diff --git a/test/end-to-end/src/declarative-shadow-dom/cmp-dsd.tsx b/test/end-to-end/src/declarative-shadow-dom/cmp-dsd.tsx index f1693706e9c..04b6c09d427 100644 --- a/test/end-to-end/src/declarative-shadow-dom/cmp-dsd.tsx +++ b/test/end-to-end/src/declarative-shadow-dom/cmp-dsd.tsx @@ -2,6 +2,7 @@ import { Component, h, Prop, State } from '@stencil/core'; @Component({ tag: 'cmp-dsd', + styleUrl: 'cmp-dsd.css', shadow: true, }) export class ComponentDSD { diff --git a/test/end-to-end/src/declarative-shadow-dom/cmp-with-slot.tsx b/test/end-to-end/src/declarative-shadow-dom/cmp-with-slot.tsx new file mode 100644 index 00000000000..08270d7890b --- /dev/null +++ b/test/end-to-end/src/declarative-shadow-dom/cmp-with-slot.tsx @@ -0,0 +1,19 @@ +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'cmp-with-slot', + shadow: true, +}) +export class ServerVSClientCmp { + render() { + 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 486ba66a1e0..2ffce6552f9 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 @@ -65,10 +65,11 @@ describe('renderToString', () => { expect(await readableToString(streamToString(input))).toContain(renderedHTML); }); - it('renders scoped component', async () => { + it('can render a simple shadow component', async () => { const { html } = await renderToString('', { serializeShadowRoot: true, fullDocument: false, + prettyHtml: true, }); expect(html).toMatchSnapshot(); }); @@ -79,6 +80,7 @@ describe('renderToString', () => { { serializeShadowRoot: true, fullDocument: false, + prettyHtml: true, }, ); expect(html).toMatchSnapshot(); @@ -89,6 +91,7 @@ describe('renderToString', () => { const { html } = await renderToString(``, { serializeShadowRoot: true, fullDocument: false, + prettyHtml: true, }); expect(html).toMatchSnapshot(); expect(html).toContain('2024 VW Vento'); @@ -102,7 +105,7 @@ describe('renderToString', () => { fullDocument: false, }, ); - expect(html).toContain('
'); + expect(html).toContain('
'); }); it('supports styles for DSD', async () => { @@ -110,7 +113,7 @@ describe('renderToString', () => { serializeShadowRoot: true, fullDocument: false, }); - expect(html).toContain('section.sc-another-car-detail{color:green}'); + expect(html).toContain('