diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a2aecbc2..b97a201df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Discard invalid `variants` and `utilities` with modifiers ([#13977](https://github.com/tailwindlabs/tailwindcss/pull/13977)) - Add missing utilities that exist in v3, such as `resize`, `fill-none`, `accent-none`, `drop-shadow-none`, and negative `hue-rotate` and `backdrop-hue-rotate` utilities ([#13971](https://github.com/tailwindlabs/tailwindcss/pull/13971)) - Don’t allow at-rule-only variants to be compounded ([#14015](https://github.com/tailwindlabs/tailwindcss/pull/14015)) +- Ensure compound variants work with variants with multiple selectors ([#14016](https://github.com/tailwindlabs/tailwindcss/pull/14016)) ### Added diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index e22b0a953..2955e415c 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -738,12 +738,25 @@ test('group-[...]', () => { test('group-*', () => { expect( - run([ - 'group-hover:flex', - 'group-focus:flex', - 'group-hover:group-focus:flex', - 'group-focus:group-hover:flex', - ]), + compileCss( + css` + @variant hocus { + &:hover, + &:focus { + @slot; + } + } + @tailwind utilities; + `, + [ + 'group-hover:flex', + 'group-focus:flex', + 'group-hocus:flex', + + 'group-hover:group-focus:flex', + 'group-focus:group-hover:flex', + ], + ), ).toMatchInlineSnapshot(` ".group-hover\\:flex:is(:where(.group):hover *) { display: flex; @@ -759,6 +772,10 @@ test('group-*', () => { .group-hover\\:group-focus\\:flex:is(:where(.group):hover *):is(:where(.group):focus *) { display: flex; + } + + .group-hocus\\:flex:is(:is(:where(.group):hover, :where(.group):focus) *) { + display: flex; }" `) @@ -816,12 +833,24 @@ test('peer-[...]', () => { test('peer-*', () => { expect( - run([ - 'peer-hover:flex', - 'peer-focus:flex', - 'peer-hover:peer-focus:flex', - 'peer-focus:peer-hover:flex', - ]), + compileCss( + css` + @variant hocus { + &:hover, + &:focus { + @slot; + } + } + @tailwind utilities; + `, + [ + 'peer-hover:flex', + 'peer-focus:flex', + 'peer-hocus:flex', + 'peer-hover:peer-focus:flex', + 'peer-focus:peer-hover:flex', + ], + ), ).toMatchInlineSnapshot(` ".peer-hover\\:flex:is(:where(.peer):hover ~ *) { display: flex; @@ -837,6 +866,10 @@ test('peer-*', () => { .peer-hover\\:peer-focus\\:flex:is(:where(.peer):hover ~ *):is(:where(.peer):focus ~ *) { display: flex; + } + + .peer-hocus\\:flex:is(:is(:where(.peer):hover, :where(.peer):focus) ~ *) { + display: flex; }" `) @@ -1502,19 +1535,39 @@ test('supports', () => { test('not', () => { expect( - run([ - 'not-[:checked]:flex', + compileCss( + css` + @variant hocus { + &:hover, + &:focus { + @slot; + } + } + @tailwind utilities; + `, + [ + 'not-[:checked]:flex', + 'not-hocus:flex', - 'group-not-[:checked]:flex', - 'group-not-[:checked]/parent-name:flex', - 'group-not-checked:flex', + 'group-not-[:checked]:flex', + 'group-not-[:checked]/parent-name:flex', + 'group-not-checked:flex', + 'group-not-hocus:flex', + 'group-not-hocus/parent-name:flex', - 'peer-not-[:checked]:flex', - 'peer-not-[:checked]/parent-name:flex', - 'peer-not-checked:flex', - ]), + 'peer-not-[:checked]:flex', + 'peer-not-[:checked]/sibling-name:flex', + 'peer-not-checked:flex', + 'peer-not-hocus:flex', + 'peer-not-hocus/sibling-name:flex', + ], + ), ).toMatchInlineSnapshot(` - ".not-\\[\\:checked\\]\\:flex:not(:checked) { + ".not-hocus\\:flex:not(:hover, :focus) { + display: flex; + } + + .not-\\[\\:checked\\]\\:flex:not(:checked) { display: flex; } @@ -1522,6 +1575,14 @@ test('not', () => { display: flex; } + .group-not-hocus\\:flex:is(:where(.group):not(:hover, :focus) *) { + display: flex; + } + + .group-not-hocus\\/parent-name\\:flex:is(:where(.group\\/parent-name):not(:hover, :focus) *) { + display: flex; + } + .group-not-\\[\\:checked\\]\\:flex:is(:where(.group):not(:checked) *) { display: flex; } @@ -1534,11 +1595,19 @@ test('not', () => { display: flex; } + .peer-not-hocus\\:flex:is(:where(.peer):not(:hover, :focus) ~ *) { + display: flex; + } + + .peer-not-hocus\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name):not(:hover, :focus) ~ *) { + display: flex; + } + .peer-not-\\[\\:checked\\]\\:flex:is(:where(.peer):not(:checked) ~ *) { display: flex; } - .peer-not-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name):not(:checked) ~ *) { + .peer-not-\\[\\:checked\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name):not(:checked) ~ *) { display: flex; }" `) @@ -1556,22 +1625,46 @@ test('not', () => { test('has', () => { expect( - run([ - 'has-[:checked]:flex', + compileCss( + css` + @variant hocus { + &:hover, + &:focus { + @slot; + } + } + @tailwind utilities; + `, + [ + 'has-[:checked]:flex', + 'has-hocus:flex', - 'group-has-[:checked]:flex', - 'group-has-[:checked]/parent-name:flex', - 'group-has-checked:flex', + 'group-has-[:checked]:flex', + 'group-has-[:checked]/parent-name:flex', + 'group-has-checked:flex', + 'group-has-hocus:flex', + 'group-has-hocus/parent-name:flex', - 'peer-has-[:checked]:flex', - 'peer-has-[:checked]/sibling-name:flex', - 'peer-has-checked:flex', - ]), + 'peer-has-[:checked]:flex', + 'peer-has-[:checked]/sibling-name:flex', + 'peer-has-checked:flex', + 'peer-has-hocus:flex', + 'peer-has-hocus/sibling-name:flex', + ], + ), ).toMatchInlineSnapshot(` ".group-has-checked\\:flex:is(:where(.group):has(:checked) *) { display: flex; } + .group-has-hocus\\:flex:is(:where(.group):has(:hover, :focus) *) { + display: flex; + } + + .group-has-hocus\\/parent-name\\:flex:is(:where(.group\\/parent-name):has(:hover, :focus) *) { + display: flex; + } + .group-has-\\[\\:checked\\]\\:flex:is(:where(.group):has(:checked) *) { display: flex; } @@ -1584,6 +1677,14 @@ test('has', () => { display: flex; } + .peer-has-hocus\\:flex:is(:where(.peer):has(:hover, :focus) ~ *) { + display: flex; + } + + .peer-has-hocus\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name):has(:hover, :focus) ~ *) { + display: flex; + } + .peer-has-\\[\\:checked\\]\\:flex:is(:where(.peer):has(:checked) ~ *) { display: flex; } @@ -1592,6 +1693,10 @@ test('has', () => { display: flex; } + .has-hocus\\:flex:has(:hover, :focus) { + display: flex; + } + .has-\\[\\:checked\\]\\:flex:has(:checked) { display: flex; }" diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index c3c50bc63..3611dd0b3 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -216,7 +216,7 @@ export function createVariants(theme: Theme): Variants { // Replace `&` in target variant with `*`, so variants like `&:hover` // become `&:not(*:hover)`. The `*` will often be optimized away. - node.selector = `&:not(${node.selector.replace('&', '*')})` + node.selector = `&:not(${node.selector.replaceAll('&', '*')})` // Track that the variant was actually applied didApply = true @@ -442,7 +442,7 @@ export function createVariants(theme: Theme): Variants { // Replace `&` in target variant with `*`, so variants like `&:hover` // become `&:has(*:hover)`. The `*` will often be optimized away. - node.selector = `&:has(${node.selector.replace('&', '*')})` + node.selector = `&:has(${node.selector.replaceAll('&', '*')})` // Track that the variant was actually applied didApply = true