Skip to content

Commit dd3441b

Browse files
RobinMalfaiteloyespthecrypticace
authored
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>
1 parent 4687777 commit dd3441b

File tree

6 files changed

+183
-0
lines changed

6 files changed

+183
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Reintroduce `max-w-screen-*` utilities that read from the `--breakpoint` namespace as deprecated utilities ([#15013](https://github.com/tailwindlabs/tailwindcss/pull/15013))
1313
- 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))
14+
- Add new `in-*` variant ([#15025](https://github.com/tailwindlabs/tailwindcss/pull/15025))
1415
- _Upgrade (experimental)_: Migrate `[&>*]` to the `*` variant ([#15022](https://github.com/tailwindlabs/tailwindcss/pull/15022))
1516
- _Upgrade (experimental)_: Migrate `[&_*]` to the `**` variant ([#15022](https://github.com/tailwindlabs/tailwindcss/pull/15022))
1617

packages/@tailwindcss-upgrade/src/template/codemods/modernize-arbitrary-values.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ test.each([
1818
['[&:first-child]:flex', 'first:flex'],
1919
['[&:not(:first-child)]:flex', 'not-first:flex'],
2020

21+
// in-* variants
22+
['[p_&]:flex', 'in-[p]:flex'],
23+
['[.foo_&]:flex', 'in-[.foo]:flex'],
24+
['[[data-visible]_&]:flex', 'in-data-visible:flex'],
25+
2126
// nth-child
2227
['[&:nth-child(2)]:flex', 'nth-2:flex'],
2328
['[&:not(:nth-child(2))]:flex', 'not-nth-2:flex'],

packages/@tailwindcss-upgrade/src/template/codemods/modernize-arbitrary-values.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@ export function modernizeArbitraryValues(
1515
let changed = false
1616

1717
for (let [variant, parent] of variants(clone)) {
18+
// Forward modifier from the root to the compound variant
19+
if (
20+
variant.kind === 'compound' &&
21+
(variant.root === 'has' || variant.root === 'not' || variant.root === 'in')
22+
) {
23+
if (variant.modifier !== null) {
24+
if ('modifier' in variant.variant) {
25+
variant.variant.modifier = variant.modifier
26+
variant.modifier = null
27+
}
28+
}
29+
}
30+
1831
// Expecting an arbitrary variant
1932
if (variant.kind !== 'arbitrary') continue
2033

@@ -98,6 +111,61 @@ export function modernizeArbitraryValues(
98111
prefixedVariant = designSystem.parseVariant('**')
99112
}
100113

114+
// Handling a child/parent combinator. E.g.: `[[data-visible]_&]` => `in-data-visible`
115+
if (
116+
// Only top-level, so `has-[&_[data-visible]]` is not supported
117+
parent === null &&
118+
// [[data-visible]___&]:flex
119+
// ^^^^^^^^^^^^^^ ^ ^
120+
ast.nodes[0].length === 3 &&
121+
ast.nodes[0].nodes[0].type === 'attribute' &&
122+
ast.nodes[0].nodes[1].type === 'combinator' &&
123+
ast.nodes[0].nodes[1].value === ' ' &&
124+
ast.nodes[0].nodes[2].type === 'nesting' &&
125+
ast.nodes[0].nodes[2].value === '&'
126+
) {
127+
ast.nodes[0].nodes = [ast.nodes[0].nodes[0]]
128+
changed = true
129+
// When handling a compound like `in-[[data-visible]]`, we will first
130+
// handle `[[data-visible]]`, then the parent `in-*` part. This means
131+
// that we can convert `[[data-visible]_&]` to `in-[[data-visible]]`.
132+
//
133+
// Later this gets converted to `in-data-visible`.
134+
Object.assign(variant, designSystem.parseVariant(`in-[${ast.toString()}]`))
135+
continue
136+
}
137+
138+
// `in-*` variant
139+
if (
140+
// Only top-level, so `has-[p_&]` is not supported
141+
parent === null &&
142+
// `[data-*]` and `[aria-*]` are handled separately
143+
!(
144+
ast.nodes[0].nodes[0].type === 'attribute' &&
145+
(ast.nodes[0].nodes[0].attribute.startsWith('data-') ||
146+
ast.nodes[0].nodes[0].attribute.startsWith('aria-'))
147+
) &&
148+
// [.foo___&]:flex
149+
// ^^^^ ^ ^
150+
ast.nodes[0].nodes.at(-1)?.type === 'nesting'
151+
) {
152+
let selector = ast.nodes[0]
153+
let nodes = selector.nodes
154+
155+
nodes.pop() // Remove the last node `&`
156+
157+
// Remove trailing whitespace
158+
let last = nodes.at(-1)
159+
while (last?.type === 'combinator' && last.value === ' ') {
160+
nodes.pop()
161+
last = nodes.at(-1)
162+
}
163+
164+
changed = true
165+
Object.assign(variant, designSystem.parseVariant(`in-[${selector.toString().trim()}]`))
166+
continue
167+
}
168+
101169
// Filter out `&`. E.g.: `&[data-foo]` => `[data-foo]`
102170
let selectorNodes = ast.nodes[0].filter((node) => node.type !== 'nesting')
103171

packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7554,6 +7554,7 @@ exports[`getVariants 1`] = `
75547554
"enabled",
75557555
"disabled",
75567556
"inert",
7557+
"in",
75577558
"has",
75587559
"aria",
75597560
"data",
@@ -7622,6 +7623,7 @@ exports[`getVariants 1`] = `
76227623
"enabled",
76237624
"disabled",
76247625
"inert",
7626+
"in",
76257627
"has",
76267628
"aria",
76277629
"data",
@@ -7674,6 +7676,7 @@ exports[`getVariants 1`] = `
76747676
"enabled",
76757677
"disabled",
76767678
"inert",
7679+
"in",
76777680
"has",
76787681
"aria",
76797682
"data",
@@ -7972,6 +7975,59 @@ exports[`getVariants 1`] = `
79727975
"selectors": [Function],
79737976
"values": [],
79747977
},
7978+
{
7979+
"hasDash": true,
7980+
"isArbitrary": true,
7981+
"name": "in",
7982+
"selectors": [Function],
7983+
"values": [
7984+
"not",
7985+
"group",
7986+
"peer",
7987+
"first",
7988+
"last",
7989+
"only",
7990+
"odd",
7991+
"even",
7992+
"first-of-type",
7993+
"last-of-type",
7994+
"only-of-type",
7995+
"visited",
7996+
"target",
7997+
"open",
7998+
"default",
7999+
"checked",
8000+
"indeterminate",
8001+
"placeholder-shown",
8002+
"autofill",
8003+
"optional",
8004+
"required",
8005+
"valid",
8006+
"invalid",
8007+
"in-range",
8008+
"out-of-range",
8009+
"read-only",
8010+
"empty",
8011+
"focus-within",
8012+
"hover",
8013+
"focus",
8014+
"focus-visible",
8015+
"active",
8016+
"enabled",
8017+
"disabled",
8018+
"inert",
8019+
"in",
8020+
"has",
8021+
"aria",
8022+
"data",
8023+
"nth",
8024+
"nth-last",
8025+
"nth-of-type",
8026+
"nth-last-of-type",
8027+
"ltr",
8028+
"rtl",
8029+
],
8030+
},
79758031
{
79768032
"hasDash": true,
79778033
"isArbitrary": true,
@@ -8013,6 +8069,7 @@ exports[`getVariants 1`] = `
80138069
"enabled",
80148070
"disabled",
80158071
"inert",
8072+
"in",
80168073
"has",
80178074
"aria",
80188075
"data",

packages/tailwindcss/src/variants.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1693,6 +1693,23 @@ test('not', async () => {
16931693
).toEqual('')
16941694
})
16951695

1696+
test('in', async () => {
1697+
expect(
1698+
await run([
1699+
'in-[p]:flex',
1700+
'in-[.group]:flex',
1701+
'not-in-[p]:flex',
1702+
'not-in-[.group]:flex',
1703+
'in-data-visible:flex',
1704+
]),
1705+
).toMatchInlineSnapshot(`
1706+
".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 {
1707+
display: flex;
1708+
}"
1709+
`)
1710+
expect(await run(['in-p:flex', 'in-foo-bar:flex'])).toEqual('')
1711+
})
1712+
16961713
test('has', async () => {
16971714
expect(
16981715
await compileCss(

packages/tailwindcss/src/variants.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,41 @@ export function createVariants(theme: Theme): Variants {
711711

712712
staticVariant('inert', ['&:is([inert], [inert] *)'])
713713

714+
variants.compound('in', Compounds.StyleRules, (ruleNode, variant) => {
715+
if (variant.modifier) return null
716+
717+
let didApply = false
718+
719+
walk([ruleNode], (node, { path }) => {
720+
if (node.kind !== 'rule') return WalkAction.Continue
721+
722+
// Throw out any candidates with variants using nested style rules
723+
for (let parent of path.slice(0, -1)) {
724+
if (parent.kind !== 'rule') continue
725+
726+
didApply = false
727+
return WalkAction.Stop
728+
}
729+
730+
// Replace `&` in target variant with `*`, so variants like `&:hover`
731+
// become `:where(*:hover) &`. The `*` will often be optimized away.
732+
node.selector = `:where(${node.selector.replaceAll('&', '*')}) &`
733+
734+
// Track that the variant was actually applied
735+
didApply = true
736+
})
737+
738+
// If the node wasn't modified, this variant is not compatible with
739+
// `in-*` so discard the candidate.
740+
if (!didApply) return null
741+
})
742+
743+
variants.suggest('in', () => {
744+
return Array.from(variants.keys()).filter((name) => {
745+
return variants.compoundsWith('in', name)
746+
})
747+
})
748+
714749
variants.compound('has', Compounds.StyleRules, (ruleNode, variant) => {
715750
if (variant.modifier) return null
716751

0 commit comments

Comments
 (0)