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:
Victor Berchet
2016-08-26 16:11:57 -07:00
committed by GitHub
parent abad6673e6
commit af63378fa0
2 changed files with 88 additions and 70 deletions

View File

@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ListWrapper} from './facade/collection';
import {StringWrapper, isBlank, isPresent} from './facade/lang'; import {StringWrapper, isBlank, isPresent} from './facade/lang';
/** /**
@ -52,7 +51,7 @@ import {StringWrapper, isBlank, isPresent} from './facade/lang';
background: red; 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 dom inside the ShadowDOM. Polymer uses one of two techniques to implement
this feature. this feature.
@ -345,13 +344,13 @@ export class ShadowCss {
private _scopeSelector( private _scopeSelector(
selector: string, scopeSelector: string, hostSelector: string, strict: boolean): string { selector: string, scopeSelector: string, hostSelector: string, strict: boolean): string {
return selector.split(',') return selector.split(',')
.map((part) => { return StringWrapper.split(part.trim(), _shadowDeepSelectors); }) .map(part => part.trim().split(_shadowDeepSelectors))
.map((deepParts) => { .map((deepParts) => {
const [shallowPart, ...otherParts] = deepParts; const [shallowPart, ...otherParts] = deepParts;
const applyScope = (shallowPart: string) => { const applyScope = (shallowPart: string) => {
if (this._selectorNeedsScoping(shallowPart, scopeSelector)) { if (this._selectorNeedsScoping(shallowPart, scopeSelector)) {
return strict && !StringWrapper.contains(shallowPart, _polyfillHostNoCombinator) ? return strict ?
this._applyStrictSelectorScope(shallowPart, scopeSelector) : this._applyStrictSelectorScope(shallowPart, scopeSelector, hostSelector) :
this._applySelectorScope(shallowPart, scopeSelector, hostSelector); this._applySelectorScope(shallowPart, scopeSelector, hostSelector);
} else { } else {
return shallowPart; return shallowPart;
@ -377,7 +376,7 @@ export class ShadowCss {
private _applySelectorScope(selector: string, scopeSelector: string, hostSelector: string): private _applySelectorScope(selector: string, scopeSelector: string, hostSelector: string):
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); return this._applySimpleSelectorScope(selector, scopeSelector, hostSelector);
} }
@ -395,38 +394,58 @@ export class ShadowCss {
// return a selector with [name] suffix on each simple selector // return a selector with [name] suffix on each simple selector
// e.g. .foo.bar > .zot becomes .foo[name].bar[name] > .zot[name] /** @internal */ // 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; const isRe = /\[is=([^\]]*)\]/g;
scopeSelector = scopeSelector = scopeSelector.replace(isRe, (_: string, ...parts: string[]) => parts[0]);
StringWrapper.replaceAllMapped(scopeSelector, isRe, (m: any /** TODO #9100 */) => m[1]);
const splits = [' ', '>', '+', '~'];
let scoped = selector;
const attrName = '[' + scopeSelector + ']'; const attrName = '[' + scopeSelector + ']';
for (let i = 0; i < splits.length; i++) {
const sep = splits[i]; const _scopeSelectorPart = (p: string) => {
const parts = scoped.split(sep); var scopedP = p.trim();
scoped = parts
.map(p => { if (scopedP.length == 0) {
// remove :host since it should be unnecessary return '';
const t = StringWrapper.replaceAll(p.trim(), _polyfillHostRe, ''); }
if (t.length > 0 && !ListWrapper.contains(splits, t) &&
!StringWrapper.contains(t, attrName)) { if (p.indexOf(_polyfillHostNoCombinator) > -1) {
const m = t.match(/([^:]*)(:*)(.*)/); scopedP = this._applySimpleSelectorScope(p, scopeSelector, hostSelector);
if (m !== null) { } else {
p = m[1] + attrName + m[2] + m[3]; // remove :host since it should be unnecessary
} var t = p.replace(_polyfillHostRe, '');
} if (t.length > 0) {
return p; const matches = t.match(/([^:]*)(:*)(.*)/);
}) if (matches !== null) {
.join(sep); 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 { private _insertPolyfillHostInCssText(selector: string): string {
selector = StringWrapper.replaceAll(selector, _colonHostContextRe, _polyfillHostContext); return selector.replace(_colonHostContextRe, _polyfillHostContext)
selector = StringWrapper.replaceAll(selector, _colonHostRe, _polyfillHost); .replace(_colonHostRe, _polyfillHost);
return selector;
} }
} }
const _cssContentNextSelectorRe = const _cssContentNextSelectorRe =
@ -444,30 +463,28 @@ const _cssColonHostRe = new RegExp('(' + _polyfillHost + _parenSuffix, 'gim');
const _cssColonHostContextRe = new RegExp('(' + _polyfillHostContext + _parenSuffix, 'gim'); const _cssColonHostContextRe = new RegExp('(' + _polyfillHostContext + _parenSuffix, 'gim');
const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator'; const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator';
const _shadowDOMSelectorsRe = [ const _shadowDOMSelectorsRe = [
/::shadow/g, /::content/g, /::shadow/g,
/::content/g,
// Deprecated selectors // Deprecated selectors
// TODO(vicb): see https://github.com/angular/clang-format/issues/16 /\/shadow-deep\//g,
// clang-format off /\/shadow\//g,
/\/shadow-deep\//g, // former /deep/
/\/shadow\//g, // former ::shadow
// clanf-format on
]; ];
const _shadowDeepSelectors = /(?:>>>)|(?:\/deep\/)/g; const _shadowDeepSelectors = /(?:>>>)|(?:\/deep\/)/g;
const _selectorReSuffix = '([>\\s~+\[.,{:][\\s\\S]*)?$'; const _selectorReSuffix = '([>\\s~+\[.,{:][\\s\\S]*)?$';
const _polyfillHostRe = new RegExp(_polyfillHost, 'im'); const _polyfillHostRe = /-shadowcsshost/gim;
const _colonHostRe = /:host/gim; const _colonHostRe = /:host/gim;
const _colonHostContextRe = /:host-context/gim; const _colonHostContextRe = /:host-context/gim;
const _commentRe = /\/\*\s*[\s\S]*?\*\//g; const _commentRe = /\/\*\s*[\s\S]*?\*\//g;
function stripComments(input:string):string { function stripComments(input: string): string {
return StringWrapper.replaceAllMapped(input, _commentRe, (_: any /** TODO #9100 */) => ''); return StringWrapper.replaceAllMapped(input, _commentRe, (_: any /** TODO #9100 */) => '');
} }
// all comments except inline source mapping ("/* #sourceMappingURL= ... */") // all comments except inline source mapping ("/* #sourceMappingURL= ... */")
const _sourceMappingUrlRe = /[\s\S]*(\/\*\s*#\s*sourceMappingURL=[\s\S]+?\*\/)\s*$/; 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); const matcher = input.match(_sourceMappingUrlRe);
return matcher ? matcher[1] : ''; return matcher ? matcher[1] : '';
} }
@ -479,38 +496,39 @@ const CLOSE_CURLY = '}';
const BLOCK_PLACEHOLDER = '%BLOCK%'; const BLOCK_PLACEHOLDER = '%BLOCK%';
export class CssRule { 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); const inputWithEscapedBlocks = escapeBlocks(input);
let nextBlockIndex = 0; let nextBlockIndex = 0;
return StringWrapper.replaceAllMapped(inputWithEscapedBlocks.escapedString, _ruleRe, function(m: any /** TODO #9100 */) { return StringWrapper.replaceAllMapped(
const selector = m[2]; inputWithEscapedBlocks.escapedString, _ruleRe, function(m: any /** TODO #9100 */) {
let content = ''; const selector = m[2];
let suffix = m[4]; let content = '';
let contentPrefix = ''; let suffix = m[4];
if (isPresent(m[4]) && m[4].startsWith('{'+BLOCK_PLACEHOLDER)) { let contentPrefix = '';
content = inputWithEscapedBlocks.blocks[nextBlockIndex++]; if (isPresent(m[4]) && m[4].startsWith('{' + BLOCK_PLACEHOLDER)) {
suffix = m[4].substring(BLOCK_PLACEHOLDER.length+1); content = inputWithEscapedBlocks.blocks[nextBlockIndex++];
contentPrefix = '{'; 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}`; const rule = ruleCallback(new CssRule(selector, content));
}); return `${m[1]}${rule.selector}${m[3]}${contentPrefix}${rule.content}${suffix}`;
});
} }
class StringWithEscapedBlocks { 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 inputParts = StringWrapper.split(input, _curlyRe);
const resultParts: any[] /** TODO #9100 */ = []; const resultParts: any[] /** TODO #9100 */ = [];
const escapedBlocks: any[] /** TODO #9100 */ = []; const escapedBlocks: any[] /** TODO #9100 */ = [];
let bracketCount = 0; let bracketCount = 0;
let currentBlockParts: any[] /** TODO #9100 */ = []; 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]; const part = inputParts[partIndex];
if (part == CLOSE_CURLY) { if (part == CLOSE_CURLY) {
bracketCount--; bracketCount--;

View File

@ -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 {beforeEach, ddescribe, describe, expect, iit, it} from '@angular/core/testing/testing_internal';
import {normalizeCSS} from '@angular/platform-browser/testing/browser_util'; import {normalizeCSS} from '@angular/platform-browser/testing/browser_util';
import {StringWrapper, isPresent} from '../src/facade/lang';
export function main() { export function main() {
describe('ShadowCss', function() { describe('ShadowCss', function() {
@ -19,7 +17,7 @@ export function main() {
const shadowCss = new ShadowCss(); const shadowCss = new ShadowCss();
const shim = shadowCss.shimCssText(css, contentAttr, hostAttr); const shim = shadowCss.shimCssText(css, contentAttr, hostAttr);
const nlRegexp = /\n/g; const nlRegexp = /\n/g;
return normalizeCSS(StringWrapper.replaceAll(shim, nlRegexp, '')); return normalizeCSS(shim.replace(nlRegexp, ''));
} }
it('should handle empty string', () => { expect(s('', 'a')).toEqual(''); }); it('should handle empty string', () => { expect(s('', 'a')).toEqual(''); });
@ -99,15 +97,17 @@ export function main() {
it('should handle :host', () => { it('should handle :host', () => {
expect(s(':host {}', 'a', 'a-host')).toEqual('[a-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) {}', 'a', 'a-host')).toEqual('[a-host].x, [a-host].y {}');
expect(s(':host(.x,.y) > .z {}', 'a', 'a-host')) 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', () => { 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) {}', 'a', 'a-host')).toEqual('[a-host].x, .x [a-host] {}');
expect(s(':host-context(.x) > .y {}', 'a', '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', () => { it('should support polyfill-next-selector', () => {
@ -120,10 +120,10 @@ export function main() {
it('should support polyfill-unscoped-rule', () => { it('should support polyfill-unscoped-rule', () => {
let css = s('polyfill-unscoped-rule {content: \'#menu > .bar\';color: blue;}', 'a'); 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'); 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', () => { it('should support multiple instances polyfill-unscoped-rule', () => {
@ -131,16 +131,16 @@ export function main() {
s('polyfill-unscoped-rule {content: \'foo\';color: blue;}' + s('polyfill-unscoped-rule {content: \'foo\';color: blue;}' +
'polyfill-unscoped-rule {content: \'bar\';color: blue;}', 'polyfill-unscoped-rule {content: \'bar\';color: blue;}',
'a'); 'a');
expect(StringWrapper.contains(css, 'foo {;color:blue;}')).toBeTruthy(); expect(css).toContain('foo {;color:blue;}');
expect(StringWrapper.contains(css, 'bar {;color:blue;}')).toBeTruthy(); expect(css).toContain('bar {;color:blue;}');
}); });
it('should support polyfill-rule', () => { it('should support polyfill-rule', () => {
let css = s('polyfill-rule {content: \':host.foo .bar\';color: blue;}', 'a', 'a-host'); 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'); 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', () => { it('should handle ::shadow', () => {