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 {
|
private _convertColonHostContext(cssText: string): string {
|
||||||
return cssText.replace(_cssColonHostContextReGlobal, selectorText => {
|
return cssText.replace(_cssColonHostContextReGlobal, selectorText => {
|
||||||
// We have captured a selector that contains a `:host-context` rule.
|
// 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)`.
|
// `:host-context(.one):host-context(.two)`.
|
||||||
|
|
||||||
const contextSelectors: string[] = [];
|
|
||||||
let match: RegExpMatchArray|null;
|
|
||||||
|
|
||||||
// Execute `_cssColonHostContextRe` over and over until we have extracted all the
|
// Execute `_cssColonHostContextRe` over and over until we have extracted all the
|
||||||
// `:host-context` selectors from this selector.
|
// `:host-context` selectors from this selector.
|
||||||
|
let match: RegExpMatchArray|null;
|
||||||
while (match = _cssColonHostContextRe.exec(selectorText)) {
|
while (match = _cssColonHostContextRe.exec(selectorText)) {
|
||||||
// `match` = [':host-context(<selectors>)<rest>', <selectors>, <rest>]
|
// `match` = [':host-context(<selectors>)<rest>', <selectors>, <rest>]
|
||||||
const contextSelector = (match[1] ?? '').trim();
|
|
||||||
if (contextSelector !== '') {
|
// The `<selectors>` could actually be a comma separated list: `:host-context(.one, .two)`.
|
||||||
contextSelectors.push(contextSelector);
|
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];
|
selectorText = match[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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. See `combineHostContextSelectors()` for more
|
||||||
return combineHostContextSelectors(contextSelectors, selectorText);
|
// 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}`)
|
`${s}${hostMarker}${otherSelectors}, ${s} ${hostMarker}${otherSelectors}`)
|
||||||
.join(',');
|
.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
|
* 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';
|
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 behavior should be for a `:host-context` with no selectors.
|
||||||
// It is not clear what the behaviour should be for a `:host-context` with no selectors.
|
// This test is checking that the result is backward compatible with previous behavior.
|
||||||
// Arguably it is actually an error that should be reported.
|
// Arguably it should actually be an error that should be reported.
|
||||||
it('should handle :host-context with no ancestor selectors', () => {
|
it('should handle :host-context with no ancestor selectors', () => {
|
||||||
expect(s('.outer :host-context .inner {}', 'contenta', 'a-host'))
|
expect(s(':host-context .inner {}', 'contenta', 'a-host'))
|
||||||
.toEqual('.outer [a-host] .inner[contenta] {}');
|
.toEqual('[a-host] .inner[contenta] {}');
|
||||||
expect(s('.outer :host-context() .inner {}', 'contenta', 'a-host'))
|
expect(s(':host-context() .inner {}', 'contenta', 'a-host'))
|
||||||
.toEqual('.outer [a-host] .inner[contenta] {}');
|
.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