Skip to content

Commit 07b7af3

Browse files
petebacondarwinthePunderWoman
authored andcommitted
fix(compiler): support multiple :host-context() selectors (#40494)
In `ViewEncapsulation.Emulated` mode, the compiler must generate additional combinations of selectors to handle the `:host-context()` pseudo-class function. Previously, when there is was more than one `:host-context()` selector in a rule, the compiler was generating invalid selectors. This commit generates all possible combinations of selectors needed to match the same elements as the native `:host-context()` selector. Fixes #19199 PR Close #40494
1 parent dc06873 commit 07b7af3

File tree

2 files changed

+126
-35
lines changed

2 files changed

+126
-35
lines changed

packages/compiler/src/shadow_css.ts

Lines changed: 86 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -260,15 +260,29 @@ export class ShadowCss {
260260
* .foo<scopeName> > .bar
261261
*/
262262
private _convertColonHost(cssText: string): string {
263-
return this._convertColonRule(cssText, _cssColonHostRe, this._colonHostPartReplacer);
263+
return cssText.replace(_cssColonHostRe, (_, hostSelectors: string, otherSelectors: string) => {
264+
if (hostSelectors) {
265+
const convertedSelectors: string[] = [];
266+
const hostSelectorArray = hostSelectors.split(',').map(p => p.trim());
267+
for (const hostSelector of hostSelectorArray) {
268+
if (!hostSelector) break;
269+
const convertedSelector =
270+
_polyfillHostNoCombinator + hostSelector.replace(_polyfillHost, '') + otherSelectors;
271+
convertedSelectors.push(convertedSelector);
272+
}
273+
return convertedSelectors.join(',');
274+
} else {
275+
return _polyfillHostNoCombinator + otherSelectors;
276+
}
277+
});
264278
}
265279

266280
/*
267281
* convert a rule like :host-context(.foo) > .bar { }
268282
*
269283
* to
270284
*
271-
* .foo<scopeName> > .bar, .foo scopeName > .bar { }
285+
* .foo<scopeName> > .bar, .foo <scopeName> > .bar { }
272286
*
273287
* and
274288
*
@@ -279,38 +293,26 @@ export class ShadowCss {
279293
* .foo<scopeName> .bar { ... }
280294
*/
281295
private _convertColonHostContext(cssText: string): string {
282-
return this._convertColonRule(
283-
cssText, _cssColonHostContextRe, this._colonHostContextPartReplacer);
284-
}
285-
286-
private _convertColonRule(cssText: string, regExp: RegExp, partReplacer: Function): string {
287-
// m[1] = :host(-context), m[2] = contents of (), m[3] rest of rule
288-
return cssText.replace(regExp, function(...m: string[]) {
289-
if (m[2]) {
290-
const parts = m[2].split(',');
291-
const r: string[] = [];
292-
for (let i = 0; i < parts.length; i++) {
293-
const p = parts[i].trim();
294-
if (!p) break;
295-
r.push(partReplacer(_polyfillHostNoCombinator, p, m[3]));
296-
}
297-
return r.join(',');
298-
} else {
299-
return _polyfillHostNoCombinator + m[3];
296+
return cssText.replace(_cssColonHostContextReGlobal, selectorText => {
297+
// We have captured a selector that contains a `:host-context` rule.
298+
// There may be more than one so `selectorText` could look like:
299+
// `:host-context(.one):host-context(.two)`.
300+
301+
const contextSelectors: string[] = [];
302+
let match: RegExpMatchArray|null;
303+
304+
// Execute `_cssColonHostContextRe` over and over until we have extracted all the
305+
// `:host-context` selectors from this selector.
306+
while (match = _cssColonHostContextRe.exec(selectorText)) {
307+
// `match` = [':host-context(<selectors>)<rest>', <selectors>, <rest>]
308+
contextSelectors.push(match[1].trim());
309+
selectorText = match[2];
300310
}
301-
});
302-
}
303-
304-
private _colonHostContextPartReplacer(host: string, part: string, suffix: string): string {
305-
if (part.indexOf(_polyfillHost) > -1) {
306-
return this._colonHostPartReplacer(host, part, suffix);
307-
} else {
308-
return host + part + suffix + ', ' + part + ' ' + host + suffix;
309-
}
310-
}
311311

312-
private _colonHostPartReplacer(host: string, part: string, suffix: string): string {
313-
return host + part.replace(_polyfillHost, '') + suffix;
312+
// The context selectors now must be combined with each other to capture all the possible
313+
// selectors that `:host-context` can match.
314+
return combineHostContextSelectors(_polyfillHostNoCombinator, contextSelectors, selectorText);
315+
});
314316
}
315317

316318
/*
@@ -534,11 +536,12 @@ const _cssContentUnscopedRuleRe =
534536
const _polyfillHost = '-shadowcsshost';
535537
// note: :host-context pre-processed to -shadowcsshostcontext.
536538
const _polyfillHostContext = '-shadowcsscontext';
537-
const _parenSuffix = ')(?:\\((' +
539+
const _parenSuffix = '(?:\\((' +
538540
'(?:\\([^)(]*\\)|[^)(]*)+?' +
539541
')\\))?([^,{]*)';
540-
const _cssColonHostRe = new RegExp('(' + _polyfillHost + _parenSuffix, 'gim');
541-
const _cssColonHostContextRe = new RegExp('(' + _polyfillHostContext + _parenSuffix, 'gim');
542+
const _cssColonHostRe = new RegExp(_polyfillHost + _parenSuffix, 'gim');
543+
const _cssColonHostContextReGlobal = new RegExp(_polyfillHostContext + _parenSuffix, 'gim');
544+
const _cssColonHostContextRe = new RegExp(_polyfillHostContext + _parenSuffix, 'im');
542545
const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator';
543546
const _polyfillHostNoCombinatorRe = /-shadowcsshost-no-combinator([^\s]*)/;
544547
const _shadowDOMSelectorsRe = [
@@ -650,3 +653,51 @@ function escapeBlocks(
650653
}
651654
return new StringWithEscapedBlocks(resultParts.join(''), escapedBlocks);
652655
}
656+
657+
/**
658+
* Combine the `contextSelectors` with the `hostMarker` and the `otherSelectors`
659+
* to create a selector that matches the same as `:host-context()`.
660+
*
661+
* Given a single context selector `A` we need to output selectors that match on the host and as an
662+
* ancestor of the host:
663+
*
664+
* ```
665+
* A <hostMarker>, A<hostMarker> {}
666+
* ```
667+
*
668+
* When there is more than one context selector we also have to create combinations of those
669+
* selectors with each other. For example if there are `A` and `B` selectors the output is:
670+
*
671+
* ```
672+
* AB<hostMarker>, AB <hostMarker>, A B<hostMarker>,
673+
* B A<hostMarker>, A B <hostMarker>, B A <hostMarker> {}
674+
* ```
675+
*
676+
* And so on...
677+
*
678+
* @param hostMarker the string that selects the host element.
679+
* @param contextSelectors an array of context selectors that will be combined.
680+
* @param otherSelectors the rest of the selectors that are not context selectors.
681+
*/
682+
function combineHostContextSelectors(
683+
hostMarker: string, contextSelectors: string[], otherSelectors: string): string {
684+
const combined: string[] = [contextSelectors.pop() || ''];
685+
while (contextSelectors.length > 0) {
686+
const length = combined.length;
687+
const contextSelector = contextSelectors.pop();
688+
for (let i = 0; i < length; i++) {
689+
const previousSelectors = combined[i];
690+
// Add the new selector as a descendant of the previous selectors
691+
combined[length * 2 + i] = previousSelectors + ' ' + contextSelector;
692+
// Add the new selector as an ancestor of the previous selectors
693+
combined[length + i] = contextSelector + ' ' + previousSelectors;
694+
// Add the new selector to act on the same element as the previous selectors
695+
combined[i] = contextSelector + previousSelectors;
696+
}
697+
}
698+
// Finally connect the selector to the `hostMarker`s: either acting directly on the host
699+
// (A<hostMarker>) or as an ancestor (A <hostMarker>).
700+
return combined
701+
.map(s => `${s}${hostMarker}${otherSelectors}, ${s} ${hostMarker}${otherSelectors}`)
702+
.join(',');
703+
}

packages/compiler/test/shadow_css_spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ import {normalizeCSS} from '@angular/platform-browser/testing/src/browser_util';
145145
.toEqual('ul[a-host] > .z[contenta], li[a-host] > .z[contenta] {}');
146146
});
147147

148+
it('should handle compound class selectors', () => {
149+
expect(s(':host(.a.b) {}', 'contenta', 'a-host')).toEqual('.a.b[a-host] {}');
150+
});
151+
148152
it('should handle multiple class selectors', () => {
149153
expect(s(':host(.x,.y) {}', 'contenta', 'a-host')).toEqual('.x[a-host], .y[a-host] {}');
150154
expect(s(':host(.x,.y) > .z {}', 'contenta', 'a-host'))
@@ -208,6 +212,42 @@ import {normalizeCSS} from '@angular/platform-browser/testing/src/browser_util';
208212
expect(s(':host-context([a=b]) {}', 'contenta', 'a-host'))
209213
.toEqual('[a=b][a-host], [a="b"] [a-host] {}');
210214
});
215+
216+
it('should handle multiple :host-context() selectors', () => {
217+
expect(s(':host-context(.one):host-context(.two) {}', 'contenta', 'a-host'))
218+
.toEqual(
219+
'.one.two[a-host], ' + // `one` and `two` both on the host
220+
'.one.two [a-host], ' + // `one` and `two` are both on the same ancestor
221+
'.one .two[a-host], ' + // `one` is an ancestor and `two` is on the host
222+
'.one .two [a-host], ' + // `one` and `two` are both ancestors (in that order)
223+
'.two .one[a-host], ' + // `two` is an ancestor and `one` is on the host
224+
'.two .one [a-host]' + // `two` and `one` are both ancestors (in that order)
225+
' {}');
226+
227+
expect(s(':host-context(.X):host-context(.Y):host-context(.Z) {}', 'contenta', 'a-host')
228+
.replace(/ \{\}$/, '')
229+
.split(/\,\s+/))
230+
.toEqual([
231+
'.X.Y.Z[a-host]',
232+
'.X.Y.Z [a-host]',
233+
'.X.Y .Z[a-host]',
234+
'.X.Y .Z [a-host]',
235+
'.X.Z .Y[a-host]',
236+
'.X.Z .Y [a-host]',
237+
'.X .Y.Z[a-host]',
238+
'.X .Y.Z [a-host]',
239+
'.X .Y .Z[a-host]',
240+
'.X .Y .Z [a-host]',
241+
'.X .Z .Y[a-host]',
242+
'.X .Z .Y [a-host]',
243+
'.Y.Z .X[a-host]',
244+
'.Y.Z .X [a-host]',
245+
'.Y .Z .X[a-host]',
246+
'.Y .Z .X [a-host]',
247+
'.Z .Y .X[a-host]',
248+
'.Z .Y .X [a-host]',
249+
]);
250+
});
211251
});
212252

213253
it('should support polyfill-next-selector', () => {

0 commit comments

Comments
 (0)