fix(compiler): support multiple selectors in `:host-context()` (#40494)
The previous commits refactored the `ShadowCss` emulator to support desirable use-cases of `:host-context()`, but it dropped support for passing a comma separated list of selectors to the `:host-context()` . This commit rectifies that omission, despite the use-case not being valid according to the ShadowDOM spec, to ensure backward compatibility with the previous implementation. PR Close #40494
This commit is contained in:
parent
679c3bf7ea
commit
645c2ef973
|
@ -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(<selectors>)<rest>', <selectors>, <rest>]
|
||||
const contextSelector = (match[1] ?? '').trim();
|
||||
if (contextSelector !== '') {
|
||||
contextSelectors.push(contextSelector);
|
||||
|
||||
// The `<selectors>` 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<T>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue