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
// 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 otherSelectors the rest of the selectors that are not context selectors.
*/
function combineHostContextSelectors(
hostMarker: string, contextSelectors: string[], otherSelectors: string): string {
function combineHostContextSelectors(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 (contextSelectors.length === 0) {
return hostMarker + otherSelectors;
@ -706,6 +708,9 @@ function combineHostContextSelectors(
// 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}`)
.map(
s => otherSelectorsHasHost ?
`${s}${otherSelectors}` :
`${s}${hostMarker}${otherSelectors}, ${s} ${hostMarker}${otherSelectors}`)
.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', () => {
let css = s('polyfill-next-selector {content: \'x > y\'} z {}', 'contenta');
expect(css).toEqual('x[contenta] > y[contenta]{}');