mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
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:
parent
4687777788
commit
dd3441bf82
@ -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))
|
||||
|
||||
|
||||
@ -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'],
|
||||
|
||||
@ -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')
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user