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.
This commit is contained in:
Robin Malfait 2024-10-11 16:41:37 +02:00 • committed by GitHub
parent 7c7acace5c
commit e6d3fa0cc4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 128 additions and 0 deletions

View file

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

View file

@ -0,0 +1,31 @@
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
import { expect, test } from 'vitest'
import { arbitraryValueToBareValue } from './arbitrary-value-to-bare-value'
test.each([
['data-[selected]:flex', 'data-selected:flex'],
['data-[foo=bar]:flex', 'data-[foo=bar]:flex'],
['supports-[gap]:flex', 'supports-gap:flex'],
['supports-[display:grid]:flex', 'supports-[display:grid]:flex'],
['group-data-[selected]:flex', 'group-data-selected:flex'],
['group-data-[foo=bar]:flex', 'group-data-[foo=bar]:flex'],
['group-has-data-[selected]:flex', 'group-has-data-selected:flex'],
['aria-[selected]:flex', 'aria-[selected]:flex'],
['aria-[selected="true"]:flex', 'aria-selected:flex'],
['aria-[selected*="true"]:flex', 'aria-[selected*="true"]:flex'],
['group-aria-[selected]:flex', 'group-aria-[selected]:flex'],
['group-aria-[selected="true"]:flex', 'group-aria-selected:flex'],
['group-has-aria-[selected]:flex', 'group-has-aria-[selected]:flex'],
['max-lg:hover:data-[selected]:flex!', 'max-lg:hover:data-selected:flex!'],
])('%s => %s', async (candidate, result) => {
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
base: __dirname,
})
expect(arbitraryValueToBareValue(designSystem, {}, candidate)).toEqual(result)
})

View file

@ -0,0 +1,94 @@
import type { Config } from 'tailwindcss'
import { parseCandidate, type Candidate, type Variant } from '../../../../tailwindcss/src/candidate'
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
import { segment } from '../../../../tailwindcss/src/utils/segment'
import { printCandidate } from '../candidates'
export function arbitraryValueToBareValue(
designSystem: DesignSystem,
_userConfig: Config,
rawCandidate: string,
): string {
for (let candidate of parseCandidate(rawCandidate, designSystem)) {
let clone = structuredClone(candidate)
let changed = false
for (let variant of variants(clone)) {
// Convert `data-[selected]` to `data-selected`
if (
variant.kind === 'functional' &&
variant.root === 'data' &&
variant.value?.kind === 'arbitrary' &&
!variant.value.value.includes('=')
) {
changed = true
variant.value = {
kind: 'named',
value: variant.value.value,
}
}
// Convert `aria-[selected="true"]` to `aria-selected`
else if (
variant.kind === 'functional' &&
variant.root === 'aria' &&
variant.value?.kind === 'arbitrary' &&
(variant.value.value.endsWith('=true') ||
variant.value.value.endsWith('="true"') ||
variant.value.value.endsWith("='true'"))
) {
let [key, _value] = segment(variant.value.value, '=')
if (
// aria-[foo~="true"]
key[key.length - 1] === '~' ||
// aria-[foo|="true"]
key[key.length - 1] === '|' ||
// aria-[foo^="true"]
key[key.length - 1] === '^' ||
// aria-[foo$="true"]
key[key.length - 1] === '$' ||
// aria-[foo*="true"]
key[key.length - 1] === '*'
) {
continue
}
changed = true
variant.value = {
kind: 'named',
value: variant.value.value.slice(0, variant.value.value.indexOf('=')),
}
}
// Convert `supports-[gap]` to `supports-gap`
else if (
variant.kind === 'functional' &&
variant.root === 'supports' &&
variant.value?.kind === 'arbitrary' &&
/^[a-z-][a-z0-9-]*$/i.test(variant.value.value)
) {
changed = true
variant.value = {
kind: 'named',
value: variant.value.value,
}
}
}
return changed ? printCandidate(designSystem, clone) : rawCandidate
}
return rawCandidate
}
function* variants(candidate: Candidate) {
function* inner(variant: Variant): Iterable<Variant> {
yield variant
if (variant.kind === 'compound') {
yield* inner(variant.variant)
}
}
for (let variant of candidate.variants) {
yield* inner(variant)
}
}

View file

@ -3,6 +3,7 @@ import path, { extname } from 'node:path'
import type { Config } from 'tailwindcss'
import type { DesignSystem } from '../../../tailwindcss/src/design-system'
import { extractRawCandidates, replaceCandidateInContent } from './candidates'
import { arbitraryValueToBareValue } from './codemods/arbitrary-value-to-bare-value'
import { automaticVarInjection } from './codemods/automatic-var-injection'
import { bgGradient } from './codemods/bg-gradient'
import { important } from './codemods/important'
@ -22,6 +23,7 @@ export const DEFAULT_MIGRATIONS: Migration[] = [
automaticVarInjection,
bgGradient,
simpleLegacyClasses,
arbitraryValueToBareValue,
variantOrder,
]