fix(compiler): do not scope nested compound selectors starting with & - #71152
rene-schakmann wants to merge 1 commit into
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
82a128c to
d6a053c
Compare
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 angular#71050
d6a053c to
d941ebf
Compare
JeanMeche
left a comment
There was a problem hiding this comment.
AGENT: I found a regression introduced by this change!
The logic scopedP.startsWith('&') causes pseudo-classes like :is() or :where() to be completely skipped—leaving their inner contents unscoped—if they are part of a compound selector starting with &.
For example, consider this CSS:
:host {
&:is(.foo, .bar > .baz) {
color: red;
}
}Before this PR, shadow_css.ts specifically parsed :is() and :where() to recursively scope their contents (to ensure encapsulation for things like .bar > .baz).
However, with this PR, _pseudoFunctionAwareScopeSelectorPart sees the & prefix and fails the pseudoSelectorParts.join('') === selectorPart check. It falls back to calling _scopeSelectorPart('&:is(.foo, .bar > .baz)'). Because the selector starts with &, it returns the entire string completely unmodified!
Output with the PR (Regression)
[a-host] {
&:is(.foo, .bar > .baz) { /* .foo and .bar are completely unscoped! */
color: red;
}
}This breaks encapsulation for .foo and .bar since they will match globally instead of being scoped to the component.
To fix this, _pseudoFunctionAwareScopeSelectorPart needs to be aware of the & prefix so it can strip it, recursively scope the inner contents of :is() / :where(), and then prepend the & back.
JeanMeche
left a comment
There was a problem hiding this comment.
AGENT: Here is a failing unit test that reproduces this regression. You can add this directly to packages/compiler/test/shadow_css/nesting_spec.ts.
Because of the startsWith('&') check, &:is(...) is returned entirely unscoped instead of recursively scoping the inner selectors .foo and .bar > .baz.
it('should scope inner selectors inside pseudo-class functions chained to an ampersand', () => {
const css = `
:host {
&:is(.foo, .bar > .baz) {
color: red;
}
}
`;
// What we EXPECT: the inner selectors are correctly scoped to the component
// What the PR currently PRODUCES: [a-host] { &:is(.foo, .bar > .baz) { ... } }
const expected = `
[a-host] {
&:is(.foo[contenta], .bar[contenta] > .baz[contenta]) {
color: red;
}
}
`;
const result = shim(css, 'contenta', 'a-host');
expect(result).toEqualCss(expected);
});
PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
Since d0d7f57 (#69885) nested rules are scoped, which also appends the content attribute to compound selectors starting with
&. Under:host,&refers to the host element, which only carries the_nghost-*attribute, so the rule silently stops matching (regression in 22.2.0):Issue Number: #71050
What is the new behavior?
A compound selector starting with
&(e.g.&.md,&:hover,&[attr]) is left unscoped, because it can only match elements already matched by the scoped parent selector. Descendants are still scoped:This also covers
:host(.foo)and:host-context(.foo)parents. For non-host parents (e.g..parent { &.modifier {} }) the output changes from&.modifier[contenta]to&.modifier, which matches the same elements since&already resolves to.parent[contenta]. The two existing expectations innesting_spec.tswere updated accordingly. Selectors that only reference&inside a pseudo-class (e.g..a:not(&)) are still scoped.Does this PR introduce a breaking change?
Other information
New tests added to
packages/compiler/test/shadow_css/nesting_spec.tsfor:host,:host(.foo),:host-context(.foo)and descendants of&. They fail without the change and pass with it.🤖 Generated with Claude Code