mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
fix(compiler): scope css keyframes in emulated view encapsulation (#42608)
Ensure that keyframes rules, defined within components with emulated
view encapsulation, are scoped to avoid collisions with keyframes in
other components.
This is achieved by renaming these keyframes to add a prefix that makes
them unique across the application.
In order to enable the handling of keyframes names defined as strings
the previous strategy of replacing quoted css content with `%QUOTED%`
(introduced in commit 7f689a2) has been removed and in its place now
only specific characters inside quotes are being replaced with
placeholder text (those are `;`, `:` and `,`, more can be added in
the future if the need arises).
Closes #33885
BREAKING CHANGE:
Keyframes names are now prefixed with the component's "scope name".
For example, the following keyframes rule in a component definition,
whose "scope name" is host-my-cmp:
@keyframes foo { ... }
will become:
@keyframes host-my-cmp_foo { ... }
Any TypeScript/JavaScript code which relied on the names of keyframes rules
will no longer match.
The recommended solutions in this case are to either:
- change the component's view encapsulation to the `None` or `ShadowDom`
- define keyframes rules in global stylesheets (e.g styles.css)
- define keyframes rules programmatically in code.
PR Close #42608
This commit is contained in:
parent
bf98c646f7
commit
f03e313f24
3 changed files with 844 additions and 37 deletions
|
|
@ -7,7 +7,27 @@
|
|||
*/
|
||||
|
||||
/**
|
||||
* This file is a port of shadowCSS from webcomponents.js to TypeScript.
|
||||
* The following set contains all keywords that can be used in the animation css shorthand
|
||||
* property and is used during the scoping of keyframes to make sure such keywords
|
||||
* are not modified.
|
||||
*/
|
||||
const animationKeywords = new Set([
|
||||
// global values
|
||||
'inherit', 'initial', 'revert', 'unset',
|
||||
// animation-direction
|
||||
'alternate', 'alternate-reverse', 'normal', 'reverse',
|
||||
// animation-fill-mode
|
||||
'backwards', 'both', 'forwards', 'none',
|
||||
// animation-play-state
|
||||
'paused', 'running',
|
||||
// animation-timing-function
|
||||
'ease', 'ease-in', 'ease-in-out', 'ease-out', 'linear', 'step-start', 'step-end',
|
||||
// `steps()` function
|
||||
'end', 'jump-both', 'jump-end', 'jump-none', 'jump-start', 'start'
|
||||
]);
|
||||
|
||||
/**
|
||||
* The following class is a port of shadowCSS from webcomponents.js to TypeScript.
|
||||
*
|
||||
* Please make sure to keep to edits in sync with the source file.
|
||||
*
|
||||
|
|
@ -131,7 +151,6 @@
|
|||
declaration. This is a directive to the styling shim to use the selector
|
||||
in comments in lieu of the next selector when running under polyfill.
|
||||
*/
|
||||
|
||||
export class ShadowCss {
|
||||
strictStyling: boolean = true;
|
||||
|
||||
|
|
@ -157,6 +176,187 @@ export class ShadowCss {
|
|||
return this._insertPolyfillRulesInCssText(cssText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process styles to add scope to keyframes.
|
||||
*
|
||||
* Modify both the names of the keyframes defined in the component styles and also the css
|
||||
* animation rules using them.
|
||||
*
|
||||
* Animation rules using keyframes defined elsewhere are not modified to allow for globally
|
||||
* defined keyframes.
|
||||
*
|
||||
* For example, we convert this css:
|
||||
*
|
||||
* ```
|
||||
* .box {
|
||||
* animation: box-animation 1s forwards;
|
||||
* }
|
||||
*
|
||||
* @keyframes box-animation {
|
||||
* to {
|
||||
* background-color: green;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* to this:
|
||||
*
|
||||
* ```
|
||||
* .box {
|
||||
* animation: scopeName_box-animation 1s forwards;
|
||||
* }
|
||||
*
|
||||
* @keyframes scopeName_box-animation {
|
||||
* to {
|
||||
* background-color: green;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param cssText the component's css text that needs to be scoped.
|
||||
* @param scopeSelector the component's scope selector.
|
||||
*
|
||||
* @returns the scoped css text.
|
||||
*/
|
||||
private _scopeKeyframesRelatedCss(cssText: string, scopeSelector: string): string {
|
||||
const unscopedKeyframesSet = new Set<string>();
|
||||
const scopedKeyframesCssText = processRules(
|
||||
cssText,
|
||||
rule => this._scopeLocalKeyframeDeclarations(rule, scopeSelector, unscopedKeyframesSet));
|
||||
return processRules(
|
||||
scopedKeyframesCssText,
|
||||
rule => this._scopeAnimationRule(rule, scopeSelector, unscopedKeyframesSet));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scopes local keyframes names, returning the updated css rule and it also
|
||||
* adds the original keyframe name to a provided set to collect all keyframes names
|
||||
* so that it can later be used to scope the animation rules.
|
||||
*
|
||||
* For example, it takes a rule such as:
|
||||
*
|
||||
* ```
|
||||
* @keyframes box-animation {
|
||||
* to {
|
||||
* background-color: green;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* and returns:
|
||||
*
|
||||
* ```
|
||||
* @keyframes scopeName_box-animation {
|
||||
* to {
|
||||
* background-color: green;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
* and as a side effect it adds "box-animation" to the `unscopedKeyframesSet` set
|
||||
*
|
||||
* @param cssRule the css rule to process.
|
||||
* @param scopeSelector the component's scope selector.
|
||||
* @param unscopedKeyframesSet the set of unscoped keyframes names (which can be
|
||||
* modified as a side effect)
|
||||
*
|
||||
* @returns the css rule modified with the scoped keyframes name.
|
||||
*/
|
||||
private _scopeLocalKeyframeDeclarations(
|
||||
rule: CssRule, scopeSelector: string, unscopedKeyframesSet: Set<string>): CssRule {
|
||||
return {
|
||||
...rule,
|
||||
selector: rule.selector.replace(
|
||||
/(^@(?:-webkit-)?keyframes(?:\s+))(['"]?)(.+)\2(\s*)$/,
|
||||
(_, start, quote, keyframeName, endSpaces) => {
|
||||
unscopedKeyframesSet.add(unescapeQuotes(keyframeName, quote));
|
||||
return `${start}${quote}${scopeSelector}_${keyframeName}${quote}${endSpaces}`;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Function used to scope a keyframes name (obtained from an animation declaration)
|
||||
* using an existing set of unscopedKeyframes names to discern if the scoping needs to be
|
||||
* performed (keyframes names of keyframes not defined in the component's css need not to be
|
||||
* scoped).
|
||||
*
|
||||
* @param keyframe the keyframes name to check.
|
||||
* @param scopeSelector the component's scope selector.
|
||||
* @param unscopedKeyframesSet the set of unscoped keyframes names.
|
||||
*
|
||||
* @returns the scoped name of the keyframe, or the original name is the name need not to be
|
||||
* scoped.
|
||||
*/
|
||||
private _scopeAnimationKeyframe(
|
||||
keyframe: string, scopeSelector: string, unscopedKeyframesSet: ReadonlySet<string>): string {
|
||||
return keyframe.replace(/^(\s*)(['"]?)(.+?)\2(\s*)$/, (_, spaces1, quote, name, spaces2) => {
|
||||
name = `${unscopedKeyframesSet.has(unescapeQuotes(name, quote)) ? scopeSelector + '_' : ''}${
|
||||
name}`;
|
||||
return `${spaces1}${quote}${name}${quote}${spaces2}`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Regular expression used to extrapolate the possible keyframes from an
|
||||
* animation declaration (with possibly multiple animation definitions)
|
||||
*
|
||||
* The regular expression can be divided in three parts
|
||||
* - (^|\s+)
|
||||
* simply captures how many (if any) leading whitespaces are present
|
||||
* - (?:(?:(['"])((?:\\\\|\\\2|(?!\2).)+)\2)|(-?[A-Za-z][\w\-]*))
|
||||
* captures two different possible keyframes, ones which are quoted or ones which are valid css
|
||||
* idents (custom properties excluded)
|
||||
* - (?=[,\s;]|$)
|
||||
* simply matches the end of the possible keyframe, valid endings are: a comma, a space, a
|
||||
* semicolon or the end of the string
|
||||
*/
|
||||
private _animationDeclarationKeyframesRe =
|
||||
/(^|\s+)(?:(?:(['"])((?:\\\\|\\\2|(?!\2).)+)\2)|(-?[A-Za-z][\w\-]*))(?=[,\s]|$)/g;
|
||||
|
||||
/**
|
||||
* Scope an animation rule so that the keyframes mentioned in such rule
|
||||
* are scoped if defined in the component's css and left untouched otherwise.
|
||||
*
|
||||
* It can scope values of both the 'animation' and 'animation-name' properties.
|
||||
*
|
||||
* @param rule css rule to scope.
|
||||
* @param scopeSelector the component's scope selector.
|
||||
* @param unscopedKeyframesSet the set of unscoped keyframes names.
|
||||
*
|
||||
* @returns the updated css rule.
|
||||
**/
|
||||
private _scopeAnimationRule(
|
||||
rule: CssRule, scopeSelector: string, unscopedKeyframesSet: ReadonlySet<string>): CssRule {
|
||||
let content = rule.content.replace(
|
||||
/((?:^|\s+)(?:-webkit-)?animation(?:\s*):(?:\s*))([^;]+)/g,
|
||||
(_, start, animationDeclarations) => start +
|
||||
animationDeclarations.replace(
|
||||
this._animationDeclarationKeyframesRe,
|
||||
(original: string, leadingSpaces: string, quote = '', quotedName: string,
|
||||
nonQuotedName: string) => {
|
||||
if (quotedName) {
|
||||
return `${leadingSpaces}${
|
||||
this._scopeAnimationKeyframe(
|
||||
`${quote}${quotedName}${quote}`, scopeSelector, unscopedKeyframesSet)}`;
|
||||
} else {
|
||||
return animationKeywords.has(nonQuotedName) ?
|
||||
original :
|
||||
`${leadingSpaces}${
|
||||
this._scopeAnimationKeyframe(
|
||||
nonQuotedName, scopeSelector, unscopedKeyframesSet)}`;
|
||||
}
|
||||
}));
|
||||
content = content.replace(
|
||||
/((?:^|\s+)(?:-webkit-)?animation-name(?:\s*):(?:\s*))([^;]+)/g,
|
||||
(_match, start, commaSeparatedKeyframes) => `${start}${
|
||||
commaSeparatedKeyframes.split(',')
|
||||
.map(
|
||||
(keyframe: string) =>
|
||||
this._scopeAnimationKeyframe(keyframe, scopeSelector, unscopedKeyframesSet))
|
||||
.join(',')}`);
|
||||
return {...rule, content};
|
||||
}
|
||||
|
||||
/*
|
||||
* Process styles to convert native ShadowDOM rules that will trip
|
||||
* up the css parser; we rely on decorating the stylesheet with inert rules.
|
||||
|
|
@ -217,6 +417,7 @@ export class ShadowCss {
|
|||
cssText = this._convertColonHostContext(cssText);
|
||||
cssText = this._convertShadowDOMSelectors(cssText);
|
||||
if (scopeSelector) {
|
||||
cssText = this._scopeKeyframesRelatedCss(cssText, scopeSelector);
|
||||
cssText = this._scopeSelectors(cssText, scopeSelector, hostSelector);
|
||||
}
|
||||
cssText = cssText + '\n' + unscopedRules;
|
||||
|
|
@ -642,39 +843,39 @@ function extractCommentsWithHash(input: string): string[] {
|
|||
}
|
||||
|
||||
const BLOCK_PLACEHOLDER = '%BLOCK%';
|
||||
const QUOTE_PLACEHOLDER = '%QUOTED%';
|
||||
const _ruleRe = /(\s*)([^;\{\}]+?)(\s*)((?:{%BLOCK%}?\s*;?)|(?:\s*;))/g;
|
||||
const _quotedRe = /%QUOTED%/g;
|
||||
const CONTENT_PAIRS = new Map([['{', '}']]);
|
||||
const QUOTE_PAIRS = new Map([[`"`, `"`], [`'`, `'`]]);
|
||||
|
||||
const COMMA_IN_PLACEHOLDER = '%COMMA_IN_PLACEHOLDER%';
|
||||
const SEMI_IN_PLACEHOLDER = '%SEMI_IN_PLACEHOLDER%';
|
||||
const COLON_IN_PLACEHOLDER = '%COLON_IN_PLACEHOLDER%';
|
||||
|
||||
const _cssCommaInPlaceholderReGlobal = new RegExp(COMMA_IN_PLACEHOLDER, 'g');
|
||||
const _cssSemiInPlaceholderReGlobal = new RegExp(SEMI_IN_PLACEHOLDER, 'g');
|
||||
const _cssColonInPlaceholderReGlobal = new RegExp(COLON_IN_PLACEHOLDER, 'g');
|
||||
|
||||
export class CssRule {
|
||||
constructor(public selector: string, public content: string) {}
|
||||
}
|
||||
|
||||
export function processRules(input: string, ruleCallback: (rule: CssRule) => CssRule): string {
|
||||
const inputWithEscapedQuotes = escapeBlocks(input, QUOTE_PAIRS, QUOTE_PLACEHOLDER);
|
||||
const inputWithEscapedBlocks =
|
||||
escapeBlocks(inputWithEscapedQuotes.escapedString, CONTENT_PAIRS, BLOCK_PLACEHOLDER);
|
||||
const escaped = escapeInStrings(input);
|
||||
const inputWithEscapedBlocks = escapeBlocks(escaped, CONTENT_PAIRS, BLOCK_PLACEHOLDER);
|
||||
let nextBlockIndex = 0;
|
||||
let nextQuoteIndex = 0;
|
||||
return inputWithEscapedBlocks.escapedString
|
||||
.replace(
|
||||
_ruleRe,
|
||||
(...m: string[]) => {
|
||||
const selector = m[2];
|
||||
let content = '';
|
||||
let suffix = m[4];
|
||||
let contentPrefix = '';
|
||||
if (suffix && suffix.startsWith('{' + BLOCK_PLACEHOLDER)) {
|
||||
content = inputWithEscapedBlocks.blocks[nextBlockIndex++];
|
||||
suffix = suffix.substring(BLOCK_PLACEHOLDER.length + 1);
|
||||
contentPrefix = '{';
|
||||
}
|
||||
const rule = ruleCallback(new CssRule(selector, content));
|
||||
return `${m[1]}${rule.selector}${m[3]}${contentPrefix}${rule.content}${suffix}`;
|
||||
})
|
||||
.replace(_quotedRe, () => inputWithEscapedQuotes.blocks[nextQuoteIndex++]);
|
||||
const escapedResult = inputWithEscapedBlocks.escapedString.replace(_ruleRe, (...m: string[]) => {
|
||||
const selector = m[2];
|
||||
let content = '';
|
||||
let suffix = m[4];
|
||||
let contentPrefix = '';
|
||||
if (suffix && suffix.startsWith('{' + BLOCK_PLACEHOLDER)) {
|
||||
content = inputWithEscapedBlocks.blocks[nextBlockIndex++];
|
||||
suffix = suffix.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 unescapeInStrings(escapedResult);
|
||||
}
|
||||
|
||||
class StringWithEscapedBlocks {
|
||||
|
|
@ -722,6 +923,113 @@ function escapeBlocks(
|
|||
return new StringWithEscapedBlocks(resultParts.join(''), escapedBlocks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Object containing as keys characters that should be substituted by placeholders
|
||||
* when found in strings during the css text parsing, and as values the respective
|
||||
* placeholders
|
||||
*/
|
||||
const ESCAPE_IN_STRING_MAP: {[key: string]: string} = {
|
||||
';': SEMI_IN_PLACEHOLDER,
|
||||
',': COMMA_IN_PLACEHOLDER,
|
||||
':': COLON_IN_PLACEHOLDER
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse the provided css text and inside strings (meaning, inside pairs of unescaped single or
|
||||
* double quotes) replace specific characters with their respective placeholders as indicated
|
||||
* by the `ESCAPE_IN_STRING_MAP` map.
|
||||
*
|
||||
* For example convert the text
|
||||
* `animation: "my-anim:at\"ion" 1s;`
|
||||
* to
|
||||
* `animation: "my-anim%COLON_IN_PLACEHOLDER%at\"ion" 1s;`
|
||||
*
|
||||
* This is necessary in order to remove the meaning of some characters when found inside strings
|
||||
* (for example `;` indicates the end of a css declaration, `,` the sequence of values and `:` the
|
||||
* division between property and value during a declaration, none of these meanings apply when such
|
||||
* characters are within strings and so in order to prevent parsing issues they need to be replaced
|
||||
* with placeholder text for the duration of the css manipulation process).
|
||||
*
|
||||
* @param input the original css text.
|
||||
*
|
||||
* @returns the css text with specific characters in strings replaced by placeholders.
|
||||
**/
|
||||
function escapeInStrings(input: string): string {
|
||||
let result = input;
|
||||
let currentQuoteChar: string|null = null;
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
const char = result[i];
|
||||
if (char === '\\') {
|
||||
i++;
|
||||
} else {
|
||||
if (currentQuoteChar !== null) {
|
||||
// index i is inside a quoted sub-string
|
||||
if (char === currentQuoteChar) {
|
||||
currentQuoteChar = null;
|
||||
} else {
|
||||
const placeholder: string|undefined = ESCAPE_IN_STRING_MAP[char];
|
||||
if (placeholder) {
|
||||
result = `${result.substr(0, i)}${placeholder}${result.substr(i + 1)}`;
|
||||
i += placeholder.length - 1;
|
||||
}
|
||||
}
|
||||
} else if (char === '\'' || char === '"') {
|
||||
currentQuoteChar = char;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace in a string all occurrences of keys in the `ESCAPE_IN_STRING_MAP` map with their
|
||||
* original representation, this is simply used to revert the changes applied by the
|
||||
* escapeInStrings function.
|
||||
*
|
||||
* For example it reverts the text:
|
||||
* `animation: "my-anim%COLON_IN_PLACEHOLDER%at\"ion" 1s;`
|
||||
* to it's original form of:
|
||||
* `animation: "my-anim:at\"ion" 1s;`
|
||||
*
|
||||
* Note: For the sake of simplicity this function does not check that the placeholders are
|
||||
* actually inside strings as it would anyway be extremely unlikely to find them outside of strings.
|
||||
*
|
||||
* @param input the css text containing the placeholders.
|
||||
*
|
||||
* @returns the css text without the placeholders.
|
||||
*/
|
||||
function unescapeInStrings(input: string): string {
|
||||
let result = input.replace(_cssCommaInPlaceholderReGlobal, ',');
|
||||
result = result.replace(_cssSemiInPlaceholderReGlobal, ';');
|
||||
result = result.replace(_cssColonInPlaceholderReGlobal, ':');
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescape all quotes present in a string, but only if the string was actually already
|
||||
* quoted.
|
||||
*
|
||||
* This generates a "canonical" representation of strings which can be used to match strings
|
||||
* which would otherwise only differ because of differently escaped quotes.
|
||||
*
|
||||
* For example it converts the string (assumed to be quoted):
|
||||
* `this \\"is\\" a \\'\\\\'test`
|
||||
* to:
|
||||
* `this "is" a '\\\\'test`
|
||||
* (note that the latter backslashes are not removed as they are not actually escaping the single
|
||||
* quote)
|
||||
*
|
||||
*
|
||||
* @param input the string possibly containing escaped quotes.
|
||||
* @param isQuoted boolean indicating whether the string was quoted inside a bigger string (if not
|
||||
* then it means that it doesn't represent an inner string and thus no unescaping is required)
|
||||
*
|
||||
* @returns the string in the "canonical" representation without escaped quotes.
|
||||
*/
|
||||
function unescapeQuotes(str: string, isQuoted: boolean): string {
|
||||
return !isQuoted ? str : str.replace(/((?:^|[^\\])(?:\\\\)*)\\(?=['"])/g, '$1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine the `contextSelectors` with the `hostMarker` and the `otherSelectors`
|
||||
* to create a selector that matches the same as `:host-context()`.
|
||||
|
|
|
|||
510
packages/compiler/test/shadow_css/keyframes_spec.ts
Normal file
510
packages/compiler/test/shadow_css/keyframes_spec.ts
Normal file
|
|
@ -0,0 +1,510 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ShadowCss} from '@angular/compiler/src/shadow_css';
|
||||
|
||||
describe('ShadowCss, keyframes and animations', () => {
|
||||
function s(css: string, contentAttr: string, hostAttr: string = '') {
|
||||
const shadowCss = new ShadowCss();
|
||||
return shadowCss.shimCssText(css, contentAttr, hostAttr);
|
||||
}
|
||||
|
||||
it('should scope keyframes rules', () => {
|
||||
const css = '@keyframes foo {0% {transform:translate(-50%) scaleX(0);}}';
|
||||
const expected = '@keyframes host-a_foo {0% {transform:translate(-50%) scaleX(0);}}';
|
||||
expect(s(css, 'host-a')).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should scope -webkit-keyframes rules', () => {
|
||||
const css = '@-webkit-keyframes foo {0% {-webkit-transform:translate(-50%) scaleX(0);}} ';
|
||||
const expected =
|
||||
'@-webkit-keyframes host-a_foo {0% {-webkit-transform:translate(-50%) scaleX(0);}}';
|
||||
expect(s(css, 'host-a')).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should scope animations using local keyframes identifiers', () => {
|
||||
const css = `
|
||||
button {
|
||||
animation: foo 10s ease;
|
||||
}
|
||||
@keyframes foo {
|
||||
0% {
|
||||
transform: translate(-50%) scaleX(0);
|
||||
}
|
||||
}
|
||||
`;
|
||||
const result = s(css, 'host-a');
|
||||
expect(result).toContain('animation: host-a_foo 10s ease;');
|
||||
});
|
||||
|
||||
it('should not scope animations using non-local keyframes identifiers', () => {
|
||||
const css = `
|
||||
button {
|
||||
animation: foo 10s ease;
|
||||
}
|
||||
`;
|
||||
const result = s(css, 'host-a');
|
||||
expect(result).toContain('animation: foo 10s ease;');
|
||||
});
|
||||
|
||||
it('should scope animation-names using local keyframes identifiers', () => {
|
||||
const css = `
|
||||
button {
|
||||
animation-name: foo;
|
||||
}
|
||||
@keyframes foo {
|
||||
0% {
|
||||
transform: translate(-50%) scaleX(0);
|
||||
}
|
||||
}
|
||||
`;
|
||||
const result = s(css, 'host-a');
|
||||
expect(result).toContain('animation-name: host-a_foo;');
|
||||
});
|
||||
|
||||
it('should not scope animation-names using non-local keyframes identifiers', () => {
|
||||
const css = `
|
||||
button {
|
||||
animation-name: foo;
|
||||
}
|
||||
`;
|
||||
const result = s(css, 'host-a');
|
||||
expect(result).toContain('animation-name: foo;');
|
||||
});
|
||||
|
||||
it('should handle (scope or not) multiple animation-names', () => {
|
||||
const css = `
|
||||
button {
|
||||
animation-name: foo, bar,baz, qux , quux ,corge ,grault ,garply, waldo;
|
||||
}
|
||||
@keyframes foo {}
|
||||
@keyframes baz {}
|
||||
@keyframes quux {}
|
||||
@keyframes grault {}
|
||||
@keyframes waldo {}`;
|
||||
const result = s(css, 'host-a');
|
||||
const animationNames = [
|
||||
'host-a_foo',
|
||||
' bar',
|
||||
'host-a_baz',
|
||||
' qux ',
|
||||
' host-a_quux ',
|
||||
'corge ',
|
||||
'host-a_grault ',
|
||||
'garply',
|
||||
' host-a_waldo',
|
||||
];
|
||||
const expected = `animation-name: ${animationNames.join(',')};`;
|
||||
expect(result).toContain(expected);
|
||||
});
|
||||
|
||||
it('should handle (scope or not) multiple animation-names defined over multiple lines', () => {
|
||||
const css = `
|
||||
button {
|
||||
animation-name: foo,
|
||||
bar,baz,
|
||||
qux ,
|
||||
quux ,
|
||||
grault,
|
||||
garply, waldo;
|
||||
}
|
||||
@keyframes foo {}
|
||||
@keyframes baz {}
|
||||
@keyframes quux {}
|
||||
@keyframes grault {}`;
|
||||
const result = s(css, 'host-a');
|
||||
['foo', 'baz', 'quux', 'grault'].forEach(
|
||||
scoped => expect(result).toContain(`host-a_${scoped}`));
|
||||
['bar', 'qux', 'garply', 'waldo'].forEach(nonScoped => {
|
||||
expect(result).toContain(nonScoped);
|
||||
expect(result).not.toContain(`host-a_${nonScoped}`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle (scope or not) multiple animation definitions in a single declaration', () => {
|
||||
const css = `
|
||||
div {
|
||||
animation: 1s ease foo, 2s bar infinite, forwards baz 3s;
|
||||
}
|
||||
|
||||
p {
|
||||
animation: 1s "foo", 2s "bar";
|
||||
}
|
||||
|
||||
span {
|
||||
animation: .5s ease 'quux',
|
||||
1s foo infinite, forwards "baz'" 1.5s,
|
||||
2s bar;
|
||||
}
|
||||
|
||||
button {
|
||||
animation: .5s bar,
|
||||
1s foo 0.3s, 2s quux;
|
||||
}
|
||||
|
||||
@keyframes bar {}
|
||||
@keyframes quux {}
|
||||
@keyframes "baz'" {}`;
|
||||
const result = s(css, 'host-a');
|
||||
expect(result).toContain('animation: 1s ease foo, 2s host-a_bar infinite, forwards baz 3s;');
|
||||
expect(result).toContain('animation: 1s "foo", 2s "host-a_bar";');
|
||||
expect(result).toContain(`
|
||||
animation: .5s host-a_bar,
|
||||
1s foo 0.3s, 2s host-a_quux;`);
|
||||
expect(result).toContain(`
|
||||
animation: .5s ease 'host-a_quux',
|
||||
1s foo infinite, forwards "host-a_baz'" 1.5s,
|
||||
2s host-a_bar;`);
|
||||
});
|
||||
|
||||
it(`should not modify css variables ending with 'animation' even if they reference a local keyframes identifier`,
|
||||
() => {
|
||||
const css = `
|
||||
button {
|
||||
--variable-animation: foo;
|
||||
}
|
||||
@keyframes foo {}`;
|
||||
const result = s(css, 'host-a');
|
||||
expect(result).toContain('--variable-animation: foo;');
|
||||
});
|
||||
|
||||
it(`should not modify css variables ending with 'animation-name' even if they reference a local keyframes identifier`,
|
||||
() => {
|
||||
const css = `
|
||||
button {
|
||||
--variable-animation-name: foo;
|
||||
}
|
||||
@keyframes foo {}`;
|
||||
const result = s(css, 'host-a');
|
||||
expect(result).toContain('--variable-animation-name: foo;');
|
||||
});
|
||||
|
||||
it('should maintain the spacing when handling (scoping or not) keyframes and animations', () => {
|
||||
const css = `
|
||||
div {
|
||||
animation-name : foo;
|
||||
animation: 5s bar 1s backwards;
|
||||
animation : 3s baz ;
|
||||
animation-name:foobar ;
|
||||
animation:1s "foo" , 2s "bar",3s "quux";
|
||||
}
|
||||
|
||||
@-webkit-keyframes bar {}
|
||||
@keyframes foobar {}
|
||||
@keyframes quux {}
|
||||
`;
|
||||
const result = s(css, 'host-a');
|
||||
expect(result).toContain('animation-name : foo;');
|
||||
expect(result).toContain('animation: 5s host-a_bar 1s backwards;');
|
||||
expect(result).toContain('animation : 3s baz ;');
|
||||
expect(result).toContain('animation-name:host-a_foobar ;');
|
||||
expect(result).toContain('@-webkit-keyframes host-a_bar {}');
|
||||
expect(result).toContain('@keyframes host-a_foobar {}');
|
||||
expect(result).toContain('animation:1s "foo" , 2s "host-a_bar",3s "host-a_quux"');
|
||||
});
|
||||
|
||||
it('should ignore keywords values when scoping local animations', () => {
|
||||
const css = `
|
||||
div {
|
||||
animation: inherit;
|
||||
animation: unset;
|
||||
animation: 3s ease reverse foo;
|
||||
animation: 5s foo 1s backwards;
|
||||
animation: none 1s foo;
|
||||
animation: .5s foo paused;
|
||||
animation: 1s running foo;
|
||||
animation: 3s linear 1s infinite running foo;
|
||||
animation: 5s foo ease;
|
||||
animation: 3s .5s infinite steps(3,end) foo;
|
||||
animation: 5s steps(9, jump-start) jump .5s;
|
||||
animation: 1s step-end steps;
|
||||
}
|
||||
|
||||
@keyframes foo {}
|
||||
@keyframes inherit {}
|
||||
@keyframes unset {}
|
||||
@keyframes ease {}
|
||||
@keyframes reverse {}
|
||||
@keyframes backwards {}
|
||||
@keyframes none {}
|
||||
@keyframes paused {}
|
||||
@keyframes linear {}
|
||||
@keyframes running {}
|
||||
@keyframes end {}
|
||||
@keyframes jump {}
|
||||
@keyframes start {}
|
||||
@keyframes steps {}
|
||||
`;
|
||||
const result = s(css, 'host-a');
|
||||
expect(result).toContain('animation: inherit;');
|
||||
expect(result).toContain('animation: unset;');
|
||||
expect(result).toContain('animation: 3s ease reverse host-a_foo;');
|
||||
expect(result).toContain('animation: 5s host-a_foo 1s backwards;');
|
||||
expect(result).toContain('animation: none 1s host-a_foo;');
|
||||
expect(result).toContain('animation: .5s host-a_foo paused;');
|
||||
expect(result).toContain('animation: 1s running host-a_foo;');
|
||||
expect(result).toContain('animation: 3s linear 1s infinite running host-a_foo;');
|
||||
expect(result).toContain('animation: 5s host-a_foo ease;');
|
||||
expect(result).toContain('animation: 3s .5s infinite steps(3,end) host-a_foo;');
|
||||
expect(result).toContain('animation: 5s steps(9, jump-start) host-a_jump .5s;');
|
||||
expect(result).toContain('animation: 1s step-end host-a_steps;');
|
||||
});
|
||||
|
||||
it('should handle the usage of quotes', () => {
|
||||
const css = `
|
||||
div {
|
||||
animation: 1.5s foo;
|
||||
}
|
||||
|
||||
p {
|
||||
animation: 1s 'foz bar';
|
||||
}
|
||||
|
||||
@keyframes 'foo' {}
|
||||
@keyframes "foz bar" {}
|
||||
@keyframes bar {}
|
||||
@keyframes baz {}
|
||||
@keyframes qux {}
|
||||
@keyframes quux {}
|
||||
`;
|
||||
const result = s(css, 'host-a');
|
||||
expect(result).toContain('@keyframes \'host-a_foo\' {}');
|
||||
expect(result).toContain('@keyframes "host-a_foz bar" {}');
|
||||
expect(result).toContain('animation: 1.5s host-a_foo;');
|
||||
expect(result).toContain('animation: 1s \'host-a_foz bar\';');
|
||||
});
|
||||
|
||||
it('should handle the usage of quotes containing escaped quotes', () => {
|
||||
const css = `
|
||||
div {
|
||||
animation: 1.5s "foo\\"bar";
|
||||
}
|
||||
|
||||
p {
|
||||
animation: 1s 'bar\\' \\'baz';
|
||||
}
|
||||
|
||||
button {
|
||||
animation-name: 'foz " baz';
|
||||
}
|
||||
|
||||
@keyframes "foo\\"bar" {}
|
||||
@keyframes "bar' 'baz" {}
|
||||
@keyframes "foz \\" baz" {}
|
||||
`;
|
||||
const result = s(css, 'host-a');
|
||||
expect(result).toContain('@keyframes "host-a_foo\\"bar" {}');
|
||||
expect(result).toContain(`@keyframes "host-a_bar' 'baz" {}`);
|
||||
expect(result).toContain('@keyframes "host-a_foz \\" baz" {}');
|
||||
expect(result).toContain('animation: 1.5s "host-a_foo\\"bar";');
|
||||
expect(result).toContain('animation: 1s \'host-a_bar\\\' \\\'baz\';');
|
||||
expect(result).toContain(`animation-name: 'host-a_foz " baz';`);
|
||||
});
|
||||
|
||||
it('should handle the usage of commas in multiple animation definitions in a single declaration',
|
||||
() => {
|
||||
const css = `
|
||||
button {
|
||||
animation: 1s "foo bar, baz", 2s 'qux quux';
|
||||
}
|
||||
|
||||
div {
|
||||
animation: 500ms foo, 1s 'bar, baz', 1500ms bar;
|
||||
}
|
||||
|
||||
p {
|
||||
animation: 3s "bar, baz", 3s 'foo, bar' 1s, 3s "qux quux";
|
||||
}
|
||||
|
||||
@keyframes "qux quux" {}
|
||||
@keyframes "bar, baz" {}
|
||||
`;
|
||||
const result = s(css, 'host-a');
|
||||
expect(result).toContain('@keyframes "host-a_qux quux" {}');
|
||||
expect(result).toContain('@keyframes "host-a_bar, baz" {}');
|
||||
expect(result).toContain(`animation: 1s "foo bar, baz", 2s 'host-a_qux quux';`);
|
||||
expect(result).toContain('animation: 500ms foo, 1s \'host-a_bar, baz\', 1500ms bar;');
|
||||
expect(result).toContain(
|
||||
`animation: 3s "host-a_bar, baz", 3s 'foo, bar' 1s, 3s "host-a_qux quux";`);
|
||||
});
|
||||
|
||||
it('should handle the usage of double quotes escaping in multiple animation definitions in a single declaration',
|
||||
() => {
|
||||
const css = `
|
||||
div {
|
||||
animation: 1s "foo", 1.5s "bar";
|
||||
animation: 2s "fo\\"o", 2.5s "bar";
|
||||
animation: 3s "foo\\"", 3.5s "bar", 3.7s "ba\\"r";
|
||||
animation: 4s "foo\\\\", 4.5s "bar", 4.7s "baz\\"";
|
||||
animation: 5s "fo\\\\\\"o", 5.5s "bar", 5.7s "baz\\"";
|
||||
}
|
||||
|
||||
@keyframes "foo" {}
|
||||
@keyframes "fo\\"o" {}
|
||||
@keyframes 'foo"' {}
|
||||
@keyframes 'foo\\\\' {}
|
||||
@keyframes bar {}
|
||||
@keyframes "ba\\"r" {}
|
||||
@keyframes "fo\\\\\\"o" {}
|
||||
`;
|
||||
const result = s(css, 'host-a');
|
||||
expect(result).toContain('@keyframes "host-a_foo" {}');
|
||||
expect(result).toContain('@keyframes "host-a_fo\\"o" {}');
|
||||
expect(result).toContain(`@keyframes 'host-a_foo"' {}`);
|
||||
expect(result).toContain('@keyframes \'host-a_foo\\\\\' {}');
|
||||
expect(result).toContain('@keyframes host-a_bar {}');
|
||||
expect(result).toContain('@keyframes "host-a_ba\\"r" {}');
|
||||
expect(result).toContain('@keyframes "host-a_fo\\\\\\"o"');
|
||||
expect(result).toContain('animation: 1s "host-a_foo", 1.5s "host-a_bar";');
|
||||
expect(result).toContain('animation: 2s "host-a_fo\\"o", 2.5s "host-a_bar";');
|
||||
expect(result).toContain(
|
||||
'animation: 3s "host-a_foo\\"", 3.5s "host-a_bar", 3.7s "host-a_ba\\"r";');
|
||||
expect(result).toContain(
|
||||
'animation: 4s "host-a_foo\\\\", 4.5s "host-a_bar", 4.7s "baz\\"";');
|
||||
expect(result).toContain(
|
||||
'animation: 5s "host-a_fo\\\\\\"o", 5.5s "host-a_bar", 5.7s "baz\\"";');
|
||||
});
|
||||
|
||||
it('should handle the usage of single quotes escaping in multiple animation definitions in a single declaration',
|
||||
() => {
|
||||
const css = `
|
||||
div {
|
||||
animation: 1s 'foo', 1.5s 'bar';
|
||||
animation: 2s 'fo\\'o', 2.5s 'bar';
|
||||
animation: 3s 'foo\\'', 3.5s 'bar', 3.7s 'ba\\'r';
|
||||
animation: 4s 'foo\\\\', 4.5s 'bar', 4.7s 'baz\\'';
|
||||
animation: 5s 'fo\\\\\\'o', 5.5s 'bar', 5.7s 'baz\\'';
|
||||
}
|
||||
|
||||
@keyframes foo {}
|
||||
@keyframes 'fo\\'o' {}
|
||||
@keyframes 'foo'' {}
|
||||
@keyframes 'foo\\\\' {}
|
||||
@keyframes "bar" {}
|
||||
@keyframes 'ba\\'r' {}
|
||||
@keyframes "fo\\\\\\'o" {}
|
||||
`;
|
||||
const result = s(css, 'host-a');
|
||||
expect(result).toContain('@keyframes host-a_foo {}');
|
||||
expect(result).toContain('@keyframes \'host-a_fo\\\'o\' {}');
|
||||
expect(result).toContain('@keyframes \'host-a_foo\'\' {}');
|
||||
expect(result).toContain('@keyframes \'host-a_foo\\\\\' {}');
|
||||
expect(result).toContain('@keyframes "host-a_bar" {}');
|
||||
expect(result).toContain('@keyframes \'host-a_ba\\\'r\' {}');
|
||||
expect(result).toContain(`@keyframes "host-a_fo\\\\\\'o" {}`);
|
||||
expect(result).toContain('animation: 1s \'host-a_foo\', 1.5s \'host-a_bar\';');
|
||||
expect(result).toContain('animation: 2s \'host-a_fo\\\'o\', 2.5s \'host-a_bar\';');
|
||||
expect(result).toContain(
|
||||
'animation: 3s \'host-a_foo\\\'\', 3.5s \'host-a_bar\', 3.7s \'host-a_ba\\\'r\';');
|
||||
expect(result).toContain(
|
||||
'animation: 4s \'host-a_foo\\\\\', 4.5s \'host-a_bar\', 4.7s \'baz\\\'\';');
|
||||
expect(result).toContain(
|
||||
'animation: 5s \'host-a_fo\\\\\\\'o\', 5.5s \'host-a_bar\', 5.7s \'baz\\\'\'');
|
||||
});
|
||||
|
||||
it('should handle the usage of mixed single and double quotes escaping in multiple animation definitions in a single declaration',
|
||||
() => {
|
||||
const css = `
|
||||
div {
|
||||
animation: 1s 'f\\"oo', 1.5s "ba\\'r";
|
||||
animation: 2s "fo\\"\\"o", 2.5s 'b\\\\"ar';
|
||||
animation: 3s 'foo\\\\', 3.5s "b\\\\\\"ar", 3.7s 'ba\\'\\"\\'r';
|
||||
animation: 4s 'fo\\'o', 4.5s 'b\\"ar\\"', 4.7s "baz\\'";
|
||||
}
|
||||
|
||||
@keyframes 'f"oo' {}
|
||||
@keyframes 'fo""o' {}
|
||||
@keyframes 'foo\\\\' {}
|
||||
@keyframes 'fo\\'o' {}
|
||||
@keyframes 'ba\\'r' {}
|
||||
@keyframes 'b\\\\"ar' {}
|
||||
@keyframes 'b\\\\\\"ar' {}
|
||||
@keyframes 'b"ar"' {}
|
||||
@keyframes 'ba\\'\\"\\'r' {}
|
||||
`;
|
||||
const result = s(css, 'host-a');
|
||||
expect(result).toContain(`@keyframes 'host-a_f"oo' {}`);
|
||||
expect(result).toContain(`@keyframes 'host-a_fo""o' {}`);
|
||||
expect(result).toContain('@keyframes \'host-a_foo\\\\\' {}');
|
||||
expect(result).toContain('@keyframes \'host-a_fo\\\'o\' {}');
|
||||
expect(result).toContain('@keyframes \'host-a_ba\\\'r\' {}');
|
||||
expect(result).toContain(`@keyframes 'host-a_b\\\\"ar' {}`);
|
||||
expect(result).toContain(`@keyframes 'host-a_b\\\\\\"ar' {}`);
|
||||
expect(result).toContain(`@keyframes 'host-a_b"ar"' {}`);
|
||||
expect(result).toContain(`@keyframes 'host-a_ba\\'\\"\\'r' {}`);
|
||||
expect(result).toContain(`animation: 1s 'host-a_f\\"oo', 1.5s "host-a_ba\\'r";`);
|
||||
expect(result).toContain(`animation: 2s "host-a_fo\\"\\"o", 2.5s 'host-a_b\\\\"ar';`);
|
||||
expect(result).toContain(
|
||||
`animation: 3s 'host-a_foo\\\\', 3.5s "host-a_b\\\\\\"ar", 3.7s 'host-a_ba\\'\\"\\'r';`);
|
||||
expect(result).toContain(
|
||||
`animation: 4s 'host-a_fo\\'o', 4.5s 'host-a_b\\"ar\\"', 4.7s "baz\\'";`);
|
||||
});
|
||||
|
||||
it('should handle the usage of commas inside quotes', () => {
|
||||
const css = `
|
||||
div {
|
||||
animation: 3s 'bar,, baz';
|
||||
}
|
||||
|
||||
p {
|
||||
animation-name: "bar,, baz", foo,'ease, linear , inherit', bar;
|
||||
}
|
||||
|
||||
@keyframes 'foo' {}
|
||||
@keyframes 'bar,, baz' {}
|
||||
@keyframes 'ease, linear , inherit' {}
|
||||
`;
|
||||
const result = s(css, 'host-a');
|
||||
expect(result).toContain('@keyframes \'host-a_bar,, baz\' {}');
|
||||
expect(result).toContain('animation: 3s \'host-a_bar,, baz\';');
|
||||
expect(result).toContain(
|
||||
`animation-name: "host-a_bar,, baz", host-a_foo,'host-a_ease, linear , inherit', bar;`);
|
||||
});
|
||||
|
||||
it('should not ignore animation keywords when they are inside quotes', () => {
|
||||
const css = `
|
||||
div {
|
||||
animation: 3s 'unset';
|
||||
}
|
||||
|
||||
button {
|
||||
animation: 5s "forwards" 1s forwards;
|
||||
}
|
||||
|
||||
@keyframes unset {}
|
||||
@keyframes forwards {}
|
||||
`;
|
||||
const result = s(css, 'host-a');
|
||||
expect(result).toContain('@keyframes host-a_unset {}');
|
||||
expect(result).toContain('@keyframes host-a_forwards {}');
|
||||
expect(result).toContain('animation: 3s \'host-a_unset\';');
|
||||
expect(result).toContain('animation: 5s "host-a_forwards" 1s forwards;');
|
||||
});
|
||||
|
||||
it('should handle css functions correctly', () => {
|
||||
const css = `
|
||||
div {
|
||||
animation: foo 0.5s alternate infinite cubic-bezier(.17, .67, .83, .67);
|
||||
}
|
||||
|
||||
button {
|
||||
animation: calc(2s / 2) calc;
|
||||
}
|
||||
|
||||
@keyframes foo {}
|
||||
@keyframes cubic-bezier {}
|
||||
@keyframes calc {}
|
||||
`;
|
||||
const result = s(css, 'host-a');
|
||||
expect(result).toContain('@keyframes host-a_cubic-bezier {}');
|
||||
expect(result).toContain('@keyframes host-a_calc {}');
|
||||
expect(result).toContain(
|
||||
'animation: host-a_foo 0.5s alternate infinite cubic-bezier(.17, .67, .83, .67);');
|
||||
expect(result).toContain('animation: calc(2s / 2) host-a_calc;');
|
||||
});
|
||||
});
|
||||
|
|
@ -126,17 +126,6 @@ import {normalizeCSS} from '@angular/platform-browser/testing/src/browser_util';
|
|||
expect(s(css, 'contenta')).toEqual(expected);
|
||||
});
|
||||
|
||||
// Check that the browser supports unprefixed CSS animation
|
||||
it('should handle keyframes rules', () => {
|
||||
const css = '@keyframes foo {0% {transform:translate(-50%) scaleX(0);}}';
|
||||
expect(s(css, 'contenta')).toEqual(css);
|
||||
});
|
||||
|
||||
it('should handle -webkit-keyframes rules', () => {
|
||||
const css = '@-webkit-keyframes foo {0% {-webkit-transform:translate(-50%) scaleX(0);}}';
|
||||
expect(s(css, 'contenta')).toEqual(css);
|
||||
});
|
||||
|
||||
it('should handle complicated selectors', () => {
|
||||
expect(s('one::before {}', 'contenta')).toEqual('one[contenta]::before {}');
|
||||
expect(s('one two {}', 'contenta')).toEqual('one[contenta] two[contenta] {}');
|
||||
Loading…
Reference in a new issue