Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion src/declarations/stencil-private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1927,7 +1927,7 @@ export interface PlatformRuntime {

export type StyleMap = Map<string, CSSStyleSheet | string>;

export type RootAppliedStyleMap = WeakMap<Element, Set<string>>;
export type RootAppliedStyleMap = WeakMap<Element | ShadowRoot, Map<string, HTMLStyleElement | null>>;

export interface ScreenshotConnector {
initBuild(opts: ScreenshotConnectorOptions): Promise<void>;
Expand Down
39 changes: 25 additions & 14 deletions src/runtime/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Comment thread
saudademjj marked this conversation as resolved.
return scopeId;
}
appliedStyles.delete(scopeId);
}
Comment thread
saudademjj marked this conversation as resolved.

const existingStyleElm: HTMLStyleElement | undefined =
((BUILD.hydrateClientSide || BUILD.hotModuleReplacement) &&
Comment thread
saudademjj marked this conversation as resolved.
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;
Comment thread
saudademjj marked this conversation as resolved.
let appliedStyleElm: HTMLStyleElement | null = styleElm;

// Apply CSP nonce to the style tag if it exists
const nonce = plt.$nonce$ ?? queryNonceMetaTagContent(win.document);
Expand Down Expand Up @@ -148,6 +159,7 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet
} else {
styleContainerNode.adoptedStyleSheets = [stylesheet, ...styleContainerNode.adoptedStyleSheets];
}
appliedStyleElm = null;
Comment thread
saudademjj marked this conversation as resolved.
} else {
/**
* If a scoped component is used within a shadow root and constructable stylesheets are
Expand All @@ -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;
Comment thread
saudademjj marked this conversation as resolved.
} else {
(styleContainerNode as HTMLElement).prepend(styleElm);
}
Expand All @@ -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)) {
/**
Expand All @@ -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)
Expand Down
37 changes: 37 additions & 0 deletions src/runtime/test/style.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,43 @@ describe('style', () => {
);
});

it('re-attaches a removed style element when the component is rendered again', async () => {
Comment thread
saudademjj marked this conversation as resolved.
@Component({
tag: 'cmp-a',
styles: `
cmp-a {
color: red;
}
`,
})
class CmpA {
render() {
return `innertext`;
}
}

const page = await newSpecPage({
components: [CmpA],
html: `<cmp-a></cmp-a>`,
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(`<cmp-a></cmp-a>`);

const reattachedStyleElm = findCmpStyle();
expect(reattachedStyleElm).toBeDefined();
expect(reattachedStyleElm!.isConnected).toBe(true);
});

describe('mode', () => {
it('md mode', async () => {
setMode(() => 'md');
Expand Down
Loading