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:
parent
7c7acace5c
commit
e6d3fa0cc4
4 changed files with 128 additions and 0 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue