Migrate aria theme keys (#18815)
This PR migrates `aria` theme keys when migrating from Tailwind CSS v3 to v4. While working on improving some of the error messages to get more insights into why migrating the JS file changed (https://github.com/tailwindlabs/tailwindcss/pull/18808), I ran into an issue where I couldn't think of a good comment to why `aria` theme keys were not being migrated. (Internally we have `aria` "blocked"). So instead of figuring out a good error message..., I just went ahead and added the migration for `aria` theme keys. Let's imagine you have the following Tailwind CSS v3 configuration: ```ts export default { content: ['./src/**/*.html'], theme: { extend: { aria: { // Built-in (not really, but visible because of intellisense) busy: 'busy="true"', // Automatically handled by bare values foo: 'foo="true"', // ^^^ ^^^ ← same names // Not automatically handled by bare values because names differ bar: 'baz="true"', // ^^^ ^^^ ← different names // Completely custom asc: 'sort="ascending"', desc: 'sort="descending"', }, }, }, } ``` Then we would generate the following Tailwind CSS v4 CSS: ```css @custom-variant aria-bar (&[aria-baz="true"]); @custom-variant aria-asc (&[aria-sort="ascending"]); @custom-variant aria-desc (&[aria-sort="descending"]); ``` Notice how we didn't generate a custom variant for `aria-busy` or `aria-foo` because those are automatically handled by bare values. We could also emit a comment near the CSS to warn about the fact that `@custom-variant` will always be sorted _after_ any other built-in variants. This could result in slightly different behavior, or different order of classes when using `prettier-plugin-tailwindcss`. I don't know how important this is, because before this PR we would just use `@config './tailwind.config.js';`. Edit: when using the `@config` we override `aria` and extend it, which means that it would be in the expected order 🤔 --------- Co-authored-by: Jordan Pittman <thecrypticace@gmail.com>
This commit is contained in:
parent
8165e04564
commit
9e498a3e78
3 changed files with 143 additions and 26 deletions
|
|
@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Discard matched variants with unknown named values ([#18799](https://github.com/tailwindlabs/tailwindcss/pull/18799))
|
||||
- Discard matched variants with non-string values ([#18799](https://github.com/tailwindlabs/tailwindcss/pull/18799))
|
||||
- Show suggestions for known `matchVariant` values ([#18798](https://github.com/tailwindlabs/tailwindcss/pull/18798))
|
||||
- Migrate `aria` theme keys to `@custom-variant` ([#18815](https://github.com/tailwindlabs/tailwindcss/pull/18815))
|
||||
|
||||
## [4.1.12] - 2025-08-13
|
||||
|
||||
|
|
|
|||
|
|
@ -965,6 +965,89 @@ test(
|
|||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'migrate aria theme keys to custom variants',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "^3",
|
||||
"@tailwindcss/upgrade": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'tailwind.config.ts': ts`
|
||||
export default {
|
||||
content: {
|
||||
relative: true,
|
||||
files: ['./src/**/*.html'],
|
||||
},
|
||||
theme: {
|
||||
extend: {
|
||||
aria: {
|
||||
// Built-in (not really, but visible because of intellisense)
|
||||
busy: 'busy="true"',
|
||||
|
||||
// Automatically handled by bare values
|
||||
foo: 'foo="true"',
|
||||
|
||||
// Quotes are optional in CSS for these kinds of attribute
|
||||
// selectors
|
||||
bar: 'bar=true',
|
||||
|
||||
// Not automatically handled by bare values because names differ
|
||||
baz: 'qux="true"',
|
||||
|
||||
// Completely custom
|
||||
asc: 'sort="ascending"',
|
||||
desc: 'sort="descending"',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`,
|
||||
'src/input.css': css`
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- src/input.css ---
|
||||
@import 'tailwindcss';
|
||||
|
||||
@custom-variant aria-baz (&[aria-qux="true"]);
|
||||
@custom-variant aria-asc (&[aria-sort="ascending"]);
|
||||
@custom-variant aria-desc (&[aria-sort="descending"]);
|
||||
|
||||
/*
|
||||
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentcolor);
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
describe('border compatibility', () => {
|
||||
test(
|
||||
'migrate border compatibility',
|
||||
|
|
|
|||
|
|
@ -97,13 +97,14 @@ async function migrateTheme(
|
|||
designSystem: DesignSystem,
|
||||
unresolvedConfig: Config,
|
||||
base: string,
|
||||
): Promise<string | null> {
|
||||
): Promise<string> {
|
||||
// Resolve the config file without applying plugins and presets, as these are
|
||||
// migrated to CSS separately.
|
||||
let configToResolve: ConfigFile = {
|
||||
base,
|
||||
config: { ...unresolvedConfig, plugins: [], presets: undefined },
|
||||
reference: false,
|
||||
src: undefined,
|
||||
}
|
||||
let { resolvedConfig, replacedThemeKeys } = resolveConfig(designSystem, [configToResolve])
|
||||
|
||||
|
|
@ -113,10 +114,47 @@ async function migrateTheme(
|
|||
|
||||
removeUnnecessarySpacingKeys(designSystem, resolvedConfig, replacedThemeKeys)
|
||||
|
||||
let css = ''
|
||||
let prevSectionKey = ''
|
||||
let css = '\n@tw-bucket theme {\n'
|
||||
css += `\n@theme {\n`
|
||||
let containsThemeKeys = false
|
||||
let themeSection: string[] = []
|
||||
let keyframesCss = ''
|
||||
let variants = new Map<string, string>()
|
||||
|
||||
// Special handling of specific theme keys:
|
||||
{
|
||||
if ('keyframes' in resolvedConfig.theme) {
|
||||
keyframesCss += keyframesToCss(resolvedConfig.theme.keyframes)
|
||||
delete resolvedConfig.theme.keyframes
|
||||
}
|
||||
|
||||
if ('container' in resolvedConfig.theme) {
|
||||
let rules = buildCustomContainerUtilityRules(resolvedConfig.theme.container, designSystem)
|
||||
if (rules.length > 0) {
|
||||
// Using `theme` instead of `utility` so it sits before the `@layer
|
||||
// base` with compatibility CSS. While this is technically a utility, it
|
||||
// makes a bit more sense to emit this closer to the `@theme` values
|
||||
// since it is needed for backwards compatibility.
|
||||
css += `\n@tw-bucket theme {\n`
|
||||
css += toCss([atRule('@utility', 'container', rules)])
|
||||
css += '}\n' // @tw-bucket
|
||||
}
|
||||
delete resolvedConfig.theme.container
|
||||
}
|
||||
|
||||
if ('aria' in resolvedConfig.theme) {
|
||||
for (let [key, value] of Object.entries(resolvedConfig.theme.aria ?? {})) {
|
||||
// Will be handled by bare values if the names match.
|
||||
// E.g.: `aria-foo:flex` should produce `[aria-foo="true"]`
|
||||
if (new RegExp(`^${key}=(['"]?)true\\1$`).test(`${value}`)) continue
|
||||
|
||||
// Create custom variant
|
||||
variants.set(`aria-${key}`, `&[aria-${value}]`)
|
||||
}
|
||||
delete resolvedConfig.theme.aria
|
||||
}
|
||||
}
|
||||
|
||||
// Convert theme values to CSS custom properties
|
||||
for (let [key, value] of themeableValues(resolvedConfig.theme)) {
|
||||
if (typeof value !== 'string' && typeof value !== 'number') {
|
||||
continue
|
||||
|
|
@ -151,14 +189,9 @@ async function migrateTheme(
|
|||
}
|
||||
}
|
||||
|
||||
if (key[0] === 'keyframes') {
|
||||
continue
|
||||
}
|
||||
containsThemeKeys = true
|
||||
|
||||
let sectionKey = createSectionKey(key)
|
||||
if (sectionKey !== prevSectionKey) {
|
||||
css += `\n`
|
||||
themeSection.push('')
|
||||
prevSectionKey = sectionKey
|
||||
}
|
||||
|
||||
|
|
@ -166,36 +199,36 @@ async function migrateTheme(
|
|||
resetNamespaces.set(key[0], true)
|
||||
let property = keyPathToCssProperty([key[0]])
|
||||
if (property !== null) {
|
||||
css += ` ${escape(`--${property}`)}-*: initial;\n`
|
||||
themeSection.push(` ${escape(`--${property}`)}-*: initial;`)
|
||||
}
|
||||
}
|
||||
|
||||
let property = keyPathToCssProperty(key)
|
||||
if (property !== null) {
|
||||
css += ` ${escape(`--${property}`)}: ${value};\n`
|
||||
themeSection.push(` ${escape(`--${property}`)}: ${value};`)
|
||||
}
|
||||
}
|
||||
|
||||
if ('keyframes' in resolvedConfig.theme) {
|
||||
containsThemeKeys = true
|
||||
css += '\n' + keyframesToCss(resolvedConfig.theme.keyframes)
|
||||
if (keyframesCss) {
|
||||
themeSection.push('', keyframesCss)
|
||||
}
|
||||
|
||||
if (!containsThemeKeys) {
|
||||
return null
|
||||
if (themeSection.length > 0) {
|
||||
css += `\n@tw-bucket theme {\n`
|
||||
css += `\n@theme {\n`
|
||||
css += themeSection.join('\n') + '\n'
|
||||
css += '}\n' // @theme
|
||||
css += '}\n' // @tw-bucket
|
||||
}
|
||||
|
||||
css += '}\n' // @theme
|
||||
|
||||
if ('container' in resolvedConfig.theme) {
|
||||
let rules = buildCustomContainerUtilityRules(resolvedConfig.theme.container, designSystem)
|
||||
if (rules.length > 0) {
|
||||
css += '\n' + toCss([atRule('@utility', 'container', rules)])
|
||||
if (variants.size > 0) {
|
||||
css += '\n@tw-bucket custom-variant {\n'
|
||||
for (let [name, selector] of variants) {
|
||||
css += `@custom-variant ${name} (${selector});\n`
|
||||
}
|
||||
css += '}\n'
|
||||
}
|
||||
|
||||
css += '}\n' // @tw-bucket
|
||||
|
||||
return css
|
||||
}
|
||||
|
||||
|
|
@ -356,7 +389,7 @@ const ALLOWED_THEME_KEYS = [
|
|||
// Used by @tailwindcss/container-queries
|
||||
'containers',
|
||||
]
|
||||
const BLOCKED_THEME_KEYS = ['supports', 'data', 'aria']
|
||||
const BLOCKED_THEME_KEYS = ['supports', 'data']
|
||||
function onlyAllowedThemeValues(theme: ThemeConfig): boolean {
|
||||
for (let key of Object.keys(theme)) {
|
||||
if (!ALLOWED_THEME_KEYS.includes(key)) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue