Move modifier of not-*, has-*, and in-* variant to sub variant (#19100)

This PR fixes an issue where a compound variant with a modifier such as
`not-group-hover/name:flex` would not generate anything because the
`/name` modifier belongs to the `not` variant, and not the compounded
`group-hover` variant.

This PR is a **workaround** (and definitely not perfect) by special
casing the `not`, `has`, and `in` variants such that their modifiers are
moved internally to the sub variant as-if the `/name` existed on
`group-hover`.

We don't do it for other compound variants such as `group` and `peer`
because then `group-peer-focus/name:underline` would result in a
breaking change:

```diff
- .group-peer-focus\\/name\\:flex:is(:where(.group\\/name):is(:where(.peer):focus ~ *) *)
+ .group-peer-focus\/name\:flex:is(:where(.group):is(:where(.peer\/name):focus ~ *) *) 
```

In case the diff is not clear, the name has moved:
<img width="1219" height="78" alt="image"
src="https://github.com/user-attachments/assets/dce7bc95-9d93-452d-a275-b3891a05a1a4"
/>


This is also a limited workaround, because if you need multiple
modifiers it won't work. I would've loved to special case this _inside_
the `not`, `has`, and `in` code that handles these variants, but we
handle the variants in a depth-first way, so by the time you are
handling the `not` variant, the sub variant was already handled...

In a perfect world, you can use something like `not-group/name-hover`
but then it becomes unambiguous because is `name` the name, is
`name-hover`?

## Test plan

Added a new test that wouldn't generate anything before this fix.

Fixes: #15772
This commit is contained in:
Robin Malfait 2025-10-09 19:30:01 +02:00 • committed by GitHub
parent 0c14df1a15
commit 3aadba7cc1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 32 additions and 1 deletions

View file

@ -14,8 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fix Safari devtools rendering issue due to `color-mix` fallback ([#19069](https://github.com/tailwindlabs/tailwindcss/pull/19069))
- Suppress Lightning CSS warnings about `:deep`, `:slotted` and `:global` ([#19094](https://github.com/tailwindlabs/tailwindcss/pull/19094))
- Suppress Lightning CSS warnings about `:deep`, `:slotted`, and `:global` ([#19094](https://github.com/tailwindlabs/tailwindcss/pull/19094))
- Fix resolving theme keys when starting with the name of another theme key in JS configs and plugins ([#19097](https://github.com/tailwindlabs/tailwindcss/pull/19097))
- Allow named groups in combination with `not-*`, `has-*`, and `in-*` ([#19100](https://github.com/tailwindlabs/tailwindcss/pull/19100))
## [4.1.14] - 2025-10-01

View file

@ -2102,6 +2102,11 @@ const variants = [
// Handle special `@` variants. These shouldn't be printed as `@-`
['@xl:', '@xl:'],
['@[123px]:', '@[123px]:'],
// Compound variants that forward modifiers
['not-group-hover/name:', 'not-group-hover/name:'],
['has-group-peer-hover/name:', 'has-group-peer-hover/name:'],
['in-group-peer-hover/name:', 'in-group-peer-hover/name:'],
]
let combinations: [string, string][] = []

View file

@ -806,6 +806,13 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia
case 'compound': {
if (value === null) return null
// Forward the modifier of the compound variants to its subVariant.
// This allows for `not-group-hover/name:flex` to work.
if (modifier && (root === 'not' || root === 'has' || root === 'in')) {
value = `${value}/${modifier}`
modifier = null
}
let subVariant = designSystem.parseVariant(value)
if (subVariant === null) return null

View file

@ -2586,6 +2586,24 @@ test('matchVariant sorts deterministically', async () => {
}
})
test('move modifier of compound variant to sub-variant if its also a compound variant', async () => {
expect(
await run([
'not-group-focus/name:flex',
'has-group-focus/name:flex',
'in-group-focus/name:flex',
// Keep the `name` on the `group`, don't move it to the `peer` because
// that would be a breaking change.
'group-peer-focus/name:flex',
]),
).toMatchInlineSnapshot(`
".not-group-focus\\/name\\:flex:not(:is(:where(.group\\/name):focus *)), .group-peer-focus\\/name\\:flex:is(:where(.group\\/name):is(:where(.peer):focus ~ *) *), :where(:is(:where(.group\\/name):focus *)) .in-group-focus\\/name\\:flex, .has-group-focus\\/name\\:flex:has(:is(:where(.group\\/name):focus *)) {
display: flex;
}"
`)
})
test.each([
// These are style rules
[['.foo'], Compounds.StyleRules],