Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scoping CSS keyframes in emulated view encapsulation #42608

Closed

Conversation

dario-piotrowicz
Copy link
Contributor

Keyframes are not scoped for Emulated view encapsulated component, practically making them global and causing css leakage

note: this does not happen with the shadowDom view encapsulation, there there is no leakage

Since keyframes are not hierarchical I don't believe this issue can be solved without modifying the keyframes names, so the only thing possible I believe is to actually rename the keyframes using the component's scope selector,

The way I did it is by scanning the css twice, in the first scan I modified/scoped the keyframes names in the way mentioned above and in the second I updated animation rules using such keyframes

Note: animations using keyframes not defined in the file are not modified, still allowing global keyframes to be defined

I understand that this is changing the actual names of the keyframes so it may not be what we actually want here, but I figured that opening a PR and starting a discussion would not hurt

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • angular.io application / infrastructure changes
  • Other... Please describe:

What is the current behavior?

Issue Number: #33885

Currently keyframes are not scoped so, if I create two different keyframe animations in two different components but with the same name, those will clash and only one of them will be applied to both components

You can see it in this example I created: demo code

What happens is that I have two different components each with a keyframes called box-animation which colors the component's background, as you can see the application does not behave as expected:
Screenshot at 2021-06-20 16-58-37

What is the new behavior?

Keyframes are scoped so that they include the component's scope selector like so:
Screenshot at 2021-06-20 16-55-49

and this allows the keyframes to remain isolated:
Screenshot at 2021-06-20 16-59-02

Does this PR introduce a breaking change?

  • Yes
  • No
    (at least I don't think so)

Other information

As I mentioned this will cause all the keyframes names in components with the Emulated view encapsulation to be modified, this is definitely not too nice/elegant but I think it's the only way to scope the keyframes and I believe that it is still better to leave them global as they are now (as that produces unwanted css leakage + it is inconsistent with what happens with ShadowDom)

@pullapprove pullapprove bot requested a review from alxhub June 20, 2021 16:16
@google-cla google-cla bot added the cla: yes label Jun 20, 2021
@dario-piotrowicz dario-piotrowicz marked this pull request as draft June 20, 2021 16:24
@ngbot ngbot bot added this to the Backlog milestone Jun 21, 2021
@dario-piotrowicz dario-piotrowicz force-pushed the keyframes-leak branch 3 times, most recently from 051d2ec to 988116d Compare June 22, 2021 22:20
@dario-piotrowicz dario-piotrowicz marked this pull request as ready for review June 22, 2021 22:22
Copy link
Member

@petebacondarwin petebacondarwin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this idea!

In general I think it would provide a positive benefit to the emulated encapsulation mode.

Because of the change to the keyframes name, I think we would have to land this in a major release, since these names could be referenced in JS code - for example in an animation event handler.

We can wait for this timeframe, IMO, since the workaround is quite straightforward - even if identifying the problem may not be!

packages/compiler/src/shadow_css.ts Show resolved Hide resolved
packages/compiler/src/shadow_css.ts Outdated Show resolved Hide resolved
packages/compiler/src/shadow_css.ts Outdated Show resolved Hide resolved
@petebacondarwin petebacondarwin added action: cleanup The PR is in need of cleanup, either due to needing a rebase or in response to comments from reviews action: review The PR is still awaiting reviews from at least one requested reviewer breaking changes target: major This PR is targeted for the next major release labels Jun 23, 2021
@petebacondarwin
Copy link
Member

For other reviewers, I checked through the Angular animations code, which also makes use of @keyframes but this does not seem to be affected by this change, since it always generates its own keyframe names, and does not reference ones in the component definition.

@dario-piotrowicz
Copy link
Contributor Author

@petebacondarwin all done as you suggested, thanks a lot 😃

The only remaining thing are the quoted keyframes,I am waiting to hear from you on those 🙂

packages/compiler/src/shadow_css.ts Outdated Show resolved Hide resolved
packages/compiler/src/shadow_css.ts Outdated Show resolved Hide resolved
@dario-piotrowicz dario-piotrowicz force-pushed the keyframes-leak branch 2 times, most recently from e54c6a4 to a99a2e0 Compare June 24, 2021 18:30
@dario-piotrowicz
Copy link
Contributor Author

Ok, last time around this was soooo close. We had to roll it back because of a single backslid target. We could try again with a fresh TGP for 15?

Thanks @dylhunn !!! 😍

Please let me know if I can help in any way 😃

@jessicajaniuk
Copy link
Contributor

Hey @dario-piotrowicz. We did find a bug here. In the case where the css is all minified into a single string, the regex does not work properly and results in bugs. Here is a test you can use to see the failure:


  it('should scope animations using local keyframes identifiers', () => {
    const css = `:host{display:block}component-a{margin-bottom:10px}.class-b{display:-webkit-box;display:-webkit-flex;display:-moz-box;display:-ms-flexbox;display:flex;gap:8px;width:100%}key-comp-a{-webkit-box-flex:1;-webkit-flex-grow:1;-moz-box-flex:1;-ms-flex-positive:1;flex-grow:1}key-comp-b{-webkit-animation:slide-left .3s .05s forwards;animation:slide-left .3s .05s forwards;-webkit-animation-timing-function:cubic-bezier(.785,.135,.15,.86);animation-timing-function:cubic-bezier(.785,.135,.15,.86);-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;opacity:0}@-webkit-keyframes slide-left{0%{opacity:0;-webkit-transform:translateX(10px);transform:translateX(10px)}to{opacity:1;-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes slide-left{0%{opacity:0;-webkit-transform:translateX(10px);transform:translateX(10px)}to{opacity:1;-webkit-transform:translateX(0);transform:translateX(0)}}`;
    const result = s(css, 'host-a');
    expect(result).toContain('-webkit-animation: host-a_slide-left .3s .05s forwards;');
    expect(result).toContain('animation: host-a_slide-left .3s .05s forwards;');
  });

@dario-piotrowicz
Copy link
Contributor Author

Hi @jessicajaniuk thanks for the test 🙂

And thank you so very much for looking into this ❤️

but, are you completely sure that this test highlights the issue?

I've started looking into it and it seems like the test can be easily fixed by removing an extra space in the checks:

-    expect(result).toContain('-webkit-animation: host-a_slide-left .3s .05s forwards;');
-    expect(result).toContain('animation: host-a_slide-left .3s .05s forwards;');
+    expect(result).toContain('-webkit-animation:host-a_slide-left .3s .05s forwards;');
+    expect(result).toContain('animation:host-a_slide-left .3s .05s forwards;');

(which to me seems correct as you can see that this is the expected behaviour in the should maintain the spacing when handling (scoping or not) keyframes and animations test)

Also by running the thing locally the keyframe animation seems to work (and be scoped) as expected:
Screenshot at 2022-09-24 13-41-34

@AndrewKushnir
Copy link
Contributor

AndrewKushnir commented Sep 24, 2022

@dario-piotrowicz quick question: did you run the test as is or you've formatted the input string? We've noticed that the issue is reproducible with the styles written in 1 line (as we have in a test), but things seem to be working correctly when the input is formatted (which potentially hints at the fact that the RegExp added in this PR might match different things in those 2 cases).

@dario-piotrowicz
Copy link
Contributor Author

@AndrewKushnir I literally just copy-pasted the exact text Jessica posted in her comment, the the only difference being the removal of the spaces in the toContain calls:
Screenshot at 2022-09-24 22-38-27

I've also rebased to the latest main to make sure that there can't be anything else different somewhere that could produce a different output

😟

@jessicajaniuk
Copy link
Contributor

jessicajaniuk commented Sep 24, 2022 via email

@dario-piotrowicz
Copy link
Contributor Author

@AndrewKushnir @jessicajaniuk sorry I actually found the issue there 😅

I've compared the result before and after:
Screenshot at 2022-09-25 21-56-13

the second animation doesn't seem to be scoped! 😓

and the tests pass because: animation:host-a_slide-left .3s .05s forwards;') is actually contained in the result (but with the webkit prefix)

sorry for the confusion 🙇 😢

I now know what to look at, thanks a lot! 🙂

@dario-piotrowicz
Copy link
Contributor Author

I've just rebased to the latest main and squashed all the fixup commits, I hope you don't mind 🙂

@dario-piotrowicz
Copy link
Contributor Author

@jessicajaniuk @AndrewKushnir it was a very very small issue 😅 , the regex accepted the animation to only be the first property defined or have spaces prefixing it, I've added that ; is also acceptable, so now it should be all good 🙂

(as you can see in my latest fixup commit)

@dario-piotrowicz dario-piotrowicz force-pushed the keyframes-leak branch 2 times, most recently from 745e92a to 11dc22a Compare September 26, 2022 19:15
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 angular#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.
Copy link
Member

@JoostK JoostK left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing work, as always! 👏

@dario-piotrowicz
Copy link
Contributor Author

Amazing work, as always! clap

Thanks so much!!! 😊

@jessicajaniuk
Copy link
Contributor

TGP is green

@jessicajaniuk jessicajaniuk added action: merge The PR is ready for merge by the caretaker and removed action: global presubmit The PR is in need of a google3 global presubmit labels Sep 27, 2022
@AndrewKushnir
Copy link
Contributor

This PR was merged into the repository by commit 051f756.

pterratpro pushed a commit to pterratpro/angular-fr that referenced this pull request Oct 4, 2022
…gular#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 angular#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 angular#42608
pterratpro pushed a commit to pterratpro/angular-fr that referenced this pull request Oct 4, 2022
…gular#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 angular#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 angular#42608
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Oct 30, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker area: core Issues related to the framework runtime breaking changes cla: yes core: CSS encapsulation target: major This PR is targeted for the next major release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

css @keyframes leak between components
9 participants