mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
fix(compiler): transform chained pseudo-selectors (#58681)
fix transformation logic for `:where` and `:is` pseudo-selectors when these selectors were used in a chain. results were often broken, the last letter of the selector was incorrectly trimmed. see tests for examples Fixes #58226 PR Close #58681
This commit is contained in:
parent
2be161d015
commit
80f56954ce
2 changed files with 83 additions and 32 deletions
|
|
@ -800,30 +800,67 @@ export class ShadowCss {
|
|||
const _pseudoFunctionAwareScopeSelectorPart = (selectorPart: string) => {
|
||||
let scopedPart = '';
|
||||
|
||||
const cssPrefixWithPseudoSelectorFunctionMatch = selectorPart.match(
|
||||
_cssPrefixWithPseudoSelectorFunction,
|
||||
);
|
||||
if (cssPrefixWithPseudoSelectorFunctionMatch) {
|
||||
const [cssPseudoSelectorFunction] = cssPrefixWithPseudoSelectorFunctionMatch;
|
||||
// Collect all outer `:where()` and `:is()` selectors,
|
||||
// counting parenthesis to keep nested selectors intact.
|
||||
const pseudoSelectorParts = [];
|
||||
let pseudoSelectorMatch: RegExpExecArray | null;
|
||||
while (
|
||||
(pseudoSelectorMatch = _cssPrefixWithPseudoSelectorFunction.exec(selectorPart)) !== null
|
||||
) {
|
||||
let openedBrackets = 1;
|
||||
let index = _cssPrefixWithPseudoSelectorFunction.lastIndex;
|
||||
|
||||
// Unwrap the pseudo selector to scope its contents.
|
||||
// For example,
|
||||
// - `:where(selectorToScope)` -> `selectorToScope`;
|
||||
// - `:is(.foo, .bar)` -> `.foo, .bar`.
|
||||
const selectorToScope = selectorPart.slice(cssPseudoSelectorFunction.length, -1);
|
||||
|
||||
if (selectorToScope.includes(_polyfillHostNoCombinator)) {
|
||||
this._shouldScopeIndicator = true;
|
||||
while (index < selectorPart.length) {
|
||||
const currentSymbol = selectorPart[index];
|
||||
index++;
|
||||
if (currentSymbol === '(') {
|
||||
openedBrackets++;
|
||||
continue;
|
||||
}
|
||||
if (currentSymbol === ')') {
|
||||
openedBrackets--;
|
||||
if (openedBrackets === 0) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const scopedInnerPart = this._scopeSelector({
|
||||
selector: selectorToScope,
|
||||
scopeSelector,
|
||||
hostSelector,
|
||||
});
|
||||
pseudoSelectorParts.push(
|
||||
`${pseudoSelectorMatch[0]}${selectorPart.slice(_cssPrefixWithPseudoSelectorFunction.lastIndex, index)}`,
|
||||
);
|
||||
_cssPrefixWithPseudoSelectorFunction.lastIndex = index;
|
||||
}
|
||||
|
||||
// Put the result back into the pseudo selector function.
|
||||
scopedPart = `${cssPseudoSelectorFunction}${scopedInnerPart})`;
|
||||
// If selector consists of only `:where()` and `:is()` on the outer level
|
||||
// scope those pseudo-selectors individually, otherwise scope the whole
|
||||
// selector.
|
||||
if (pseudoSelectorParts.join('') === selectorPart) {
|
||||
scopedPart = pseudoSelectorParts
|
||||
.map((selectorPart) => {
|
||||
const [cssPseudoSelectorFunction] =
|
||||
selectorPart.match(_cssPrefixWithPseudoSelectorFunction) ?? [];
|
||||
|
||||
// Unwrap the pseudo selector to scope its contents.
|
||||
// For example,
|
||||
// - `:where(selectorToScope)` -> `selectorToScope`;
|
||||
// - `:is(.foo, .bar)` -> `.foo, .bar`.
|
||||
const selectorToScope = selectorPart.slice(cssPseudoSelectorFunction?.length, -1);
|
||||
|
||||
if (selectorToScope.includes(_polyfillHostNoCombinator)) {
|
||||
this._shouldScopeIndicator = true;
|
||||
}
|
||||
|
||||
const scopedInnerPart = this._scopeSelector({
|
||||
selector: selectorToScope,
|
||||
scopeSelector,
|
||||
hostSelector,
|
||||
});
|
||||
|
||||
// Put the result back into the pseudo selector function.
|
||||
return `${cssPseudoSelectorFunction}${scopedInnerPart})`;
|
||||
})
|
||||
.join('');
|
||||
} else {
|
||||
this._shouldScopeIndicator =
|
||||
this._shouldScopeIndicator || selectorPart.includes(_polyfillHostNoCombinator);
|
||||
|
|
@ -962,7 +999,7 @@ class SafeSelector {
|
|||
}
|
||||
|
||||
const _cssScopedPseudoFunctionPrefix = '(:(where|is)\\()?';
|
||||
const _cssPrefixWithPseudoSelectorFunction = /^:(where|is)\(/i;
|
||||
const _cssPrefixWithPseudoSelectorFunction = /:(where|is)\(/gi;
|
||||
const _cssContentNextSelectorRe =
|
||||
/polyfill-next-selector[^}]*content:[\s]*?(['"])(.*?)\1[;\s]*}([^{]*?){/gim;
|
||||
const _cssContentRuleRe = /(polyfill-rule)[^}]*(content:[\s]*(['"])(.*?)\3)[;\s]*[^}]*}/gim;
|
||||
|
|
|
|||
|
|
@ -93,9 +93,8 @@ describe('ShadowCss', () => {
|
|||
'div[contenta]:where(.one) {}',
|
||||
);
|
||||
expect(shim('div:where() {}', 'contenta', 'hosta')).toEqualCss('div[contenta]:where() {}');
|
||||
// See `xit('should parse concatenated pseudo selectors'`
|
||||
expect(shim(':where(a):where(b) {}', 'contenta', 'hosta')).toEqualCss(
|
||||
':where(a)[contenta]:where(b) {}',
|
||||
':where(a[contenta]):where(b[contenta]) {}',
|
||||
);
|
||||
expect(shim('*:where(.one) {}', 'contenta', 'hosta')).toEqualCss('*[contenta]:where(.one) {}');
|
||||
expect(shim('*:where(.one) ::ng-deep .foo {}', 'contenta', 'hosta')).toEqualCss(
|
||||
|
|
@ -103,15 +102,6 @@ describe('ShadowCss', () => {
|
|||
);
|
||||
});
|
||||
|
||||
xit('should parse concatenated pseudo selectors', () => {
|
||||
// Current logic leads to a result with an outer scope
|
||||
// It could be changed, to not increase specificity
|
||||
// Requires a more complex parsing
|
||||
expect(shim(':where(a):where(b) {}', 'contenta', 'hosta')).toEqualCss(
|
||||
':where(a[contenta]):where(b[contenta]) {}',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle pseudo functions correctly', () => {
|
||||
// :where()
|
||||
expect(shim(':where(.one) {}', 'contenta', 'hosta')).toEqualCss(':where(.one[contenta]) {}');
|
||||
|
|
@ -200,6 +190,18 @@ describe('ShadowCss', () => {
|
|||
).toEqualCss(
|
||||
':where(:where(a[contenta]:has(.foo), b[contenta]) :is(.one[contenta], .two[contenta]:where(.foo > .bar))) {}',
|
||||
);
|
||||
expect(shim(':where(.two):first-child {}', 'contenta', 'hosta')).toEqualCss(
|
||||
'[contenta]:where(.two):first-child {}',
|
||||
);
|
||||
expect(shim(':first-child:where(.two) {}', 'contenta', 'hosta')).toEqualCss(
|
||||
'[contenta]:first-child:where(.two) {}',
|
||||
);
|
||||
expect(shim(':where(.two):nth-child(3) {}', 'contenta', 'hosta')).toEqualCss(
|
||||
'[contenta]:where(.two):nth-child(3) {}',
|
||||
);
|
||||
expect(shim('table :where(td, th):hover { color: lime; }', 'contenta', 'hosta')).toEqualCss(
|
||||
'table[contenta] [contenta]:where(td, th):hover { color:lime;}',
|
||||
);
|
||||
|
||||
// complex selectors
|
||||
expect(shim(':host:is([foo],[foo-2])>div.example-2 {}', 'contenta', 'a-host')).toEqualCss(
|
||||
|
|
@ -272,6 +274,18 @@ describe('ShadowCss', () => {
|
|||
expect(shim('.one :where(:host, .two) {}', 'contenta', 'hosta')).toEqualCss(
|
||||
'.one :where([hosta], .two[contenta]) {}',
|
||||
);
|
||||
expect(shim(':is(.foo):is(:host):is(.two) {}', 'contenta', 'hosta')).toEqualCss(
|
||||
':is(.foo):is([hosta]):is(.two[contenta]) {}',
|
||||
);
|
||||
expect(shim(':where(.one, :host .two):first-letter {}', 'contenta', 'hosta')).toEqualCss(
|
||||
'[contenta]:where(.one, [hosta] .two):first-letter {}',
|
||||
);
|
||||
expect(shim(':first-child:where(.one, :host .two) {}', 'contenta', 'hosta')).toEqualCss(
|
||||
'[contenta]:first-child:where(.one, [hosta] .two) {}',
|
||||
);
|
||||
expect(
|
||||
shim(':where(.one, :host .two):nth-child(3):is(.foo, a:where(.bar)) {}', 'contenta', 'hosta'),
|
||||
).toEqualCss('[contenta]:where(.one, [hosta] .two):nth-child(3):is(.foo, a:where(.bar)) {}');
|
||||
});
|
||||
|
||||
it('should handle escaped selector with space (if followed by a hex char)', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue