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
This commit is contained in:
parent
d6440c1808
commit
ba3f99d7cc
|
@ -260,7 +260,21 @@ export class ShadowCss {
|
|||
* .foo<scopeName> > .bar
|
||||
*/
|
||||
private _convertColonHost(cssText: string): string {
|
||||
return this._convertColonRule(cssText, _cssColonHostRe, this._colonHostPartReplacer);
|
||||
return cssText.replace(_cssColonHostRe, (_, hostSelectors: string, otherSelectors: string) => {
|
||||
if (hostSelectors) {
|
||||
const convertedSelectors: string[] = [];
|
||||
const hostSelectorArray = hostSelectors.split(',').map(p => p.trim());
|
||||
for (const hostSelector of hostSelectorArray) {
|
||||
if (!hostSelector) break;
|
||||
const convertedSelector =
|
||||
_polyfillHostNoCombinator + hostSelector.replace(_polyfillHost, '') + otherSelectors;
|
||||
convertedSelectors.push(convertedSelector);
|
||||
}
|
||||
return convertedSelectors.join(',');
|
||||
} else {
|
||||
return _polyfillHostNoCombinator + otherSelectors;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -268,7 +282,7 @@ export class ShadowCss {
|
|||
*
|
||||
* to
|
||||
*
|
||||
* .foo<scopeName> > .bar, .foo scopeName > .bar { }
|
||||
* .foo<scopeName> > .bar, .foo <scopeName> > .bar { }
|
||||
*
|
||||
* and
|
||||
*
|
||||
|
@ -279,40 +293,31 @@ export class ShadowCss {
|
|||
* .foo<scopeName> .bar { ... }
|
||||
*/
|
||||
private _convertColonHostContext(cssText: string): string {
|
||||
return this._convertColonRule(
|
||||
cssText, _cssColonHostContextRe, this._colonHostContextPartReplacer);
|
||||
}
|
||||
return cssText.replace(_cssColonHostContextReGlobal, selectorText => {
|
||||
// We have captured a selector that contains a `:host-context` rule.
|
||||
// There may be more than one so `selectorText` could look like:
|
||||
// `:host-context(.one):host-context(.two)`.
|
||||
|
||||
private _convertColonRule(cssText: string, regExp: RegExp, partReplacer: Function): string {
|
||||
// m[1] = :host(-context), m[2] = contents of (), m[3] rest of rule
|
||||
return cssText.replace(regExp, function(...m: string[]) {
|
||||
if (m[2]) {
|
||||
const parts = m[2].split(',');
|
||||
const r: string[] = [];
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const p = parts[i].trim();
|
||||
if (!p) break;
|
||||
r.push(partReplacer(_polyfillHostNoCombinator, p, m[3]));
|
||||
const contextSelectors: string[] = [];
|
||||
let match: RegExpMatchArray|null;
|
||||
|
||||
// Execute `_cssColonHostContextRe` over and over until we have extracted all the
|
||||
// `:host-context` selectors from this selector.
|
||||
while (match = _cssColonHostContextRe.exec(selectorText)) {
|
||||
// `match` = [':host-context(<selectors>)<rest>', <selectors>, <rest>]
|
||||
const contextSelector = (match[1] ?? '').trim();
|
||||
if (contextSelector !== '') {
|
||||
contextSelectors.push(contextSelector);
|
||||
}
|
||||
return r.join(',');
|
||||
} else {
|
||||
return _polyfillHostNoCombinator + m[3];
|
||||
selectorText = match[2];
|
||||
}
|
||||
|
||||
// The context selectors now must be combined with each other to capture all the possible
|
||||
// selectors that `:host-context` can match.
|
||||
return combineHostContextSelectors(_polyfillHostNoCombinator, contextSelectors, selectorText);
|
||||
});
|
||||
}
|
||||
|
||||
private _colonHostContextPartReplacer(host: string, part: string, suffix: string): string {
|
||||
if (part.indexOf(_polyfillHost) > -1) {
|
||||
return this._colonHostPartReplacer(host, part, suffix);
|
||||
} else {
|
||||
return host + part + suffix + ', ' + part + ' ' + host + suffix;
|
||||
}
|
||||
}
|
||||
|
||||
private _colonHostPartReplacer(host: string, part: string, suffix: string): string {
|
||||
return host + part.replace(_polyfillHost, '') + suffix;
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert combinators like ::shadow and pseudo-elements like ::content
|
||||
* by replacing with space.
|
||||
|
@ -534,11 +539,12 @@ const _cssContentUnscopedRuleRe =
|
|||
const _polyfillHost = '-shadowcsshost';
|
||||
// note: :host-context pre-processed to -shadowcsshostcontext.
|
||||
const _polyfillHostContext = '-shadowcsscontext';
|
||||
const _parenSuffix = ')(?:\\((' +
|
||||
const _parenSuffix = '(?:\\((' +
|
||||
'(?:\\([^)(]*\\)|[^)(]*)+?' +
|
||||
')\\))?([^,{]*)';
|
||||
const _cssColonHostRe = new RegExp('(' + _polyfillHost + _parenSuffix, 'gim');
|
||||
const _cssColonHostContextRe = new RegExp('(' + _polyfillHostContext + _parenSuffix, 'gim');
|
||||
const _cssColonHostRe = new RegExp(_polyfillHost + _parenSuffix, 'gim');
|
||||
const _cssColonHostContextReGlobal = new RegExp(_polyfillHostContext + _parenSuffix, 'gim');
|
||||
const _cssColonHostContextRe = new RegExp(_polyfillHostContext + _parenSuffix, 'im');
|
||||
const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator';
|
||||
const _polyfillHostNoCombinatorRe = /-shadowcsshost-no-combinator([^\s]*)/;
|
||||
const _shadowDOMSelectorsRe = [
|
||||
|
@ -650,3 +656,56 @@ function escapeBlocks(
|
|||
}
|
||||
return new StringWithEscapedBlocks(resultParts.join(''), escapedBlocks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine the `contextSelectors` with the `hostMarker` and the `otherSelectors`
|
||||
* to create a selector that matches the same as `:host-context()`.
|
||||
*
|
||||
* Given a single context selector `A` we need to output selectors that match on the host and as an
|
||||
* ancestor of the host:
|
||||
*
|
||||
* ```
|
||||
* A <hostMarker>, A<hostMarker> {}
|
||||
* ```
|
||||
*
|
||||
* When there is more than one context selector we also have to create combinations of those
|
||||
* selectors with each other. For example if there are `A` and `B` selectors the output is:
|
||||
*
|
||||
* ```
|
||||
* AB<hostMarker>, AB <hostMarker>, A B<hostMarker>,
|
||||
* B A<hostMarker>, A B <hostMarker>, B A <hostMarker> {}
|
||||
* ```
|
||||
*
|
||||
* And so on...
|
||||
*
|
||||
* @param hostMarker the string that selects the host element.
|
||||
* @param contextSelectors an array of context selectors that will be combined.
|
||||
* @param otherSelectors the rest of the selectors that are not context selectors.
|
||||
*/
|
||||
function combineHostContextSelectors(
|
||||
hostMarker: string, contextSelectors: string[], otherSelectors: string): string {
|
||||
// If there are no context selectors then just output a host marker
|
||||
if (contextSelectors.length === 0) {
|
||||
return hostMarker + otherSelectors;
|
||||
}
|
||||
|
||||
const combined: string[] = [contextSelectors.pop() || ''];
|
||||
while (contextSelectors.length > 0) {
|
||||
const length = combined.length;
|
||||
const contextSelector = contextSelectors.pop();
|
||||
for (let i = 0; i < length; i++) {
|
||||
const previousSelectors = combined[i];
|
||||
// Add the new selector as a descendant of the previous selectors
|
||||
combined[length * 2 + i] = previousSelectors + ' ' + contextSelector;
|
||||
// Add the new selector as an ancestor of the previous selectors
|
||||
combined[length + i] = contextSelector + ' ' + previousSelectors;
|
||||
// Add the new selector to act on the same element as the previous selectors
|
||||
combined[i] = contextSelector + previousSelectors;
|
||||
}
|
||||
}
|
||||
// Finally connect the selector to the `hostMarker`s: either acting directly on the host
|
||||
// (A<hostMarker>) or as an ancestor (A <hostMarker>).
|
||||
return combined
|
||||
.map(s => `${s}${hostMarker}${otherSelectors}, ${s} ${hostMarker}${otherSelectors}`)
|
||||
.join(',');
|
||||
}
|
||||
|
|
|
@ -145,6 +145,10 @@ import {normalizeCSS} from '@angular/platform-browser/testing/src/browser_util';
|
|||
.toEqual('ul[a-host] > .z[contenta], li[a-host] > .z[contenta] {}');
|
||||
});
|
||||
|
||||
it('should handle compound class selectors', () => {
|
||||
expect(s(':host(.a.b) {}', 'contenta', 'a-host')).toEqual('.a.b[a-host] {}');
|
||||
});
|
||||
|
||||
it('should handle multiple class selectors', () => {
|
||||
expect(s(':host(.x,.y) {}', 'contenta', 'a-host')).toEqual('.x[a-host], .y[a-host] {}');
|
||||
expect(s(':host(.x,.y) > .z {}', 'contenta', 'a-host'))
|
||||
|
@ -208,6 +212,52 @@ import {normalizeCSS} from '@angular/platform-browser/testing/src/browser_util';
|
|||
expect(s(':host-context([a=b]) {}', 'contenta', 'a-host'))
|
||||
.toEqual('[a=b][a-host], [a="b"] [a-host] {}');
|
||||
});
|
||||
|
||||
it('should handle multiple :host-context() selectors', () => {
|
||||
expect(s(':host-context(.one):host-context(.two) {}', 'contenta', 'a-host'))
|
||||
.toEqual(
|
||||
'.one.two[a-host], ' + // `one` and `two` both on the host
|
||||
'.one.two [a-host], ' + // `one` and `two` are both on the same ancestor
|
||||
'.one .two[a-host], ' + // `one` is an ancestor and `two` is on the host
|
||||
'.one .two [a-host], ' + // `one` and `two` are both ancestors (in that order)
|
||||
'.two .one[a-host], ' + // `two` is an ancestor and `one` is on the host
|
||||
'.two .one [a-host]' + // `two` and `one` are both ancestors (in that order)
|
||||
' {}');
|
||||
|
||||
expect(s(':host-context(.X):host-context(.Y):host-context(.Z) {}', 'contenta', 'a-host')
|
||||
.replace(/ \{\}$/, '')
|
||||
.split(/\,\s+/))
|
||||
.toEqual([
|
||||
'.X.Y.Z[a-host]',
|
||||
'.X.Y.Z [a-host]',
|
||||
'.X.Y .Z[a-host]',
|
||||
'.X.Y .Z [a-host]',
|
||||
'.X.Z .Y[a-host]',
|
||||
'.X.Z .Y [a-host]',
|
||||
'.X .Y.Z[a-host]',
|
||||
'.X .Y.Z [a-host]',
|
||||
'.X .Y .Z[a-host]',
|
||||
'.X .Y .Z [a-host]',
|
||||
'.X .Z .Y[a-host]',
|
||||
'.X .Z .Y [a-host]',
|
||||
'.Y.Z .X[a-host]',
|
||||
'.Y.Z .X [a-host]',
|
||||
'.Y .Z .X[a-host]',
|
||||
'.Y .Z .X [a-host]',
|
||||
'.Z .Y .X[a-host]',
|
||||
'.Z .Y .X [a-host]',
|
||||
]);
|
||||
});
|
||||
|
||||
// This test is checking backward compatibility.
|
||||
// It is not clear what the behaviour should be for a `:host-context` with no selectors.
|
||||
// Arguably it is actually an error that should be reported.
|
||||
it('should handle :host-context with no ancestor selectors', () => {
|
||||
expect(s('.outer :host-context .inner {}', 'contenta', 'a-host'))
|
||||
.toEqual('.outer [a-host] .inner[contenta] {}');
|
||||
expect(s('.outer :host-context() .inner {}', 'contenta', 'a-host'))
|
||||
.toEqual('.outer [a-host] .inner[contenta] {}');
|
||||
});
|
||||
});
|
||||
|
||||
it('should support polyfill-next-selector', () => {
|
||||
|
|
Loading…
Reference in New Issue