Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new in-* variant #15025

Merged
merged 12 commits into from
Nov 18, 2024
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
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('&', '*')}) &`
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is basically the same implementation as the has-* variant, but the selector is a bit different.


// 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
})

RobinMalfait marked this conversation as resolved.
Show resolved Hide resolved
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