fix(compiler): handle `:host-context` and `:host` in the same selector (#40494)

In `ViewEncapsulation.Emulated` mode the compiler converts `:host` and
`:host-context` pseudo classes into new CSS selectors.

Previously, when there was both `:host-context` and `:host` classes in a
selector, the compiler was generating incorrect selectors. There are two
scenarios:

* Both classes are on the same element (i.e. not separated). E.g.
  `:host-context(.foo):host(.bar)`. This setup should only match the
  host element if it has both `foo` and `bar` classes. So the generated
  CSS selector should be: `.foo.bar<hostmarker>`.
* The `:host` class is on a descendant of the `:host-context`. E.g.
  `:host-context(.foo) :host(.bar)`. This setup should only match the
  `.foo` selector if it is a proper ancestor of the host (and not on the
  host itself). So the generated CSS selector should be:
  `.foo .bar<hostmarker>`.

This commit fixes the generation to handle these scenarios.

Fixes #14349

PR Close #40494
This commit is contained in:
Pete Bacon Darwin 2021-01-24 21:13:25 +00:00 committed by Joey Perrott
parent ba3f99d7cc
commit 679c3bf7ea
2 changed files with 24 additions and 4 deletions

View File

@ -314,7 +314,7 @@ export class ShadowCss {
// The context selectors now must be combined with each other to capture all the possible // The context selectors now must be combined with each other to capture all the possible
// selectors that `:host-context` can match. // selectors that `:host-context` can match.
return combineHostContextSelectors(_polyfillHostNoCombinator, contextSelectors, selectorText); return combineHostContextSelectors(contextSelectors, selectorText);
}); });
} }
@ -682,8 +682,10 @@ function escapeBlocks(
* @param contextSelectors an array of context selectors that will be combined. * @param contextSelectors an array of context selectors that will be combined.
* @param otherSelectors the rest of the selectors that are not context selectors. * @param otherSelectors the rest of the selectors that are not context selectors.
*/ */
function combineHostContextSelectors( function combineHostContextSelectors(contextSelectors: string[], otherSelectors: string): string {
hostMarker: string, contextSelectors: string[], otherSelectors: string): string { const hostMarker = _polyfillHostNoCombinator;
const otherSelectorsHasHost = _polyfillHostRe.test(otherSelectors);
// If there are no context selectors then just output a host marker // If there are no context selectors then just output a host marker
if (contextSelectors.length === 0) { if (contextSelectors.length === 0) {
return hostMarker + otherSelectors; return hostMarker + otherSelectors;
@ -706,6 +708,9 @@ function combineHostContextSelectors(
// Finally connect the selector to the `hostMarker`s: either acting directly on the host // Finally connect the selector to the `hostMarker`s: either acting directly on the host
// (A<hostMarker>) or as an ancestor (A <hostMarker>). // (A<hostMarker>) or as an ancestor (A <hostMarker>).
return combined return combined
.map(s => `${s}${hostMarker}${otherSelectors}, ${s} ${hostMarker}${otherSelectors}`) .map(
s => otherSelectorsHasHost ?
`${s}${otherSelectors}` :
`${s}${hostMarker}${otherSelectors}, ${s} ${hostMarker}${otherSelectors}`)
.join(','); .join(',');
} }

View File

@ -260,6 +260,21 @@ import {normalizeCSS} from '@angular/platform-browser/testing/src/browser_util';
}); });
}); });
describe((':host-context and :host combination selector'), () => {
it('should handle selectors on the same element', () => {
expect(s(':host-context(div):host(.x) > .y {}', 'contenta', 'a-host'))
.toEqual('div.x[a-host] > .y[contenta] {}');
});
it('should handle selectors on different elements', () => {
expect(s(':host-context(div) :host(.x) > .y {}', 'contenta', 'a-host'))
.toEqual('div .x[a-host] > .y[contenta] {}');
expect(s(':host-context(div) > :host(.x) > .y {}', 'contenta', 'a-host'))
.toEqual('div > .x[a-host] > .y[contenta] {}');
});
});
it('should support polyfill-next-selector', () => { it('should support polyfill-next-selector', () => {
let css = s('polyfill-next-selector {content: \'x > y\'} z {}', 'contenta'); let css = s('polyfill-next-selector {content: \'x > y\'} z {}', 'contenta');
expect(css).toEqual('x[contenta] > y[contenta]{}'); expect(css).toEqual('x[contenta] > y[contenta]{}');