Skip to content

Commit e6d3fa0

Browse files
authored
Migrate aria-*, data-* and supports-* variants from arbitrary values to bare values (#14644)
This PR adds a new codemod that can migrate `data-*` and `aria-*` variants using arbitrary values to bare values. In Tailwind CSS v3, if you want to conditionally apply a class using data attributes, then you can write `data-[selected]:flex`. This requires the DOM element to have a `data-selected=""` attribute. In Tailwind CSS v4 we can simplify this, by dropping the brackets and by using `data-selected:flex` directly. This migration operates on the internal AST, which means that this also just works for compound variants such as `group-has-data-[selected]:flex` (which turns into `group-has-data-selected:flex`). Additionally, this codemod is also applicable to `aria-*` variants. The biggest difference is that in v4 `aria-selected` maps to an attribute of `aria-selected="true"`. This means that we can only migrate `aria=[selected="true"]:flex` to `aria-selected:flex`. Last but not least, we also migrate `supports-[gap]` to `supports-gap` if the passed in value looks like a property. If not, e.g.: `supports-[display:grid]` then it stays as-is.
1 parent 7c7acac commit e6d3fa0

File tree

4 files changed

+128
-0
lines changed

4 files changed

+128
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
- _Upgrade (experimental)_: Fully convert simple JS configs to CSS ([#14639](https://github.com/tailwindlabs/tailwindcss/pull/14639))
1818
- _Upgrade (experimental)_: Migrate `@media screen(…)` when running codemods ([#14603](https://github.com/tailwindlabs/tailwindcss/pull/14603))
1919
- _Upgrade (experimental)_: Inject `@config "…"` when a `tailwind.config.{js,ts,…}` is detected ([#14635](https://github.com/tailwindlabs/tailwindcss/pull/14635))
20+
- _Upgrade (experimental)_: Migrate `aria-*`, `data-*`, and `supports-*` variants from arbitrary values to bare values ([#14644](https://github.com/tailwindlabs/tailwindcss/pull/14644))
2021

2122
### Fixed
2223

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
2+
import { expect, test } from 'vitest'
3+
import { arbitraryValueToBareValue } from './arbitrary-value-to-bare-value'
4+
5+
test.each([
6+
['data-[selected]:flex', 'data-selected:flex'],
7+
['data-[foo=bar]:flex', 'data-[foo=bar]:flex'],
8+
9+
['supports-[gap]:flex', 'supports-gap:flex'],
10+
['supports-[display:grid]:flex', 'supports-[display:grid]:flex'],
11+
12+
['group-data-[selected]:flex', 'group-data-selected:flex'],
13+
['group-data-[foo=bar]:flex', 'group-data-[foo=bar]:flex'],
14+
['group-has-data-[selected]:flex', 'group-has-data-selected:flex'],
15+
16+
['aria-[selected]:flex', 'aria-[selected]:flex'],
17+
['aria-[selected="true"]:flex', 'aria-selected:flex'],
18+
['aria-[selected*="true"]:flex', 'aria-[selected*="true"]:flex'],
19+
20+
['group-aria-[selected]:flex', 'group-aria-[selected]:flex'],
21+
['group-aria-[selected="true"]:flex', 'group-aria-selected:flex'],
22+
['group-has-aria-[selected]:flex', 'group-has-aria-[selected]:flex'],
23+
24+
['max-lg:hover:data-[selected]:flex!', 'max-lg:hover:data-selected:flex!'],
25+
])('%s => %s', async (candidate, result) => {
26+
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
27+
base: __dirname,
28+
})
29+
30+
expect(arbitraryValueToBareValue(designSystem, {}, candidate)).toEqual(result)
31+
})
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import type { Config } from 'tailwindcss'
2+
import { parseCandidate, type Candidate, type Variant } from '../../../../tailwindcss/src/candidate'
3+
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
4+
import { segment } from '../../../../tailwindcss/src/utils/segment'
5+
import { printCandidate } from '../candidates'
6+
7+
export function arbitraryValueToBareValue(
8+
designSystem: DesignSystem,
9+
_userConfig: Config,
10+
rawCandidate: string,
11+
): string {
12+
for (let candidate of parseCandidate(rawCandidate, designSystem)) {
13+
let clone = structuredClone(candidate)
14+
let changed = false
15+
for (let variant of variants(clone)) {
16+
// Convert `data-[selected]` to `data-selected`
17+
if (
18+
variant.kind === 'functional' &&
19+
variant.root === 'data' &&
20+
variant.value?.kind === 'arbitrary' &&
21+
!variant.value.value.includes('=')
22+
) {
23+
changed = true
24+
variant.value = {
25+
kind: 'named',
26+
value: variant.value.value,
27+
}
28+
}
29+
30+
// Convert `aria-[selected="true"]` to `aria-selected`
31+
else if (
32+
variant.kind === 'functional' &&
33+
variant.root === 'aria' &&
34+
variant.value?.kind === 'arbitrary' &&
35+
(variant.value.value.endsWith('=true') ||
36+
variant.value.value.endsWith('="true"') ||
37+
variant.value.value.endsWith("='true'"))
38+
) {
39+
let [key, _value] = segment(variant.value.value, '=')
40+
if (
41+
// aria-[foo~="true"]
42+
key[key.length - 1] === '~' ||
43+
// aria-[foo|="true"]
44+
key[key.length - 1] === '|' ||
45+
// aria-[foo^="true"]
46+
key[key.length - 1] === '^' ||
47+
// aria-[foo$="true"]
48+
key[key.length - 1] === '$' ||
49+
// aria-[foo*="true"]
50+
key[key.length - 1] === '*'
51+
) {
52+
continue
53+
}
54+
55+
changed = true
56+
variant.value = {
57+
kind: 'named',
58+
value: variant.value.value.slice(0, variant.value.value.indexOf('=')),
59+
}
60+
}
61+
62+
// Convert `supports-[gap]` to `supports-gap`
63+
else if (
64+
variant.kind === 'functional' &&
65+
variant.root === 'supports' &&
66+
variant.value?.kind === 'arbitrary' &&
67+
/^[a-z-][a-z0-9-]*$/i.test(variant.value.value)
68+
) {
69+
changed = true
70+
variant.value = {
71+
kind: 'named',
72+
value: variant.value.value,
73+
}
74+
}
75+
}
76+
77+
return changed ? printCandidate(designSystem, clone) : rawCandidate
78+
}
79+
80+
return rawCandidate
81+
}
82+
83+
function* variants(candidate: Candidate) {
84+
function* inner(variant: Variant): Iterable<Variant> {
85+
yield variant
86+
if (variant.kind === 'compound') {
87+
yield* inner(variant.variant)
88+
}
89+
}
90+
91+
for (let variant of candidate.variants) {
92+
yield* inner(variant)
93+
}
94+
}

packages/@tailwindcss-upgrade/src/template/migrate.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path, { extname } from 'node:path'
33
import type { Config } from 'tailwindcss'
44
import type { DesignSystem } from '../../../tailwindcss/src/design-system'
55
import { extractRawCandidates, replaceCandidateInContent } from './candidates'
6+
import { arbitraryValueToBareValue } from './codemods/arbitrary-value-to-bare-value'
67
import { automaticVarInjection } from './codemods/automatic-var-injection'
78
import { bgGradient } from './codemods/bg-gradient'
89
import { important } from './codemods/important'
@@ -22,6 +23,7 @@ export const DEFAULT_MIGRATIONS: Migration[] = [
2223
automaticVarInjection,
2324
bgGradient,
2425
simpleLegacyClasses,
26+
arbitraryValueToBareValue,
2527
variantOrder,
2628
]
2729

0 commit comments

Comments
 (0)