diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 94b17714ed25..9c27ba81c4e8 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -5334,6 +5334,180 @@ describe('@variant', () => { `) }) + describe('comma-separated `@variant` rules', () => { + it('should be possible to use comma-separated `@variant` rules', async () => { + await expect( + compileCss( + css` + .btn { + background: black; + + @variant hover, focus { + background: red; + } + } + @tailwind utilities; + `, + [], + ), + ).resolves.toMatchInlineSnapshot(` + ".btn { + background: #000; + } + + @media (hover: hover) { + .btn:hover { + background: red; + } + } + + .btn:focus { + background: red; + }" + `) + }) + + it('should handle three or more variants', async () => { + await expect( + compileCss( + css` + .btn { + background: black; + + @variant hover, focus, active { + background: red; + } + } + @tailwind utilities; + `, + [], + ), + ).resolves.toMatchInlineSnapshot(` + ".btn { + background: #000; + } + + @media (hover: hover) { + .btn:hover { + background: red; + } + } + + .btn:focus, .btn:active { + background: red; + }" + `) + }) + + it('should handle whitespace variations (no space after comma)', async () => { + await expect( + compileCss( + css` + .btn { + background: black; + + @variant hover,focus { + background: red; + } + } + @tailwind utilities; + `, + [], + ), + ).resolves.toMatchInlineSnapshot(` + ".btn { + background: #000; + } + + @media (hover: hover) { + .btn:hover { + background: red; + } + } + + .btn:focus { + background: red; + }" + `) + }) + + it('should handle whitespace variations (space before and after comma)', async () => { + await expect( + compileCss( + css` + .btn { + background: black; + + @variant hover , focus { + background: red; + } + } + @tailwind utilities; + `, + [], + ), + ).resolves.toMatchInlineSnapshot(` + ".btn { + background: #000; + } + + @media (hover: hover) { + .btn:hover { + background: red; + } + } + + .btn:focus { + background: red; + }" + `) + }) + + it('should handle nested comma-separated variants', async () => { + await expect( + compileCss( + css` + .btn { + background: black; + + @variant hover, focus { + background: red; + + @variant active, disabled { + background: blue; + } + } + } + @tailwind utilities; + `, + [], + ), + ).resolves.toMatchInlineSnapshot(` + ".btn { + background: #000; + } + + @media (hover: hover) { + .btn:hover { + background: red; + } + + .btn:hover:active, .btn:hover:disabled { + background: #00f; + } + } + + .btn:focus { + background: red; + } + + .btn:focus:active, .btn:focus:disabled { + background: #00f; + }" + `) + }) + }) + it('should be possible to use `@variant` with a funky looking variants', async () => { await expect( compileCss( diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 82a2b8592cfe..884125a1b565 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -1212,24 +1212,28 @@ export function substituteAtVariant(ast: AstNode[], designSystem: DesignSystem): walk(ast, (variantNode) => { if (variantNode.kind !== 'at-rule' || variantNode.name !== '@variant') return - // Starting with the `&` rule node - let node = styleRule('&', variantNode.nodes) - - let variant = variantNode.params + let variants = segment(variantNode.params, ',').map((variant) => variant.trim()) + let nodes: AstNode[] = [] + for (let variant of variants) { + // Starting with the `&` rule node + let node = styleRule('&', variantNode.nodes.map(cloneAstNode)) + + let variantAst = designSystem.parseVariant(variant) + if (variantAst === null) { + throw new Error(`Cannot use \`@variant\` with unknown variant: ${variant}`) + } - let variantAst = designSystem.parseVariant(variant) - if (variantAst === null) { - throw new Error(`Cannot use \`@variant\` with unknown variant: ${variant}`) - } + let result = applyVariant(node, variantAst, designSystem.variants) + if (result === null) { + throw new Error(`Cannot use \`@variant\` with variant: ${variant}`) + } - let result = applyVariant(node, variantAst, designSystem.variants) - if (result === null) { - throw new Error(`Cannot use \`@variant\` with variant: ${variant}`) + nodes.push(node) } // Update the variant at-rule node, to be the `&` rule node features |= Features.Variants - return WalkAction.Replace(node) + return WalkAction.Replace(nodes) }) return features }