Add new in-* variant (#15025)

This PR adds a new `in-*` variant that allows you to apply utilities
when you are in a certain selector.

While doing research for codemods, we notice that some people use
`group-[]:flex` (yep, the arbitrary value is empty…). The idea behind is
that people want to know if you are in a `.group` or not.

Similarly, some people use `group-[]/name:flex` to know when you are in
a `.group/name` class or not.

This new `in-*` variant allows you to do that without any hacks.

If you want to check whether you are inside of a `p` tag, then you can
write `in-[p]:flex`. If you want to check that you are inside of a
`.group`, you can write `in-[.group]`.

This variant is also a compound variant, which means that you can write
`in-data-visible:flex` which generates the following CSS:
```css
:where([data-visible]) .in-data-visible\:flex {
  display: flex;
}
```

This variant also compounds with `not-*`, for example:
`not-in-[.group]:flex`.

Additionally, this PR also includes a codemod to convert `group-[]:flex`
to `in-[.group]:flex`.

---

This was proposed before for v3 in #13912

---------

Co-authored-by: Eloy Espinaco <eloyesp@gmail.com>
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
This commit is contained in:
Robin Malfait 2024-11-18 16:28:16 +01:00 committed by GitHub
parent 4687777788
commit dd3441bf82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 183 additions and 0 deletions

View File

@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Reintroduce `max-w-screen-*` utilities that read from the `--breakpoint` namespace as deprecated utilities ([#15013](https://github.com/tailwindlabs/tailwindcss/pull/15013))
- Support using CSS variables as arbitrary values without `var(…)` by using parentheses instead of square brackets (e.g. `bg-(--my-color)`) ([#15020](https://github.com/tailwindlabs/tailwindcss/pull/15020))
- Add new `in-*` variant ([#15025](https://github.com/tailwindlabs/tailwindcss/pull/15025))
- _Upgrade (experimental)_: Migrate `[&>*]` to the `*` variant ([#15022](https://github.com/tailwindlabs/tailwindcss/pull/15022))
- _Upgrade (experimental)_: Migrate `[&_*]` to the `**` variant ([#15022](https://github.com/tailwindlabs/tailwindcss/pull/15022))

View File

@ -18,6 +18,11 @@ test.each([
['[&:first-child]:flex', 'first:flex'],
['[&:not(:first-child)]:flex', 'not-first:flex'],
// in-* variants
['[p_&]:flex', 'in-[p]:flex'],
['[.foo_&]:flex', 'in-[.foo]:flex'],
['[[data-visible]_&]:flex', 'in-data-visible:flex'],
// nth-child
['[&:nth-child(2)]:flex', 'nth-2:flex'],
['[&:not(:nth-child(2))]:flex', 'not-nth-2:flex'],

View File

@ -15,6 +15,19 @@ export function modernizeArbitraryValues(
let changed = false
for (let [variant, parent] of variants(clone)) {
// Forward modifier from the root to the compound variant
if (
variant.kind === 'compound' &&
(variant.root === 'has' || variant.root === 'not' || variant.root === 'in')
) {
if (variant.modifier !== null) {
if ('modifier' in variant.variant) {
variant.variant.modifier = variant.modifier
variant.modifier = null
}
}
}
// Expecting an arbitrary variant
if (variant.kind !== 'arbitrary') continue
@ -98,6 +111,61 @@ export function modernizeArbitraryValues(
prefixedVariant = designSystem.parseVariant('**')
}
// Handling a child/parent combinator. E.g.: `[[data-visible]_&]` => `in-data-visible`
if (
// Only top-level, so `has-[&_[data-visible]]` is not supported
parent === null &&
// [[data-visible]___&]:flex
// ^^^^^^^^^^^^^^ ^ ^
ast.nodes[0].length === 3 &&
ast.nodes[0].nodes[0].type === 'attribute' &&
ast.nodes[0].nodes[1].type === 'combinator' &&
ast.nodes[0].nodes[1].value === ' ' &&
ast.nodes[0].nodes[2].type === 'nesting' &&
ast.nodes[0].nodes[2].value === '&'
) {
ast.nodes[0].nodes = [ast.nodes[0].nodes[0]]
changed = true
// When handling a compound like `in-[[data-visible]]`, we will first
// handle `[[data-visible]]`, then the parent `in-*` part. This means
// that we can convert `[[data-visible]_&]` to `in-[[data-visible]]`.
//
// Later this gets converted to `in-data-visible`.
Object.assign(variant, designSystem.parseVariant(`in-[${ast.toString()}]`))
continue
}
// `in-*` variant
if (
// Only top-level, so `has-[p_&]` is not supported
parent === null &&
// `[data-*]` and `[aria-*]` are handled separately
!(
ast.nodes[0].nodes[0].type === 'attribute' &&
(ast.nodes[0].nodes[0].attribute.startsWith('data-') ||
ast.nodes[0].nodes[0].attribute.startsWith('aria-'))
) &&
// [.foo___&]:flex
// ^^^^ ^ ^
ast.nodes[0].nodes.at(-1)?.type === 'nesting'
) {
let selector = ast.nodes[0]
let nodes = selector.nodes
nodes.pop() // Remove the last node `&`
// Remove trailing whitespace
let last = nodes.at(-1)
while (last?.type === 'combinator' && last.value === ' ') {
nodes.pop()
last = nodes.at(-1)
}
changed = true
Object.assign(variant, designSystem.parseVariant(`in-[${selector.toString().trim()}]`))
continue
}
// Filter out `&`. E.g.: `&[data-foo]` => `[data-foo]`
let selectorNodes = ast.nodes[0].filter((node) => node.type !== 'nesting')

View File

@ -7554,6 +7554,7 @@ exports[`getVariants 1`] = `
"enabled",
"disabled",
"inert",
"in",
"has",
"aria",
"data",
@ -7622,6 +7623,7 @@ exports[`getVariants 1`] = `
"enabled",
"disabled",
"inert",
"in",
"has",
"aria",
"data",
@ -7674,6 +7676,7 @@ exports[`getVariants 1`] = `
"enabled",
"disabled",
"inert",
"in",
"has",
"aria",
"data",
@ -7972,6 +7975,59 @@ exports[`getVariants 1`] = `
"selectors": [Function],
"values": [],
},
{
"hasDash": true,
"isArbitrary": true,
"name": "in",
"selectors": [Function],
"values": [
"not",
"group",
"peer",
"first",
"last",
"only",
"odd",
"even",
"first-of-type",
"last-of-type",
"only-of-type",
"visited",
"target",
"open",
"default",
"checked",
"indeterminate",
"placeholder-shown",
"autofill",
"optional",
"required",
"valid",
"invalid",
"in-range",
"out-of-range",
"read-only",
"empty",
"focus-within",
"hover",
"focus",
"focus-visible",
"active",
"enabled",
"disabled",
"inert",
"in",
"has",
"aria",
"data",
"nth",
"nth-last",
"nth-of-type",
"nth-last-of-type",
"ltr",
"rtl",
],
},
{
"hasDash": true,
"isArbitrary": true,
@ -8013,6 +8069,7 @@ exports[`getVariants 1`] = `
"enabled",
"disabled",
"inert",
"in",
"has",
"aria",
"data",

View File

@ -1693,6 +1693,23 @@ test('not', async () => {
).toEqual('')
})
test('in', async () => {
expect(
await run([
'in-[p]:flex',
'in-[.group]:flex',
'not-in-[p]:flex',
'not-in-[.group]:flex',
'in-data-visible:flex',
]),
).toMatchInlineSnapshot(`
".not-in-\\[\\.group\\]\\:flex:not(:where(.group) *), .not-in-\\[p\\]\\:flex:not(:where(:is(p)) *), :where([data-visible]) .in-data-visible\\:flex, :where(.group) .in-\\[\\.group\\]\\:flex, :where(:is(p)) .in-\\[p\\]\\:flex {
display: flex;
}"
`)
expect(await run(['in-p:flex', 'in-foo-bar:flex'])).toEqual('')
})
test('has', async () => {
expect(
await compileCss(

View File

@ -711,6 +711,41 @@ export function createVariants(theme: Theme): Variants {
staticVariant('inert', ['&:is([inert], [inert] *)'])
variants.compound('in', Compounds.StyleRules, (ruleNode, variant) => {
if (variant.modifier) return null
let didApply = false
walk([ruleNode], (node, { path }) => {
if (node.kind !== 'rule') return WalkAction.Continue
// Throw out any candidates with variants using nested style rules
for (let parent of path.slice(0, -1)) {
if (parent.kind !== 'rule') continue
didApply = false
return WalkAction.Stop
}
// Replace `&` in target variant with `*`, so variants like `&:hover`
// become `:where(*:hover) &`. The `*` will often be optimized away.
node.selector = `:where(${node.selector.replaceAll('&', '*')}) &`
// Track that the variant was actually applied
didApply = true
})
// If the node wasn't modified, this variant is not compatible with
// `in-*` so discard the candidate.
if (!didApply) return null
})
variants.suggest('in', () => {
return Array.from(variants.keys()).filter((name) => {
return variants.compoundsWith('in', name)
})
})
variants.compound('has', Compounds.StyleRules, (ruleNode, variant) => {
if (variant.modifier) return null