Skip to content

Commit 21be258

Browse files
GeorgySergadevversion
authored andcommitted
fix(compiler): scope :host-context inside pseudo selectors, do not decrease specificity (#57796)
parse constructions like `:where(:host-context(.foo))` correctly revert logic which lead to decreased specificity if `:where` was applied to another selector, for example `div` is transformed to `div[contenta]` with specificity of (0,1,1) so `div:where(.foo)` should not decrease it leading to `div[contenta]:where(.foo)` with the same specificity (0,1,1) instead of `div:where(.foo[contenta])` with specificity equal to (0,0,1) PR Close #57796
1 parent d325f9b commit 21be258

File tree

3 files changed

+87
-34
lines changed

3 files changed

+87
-34
lines changed

packages/compiler/src/shadow_css.ts

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ export class ShadowCss {
539539
* .foo<scopeName> .bar { ... }
540540
*/
541541
private _convertColonHostContext(cssText: string): string {
542-
return cssText.replace(_cssColonHostContextReGlobal, (selectorText) => {
542+
return cssText.replace(_cssColonHostContextReGlobal, (selectorText, pseudoPrefix) => {
543543
// We have captured a selector that contains a `:host-context` rule.
544544

545545
// For backward compatibility `:host-context` may contain a comma separated list of selectors.
@@ -594,10 +594,12 @@ export class ShadowCss {
594594
}
595595

596596
// The context selectors now must be combined with each other to capture all the possible
597-
// selectors that `:host-context` can match. See `combineHostContextSelectors()` for more
597+
// selectors that `:host-context` can match. See `_combineHostContextSelectors()` for more
598598
// info about how this is done.
599599
return contextSelectorGroups
600-
.map((contextSelectors) => combineHostContextSelectors(contextSelectors, selectorText))
600+
.map((contextSelectors) =>
601+
_combineHostContextSelectors(contextSelectors, selectorText, pseudoPrefix),
602+
)
601603
.join(', ');
602604
});
603605
}
@@ -800,18 +802,12 @@ export class ShadowCss {
800802
_cssPrefixWithPseudoSelectorFunction,
801803
);
802804
if (cssPrefixWithPseudoSelectorFunctionMatch) {
803-
const [cssPseudoSelectorFunction, mainSelector, pseudoSelector] =
804-
cssPrefixWithPseudoSelectorFunctionMatch;
805-
const hasOuterHostNoCombinator = mainSelector.includes(_polyfillHostNoCombinator);
806-
const scopedMainSelector = mainSelector.replace(
807-
_polyfillExactHostNoCombinatorReGlobal,
808-
`[${hostSelector}]`,
809-
);
810-
811-
// Unwrap the pseudo selector, to scope its contents.
805+
const [cssPseudoSelectorFunction] = cssPrefixWithPseudoSelectorFunctionMatch;
806+
807+
// Unwrap the pseudo selector to scope its contents.
812808
// For example,
813809
// - `:where(selectorToScope)` -> `selectorToScope`;
814-
// - `div:is(.foo, .bar)` -> `.foo, .bar`.
810+
// - `:is(.foo, .bar)` -> `.foo, .bar`.
815811
const selectorToScope = selectorPart.slice(cssPseudoSelectorFunction.length, -1);
816812

817813
if (selectorToScope.includes(_polyfillHostNoCombinator)) {
@@ -825,9 +821,7 @@ export class ShadowCss {
825821
});
826822

827823
// Put the result back into the pseudo selector function.
828-
scopedPart = `${scopedMainSelector}:${pseudoSelector}(${scopedInnerPart})`;
829-
830-
this._shouldScopeIndicator = this._shouldScopeIndicator || hasOuterHostNoCombinator;
824+
scopedPart = `${cssPseudoSelectorFunction}${scopedInnerPart})`;
831825
} else {
832826
this._shouldScopeIndicator =
833827
this._shouldScopeIndicator || selectorPart.includes(_polyfillHostNoCombinator);
@@ -965,7 +959,8 @@ class SafeSelector {
965959
}
966960
}
967961

968-
const _cssPrefixWithPseudoSelectorFunction = /^([^:]*):(where|is)\(/i;
962+
const _cssScopedPseudoFunctionPrefix = '(:(where|is)\\()?';
963+
const _cssPrefixWithPseudoSelectorFunction = /^:(where|is)\(/i;
969964
const _cssContentNextSelectorRe =
970965
/polyfill-next-selector[^}]*content:[\s]*?(['"])(.*?)\1[;\s]*}([^{]*?){/gim;
971966
const _cssContentRuleRe = /(polyfill-rule)[^}]*(content:[\s]*(['"])(.*?)\3)[;\s]*[^}]*}/gim;
@@ -976,13 +971,15 @@ const _polyfillHost = '-shadowcsshost';
976971
const _polyfillHostContext = '-shadowcsscontext';
977972
const _parenSuffix = '(?:\\((' + '(?:\\([^)(]*\\)|[^)(]*)+?' + ')\\))?([^,{]*)';
978973
const _cssColonHostRe = new RegExp(_polyfillHost + _parenSuffix, 'gim');
979-
const _cssColonHostContextReGlobal = new RegExp(_polyfillHostContext + _parenSuffix, 'gim');
974+
const _cssColonHostContextReGlobal = new RegExp(
975+
_cssScopedPseudoFunctionPrefix + '(' + _polyfillHostContext + _parenSuffix + ')',
976+
'gim',
977+
);
980978
const _cssColonHostContextRe = new RegExp(_polyfillHostContext + _parenSuffix, 'im');
981979
const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator';
982980
const _polyfillHostNoCombinatorWithinPseudoFunction = new RegExp(
983981
`:.*\\(.*${_polyfillHostNoCombinator}.*\\)`,
984982
);
985-
const _polyfillExactHostNoCombinatorReGlobal = /-shadowcsshost-no-combinator/g;
986983
const _polyfillHostNoCombinatorRe = /-shadowcsshost-no-combinator([^\s]*)/;
987984
const _polyfillHostNoCombinatorReGlobal = new RegExp(_polyfillHostNoCombinatorRe, 'g');
988985
const _shadowDOMSelectorsRe = [
@@ -1235,7 +1232,11 @@ function unescapeQuotes(str: string, isQuoted: boolean): string {
12351232
* @param contextSelectors an array of context selectors that will be combined.
12361233
* @param otherSelectors the rest of the selectors that are not context selectors.
12371234
*/
1238-
function combineHostContextSelectors(contextSelectors: string[], otherSelectors: string): string {
1235+
function _combineHostContextSelectors(
1236+
contextSelectors: string[],
1237+
otherSelectors: string,
1238+
pseudoPrefix = '',
1239+
): string {
12391240
const hostMarker = _polyfillHostNoCombinator;
12401241
_polyfillHostRe.lastIndex = 0; // reset the regex to ensure we get an accurate test
12411242
const otherSelectorsHasHost = _polyfillHostRe.test(otherSelectors);
@@ -1264,8 +1265,8 @@ function combineHostContextSelectors(contextSelectors: string[], otherSelectors:
12641265
return combined
12651266
.map((s) =>
12661267
otherSelectorsHasHost
1267-
? `${s}${otherSelectors}`
1268-
: `${s}${hostMarker}${otherSelectors}, ${s} ${hostMarker}${otherSelectors}`,
1268+
? `${pseudoPrefix}${s}${otherSelectors}`
1269+
: `${pseudoPrefix}${s}${hostMarker}${otherSelectors}, ${pseudoPrefix}${s} ${hostMarker}${otherSelectors}`,
12691270
)
12701271
.join(',');
12711272
}

packages/compiler/test/shadow_css/host_and_host_context_spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,42 @@ describe('ShadowCss, :host and :host-context', () => {
107107
});
108108

109109
describe(':host-context', () => {
110+
it('should transform :host-context with pseudo selectors', () => {
111+
expect(
112+
shim(':host-context(backdrop:not(.borderless)) .backdrop {}', 'contenta', 'hosta'),
113+
).toEqualCss(
114+
'backdrop:not(.borderless)[hosta] .backdrop[contenta], backdrop:not(.borderless) [hosta] .backdrop[contenta] {}',
115+
);
116+
expect(shim(':where(:host-context(backdrop)) {}', 'contenta', 'hosta')).toEqualCss(
117+
':where(backdrop[hosta]), :where(backdrop [hosta]) {}',
118+
);
119+
expect(shim(':where(:host-context(outer1)) :host(bar) {}', 'contenta', 'hosta')).toEqualCss(
120+
':where(outer1) bar[hosta] {}',
121+
);
122+
expect(
123+
shim(':where(:host-context(.one)) :where(:host-context(.two)) {}', 'contenta', 'a-host'),
124+
).toEqualCss(
125+
':where(.one.two[a-host]), ' + // `one` and `two` both on the host
126+
':where(.one.two [a-host]), ' + // `one` and `two` are both on the same ancestor
127+
':where(.one .two[a-host]), ' + // `one` is an ancestor and `two` is on the host
128+
':where(.one .two [a-host]), ' + // `one` and `two` are both ancestors (in that order)
129+
':where(.two .one[a-host]), ' + // `two` is an ancestor and `one` is on the host
130+
':where(.two .one [a-host])' + // `two` and `one` are both ancestors (in that order)
131+
' {}',
132+
);
133+
expect(
134+
shim(':where(:host-context(backdrop)) .foo ~ .bar {}', 'contenta', 'hosta'),
135+
).toEqualCss(
136+
':where(backdrop[hosta]) .foo[contenta] ~ .bar[contenta], :where(backdrop [hosta]) .foo[contenta] ~ .bar[contenta] {}',
137+
);
138+
expect(shim(':where(:host-context(backdrop)) :host {}', 'contenta', 'hosta')).toEqualCss(
139+
':where(backdrop) [hosta] {}',
140+
);
141+
expect(shim('div:where(:host-context(backdrop)) :host {}', 'contenta', 'hosta')).toEqualCss(
142+
'div:where(backdrop) [hosta] {}',
143+
);
144+
});
145+
110146
it('should handle tag selector', () => {
111147
expect(shim(':host-context(div) {}', 'contenta', 'a-host')).toEqualCss(
112148
'div[a-host], div [a-host] {}',

packages/compiler/test/shadow_css/shadow_css_spec.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,19 +69,14 @@ describe('ShadowCss', () => {
6969
expect(shim('[attr] {}', 'contenta')).toEqualCss('[attr][contenta] {}');
7070
});
7171

72-
it('should transform :host and :host-context with attributes and pseudo selectors', () => {
72+
it('should transform :host with attributes', () => {
7373
expect(shim(':host [attr] {}', 'contenta', 'hosta')).toEqualCss('[hosta] [attr][contenta] {}');
7474
expect(shim(':host(create-first-project) {}', 'contenta', 'hosta')).toEqualCss(
7575
'create-first-project[hosta] {}',
7676
);
7777
expect(shim(':host[attr] {}', 'contenta', 'hosta')).toEqualCss('[attr][hosta] {}');
7878
expect(shim(':host[attr]:where(:not(.cm-button)) {}', 'contenta', 'hosta')).toEqualCss(
79-
'[hosta][attr]:where(:not(.cm-button)) {}',
80-
);
81-
expect(
82-
shim(':host-context(backdrop:not(.borderless)) .backdrop {}', 'contenta', 'hosta'),
83-
).toEqualCss(
84-
'backdrop:not(.borderless)[hosta] .backdrop[contenta], backdrop:not(.borderless) [hosta] .backdrop[contenta] {}',
79+
'[attr][hosta]:where(:not(.cm-button)) {}',
8580
);
8681
});
8782

@@ -94,6 +89,27 @@ describe('ShadowCss', () => {
9489
expect(shim('.one\\:two .three\\:four {}', 'contenta')).toEqualCss(
9590
'.one\\:two[contenta] .three\\:four[contenta] {}',
9691
);
92+
expect(shim('div:where(.one) {}', 'contenta', 'hosta')).toEqualCss(
93+
'div[contenta]:where(.one) {}',
94+
);
95+
expect(shim('div:where() {}', 'contenta', 'hosta')).toEqualCss('div[contenta]:where() {}');
96+
// See `xit('should parse concatenated pseudo selectors'`
97+
expect(shim(':where(a):where(b) {}', 'contenta', 'hosta')).toEqualCss(
98+
':where(a)[contenta]:where(b) {}',
99+
);
100+
expect(shim('*:where(.one) {}', 'contenta', 'hosta')).toEqualCss('*[contenta]:where(.one) {}');
101+
expect(shim('*:where(.one) ::ng-deep .foo {}', 'contenta', 'hosta')).toEqualCss(
102+
'*[contenta]:where(.one) .foo {}',
103+
);
104+
});
105+
106+
xit('should parse concatenated pseudo selectors', () => {
107+
// Current logic leads to a result with an outer scope
108+
// It could be changed, to not increase specificity
109+
// Requires a more complex parsing
110+
expect(shim(':where(a):where(b) {}', 'contenta', 'hosta')).toEqualCss(
111+
':where(a[contenta]):where(b[contenta]) {}',
112+
);
97113
});
98114

99115
it('should handle pseudo functions correctly', () => {
@@ -136,7 +152,7 @@ describe('ShadowCss', () => {
136152
expect(shim(':where([foo]) {}', 'contenta', 'hosta')).toEqualCss(':where([foo][contenta]) {}');
137153

138154
// :is()
139-
expect(shim('div:is(.foo) {}', 'contenta', 'a-host')).toEqualCss('div:is(.foo[contenta]) {}');
155+
expect(shim('div:is(.foo) {}', 'contenta', 'a-host')).toEqualCss('div[contenta]:is(.foo) {}');
140156
expect(shim(':is(.dark :host) {}', 'contenta', 'a-host')).toEqualCss(':is(.dark [a-host]) {}');
141157
expect(shim(':is(.dark) :is(:host) {}', 'contenta', 'a-host')).toEqualCss(
142158
':is(.dark) :is([a-host]) {}',
@@ -182,12 +198,12 @@ describe('ShadowCss', () => {
182198
'hosta',
183199
),
184200
).toEqualCss(
185-
':where(:where(a[contenta]:has(.foo), b[contenta]) :is(.one[contenta], .two:where(.foo[contenta] > .bar[contenta]))) {}',
201+
':where(:where(a[contenta]:has(.foo), b[contenta]) :is(.one[contenta], .two[contenta]:where(.foo > .bar))) {}',
186202
);
187203

188204
// complex selectors
189205
expect(shim(':host:is([foo],[foo-2])>div.example-2 {}', 'contenta', 'a-host')).toEqualCss(
190-
'[a-host]:is([foo], [foo-2]) > div.example-2[contenta] {}',
206+
'[a-host]:is([foo],[foo-2]) > div.example-2[contenta] {}',
191207
);
192208
expect(shim(':host:is([foo], [foo-2]) > div.example-2 {}', 'contenta', 'a-host')).toEqualCss(
193209
'[a-host]:is([foo], [foo-2]) > div.example-2[contenta] {}',
@@ -220,11 +236,11 @@ describe('ShadowCss', () => {
220236
'.header[contenta]:not(.admin) {}',
221237
);
222238
expect(shim('.header:is(:host > .toolbar, :host ~ .panel) {}', 'contenta', 'hosta')).toEqualCss(
223-
'.header:is([hosta] > .toolbar[contenta], [hosta] ~ .panel[contenta]) {}',
239+
'.header[contenta]:is([hosta] > .toolbar, [hosta] ~ .panel) {}',
224240
);
225241
expect(
226242
shim('.header:where(:host > .toolbar, :host ~ .panel) {}', 'contenta', 'hosta'),
227-
).toEqualCss('.header:where([hosta] > .toolbar[contenta], [hosta] ~ .panel[contenta]) {}');
243+
).toEqualCss('.header[contenta]:where([hosta] > .toolbar, [hosta] ~ .panel) {}');
228244
expect(shim('.header:not(.admin, :host.super .header) {}', 'contenta', 'hosta')).toEqualCss(
229245
'.header[contenta]:not(.admin, .super[hosta] .header) {}',
230246
);

0 commit comments

Comments
 (0)