fix(ShadowCss): properly shim selectors after :host and :host-context (#10997)
fixes #5390 Before the change: // original CSS :host .foo .bar {...} .foo .bar {...} // translated to [_nghost-shh-2] .foo .bar {...} .foo[_ngcontent-shh-2] .bar[_ngcontent-shh-2] {...} Note that `.foo` and `.bar` where not scoped and would then apply to nested components. With this change those selectors are scoped (as they are without `:host`). You can explicitly apply the style to inner component by using `>>>` or `/deep/`: `:host >>> .foo`
This commit is contained in:
parent
abad6673e6
commit
af63378fa0
|
@ -6,7 +6,6 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ListWrapper} from './facade/collection';
|
||||
import {StringWrapper, isBlank, isPresent} from './facade/lang';
|
||||
|
||||
/**
|
||||
|
@ -52,7 +51,7 @@ import {StringWrapper, isBlank, isPresent} from './facade/lang';
|
|||
background: red;
|
||||
}
|
||||
|
||||
* encapsultion: Styles defined within ShadowDOM, apply only to
|
||||
* encapsulation: Styles defined within ShadowDOM, apply only to
|
||||
dom inside the ShadowDOM. Polymer uses one of two techniques to implement
|
||||
this feature.
|
||||
|
||||
|
@ -345,13 +344,13 @@ export class ShadowCss {
|
|||
private _scopeSelector(
|
||||
selector: string, scopeSelector: string, hostSelector: string, strict: boolean): string {
|
||||
return selector.split(',')
|
||||
.map((part) => { return StringWrapper.split(part.trim(), _shadowDeepSelectors); })
|
||||
.map(part => part.trim().split(_shadowDeepSelectors))
|
||||
.map((deepParts) => {
|
||||
const [shallowPart, ...otherParts] = deepParts;
|
||||
const applyScope = (shallowPart: string) => {
|
||||
if (this._selectorNeedsScoping(shallowPart, scopeSelector)) {
|
||||
return strict && !StringWrapper.contains(shallowPart, _polyfillHostNoCombinator) ?
|
||||
this._applyStrictSelectorScope(shallowPart, scopeSelector) :
|
||||
return strict ?
|
||||
this._applyStrictSelectorScope(shallowPart, scopeSelector, hostSelector) :
|
||||
this._applySelectorScope(shallowPart, scopeSelector, hostSelector);
|
||||
} else {
|
||||
return shallowPart;
|
||||
|
@ -377,7 +376,7 @@ export class ShadowCss {
|
|||
|
||||
private _applySelectorScope(selector: string, scopeSelector: string, hostSelector: string):
|
||||
string {
|
||||
// Difference from webcomponentsjs: scopeSelector could not be an array
|
||||
// Difference from webcomponents.js: scopeSelector could not be an array
|
||||
return this._applySimpleSelectorScope(selector, scopeSelector, hostSelector);
|
||||
}
|
||||
|
||||
|
@ -395,38 +394,58 @@ export class ShadowCss {
|
|||
|
||||
// return a selector with [name] suffix on each simple selector
|
||||
// e.g. .foo.bar > .zot becomes .foo[name].bar[name] > .zot[name] /** @internal */
|
||||
private _applyStrictSelectorScope(selector: string, scopeSelector: string): string {
|
||||
private _applyStrictSelectorScope(selector: string, scopeSelector: string, hostSelector: string):
|
||||
string {
|
||||
const isRe = /\[is=([^\]]*)\]/g;
|
||||
scopeSelector =
|
||||
StringWrapper.replaceAllMapped(scopeSelector, isRe, (m: any /** TODO #9100 */) => m[1]);
|
||||
const splits = [' ', '>', '+', '~'];
|
||||
let scoped = selector;
|
||||
scopeSelector = scopeSelector.replace(isRe, (_: string, ...parts: string[]) => parts[0]);
|
||||
|
||||
const attrName = '[' + scopeSelector + ']';
|
||||
for (let i = 0; i < splits.length; i++) {
|
||||
const sep = splits[i];
|
||||
const parts = scoped.split(sep);
|
||||
scoped = parts
|
||||
.map(p => {
|
||||
// remove :host since it should be unnecessary
|
||||
const t = StringWrapper.replaceAll(p.trim(), _polyfillHostRe, '');
|
||||
if (t.length > 0 && !ListWrapper.contains(splits, t) &&
|
||||
!StringWrapper.contains(t, attrName)) {
|
||||
const m = t.match(/([^:]*)(:*)(.*)/);
|
||||
if (m !== null) {
|
||||
p = m[1] + attrName + m[2] + m[3];
|
||||
}
|
||||
}
|
||||
return p;
|
||||
})
|
||||
.join(sep);
|
||||
|
||||
const _scopeSelectorPart = (p: string) => {
|
||||
var scopedP = p.trim();
|
||||
|
||||
if (scopedP.length == 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (p.indexOf(_polyfillHostNoCombinator) > -1) {
|
||||
scopedP = this._applySimpleSelectorScope(p, scopeSelector, hostSelector);
|
||||
} else {
|
||||
// remove :host since it should be unnecessary
|
||||
var t = p.replace(_polyfillHostRe, '');
|
||||
if (t.length > 0) {
|
||||
const matches = t.match(/([^:]*)(:*)(.*)/);
|
||||
if (matches !== null) {
|
||||
scopedP = matches[1] + attrName + matches[2] + matches[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scopedP;
|
||||
};
|
||||
|
||||
const sep = /( |>|\+|~)\s*/g;
|
||||
const scopeAfter = selector.indexOf(_polyfillHostNoCombinator);
|
||||
|
||||
let scoped = '';
|
||||
let startIndex = 0;
|
||||
let res: RegExpExecArray;
|
||||
|
||||
while ((res = sep.exec(selector)) !== null) {
|
||||
const separator = res[1];
|
||||
const part = selector.slice(startIndex, res.index).trim();
|
||||
// if a selector appears before :host-context it should not be shimmed as it
|
||||
// matches on ancestor elements and not on elements in the host's shadow
|
||||
const scopedPart = startIndex >= scopeAfter ? _scopeSelectorPart(part) : part;
|
||||
scoped += `${scopedPart} ${separator} `;
|
||||
startIndex = sep.lastIndex;
|
||||
}
|
||||
return scoped;
|
||||
return scoped + _scopeSelectorPart(selector.substring(startIndex));
|
||||
}
|
||||
|
||||
private _insertPolyfillHostInCssText(selector: string): string {
|
||||
selector = StringWrapper.replaceAll(selector, _colonHostContextRe, _polyfillHostContext);
|
||||
selector = StringWrapper.replaceAll(selector, _colonHostRe, _polyfillHost);
|
||||
return selector;
|
||||
return selector.replace(_colonHostContextRe, _polyfillHostContext)
|
||||
.replace(_colonHostRe, _polyfillHost);
|
||||
}
|
||||
}
|
||||
const _cssContentNextSelectorRe =
|
||||
|
@ -444,30 +463,28 @@ const _cssColonHostRe = new RegExp('(' + _polyfillHost + _parenSuffix, 'gim');
|
|||
const _cssColonHostContextRe = new RegExp('(' + _polyfillHostContext + _parenSuffix, 'gim');
|
||||
const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator';
|
||||
const _shadowDOMSelectorsRe = [
|
||||
/::shadow/g, /::content/g,
|
||||
/::shadow/g,
|
||||
/::content/g,
|
||||
// Deprecated selectors
|
||||
// TODO(vicb): see https://github.com/angular/clang-format/issues/16
|
||||
// clang-format off
|
||||
/\/shadow-deep\//g, // former /deep/
|
||||
/\/shadow\//g, // former ::shadow
|
||||
// clanf-format on
|
||||
/\/shadow-deep\//g,
|
||||
/\/shadow\//g,
|
||||
];
|
||||
const _shadowDeepSelectors = /(?:>>>)|(?:\/deep\/)/g;
|
||||
const _selectorReSuffix = '([>\\s~+\[.,{:][\\s\\S]*)?$';
|
||||
const _polyfillHostRe = new RegExp(_polyfillHost, 'im');
|
||||
const _polyfillHostRe = /-shadowcsshost/gim;
|
||||
const _colonHostRe = /:host/gim;
|
||||
const _colonHostContextRe = /:host-context/gim;
|
||||
|
||||
const _commentRe = /\/\*\s*[\s\S]*?\*\//g;
|
||||
|
||||
function stripComments(input:string):string {
|
||||
function stripComments(input: string): string {
|
||||
return StringWrapper.replaceAllMapped(input, _commentRe, (_: any /** TODO #9100 */) => '');
|
||||
}
|
||||
|
||||
// all comments except inline source mapping ("/* #sourceMappingURL= ... */")
|
||||
const _sourceMappingUrlRe = /[\s\S]*(\/\*\s*#\s*sourceMappingURL=[\s\S]+?\*\/)\s*$/;
|
||||
|
||||
function extractSourceMappingUrl(input:string):string {
|
||||
function extractSourceMappingUrl(input: string): string {
|
||||
const matcher = input.match(_sourceMappingUrlRe);
|
||||
return matcher ? matcher[1] : '';
|
||||
}
|
||||
|
@ -479,38 +496,39 @@ const CLOSE_CURLY = '}';
|
|||
const BLOCK_PLACEHOLDER = '%BLOCK%';
|
||||
|
||||
export class CssRule {
|
||||
constructor(public selector:string, public content:string) {}
|
||||
constructor(public selector: string, public content: string) {}
|
||||
}
|
||||
|
||||
export function processRules(input:string, ruleCallback:Function):string {
|
||||
export function processRules(input: string, ruleCallback: Function): string {
|
||||
const inputWithEscapedBlocks = escapeBlocks(input);
|
||||
let nextBlockIndex = 0;
|
||||
return StringWrapper.replaceAllMapped(inputWithEscapedBlocks.escapedString, _ruleRe, function(m: any /** TODO #9100 */) {
|
||||
const selector = m[2];
|
||||
let content = '';
|
||||
let suffix = m[4];
|
||||
let contentPrefix = '';
|
||||
if (isPresent(m[4]) && m[4].startsWith('{'+BLOCK_PLACEHOLDER)) {
|
||||
content = inputWithEscapedBlocks.blocks[nextBlockIndex++];
|
||||
suffix = m[4].substring(BLOCK_PLACEHOLDER.length+1);
|
||||
contentPrefix = '{';
|
||||
}
|
||||
const rule = ruleCallback(new CssRule(selector, content));
|
||||
return `${m[1]}${rule.selector}${m[3]}${contentPrefix}${rule.content}${suffix}`;
|
||||
});
|
||||
return StringWrapper.replaceAllMapped(
|
||||
inputWithEscapedBlocks.escapedString, _ruleRe, function(m: any /** TODO #9100 */) {
|
||||
const selector = m[2];
|
||||
let content = '';
|
||||
let suffix = m[4];
|
||||
let contentPrefix = '';
|
||||
if (isPresent(m[4]) && m[4].startsWith('{' + BLOCK_PLACEHOLDER)) {
|
||||
content = inputWithEscapedBlocks.blocks[nextBlockIndex++];
|
||||
suffix = m[4].substring(BLOCK_PLACEHOLDER.length + 1);
|
||||
contentPrefix = '{';
|
||||
}
|
||||
const rule = ruleCallback(new CssRule(selector, content));
|
||||
return `${m[1]}${rule.selector}${m[3]}${contentPrefix}${rule.content}${suffix}`;
|
||||
});
|
||||
}
|
||||
|
||||
class StringWithEscapedBlocks {
|
||||
constructor(public escapedString:string, public blocks:string[]) {}
|
||||
constructor(public escapedString: string, public blocks: string[]) {}
|
||||
}
|
||||
|
||||
function escapeBlocks(input:string):StringWithEscapedBlocks {
|
||||
function escapeBlocks(input: string): StringWithEscapedBlocks {
|
||||
const inputParts = StringWrapper.split(input, _curlyRe);
|
||||
const resultParts: any[] /** TODO #9100 */ = [];
|
||||
const escapedBlocks: any[] /** TODO #9100 */ = [];
|
||||
let bracketCount = 0;
|
||||
let currentBlockParts: any[] /** TODO #9100 */ = [];
|
||||
for (let partIndex = 0; partIndex<inputParts.length; partIndex++) {
|
||||
for (let partIndex = 0; partIndex < inputParts.length; partIndex++) {
|
||||
const part = inputParts[partIndex];
|
||||
if (part == CLOSE_CURLY) {
|
||||
bracketCount--;
|
||||
|
|
|
@ -10,8 +10,6 @@ import {CssRule, ShadowCss, processRules} from '@angular/compiler/src/shadow_css
|
|||
import {beforeEach, ddescribe, describe, expect, iit, it} from '@angular/core/testing/testing_internal';
|
||||
import {normalizeCSS} from '@angular/platform-browser/testing/browser_util';
|
||||
|
||||
import {StringWrapper, isPresent} from '../src/facade/lang';
|
||||
|
||||
export function main() {
|
||||
describe('ShadowCss', function() {
|
||||
|
||||
|
@ -19,7 +17,7 @@ export function main() {
|
|||
const shadowCss = new ShadowCss();
|
||||
const shim = shadowCss.shimCssText(css, contentAttr, hostAttr);
|
||||
const nlRegexp = /\n/g;
|
||||
return normalizeCSS(StringWrapper.replaceAll(shim, nlRegexp, ''));
|
||||
return normalizeCSS(shim.replace(nlRegexp, ''));
|
||||
}
|
||||
|
||||
it('should handle empty string', () => { expect(s('', 'a')).toEqual(''); });
|
||||
|
@ -99,15 +97,17 @@ export function main() {
|
|||
|
||||
it('should handle :host', () => {
|
||||
expect(s(':host {}', 'a', 'a-host')).toEqual('[a-host] {}');
|
||||
|
||||
expect(s(':host(.x,.y) {}', 'a', 'a-host')).toEqual('[a-host].x, [a-host].y {}');
|
||||
|
||||
expect(s(':host(.x,.y) > .z {}', 'a', 'a-host'))
|
||||
.toEqual('[a-host].x > .z, [a-host].y > .z {}');
|
||||
.toEqual('[a-host].x > .z[a], [a-host].y > .z[a] {}');
|
||||
});
|
||||
|
||||
it('should handle :host-context', () => {
|
||||
expect(s(':host-context(.x) {}', 'a', 'a-host')).toEqual('[a-host].x, .x [a-host] {}');
|
||||
expect(s(':host-context(.x) > .y {}', 'a', 'a-host'))
|
||||
.toEqual('[a-host].x > .y, .x [a-host] > .y {}');
|
||||
.toEqual('[a-host].x > .y[a], .x [a-host] > .y[a] {}');
|
||||
});
|
||||
|
||||
it('should support polyfill-next-selector', () => {
|
||||
|
@ -120,10 +120,10 @@ export function main() {
|
|||
|
||||
it('should support polyfill-unscoped-rule', () => {
|
||||
let css = s('polyfill-unscoped-rule {content: \'#menu > .bar\';color: blue;}', 'a');
|
||||
expect(StringWrapper.contains(css, '#menu > .bar {;color:blue;}')).toBeTruthy();
|
||||
expect(css).toContain('#menu > .bar {;color:blue;}');
|
||||
|
||||
css = s('polyfill-unscoped-rule {content: "#menu > .bar";color: blue;}', 'a');
|
||||
expect(StringWrapper.contains(css, '#menu > .bar {;color:blue;}')).toBeTruthy();
|
||||
expect(css).toContain('#menu > .bar {;color:blue;}');
|
||||
});
|
||||
|
||||
it('should support multiple instances polyfill-unscoped-rule', () => {
|
||||
|
@ -131,16 +131,16 @@ export function main() {
|
|||
s('polyfill-unscoped-rule {content: \'foo\';color: blue;}' +
|
||||
'polyfill-unscoped-rule {content: \'bar\';color: blue;}',
|
||||
'a');
|
||||
expect(StringWrapper.contains(css, 'foo {;color:blue;}')).toBeTruthy();
|
||||
expect(StringWrapper.contains(css, 'bar {;color:blue;}')).toBeTruthy();
|
||||
expect(css).toContain('foo {;color:blue;}');
|
||||
expect(css).toContain('bar {;color:blue;}');
|
||||
});
|
||||
|
||||
it('should support polyfill-rule', () => {
|
||||
let css = s('polyfill-rule {content: \':host.foo .bar\';color: blue;}', 'a', 'a-host');
|
||||
expect(css).toEqual('[a-host].foo .bar {;color:blue;}');
|
||||
expect(css).toEqual('[a-host].foo .bar[a] {;color:blue;}');
|
||||
|
||||
css = s('polyfill-rule {content: ":host.foo .bar";color:blue;}', 'a', 'a-host');
|
||||
expect(css).toEqual('[a-host].foo .bar {;color:blue;}');
|
||||
expect(css).toEqual('[a-host].foo .bar[a] {;color:blue;}');
|
||||
});
|
||||
|
||||
it('should handle ::shadow', () => {
|
||||
|
|
Loading…
Reference in New Issue