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:
@ -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--;
|
||||||
|
@ -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', () => {
|
||||||
|
Reference in New Issue
Block a user