diff --git a/src/declarations/stencil-private.ts b/src/declarations/stencil-private.ts index f6b2941f50e..f252f8287d0 100644 --- a/src/declarations/stencil-private.ts +++ b/src/declarations/stencil-private.ts @@ -1927,7 +1927,7 @@ export interface PlatformRuntime { export type StyleMap = Map; -export type RootAppliedStyleMap = WeakMap>; +export type RootAppliedStyleMap = WeakMap>; export interface ScreenshotConnector { initBuild(opts: ScreenshotConnectorOptions): Promise; diff --git a/src/runtime/styles.ts b/src/runtime/styles.ts index a4da2e5377f..241a2ad63fa 100644 --- a/src/runtime/styles.ts +++ b/src/runtime/styles.ts @@ -73,22 +73,33 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet let appliedStyles = rootAppliedStyles.get(styleContainerNode); let styleElm; if (!appliedStyles) { - rootAppliedStyles.set(styleContainerNode, (appliedStyles = new Set())); + rootAppliedStyles.set(styleContainerNode, (appliedStyles = new Map())); } - // Check if style element already exists (for HMR updates) - // For shadow DOM components, directly update their dedicated style element - // For scoped components, check if they have their own HMR-created style element - const existingStyleElm: HTMLStyleElement = - (BUILD.hydrateClientSide || BUILD.hotModuleReplacement) && - styleContainerNode.querySelector(`[${HYDRATED_STYLE_ID}="${scopeId}"]`); + // Check if tracked element is still in the DOM (fixes #6637) + const trackedElm = appliedStyles.get(scopeId); + if (trackedElm !== undefined) { + if (trackedElm === null || trackedElm.parentNode === styleContainerNode) { + if (BUILD.hotModuleReplacement && trackedElm !== null && trackedElm.textContent !== style) { + trackedElm.textContent = style; + } + return scopeId; + } + appliedStyles.delete(scopeId); + } + + const existingStyleElm: HTMLStyleElement | undefined = + ((BUILD.hydrateClientSide || BUILD.hotModuleReplacement) && + styleContainerNode.querySelector(`[${HYDRATED_STYLE_ID}="${scopeId}"]`)) || + undefined; if (existingStyleElm) { - // Update existing style element (for hydration or HMR) existingStyleElm.textContent = style; - } else if (!appliedStyles.has(scopeId)) { + appliedStyles.set(scopeId, existingStyleElm); + } else { styleElm = win.document.createElement('style'); styleElm.textContent = style; + let appliedStyleElm: HTMLStyleElement | null = styleElm; // Apply CSP nonce to the style tag if it exists const nonce = plt.$nonce$ ?? queryNonceMetaTagContent(win.document); @@ -148,6 +159,7 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet } else { styleContainerNode.adoptedStyleSheets = [stylesheet, ...styleContainerNode.adoptedStyleSheets]; } + appliedStyleElm = null; } else { /** * If a scoped component is used within a shadow root and constructable stylesheets are @@ -165,6 +177,7 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet const existingStyleContainer: HTMLStyleElement = styleContainerNode.querySelector('style'); if (existingStyleContainer && !BUILD.hotModuleReplacement) { existingStyleContainer.textContent = style + existingStyleContainer.textContent; + appliedStyleElm = existingStyleContainer; } else { (styleContainerNode as HTMLElement).prepend(styleElm); } @@ -186,14 +199,12 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet styleElm.textContent += SLOT_FB_CSS; } - if (appliedStyles) { - appliedStyles.add(scopeId); - } + appliedStyles.set(scopeId, appliedStyleElm); } } else if (BUILD.constructableCSS) { let appliedStyles = rootAppliedStyles.get(styleContainerNode); if (!appliedStyles) { - rootAppliedStyles.set(styleContainerNode, (appliedStyles = new Set())); + rootAppliedStyles.set(styleContainerNode, (appliedStyles = new Map())); } if (!appliedStyles.has(scopeId)) { /** @@ -220,7 +231,7 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet styleContainerNode.adoptedStyleSheets = [...styleContainerNode.adoptedStyleSheets, stylesheet]; } - appliedStyles.add(scopeId); + appliedStyles.set(scopeId, null); // Remove SSR style element from shadow root now that adoptedStyleSheets is in use // Only remove from shadow roots, not from document head (for scoped components) diff --git a/src/runtime/test/style.spec.tsx b/src/runtime/test/style.spec.tsx index ed85d1f5bd8..c02b3d93d99 100644 --- a/src/runtime/test/style.spec.tsx +++ b/src/runtime/test/style.spec.tsx @@ -56,6 +56,43 @@ describe('style', () => { ); }); + it('re-attaches a removed style element when the component is rendered again', async () => { + @Component({ + tag: 'cmp-a', + styles: ` + cmp-a { + color: red; + } + `, + }) + class CmpA { + render() { + return `innertext`; + } + } + + const page = await newSpecPage({ + components: [CmpA], + html: ``, + attachStyles: true, + }); + + const findCmpStyle = () => + Array.from(page.doc.head.querySelectorAll('style')).find((styleElm) => styleElm.textContent?.includes('color: red')); + + const initialStyleElm = findCmpStyle(); + expect(initialStyleElm).toBeDefined(); + + initialStyleElm!.remove(); + expect(findCmpStyle()).toBeUndefined(); + + await page.setContent(``); + + const reattachedStyleElm = findCmpStyle(); + expect(reattachedStyleElm).toBeDefined(); + expect(reattachedStyleElm!.isConnected).toBe(true); + }); + describe('mode', () => { it('md mode', async () => { setMode(() => 'md');