Skip to content

Commit

Permalink
Add new in-* variant (#15025)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
3 people authored Nov 18, 2024
1 parent 4687777 commit dd3441b
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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')

Expand Down
57 changes: 57 additions & 0 deletions packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -7554,6 +7554,7 @@ exports[`getVariants 1`] = `
"enabled",
"disabled",
"inert",
"in",
"has",
"aria",
"data",
Expand Down Expand Up @@ -7622,6 +7623,7 @@ exports[`getVariants 1`] = `
"enabled",
"disabled",
"inert",
"in",
"has",
"aria",
"data",
Expand Down Expand Up @@ -7674,6 +7676,7 @@ exports[`getVariants 1`] = `
"enabled",
"disabled",
"inert",
"in",
"has",
"aria",
"data",
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -8013,6 +8069,7 @@ exports[`getVariants 1`] = `
"enabled",
"disabled",
"inert",
"in",
"has",
"aria",
"data",
Expand Down
17 changes: 17 additions & 0 deletions packages/tailwindcss/src/variants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
35 changes: 35 additions & 0 deletions packages/tailwindcss/src/variants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit dd3441b

Please sign in to comment.