Repository: tailwindlabs/tailwindcss (ts)

Issue:
group-*:animate-* generates invalid keyframe definition
### What version of Tailwind CSS are you using?

v2.2.0

### What build tool (or framework if it abstracts the build tool) are you using?

postcss@7.0.36, webpack@5.39.1

### What version of Node.js are you using?

v15.10.0

### What browser are you using?

Chrome

### What operating system are you using?

macOS

### Reproduction repository

https://play.tailwindcss.com/DrsR2rdrZO

### Describe your issue

Adding an animation that should only be applied to group children (e.g. group-focus group-hover) generates invalid keyframes (offsets are missing):
```css
@keyframes bounce {

	, {
		-webkit-transform: translateY(-25%);
		        transform: translateY(-25%);
		-webkit-animation-timing-function: cubic-bezier(0.8,0,1,1);
		        animation-timing-function: cubic-bezier(0.8,0,1,1);
	}

	 {
		-webkit-transform: none;
		        transform: none;
		-webkit-animation-timing-function: cubic-bezier(0,0,0.2,1);
		        animation-timing-function: cubic-bezier(0,0,0.2,1);
	}
}
.group:focus .group-focus\:animate-bounce {
	-webkit-animation: bounce 1s infinite;
	        animation: bounce 1s infinite;
}
```

expected (taken from non-group animate-* classes):
```css
@keyframes bounce {

	0%, 100% {
		-webkit-transform: translateY(-25%);
		        transform: translateY(-25%);
		-webkit-animation-timing-function: cubic-bezier(0.8,0,1,1);
		        animation-timing-function: cubic-bezier(0.8,0,1,1);
	}

	50% {
		-webkit-transform: none;
		        transform: none;
		-webkit-animation-timing-function: cubic-bezier(0,0,0.2,1);
		        animation-timing-function: cubic-bezier(0,0,0.2,1);
	}
}
.animate-bounce {
	-webkit-animation: bounce 1s infinite;
	        animation: bounce 1s infinite;
}
```

Pull request:
Fix malformed keyframes when using class variants
Fixes #4705

This PR makes it so that `transformAllSelectors` skips keyframe selectors. This fixes malformed keyframe definitions.

Patch:
diff --git a/src/plugins/animation.js b/src/plugins/animation.js
index 40cc1016..e028fb1d 100644
--- a/src/plugins/animation.js
+++ b/src/plugins/animation.js
@@ -11,7 +11,6 @@ export default function () {
             {
               [`@keyframes ${prefixName(key)}`]: value,
             },
-            { respectVariants: false },
           ],
         ]
       })
diff --git a/src/util/isKeyframeRule.js b/src/util/isKeyframeRule.js
new file mode 100644
index 00000000..a745e979
--- /dev/null
+++ b/src/util/isKeyframeRule.js
@@ -0,0 +1,3 @@
+export default function isKeyframeRule(rule) {
+  return rule.parent && rule.parent.type === 'atrule' && /keyframes$/.test(rule.parent.name)
+}
diff --git a/src/util/pluginUtils.js b/src/util/pluginUtils.js
index 8d67f35e..5babd0c3 100644
--- a/src/util/pluginUtils.js
+++ b/src/util/pluginUtils.js
@@ -3,6 +3,7 @@ import postcss from 'postcss'
 import createColor from 'color'
 import escapeCommas from './escapeCommas'
 import { withAlphaValue } from './withAlphaVariable'
+import isKeyframeRule from './isKeyframeRule'
 
 export function applyPseudoToMarker(selector, marker, state, join) {
   let states = [state]
@@ -72,6 +73,9 @@ export function updateLastClasses(selectors, updateClass) {
 export function transformAllSelectors(transformSelector, { wrap, withRule } = {}) {
   return ({ container }) => {
     container.walkRules((rule) => {
+      if (isKeyframeRule(rule)) {
+        return rule
+      }
       let transformed = rule.selector.split(',').map(transformSelector).join(',')
       rule.selector = transformed
       if (withRule) {
diff --git a/src/util/processPlugins.js b/src/util/processPlugins.js
index b11613c6..afd5d51c 100644
--- a/src/util/processPlugins.js
+++ b/src/util/processPlugins.js
@@ -10,6 +10,7 @@ import wrapWithVariants from '../util/wrapWithVariants'
 import cloneNodes from '../util/cloneNodes'
 import transformThemeValue from './transformThemeValue'
 import nameClass from '../util/nameClass'
+import isKeyframeRule from '../util/isKeyframeRule'
 
 function parseStyles(styles) {
   if (!Array.isArray(styles)) {
@@ -28,10 +29,6 @@ function wrapWithLayer(rules, layer) {
     .append(cloneNodes(Array.isArray(rules) ? rules : [rules]))
 }
 
-function isKeyframeRule(rule) {
-  return rule.parent && rule.parent.type === 'atrule' && /keyframes$/.test(rule.parent.name)
-}
-
 export default function (plugins, config) {
   const pluginBaseStyles = []
   const pluginComponents = []


Test patch:
diff --git a/tests/jit/animations.test.css b/tests/jit/animations.test.css
new file mode 100644
index 00000000..78244c7d
--- /dev/null
+++ b/tests/jit/animations.test.css
@@ -0,0 +1,32 @@
+@keyframes spin {
+  to {
+    transform: rotate(360deg);
+  }
+}
+.animate-spin {
+  animation: spin 1s linear infinite;
+}
+@keyframes ping {
+  75%,
+  100% {
+    transform: scale(2);
+    opacity: 0;
+  }
+}
+.hover\:animate-ping:hover {
+  animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
+}
+@keyframes bounce {
+  0%,
+  100% {
+    transform: translateY(-25%);
+    animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
+  }
+  50% {
+    transform: none;
+    animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
+  }
+}
+.group:hover .group-hover\:animate-bounce {
+  animation: bounce 1s infinite;
+}
diff --git a/tests/jit/animations.test.html b/tests/jit/animations.test.html
new file mode 100644
index 00000000..3d5a919e
--- /dev/null
+++ b/tests/jit/animations.test.html
@@ -0,0 +1,3 @@
+<div class="animate-spin"></div>
+<div class="hover:animate-ping"></div>
+<div class="group-hover:animate-bounce"></div>
diff --git a/tests/jit/animations.test.js b/tests/jit/animations.test.js
new file mode 100644
index 00000000..7ddedcf4
--- /dev/null
+++ b/tests/jit/animations.test.js
@@ -0,0 +1,30 @@
+import postcss from 'postcss'
+import fs from 'fs'
+import path from 'path'
+import tailwind from '../../src/jit/index.js'
+
+function run(input, config = {}) {
+  return postcss(tailwind(config)).process(input, {
+    from: path.resolve(__filename),
+  })
+}
+
+test('animations', () => {
+  let config = {
+    darkMode: 'class',
+    mode: 'jit',
+    purge: [path.resolve(__dirname, './animations.test.html')],
+    corePlugins: {},
+    theme: {},
+    plugins: [],
+  }
+
+  let css = `@tailwind utilities`
+
+  return run(css, config).then((result) => {
+    let expectedPath = path.resolve(__dirname, './animations.test.css')
+    let expected = fs.readFileSync(expectedPath, 'utf8')
+
+    expect(result.css).toMatchFormattedCss(expected)
+  })
+})