diff --git a/packages/compiler/src/shadow_css.ts b/packages/compiler/src/shadow_css.ts index 2b8ec4eb5d..fc0a295186 100644 --- a/packages/compiler/src/shadow_css.ts +++ b/packages/compiler/src/shadow_css.ts @@ -295,26 +295,62 @@ export class ShadowCss { private _convertColonHostContext(cssText: string): string { 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: + + // For backward compatibility `:host-context` may contain a comma separated list of selectors. + // Each context selector group will contain a list of host-context selectors that must match + // an ancestor of the host. + // (Normally `contextSelectorGroups` will only contain a single array of context selectors.) + const contextSelectorGroups: string[][] = [[]]; + + // There may be more than `:host-context` in this selector so `selectorText` could look like: // `:host-context(.one):host-context(.two)`. - - const contextSelectors: string[] = []; - let match: RegExpMatchArray|null; - // Execute `_cssColonHostContextRe` over and over until we have extracted all the // `:host-context` selectors from this selector. + let match: RegExpMatchArray|null; while (match = _cssColonHostContextRe.exec(selectorText)) { // `match` = [':host-context()', , ] - const contextSelector = (match[1] ?? '').trim(); - if (contextSelector !== '') { - contextSelectors.push(contextSelector); + + // The `` could actually be a comma separated list: `:host-context(.one, .two)`. + const newContextSelectors = + (match[1] ?? '').trim().split(',').map(m => m.trim()).filter(m => m !== ''); + + // We must duplicate the current selector group for each of these new selectors. + // For example if the current groups are: + // ``` + // [ + // ['a', 'b', 'c'], + // ['x', 'y', 'z'], + // ] + // ``` + // And we have a new set of comma separated selectors: `:host-context(m,n)` then the new + // groups are: + // ``` + // [ + // ['a', 'b', 'c', 'm'], + // ['x', 'y', 'z', 'm'], + // ['a', 'b', 'c', 'n'], + // ['x', 'y', 'z', 'n'], + // ] + // ``` + const contextSelectorGroupsLength = contextSelectorGroups.length; + repeatGroups(contextSelectorGroups, newContextSelectors.length); + for (let i = 0; i < newContextSelectors.length; i++) { + for (let j = 0; j < contextSelectorGroupsLength; j++) { + contextSelectorGroups[j + (i * contextSelectorGroupsLength)].push( + newContextSelectors[i]); + } } + + // Update the `selectorText` and see repeat to see if there are more `:host-context`s. 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(contextSelectors, selectorText); + // selectors that `:host-context` can match. See `combineHostContextSelectors()` for more + // info about how this is done. + return contextSelectorGroups + .map(contextSelectors => combineHostContextSelectors(contextSelectors, selectorText)) + .join(', '); }); } @@ -714,3 +750,23 @@ function combineHostContextSelectors(contextSelectors: string[], otherSelectors: `${s}${hostMarker}${otherSelectors}, ${s} ${hostMarker}${otherSelectors}`) .join(','); } + +/** + * Mutate the given `groups` array so that there are `multiples` clones of the original array + * stored. + * + * For example `repeatGroups([a, b], 3)` will result in `[a, b, a, b, a, b]` - but importantly the + * newly added groups will be clones of the original. + * + * @param groups An array of groups of strings that will be repeated. This array is mutated + * in-place. + * @param multiples The number of times the current groups should appear. + */ +export function repeatGroups(groups: string[][], multiples: number): void { + const length = groups.length; + for (let i = 1; i < multiples; i++) { + for (let j = 0; j < length; j++) { + groups[j + (i * length)] = groups[j].slice(0); + } + } +} diff --git a/packages/compiler/test/shadow_css_spec.ts b/packages/compiler/test/shadow_css_spec.ts index ac0560f82b..62642667b6 100644 --- a/packages/compiler/test/shadow_css_spec.ts +++ b/packages/compiler/test/shadow_css_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {CssRule, processRules, ShadowCss} from '@angular/compiler/src/shadow_css'; +import {CssRule, processRules, repeatGroups, ShadowCss} from '@angular/compiler/src/shadow_css'; import {normalizeCSS} from '@angular/platform-browser/testing/src/browser_util'; { @@ -249,14 +249,27 @@ import {normalizeCSS} from '@angular/platform-browser/testing/src/browser_util'; ]); }); - // 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 is not clear what the behavior should be for a `:host-context` with no selectors. + // This test is checking that the result is backward compatible with previous behavior. + // Arguably it should actually be 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] {}'); + expect(s(':host-context .inner {}', 'contenta', 'a-host')) + .toEqual('[a-host] .inner[contenta] {}'); + expect(s(':host-context() .inner {}', 'contenta', 'a-host')) + .toEqual('[a-host] .inner[contenta] {}'); + }); + + // More than one selector such as this is not valid as part of the :host-context spec. + // This test is checking that the result is backward compatible with previous behavior. + // Arguably it should actually be an error that should be reported. + it('should handle selectors', () => { + expect(s(':host-context(.one,.two) .inner {}', 'contenta', 'a-host')) + .toEqual( + '.one[a-host] .inner[contenta], ' + + '.one [a-host] .inner[contenta], ' + + '.two[a-host] .inner[contenta], ' + + '.two [a-host] .inner[contenta] ' + + '{}'); }); }); @@ -482,4 +495,32 @@ import {normalizeCSS} from '@angular/platform-browser/testing/src/browser_util'; }); }); }); + + describe('repeatGroups()', () => { + it('should do nothing if `multiples` is 0', () => { + const groups = [['a1', 'b1', 'c1'], ['a2', 'b2', 'c2']]; + repeatGroups(groups, 0); + expect(groups).toEqual([['a1', 'b1', 'c1'], ['a2', 'b2', 'c2']]); + }); + + it('should do nothing if `multiples` is 1', () => { + const groups = [['a1', 'b1', 'c1'], ['a2', 'b2', 'c2']]; + repeatGroups(groups, 1); + expect(groups).toEqual([['a1', 'b1', 'c1'], ['a2', 'b2', 'c2']]); + }); + + it('should add clones of the original groups if `multiples` is greater than 1', () => { + const group1 = ['a1', 'b1', 'c1']; + const group2 = ['a2', 'b2', 'c2']; + const groups = [group1, group2]; + repeatGroups(groups, 3); + expect(groups).toEqual([group1, group2, group1, group2, group1, group2]); + expect(groups[0]).toBe(group1); + expect(groups[1]).toBe(group2); + expect(groups[2]).not.toBe(group1); + expect(groups[3]).not.toBe(group2); + expect(groups[4]).not.toBe(group1); + expect(groups[5]).not.toBe(group2); + }); + }); }