Improve in-* variant migrations (#15054)

While testing the codemods on some projects, I noticed some issues with
the migration to the new `in-*` variant.

One such example is that we checked for `&` at the end, instead of ` &`
(the whitespace is significant).

This meant that `[figure>&]:my-0` was converted to `in-[figure>]:my-0`
which is wrong. In this case, we want to keep it as `[figure>&]:my-0`.

Additionally this PR brings back the migration from `group-[]:flex` to
`in-[.group]:flex`. If you are using a prefix, then `group-[]:tw-flex`
is migrated to `tw:in-[.tw\:group]:flex`.

Last but not least, this does some internal refactors to group
migrations logically together.
This commit is contained in:
Robin Malfait 2024-11-20 15:47:26 +01:00 • committed by GitHub
parent 8b098fc83d
commit 57be02a983
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 378 additions and 310 deletions

View file

@ -7,7 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- Nothing yet!
### Added
- _Upgrade (experimental)_: Convert `group-[]:flex` to `in-[.group]:flex` ([#15054](https://github.com/tailwindlabs/tailwindcss/pull/15054))
### Fixed
- _Upgrade (experimental)_: Ensure migrating to the `in-*` requires a descendant selector ([#15054](https://github.com/tailwindlabs/tailwindcss/pull/15054))
## [4.0.0-alpha.35] - 2024-11-20

View file

@ -1,6 +1,7 @@
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
import { expect, test } from 'vitest'
import { modernizeArbitraryValues } from './modernize-arbitrary-values'
import { prefix } from './prefix'
test.each([
// Arbitrary variants
@ -22,6 +23,20 @@ test.each([
['[p_&]:flex', 'in-[p]:flex'],
['[.foo_&]:flex', 'in-[.foo]:flex'],
['[[data-visible]_&]:flex', 'in-data-visible:flex'],
// Multiple selectors, should stay as-is
['[[data-foo][data-bar]_&]:flex', '[[data-foo][data-bar]_&]:flex'],
// Using `>` instead of ` ` should not be transformed:
['[figure>&]:my-0', '[figure>&]:my-0'],
// Some extreme examples of what happens in the wild:
['group-[]:flex', 'in-[.group]:flex'],
['group-[]/name:flex', 'in-[.group\\/name]:flex'],
// These shouldn't happen in the real world (because compound variants are
// new). But this could happen once we allow codemods to run in v4+ projects.
['has-group-[]:flex', 'has-in-[.group]:flex'],
['has-group-[]/name:flex', 'has-in-[.group\\/name]:flex'],
['not-group-[]:flex', 'not-in-[.group]:flex'],
['not-group-[]/name:flex', 'not-in-[.group\\/name]:flex'],
// nth-child
['[&:nth-child(2)]:flex', 'nth-2:flex'],
@ -77,3 +92,33 @@ test.each([
expect(modernizeArbitraryValues(designSystem, {}, candidate)).toEqual(result)
})
test.each([
// Should not prefix classes in arbitrary values
['[.foo_&]:tw-flex', 'tw:in-[.foo]:flex'],
// Should migrate `.group` classes
['group-[]:tw-flex', 'tw:in-[.tw\\:group]:flex'],
['group-[]/name:tw-flex', 'tw:in-[.tw\\:group\\/name]:flex'],
// However, `.group` inside of an arbitrary variant should not be prefixed:
['[.group_&]:tw-flex', 'tw:in-[.group]:flex'],
// These shouldn't happen in the real world (because compound variants are
// new). But this could happen once we allow codemods to run in v4+ projects.
['has-group-[]:tw-flex', 'tw:has-in-[.tw\\:group]:flex'],
['has-group-[]/name:tw-flex', 'tw:has-in-[.tw\\:group\\/name]:flex'],
['not-group-[]:tw-flex', 'tw:not-in-[.tw\\:group]:flex'],
['not-group-[]/name:tw-flex', 'tw:not-in-[.tw\\:group\\/name]:flex'],
])('%s => %s', async (candidate, result) => {
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss" prefix(tw);', {
base: __dirname,
})
expect(
[prefix, modernizeArbitraryValues].reduce(
(acc, step) => step(designSystem, { prefix: 'tw-' }, acc),
candidate,
),
).toEqual(result)
})

View file

@ -5,6 +5,14 @@ import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
import { isPositiveInteger } from '../../../../tailwindcss/src/utils/infer-data-type'
import { printCandidate } from '../candidates'
function memcpy<T extends object, U extends object | null>(target: T, source: U): U {
// Clear out the target object, otherwise inspecting the final object will
// look very confusing.
for (let key in target) delete target[key]
return Object.assign(target, source)
}
export function modernizeArbitraryValues(
designSystem: DesignSystem,
_userConfig: Config,
@ -28,336 +36,345 @@ export function modernizeArbitraryValues(
}
}
// Promote `group-[]:flex` to `in-[.group]:flex`
// ^^ Yes, this is empty
// Promote `group-[]/name:flex` to `in-[.group\/name]:flex`
if (
variant.kind === 'compound' &&
variant.root === 'group' &&
variant.variant.kind === 'arbitrary' &&
variant.variant.selector === '&:is()'
) {
// `group-[]`
if (variant.modifier === null) {
changed = true
memcpy(
variant,
designSystem.parseVariant(
designSystem.theme.prefix
? `in-[.${designSystem.theme.prefix}\\:group]`
: 'in-[.group]',
),
)
}
// `group-[]/name`
else if (variant.modifier.kind === 'named') {
changed = true
memcpy(
variant,
designSystem.parseVariant(
designSystem.theme.prefix
? `in-[.${designSystem.theme.prefix}\\:group\\/${variant.modifier.value}]`
: `in-[.group\\/${variant.modifier.value}]`,
),
)
}
continue
}
// Expecting an arbitrary variant
if (variant.kind !== 'arbitrary') continue
if (variant.kind === 'arbitrary') {
// Expecting a non-relative arbitrary variant
if (variant.relative) continue
// Expecting a non-relative arbitrary variant
if (variant.relative) continue
let ast = SelectorParser().astSync(variant.selector)
let ast = SelectorParser().astSync(variant.selector)
// Expecting a single selector node
if (ast.nodes.length !== 1) continue
let prefixedVariant: Variant | null = null
// `[&>*]` can be replaced with `*`
if (
// Only top-level, so `has-[&>*]` is not supported
parent === null &&
// [&_>_*]:flex
// ^ ^ ^
ast.nodes[0].length === 3 &&
ast.nodes[0].nodes[0].type === 'nesting' &&
ast.nodes[0].nodes[0].value === '&' &&
ast.nodes[0].nodes[1].type === 'combinator' &&
ast.nodes[0].nodes[1].value === '>' &&
ast.nodes[0].nodes[2].type === 'universal'
) {
changed = true
Object.assign(variant, designSystem.parseVariant('*'))
continue
}
// `[&_*]` can be replaced with `**`
if (
// Only top-level, so `has-[&_*]` is not supported
parent === null &&
// [&_*]:flex
// ^ ^
ast.nodes[0].length === 3 &&
ast.nodes[0].nodes[0].type === 'nesting' &&
ast.nodes[0].nodes[0].value === '&' &&
ast.nodes[0].nodes[1].type === 'combinator' &&
ast.nodes[0].nodes[1].value === ' ' &&
ast.nodes[0].nodes[2].type === 'universal'
) {
changed = true
Object.assign(variant, designSystem.parseVariant('**'))
continue
}
// Handling a child combinator. E.g.: `[&>[data-visible]]` => `*: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 === 'nesting' &&
ast.nodes[0].nodes[0].value === '&' &&
ast.nodes[0].nodes[1].type === 'combinator' &&
ast.nodes[0].nodes[1].value === '>' &&
ast.nodes[0].nodes[2].type === 'attribute'
) {
ast.nodes[0].nodes = [ast.nodes[0].nodes[2]]
prefixedVariant = designSystem.parseVariant('*')
}
// Handling a grand child combinator. E.g.: `[&_[data-visible]]` => `**: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 === 'nesting' &&
ast.nodes[0].nodes[0].value === '&' &&
ast.nodes[0].nodes[1].type === 'combinator' &&
ast.nodes[0].nodes[1].value === ' ' &&
ast.nodes[0].nodes[2].type === 'attribute'
) {
ast.nodes[0].nodes = [ast.nodes[0].nodes[2]]
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')
// Expecting a single selector (normal selector or attribute selector)
if (selectorNodes.length !== 1) continue
let target = selectorNodes[0]
if (target.type === 'pseudo' && target.value === ':is') {
// Expecting a single selector node
if (target.nodes.length !== 1) continue
if (ast.nodes.length !== 1) continue
// Expecting a single attribute selector
if (target.nodes[0].nodes.length !== 1) continue
// Unwrap the selector from inside `&:is(…)`
target = target.nodes[0].nodes[0]
}
// Expecting a pseudo selector
if (target.type === 'pseudo') {
let targetNode = target
let compoundNot = false
if (target.value === ':not') {
compoundNot = true
if (target.nodes.length !== 1) continue
if (target.nodes[0].type !== 'selector') continue
if (target.nodes[0].nodes.length !== 1) continue
if (target.nodes[0].nodes[0].type !== 'pseudo') continue
targetNode = target.nodes[0].nodes[0]
// `[&>*]` can be replaced with `*`
if (
// Only top-level, so `has-[&>*]` is not supported
parent === null &&
// [&_>_*]:flex
// ^ ^ ^
ast.nodes[0].length === 3 &&
ast.nodes[0].nodes[0].type === 'nesting' &&
ast.nodes[0].nodes[0].value === '&' &&
ast.nodes[0].nodes[1].type === 'combinator' &&
ast.nodes[0].nodes[1].value === '>' &&
ast.nodes[0].nodes[2].type === 'universal'
) {
changed = true
memcpy(variant, designSystem.parseVariant('*'))
continue
}
let newVariant = ((value) => {
// `[&_*]` can be replaced with `**`
if (
// Only top-level, so `has-[&_*]` is not supported
parent === null &&
// [&_*]:flex
// ^ ^
ast.nodes[0].length === 3 &&
ast.nodes[0].nodes[0].type === 'nesting' &&
ast.nodes[0].nodes[0].value === '&' &&
ast.nodes[0].nodes[1].type === 'combinator' &&
ast.nodes[0].nodes[1].value === ' ' &&
ast.nodes[0].nodes[2].type === 'universal'
) {
changed = true
memcpy(variant, designSystem.parseVariant('**'))
continue
}
// `in-*` variant. If the selector ends with ` &`, we can convert it to an
// `in-*` variant.
//
// E.g.: `[[data-visible]_&]` => `in-data-visible`
if (
// Only top-level, so `in-[&_[data-visible]]` is not supported
parent === null &&
// [[data-visible]___&]:flex
// ^^^^^^^^^^^^^^ ^ ^
ast.nodes[0].nodes.length === 3 &&
ast.nodes[0].nodes[1].type === 'combinator' &&
ast.nodes[0].nodes[1].value === ' ' &&
ast.nodes[0].nodes[2].type === 'nesting'
) {
ast.nodes[0].nodes.pop() // Remove the nesting node
ast.nodes[0].nodes.pop() // Remove the combinator
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]]`.
//
if (value === ':first-letter') return 'first-letter'
else if (value === ':first-line') return 'first-line'
//
else if (value === ':file-selector-button') return 'file'
else if (value === ':placeholder') return 'placeholder'
else if (value === ':backdrop') return 'backdrop'
// Positional
else if (value === ':first-child') return 'first'
else if (value === ':last-child') return 'last'
else if (value === ':only-child') return 'only'
else if (value === ':first-of-type') return 'first-of-type'
else if (value === ':last-of-type') return 'last-of-type'
else if (value === ':only-of-type') return 'only-of-type'
// State
else if (value === ':visited') return 'visited'
else if (value === ':target') return 'target'
// Forms
else if (value === ':default') return 'default'
else if (value === ':checked') return 'checked'
else if (value === ':indeterminate') return 'indeterminate'
else if (value === ':placeholder-shown') return 'placeholder-shown'
else if (value === ':autofill') return 'autofill'
else if (value === ':optional') return 'optional'
else if (value === ':required') return 'required'
else if (value === ':valid') return 'valid'
else if (value === ':invalid') return 'invalid'
else if (value === ':in-range') return 'in-range'
else if (value === ':out-of-range') return 'out-of-range'
else if (value === ':read-only') return 'read-only'
// Content
else if (value === ':empty') return 'empty'
// Interactive
else if (value === ':focus-within') return 'focus-within'
else if (value === ':focus') return 'focus'
else if (value === ':focus-visible') return 'focus-visible'
else if (value === ':active') return 'active'
else if (value === ':enabled') return 'enabled'
else if (value === ':disabled') return 'disabled'
//
if (
value === ':nth-child' &&
targetNode.nodes.length === 1 &&
targetNode.nodes[0].nodes.length === 1 &&
targetNode.nodes[0].nodes[0].type === 'tag' &&
targetNode.nodes[0].nodes[0].value === 'odd'
) {
if (compoundNot) {
compoundNot = false
return 'even'
}
return 'odd'
// Later this gets converted to `in-data-visible`.
memcpy(variant, designSystem.parseVariant(`in-[${ast.toString()}]`))
continue
}
let prefixedVariant: Variant | null = null
// Handling a child combinator. E.g.: `[&>[data-visible]]` => `*: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 === 'nesting' &&
ast.nodes[0].nodes[0].value === '&' &&
ast.nodes[0].nodes[1].type === 'combinator' &&
ast.nodes[0].nodes[1].value === '>' &&
ast.nodes[0].nodes[2].type === 'attribute'
) {
ast.nodes[0].nodes = [ast.nodes[0].nodes[2]]
prefixedVariant = designSystem.parseVariant('*')
}
// Handling a grand child combinator. E.g.: `[&_[data-visible]]` => `**: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 === 'nesting' &&
ast.nodes[0].nodes[0].value === '&' &&
ast.nodes[0].nodes[1].type === 'combinator' &&
ast.nodes[0].nodes[1].value === ' ' &&
ast.nodes[0].nodes[2].type === 'attribute'
) {
ast.nodes[0].nodes = [ast.nodes[0].nodes[2]]
prefixedVariant = designSystem.parseVariant('**')
}
// Filter out `&`. E.g.: `&[data-foo]` => `[data-foo]`
let selectorNodes = ast.nodes[0].filter((node) => node.type !== 'nesting')
// Expecting a single selector (normal selector or attribute selector)
if (selectorNodes.length !== 1) continue
let target = selectorNodes[0]
if (target.type === 'pseudo' && target.value === ':is') {
// Expecting a single selector node
if (target.nodes.length !== 1) continue
// Expecting a single attribute selector
if (target.nodes[0].nodes.length !== 1) continue
// Unwrap the selector from inside `&:is(…)`
target = target.nodes[0].nodes[0]
}
// Expecting a pseudo selector
if (target.type === 'pseudo') {
let targetNode = target
let compoundNot = false
if (target.value === ':not') {
compoundNot = true
if (target.nodes.length !== 1) continue
if (target.nodes[0].type !== 'selector') continue
if (target.nodes[0].nodes.length !== 1) continue
if (target.nodes[0].nodes[0].type !== 'pseudo') continue
targetNode = target.nodes[0].nodes[0]
}
if (
value === ':nth-child' &&
targetNode.nodes.length === 1 &&
targetNode.nodes[0].nodes.length === 1 &&
targetNode.nodes[0].nodes[0].type === 'tag' &&
targetNode.nodes[0].nodes[0].value === 'even'
) {
if (compoundNot) {
compoundNot = false
let newVariant = ((value) => {
//
if (value === ':first-letter') return 'first-letter'
else if (value === ':first-line') return 'first-line'
//
else if (value === ':file-selector-button') return 'file'
else if (value === ':placeholder') return 'placeholder'
else if (value === ':backdrop') return 'backdrop'
// Positional
else if (value === ':first-child') return 'first'
else if (value === ':last-child') return 'last'
else if (value === ':only-child') return 'only'
else if (value === ':first-of-type') return 'first-of-type'
else if (value === ':last-of-type') return 'last-of-type'
else if (value === ':only-of-type') return 'only-of-type'
// State
else if (value === ':visited') return 'visited'
else if (value === ':target') return 'target'
// Forms
else if (value === ':default') return 'default'
else if (value === ':checked') return 'checked'
else if (value === ':indeterminate') return 'indeterminate'
else if (value === ':placeholder-shown') return 'placeholder-shown'
else if (value === ':autofill') return 'autofill'
else if (value === ':optional') return 'optional'
else if (value === ':required') return 'required'
else if (value === ':valid') return 'valid'
else if (value === ':invalid') return 'invalid'
else if (value === ':in-range') return 'in-range'
else if (value === ':out-of-range') return 'out-of-range'
else if (value === ':read-only') return 'read-only'
// Content
else if (value === ':empty') return 'empty'
// Interactive
else if (value === ':focus-within') return 'focus-within'
else if (value === ':focus') return 'focus'
else if (value === ':focus-visible') return 'focus-visible'
else if (value === ':active') return 'active'
else if (value === ':enabled') return 'enabled'
else if (value === ':disabled') return 'disabled'
//
if (
value === ':nth-child' &&
targetNode.nodes.length === 1 &&
targetNode.nodes[0].nodes.length === 1 &&
targetNode.nodes[0].nodes[0].type === 'tag' &&
targetNode.nodes[0].nodes[0].value === 'odd'
) {
if (compoundNot) {
compoundNot = false
return 'even'
}
return 'odd'
}
return 'even'
}
for (let [selector, variantName] of [
[':nth-child', 'nth'],
[':nth-last-child', 'nth-last'],
[':nth-of-type', 'nth-of-type'],
[':nth-last-of-type', 'nth-of-last-type'],
]) {
if (value === selector && targetNode.nodes.length === 1) {
if (
targetNode.nodes[0].nodes.length === 1 &&
targetNode.nodes[0].nodes[0].type === 'tag' &&
isPositiveInteger(targetNode.nodes[0].nodes[0].value)
) {
return `${variantName}-${targetNode.nodes[0].nodes[0].value}`
if (
value === ':nth-child' &&
targetNode.nodes.length === 1 &&
targetNode.nodes[0].nodes.length === 1 &&
targetNode.nodes[0].nodes[0].type === 'tag' &&
targetNode.nodes[0].nodes[0].value === 'even'
) {
if (compoundNot) {
compoundNot = false
return 'odd'
}
return `${variantName}-[${targetNode.nodes[0].toString()}]`
return 'even'
}
for (let [selector, variantName] of [
[':nth-child', 'nth'],
[':nth-last-child', 'nth-last'],
[':nth-of-type', 'nth-of-type'],
[':nth-last-of-type', 'nth-of-last-type'],
]) {
if (value === selector && targetNode.nodes.length === 1) {
if (
targetNode.nodes[0].nodes.length === 1 &&
targetNode.nodes[0].nodes[0].type === 'tag' &&
isPositiveInteger(targetNode.nodes[0].nodes[0].value)
) {
return `${variantName}-${targetNode.nodes[0].nodes[0].value}`
}
return `${variantName}-[${targetNode.nodes[0].toString()}]`
}
}
return null
})(targetNode.value)
if (newVariant === null) continue
// Add `not-` prefix
if (compoundNot) newVariant = `not-${newVariant}`
let parsed = designSystem.parseVariant(newVariant)
if (parsed === null) continue
// Update original variant
changed = true
memcpy(variant, parsed)
}
// Expecting an attribute selector
else if (target.type === 'attribute') {
// Attribute selectors
let attributeKey = target.attribute
let attributeValue = target.value
? target.quoted
? `${target.quoteMark}${target.value}${target.quoteMark}`
: target.value
: null
// Insensitive attribute selectors. E.g.: `[data-foo="value" i]`
// ^
if (target.insensitive && attributeValue) {
attributeValue += ' i'
}
return null
})(targetNode.value)
let operator = target.operator ?? '='
if (newVariant === null) continue
// Migrate `data-*`
if (attributeKey.startsWith('data-')) {
changed = true
attributeKey = attributeKey.slice(5) // Remove `data-`
memcpy(variant, {
kind: 'functional',
root: 'data',
modifier: null,
value:
attributeValue === null
? { kind: 'named', value: attributeKey }
: { kind: 'arbitrary', value: `${attributeKey}${operator}${attributeValue}` },
} satisfies Variant)
}
// Add `not-` prefix
if (compoundNot) newVariant = `not-${newVariant}`
let parsed = designSystem.parseVariant(newVariant)
if (parsed === null) continue
// Update original variant
changed = true
Object.assign(variant, parsed)
}
// Expecting an attribute selector
else if (target.type === 'attribute') {
// Attribute selectors
let attributeKey = target.attribute
let attributeValue = target.value
? target.quoted
? `${target.quoteMark}${target.value}${target.quoteMark}`
: target.value
: null
// Insensitive attribute selectors. E.g.: `[data-foo="value" i]`
// ^
if (target.insensitive && attributeValue) {
attributeValue += ' i'
// Migrate `aria-*`
else if (attributeKey.startsWith('aria-')) {
changed = true
attributeKey = attributeKey.slice(5) // Remove `aria-`
memcpy(variant, {
kind: 'functional',
root: 'aria',
modifier: null,
value:
attributeValue === null
? { kind: 'arbitrary', value: attributeKey } // aria-[foo]
: operator === '=' && target.value === 'true' && !target.insensitive
? { kind: 'named', value: attributeKey } // aria-[foo="true"] or aria-[foo='true'] or aria-[foo=true]
: { kind: 'arbitrary', value: `${attributeKey}${operator}${attributeValue}` }, // aria-[foo~="true"], aria-[foo|="true"], …
} satisfies Variant)
}
}
let operator = target.operator ?? '='
if (prefixedVariant) {
let idx = clone.variants.indexOf(variant)
if (idx === -1) continue
// Migrate `data-*`
if (attributeKey.startsWith('data-')) {
changed = true
attributeKey = attributeKey.slice(5) // Remove `data-`
Object.assign(variant, {
kind: 'functional',
root: 'data',
modifier: null,
value:
attributeValue === null
? { kind: 'named', value: attributeKey }
: { kind: 'arbitrary', value: `${attributeKey}${operator}${attributeValue}` },
} satisfies Variant)
// Ensure we inject the prefixed variant
clone.variants.splice(idx, 1, variant, prefixedVariant)
}
// Migrate `aria-*`
else if (attributeKey.startsWith('aria-')) {
changed = true
attributeKey = attributeKey.slice(5) // Remove `aria-`
Object.assign(variant, {
kind: 'functional',
root: 'aria',
modifier: null,
value:
attributeValue === null
? { kind: 'arbitrary', value: attributeKey } // aria-[foo]
: operator === '=' && target.value === 'true' && !target.insensitive
? { kind: 'named', value: attributeKey } // aria-[foo="true"] or aria-[foo='true'] or aria-[foo=true]
: { kind: 'arbitrary', value: `${attributeKey}${operator}${attributeValue}` }, // aria-[foo~="true"], aria-[foo|="true"], …
} satisfies Variant)
}
}
if (prefixedVariant) {
let idx = clone.variants.indexOf(variant)
if (idx === -1) continue
// Ensure we inject the prefixed variant
clone.variants.splice(idx, 1, variant, prefixedVariant)
}
}