Skip to content

Commit 82a128c

Browse files
fix(compiler): do not scope nested compound selectors starting with &
Since nested CSS rules are scoped, a nested compound selector starting with `&` was getting the content attribute appended. For example, `:host { &.foo {} }` produced `[_nghost-c1] { &.foo[_ngcontent-c1] {} }`. Because `&` refers to the host element there and the host element doesn't carry its own content attribute, the rule stopped matching. A compound selector starting with `&` can only match elements that are already matched by the scoped parent selector, so it doesn't need to be scoped again. Descendants (e.g. `& .child`) are still scoped. Fixes #71050 Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent c0dc8c4 commit 82a128c

2 files changed

Lines changed: 81 additions & 3 deletions

File tree

‎packages/compiler/src/shadow_css.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,11 @@ export class ShadowCss {
753753
const _scopeSelectorPart = (p: string) => {
754754
let scopedP = p.trim();
755755

756-
if (!scopedP || scopedP === '&') {
756+
// A compound selector starting with `&` (e.g. `&.foo`) only matches elements that are
757+
// already matched by the scoped parent selector so it doesn't need to be scoped again.
758+
// Adding the content attribute would also prevent it from matching when the parent is
759+
// the host element (e.g. `:host { &.foo {} }`), because the host doesn't have it.
760+
if (!scopedP || scopedP.startsWith('&')) {
757761
return p;
758762
}
759763

‎packages/compiler/test/shadow_css/nesting_spec.ts‎

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ describe('ShadowCss nesting', () => {
8282
.parent[contenta] {
8383
color: blue;
8484
85-
&.modifier[contenta] {
85+
&.modifier {
8686
color: red;
8787
}
8888
}
@@ -337,6 +337,80 @@ describe('ShadowCss nesting', () => {
337337
expect(result).toEqualCss(expected);
338338
});
339339

340+
it('should not scope compound selectors starting with an ampersand nested in :host', () => {
341+
const css = `
342+
:host {
343+
display: block;
344+
345+
&.modifier {
346+
width: 70px;
347+
}
348+
349+
&:hover, &[disabled] {
350+
color: red;
351+
}
352+
}
353+
`;
354+
355+
const expected = `
356+
[a-host] {
357+
display: block;
358+
359+
&.modifier {
360+
width: 70px;
361+
}
362+
363+
&:hover, &[disabled] {
364+
color: red;
365+
}
366+
}
367+
`;
368+
369+
const result = shim(css, 'contenta', 'a-host');
370+
expect(result).toEqualCss(expected);
371+
});
372+
373+
it('should scope descendants of an ampersand nested in :host', () => {
374+
const css = `
375+
:host {
376+
& .child {
377+
color: red;
378+
}
379+
380+
&.modifier > .child {
381+
color: blue;
382+
}
383+
}
384+
`;
385+
386+
const expected = `
387+
[a-host] {
388+
& .child[contenta] {
389+
color: red;
390+
}
391+
392+
&.modifier > .child[contenta] {
393+
color: blue;
394+
}
395+
}
396+
`;
397+
398+
const result = shim(css, 'contenta', 'a-host');
399+
expect(result).toEqualCss(expected);
400+
});
401+
402+
it('should not scope compound selectors starting with an ampersand nested in :host with a selector', () => {
403+
expect(shim(':host(.foo) { &.bar { color: red; } }', 'contenta', 'a-host')).toEqualCss(
404+
'.foo[a-host] { &.bar { color: red; } }',
405+
);
406+
});
407+
408+
it('should not scope compound selectors starting with an ampersand nested in :host-context', () => {
409+
expect(shim(':host-context(.dark) { &.bar { color: red; } }', 'contenta', 'a-host')).toEqualCss(
410+
'.dark[a-host], .dark [a-host] { &.bar { color: red; } }',
411+
);
412+
});
413+
340414
it('should shim selectors nested in :host-context', () => {
341415
const css = `
342416
:host-context(.foo) {
@@ -409,7 +483,7 @@ describe('ShadowCss nesting', () => {
409483
color: red;
410484
411485
@media (width >= 1024px) {
412-
&.modifier[contenta] {
486+
&.modifier {
413487
color: blue;
414488
}
415489
}

0 commit comments

Comments
 (0)