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

Animation keyframes are not localized, breaking other styled components expected behavior #2871

Closed
n8sabes opened this issue Aug 30, 2022 · 10 comments

Comments

@n8sabes
Copy link

n8sabes commented Aug 30, 2022

Animation @keyframes in v11.10.0 are not localized, breaking other styled component's expected behavior.

The expected behavior is for the style to use the localized keyframes name: animation-m9qj3z, but instead the keyframes are written to the global scope and used across all components that might use the same name -- in this case spin.

Bug Sandbox Example

  1. Is this a bug or expected behavior?
  2. I found another issue that seems to suggest localizing is critical Localized jsx namespace #1941
  3. Is there an easy fix, or a temporary workaround such as via editing compile output of stylis?

My implementation dynamically loads components with their respective styles at runtime. Therefore, I cannot ensure all 3rd party libraries won't reuse common animation keyframe names such as spin, fade, etc.

Example code:

import styled from "@emotion/styled";
import { css, keyframes } from "@emotion/css";

const PageContainer = styled.div`
  display: flex;
  flex-direction: column;
  flex-wrap: nowrap;
  justify-content: center;
  align-items: center;
  align-content: center;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  overflow: hidden;
  overflow: visible;
  user-select: auto;
`;

const box1Css = `
  @keyframes spin {
    from { transform: rotate(0); }
    to { transform: rotate(10deg) scale(0.25); background: black; color: white; }
  }
  @media (min-width: 500px) { font-size: 0.25em; }
  animation: spin 4s forwards;
  border: 4px dashed deeppink;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 50%;
  width: 100%;
`;

const box2Css = `
  @keyframes spin {
    from { transform: rotate(0); }
    to { transform: rotate(-10deg) scale(0.75); background: orange; color: blue; }
  }
  @media (min-width: 500px) { font-size: 2.5em; }
  animation: spin 4s forwards;
  border: 4px dashed deeppink;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 50%;
  width: 100%;
`;

const Box1 = styled.div(box1Css);
const Box2 = styled.div(box2Css);
const box1CssResult = css(box1Css);
const box2CssResult = css(box2Css);
const box1KeyframesResult = keyframes(box1Css);
const box2KeyframesResult = keyframes(box2Css);

export default function App() {
  return (
    <PageContainer>
      <div>
        <b>@keyframes names in @emotion v11.10.0</b> are not localized, breaking other styled
        components expected behavior.
      </div>
      <ul>
        <li>box1CssResult = {box1CssResult}</li>
        <li>box2CssResult = {box2CssResult}</li>
        <li>box1KeyframesResult = {box1KeyframesResult}</li>
        <li>box2KeyframesResult = {box2KeyframesResult}</li>
      </ul>
      <Box1>
        styled Box 1
        <br />
        should rotate clockwise to black/white
      </Box1>
      <div className={box1CssResult}>
        css Box 1
        <br />
        should rotate clockwise to black/white
      </div>
      <Box2>
        styled Box 2
        <br />
        should rotate counter-clockwise to orange/blue
      </Box2>
      <div className={box2CssResult}>
        css Box 2
        <br />
        should rotate counter-clockwise to orange/blue
      </div>
    </PageContainer>
  );
}
@Andarist
Copy link
Member

That's because you didn't use our keyframes export that generates a unique name for a keyframe:
https://codesandbox.io/s/emotion-keyframes-not-localized-forked-9qguy6?file=/src/App.tsx

@n8sabes
Copy link
Author

n8sabes commented Aug 30, 2022

@Andarist, thanks for the quick response.

Indeed, I have used keyframes in the past and injected the localized names into subsequent style strings, as you have done. In most situations, this works well, when it's within the primary application code.

However, the end-game here is to enable downstream "style artisans" (pseudo developers) to write standard css text blocks, inclusive of keyframes using the exact same syntax as a .css file These localized css blocks are dynamically imported by Typescript components at runtime which is why ${localizedName} is not easily doable. Thus, my desire to localize everything within a widget.css text block/file.

From trying different things with @emotion and stylis, it seems very closely attainable.

If I cannot figure out a way to adhere to css file-like syntax and localized it entirely, it becomes much more complex to educate style artisans in how to break up different pieces of their css into JSON key/values, as well as much more complicated on the back-end. I do not mind doing necessary work on the back-end once I find a way to achieve this.

As an alternative, I have tried compile with stylis and then localize names manually, but have not attained a working result so far. I have an inquiry on the stylis project that you may have seen, attempting it from that angle.

Is there any way to use @emotion to localize an entire css text block, inclusive of the keyframes and classes, and then get a map from the original to localized names?

Here is a css block example I am hoping to fully localize, and then map names in/out:

const cssString = `
  @keyframes spin {
    from { transform: rotate(0); }
    to { transform: rotate(180deg) }
  }

  .foo {
    @media (min-width: 500px) { font-size: 2.5em; }
    animation: spin 4s forwards;
    border: 4px dashed deeppink;
    display: flex;
    align-items: center;
    justify-content: center;
    height: 50%;
    width: 100%;
  }

  .bar {
    @media (min-width: 500px) { font-size: 0.5em; }
    animation: spin 4s forwards;
    border: 4px dashed limegreen;
    display: flex;
    align-items: center;
    justify-content: center;
    height: 50%;
    width: 100%;
  }
`;

const compiledResult = compile(cssString);

Your thoughts and help with this is sincerely appreciated.

@Andarist
Copy link
Member

Is there any way to use @emotion to localize an entire css text block, inclusive of the keyframes and classes, and then get a map from the original to localized names?

It's not possible at the moment - it has been designed to use this separate keyframes function for this purpose.

I think though that what you want is very much doable with Emotion and Stylis. It will come at some extra parsing cost, but I don't think that this is a problem in this case - and that extra cost could probably be even avoided if you would mimick our serialized styles object.

The idea is to do this:

  1. compile your cssString with stylis.compile
  2. partition the output into two groups: one that would contain the generated @keyframes and one that would contain the rest
  3. grab the content of your keyframes, pass them through our keyframes function and create a map between defined names and the generated names
  4. now traverse all of the other rules and replace original names for the generated ones in the parsed declarations
  5. now you should be able to stringify your rules (using stylis.stringify and stuff) and pass the result to our css

@n8sabes
Copy link
Author

n8sabes commented Aug 30, 2022

That's pretty much the trajectory I am on now. I am recursively parsing through the compiled object:

  1. Pulling out keyframes
    1.1 Processing each keyframe with emotion.keyframes
    1.2 Pushing into a keyframesName : localizedName map
  2. Pulling out each style class
    2.1. Updating references to keyframes --> THIS iS A CHALLENGE!!
    2.2 Processing the isolated style with emotion.css
    2.3 Pushing it into a className : localizedName map
  3. Using targeted classes (and/or stringifying to inject as a style element SSR to fix runtime Cumulative Layout Shifts "in certain SSR edge-case situations")

While realizing I have a slightly deeper knowledge of @emotion and stylis than I thought, my current knowledge gap and concern is how to write keyframe renaming logic that is 100% effective with all the syntax flavors possible.

Likely a RegEx to be applied to both the animation shorthand, and animation-name attribute, INCLUDING multiple comma delimited animations. Some syntax examples that come to mind:

    animation: spin 4s forwards;
    animation: 4s forwards spin;
    animation-name: spin;
    animation-duration: 4s;
    animation-fill-mode: forwards;
    animation-name: spin, colorize;
    animation-duration: 4s;
    animation-fill-mode: forwards;

If you have any thoughts on how to match all animation references to the keyframes, I welcome your idea(s). If not, go ahead an close this issue as I think there is enough on the topic for the community to learn from.

A full css file (text block) localize function would be nice -- WISH LIST!!

Thank you @Andarist!

@thysultan
Copy link

@n8sabes From a strictly stylis perspective, consider the following input:

const AST = compile(`
a {
  b {
    @keyframes spin { from { color: red; } to { color: blue; } }
    animation: spin 4s forwards;
  }
}
`)

That would produces AST nodes for the the animation declaration, the keyframes block, the b selector and the a selector. Where by from either of them you can walk back up the tree to the root a selector, given this fact, you could always write a plugin that on only animation props and keyframe blocks, walks back up the tree to the root selector to us it as a unique id to add to both the keyframe names and animation names or use all the parent selector in the path to create a unique-path of all the selectors(going up) it takes to get to the nearest common block...

Such that the above would convert both references to spin to spin-b-a

Beyond that finding the animation name in the short hand syntax is ruling out valid options like the valid keywords(forwards, backwards, etc) that can appear, numbers etc, anything else being a valid animation name, previous version of stylis had this logic if you want to dig in, it was in the prefixer logic as it related to animation-name and keyframes.

@n8sabes
Copy link
Author

n8sabes commented Aug 30, 2022

Thank you @thysultan.

There are pros and cons to the example. It seems the example syntax would require the semi-technical-non-developer "style artisans" to be educated on special nesting/formatting syntax vs straight forward css file syntax. This pushes me toward the brute force localized naming approach, as much as I don't want to do this.

You have good points that I am going to explore further, agreed on reserved keyword mapping issues I had not yet considered, and will find time to look at some (legacy?) stylis code to see what I can learn.

Community Compliment -- I have truly enjoyed working with @emotion and stylis libraries, but have even more so appreciated the awesome people working on the projects.

@n8sabes
Copy link
Author

n8sabes commented Aug 30, 2022

@thysultan, can you tell me what version of stylis?

@n8sabes
Copy link
Author

n8sabes commented Aug 31, 2022

@Andarist, @thysultan, Et al.

What do you think of this to replace all possible usage syntaxes of animation name with the localized name when traversing the stylis compiled object fields or css text blocks?

RegEx Sandobx of the following regular expression

/\bspin\b/gm

Code Sandbox of the following code

export const localizeAnimationName = (cssString: string, name: string, localizedName: string): string => {
  const nameRegEx = new RegExp(`\\b${name}\\b`, "gm");
  return cssString.replaceAll(nameRegEx, localizedName);
};

The above regex + code selects the following usage variations, taking into consideration preamble and post amble characters.

spin
spin 4s forwards
animation: 4s forwards spin;
animation-name: spin;
animation-name:spin, colorize;
animation-name:colorize,spin;
animation: 
spin
 4s forwards;
animation-name:colorize,spin

** SHOULD NOT MATCH **
spinX
animation-name: Xspin , colorize;
animation-name: spinX, colorize;
animation-name: XspinX, colorize;
animation: 4s forwards SPIN;

@Andarist
Copy link
Member

The approach looks OK to me

@n8sabes
Copy link
Author

n8sabes commented Sep 12, 2022

@Andarist and @thysultan, I've written a couple helper functions that leverage both @emotion and stylis to fully localize a css block to a named context. It handles multiple classes, keyframes, etc. within the css block. If this functionality would be of value, I'd be happy to share it for review and potential inclusion as a helper utility function.

As always, thanks for the help.

Closing this issue.

@n8sabes n8sabes closed this as completed Sep 12, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants