Don’t allow at-rule-only variants to be compounded (#14015)

* Don’t allow at-rule-only variants to be compounded

* Update changelog

---------

Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
This commit is contained in:
Jordan Pittman 2024-07-17 14:52:29 -04:00 committed by GitHub
parent cf846a5ff6
commit e8546df800
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 126 additions and 10 deletions

View File

@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ensure opacity modifier with variables work with `color-mix()` ([#13972](https://github.com/tailwindlabs/tailwindcss/pull/13972))
- 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))
- Dont allow at-rule-only variants to be compounded ([#14015](https://github.com/tailwindlabs/tailwindcss/pull/14015))
### Added

View File

@ -725,6 +725,15 @@ test('group-[...]', () => {
display: flex;
}"
`)
expect(
compileCss(
css`
@tailwind utilities;
`,
['group-[@media_foo]:flex'],
),
).toEqual('')
})
test('group-*', () => {
@ -752,6 +761,16 @@ test('group-*', () => {
display: flex;
}"
`)
expect(
compileCss(
css`
@variant custom-at-rule (@media foo);
@tailwind utilities;
`,
['group-custom-at-rule:flex'],
),
).toEqual('')
})
test('peer-[...]', () => {
@ -784,6 +803,15 @@ test('peer-[...]', () => {
display: flex;
}"
`)
expect(
compileCss(
css`
@tailwind utilities;
`,
['peer-[@media_foo]:flex'],
),
).toEqual('')
})
test('peer-*', () => {
@ -811,6 +839,16 @@ test('peer-*', () => {
display: flex;
}"
`)
expect(
compileCss(
css`
@variant custom-at-rule (@media foo);
@tailwind utilities;
`,
['peer-custom-at-rule:flex'],
),
).toEqual('')
})
test('ltr', () => {
@ -1505,7 +1543,15 @@ test('not', () => {
}"
`)
expect(run(['not-[:checked]/foo:flex'])).toEqual('')
expect(
compileCss(
css`
@variant custom-at-rule (@media foo);
@tailwind utilities;
`,
['not-[:checked]/foo:flex', 'not-[@media_print]:flex', 'not-custom-at-rule:flex'],
),
).toEqual('')
})
test('has', () => {
@ -1518,7 +1564,7 @@ test('has', () => {
'group-has-checked:flex',
'peer-has-[:checked]:flex',
'peer-has-[:checked]/parent-name:flex',
'peer-has-[:checked]/sibling-name:flex',
'peer-has-checked:flex',
]),
).toMatchInlineSnapshot(`
@ -1542,7 +1588,7 @@ test('has', () => {
display: flex;
}
.peer-has-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name):has(:checked) ~ *) {
.peer-has-\\[\\:checked\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name):has(:checked) ~ *) {
display: flex;
}
@ -1550,7 +1596,16 @@ test('has', () => {
display: flex;
}"
`)
expect(run(['has-[:checked]/foo:flex'])).toEqual('')
expect(
compileCss(
css`
@variant custom-at-rule (@media foo);
@tailwind utilities;
`,
['has-[:checked]/foo:flex', 'has-[@media_print]:flex', 'has-custom-at-rule:flex'],
),
).toEqual('')
})
test('aria', () => {

View File

@ -205,7 +205,28 @@ export function createVariants(theme: Theme): Variants {
variants.compound('not', (ruleNode, variant) => {
if (variant.modifier) return null
ruleNode.selector = `&:not(${ruleNode.selector.replace('&', '*')})`
let didApply = false
walk([ruleNode], (node) => {
if (node.kind !== 'rule') return WalkAction.Continue
// Skip past at-rules, and continue traversing the children of the at-rule
if (node.selector[0] === '@') return WalkAction.Continue
// Replace `&` in target variant with `*`, so variants like `&:hover`
// become `&:not(*:hover)`. The `*` will often be optimized away.
node.selector = `&:not(${node.selector.replace('&', '*')})`
// Track that the variant was actually applied
didApply = true
})
// If the node wasn't modified, this variant is not compatible with
// `not-*` so discard the candidate.
if (!didApply) {
return null
}
})
variants.compound('group', (ruleNode, variant) => {
@ -215,6 +236,8 @@ export function createVariants(theme: Theme): Variants {
? `:where(.group\\/${variant.modifier.value})`
: ':where(.group)'
let didApply = false
walk([ruleNode], (node) => {
if (node.kind !== 'rule') return WalkAction.Continue
@ -234,10 +257,17 @@ export function createVariants(theme: Theme): Variants {
node.selector = `:is(${node.selector})`
}
// Use `:where` to make sure the specificity of group variants isn't higher
// than the specificity of other variants.
node.selector = `&:is(${node.selector} *)`
// Track that the variant was actually applied
didApply = true
})
// If the node wasn't modified, this variant is not compatible with
// `group-*` so discard the candidate.
if (!didApply) {
return null
}
})
variants.suggest('group', () => {
@ -253,6 +283,8 @@ export function createVariants(theme: Theme): Variants {
? `:where(.peer\\/${variant.modifier.value})`
: ':where(.peer)'
let didApply = false
walk([ruleNode], (node) => {
if (node.kind !== 'rule') return WalkAction.Continue
@ -272,10 +304,17 @@ export function createVariants(theme: Theme): Variants {
node.selector = `:is(${node.selector})`
}
// Use `:where` to make sure the specificity of group variants isn't higher
// than the specificity of other variants.
node.selector = `&:is(${node.selector} ~ *)`
// Track that the variant was actually applied
didApply = true
})
// If the node wasn't modified, this variant is not compatible with
// `peer-*` so discard the candidate.
if (!didApply) {
return null
}
})
variants.suggest('peer', () => {
@ -392,7 +431,28 @@ export function createVariants(theme: Theme): Variants {
variants.compound('has', (ruleNode, variant) => {
if (variant.modifier) return null
ruleNode.selector = `&:has(${ruleNode.selector.replace('&', '*')})`
let didApply = false
walk([ruleNode], (node) => {
if (node.kind !== 'rule') return WalkAction.Continue
// Skip past at-rules, and continue traversing the children of the at-rule
if (node.selector[0] === '@') return WalkAction.Continue
// Replace `&` in target variant with `*`, so variants like `&:hover`
// become `&:has(*:hover)`. The `*` will often be optimized away.
node.selector = `&:has(${node.selector.replace('&', '*')})`
// Track that the variant was actually applied
didApply = true
})
// If the node wasn't modified, this variant is not compatible with
// `has-*` so discard the candidate.
if (!didApply) {
return null
}
})
variants.suggest('has', () => {