diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b14b8cefcef..a2b584476fd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add support for `aria`, `supports`, and `data` variants defined in JS config files ([#14407](https://github.com/tailwindlabs/tailwindcss/pull/14407)) + ### Fixed - Support `borderRadius.*` as an alias for `--radius-*` when using dot notation inside the `theme()` function ([#14436](https://github.com/tailwindlabs/tailwindcss/pull/14436)) diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index 6f9b5ce1091b..b21d524f1456 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -10,6 +10,7 @@ import { resolveConfig } from './config/resolve-config' import type { UserConfig } from './config/types' import { darkModePlugin } from './dark-mode' import { buildPluginApi, type CssPluginOptions, type Plugin } from './plugin-api' +import { registerThemeVariantOverrides } from './theme-variants' export async function applyCompatibilityHooks({ designSystem, @@ -185,6 +186,8 @@ export async function applyCompatibilityHooks({ // core utilities already read from. applyConfigToTheme(designSystem, userConfig) + registerThemeVariantOverrides(resolvedConfig, designSystem) + // Replace `resolveThemeValue` with a version that is backwards compatible // with dot-notation but also aware of any JS theme configurations registered // by plugins or JS config files. This is significantly slower than just diff --git a/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts b/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts index 65066ee07bf9..d1be15ba6b59 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts @@ -1,9 +1,9 @@ -import { test } from 'vitest' +import { expect, test } from 'vitest' import { buildDesignSystem } from '../design-system' import { Theme } from '../theme' import { applyConfigToTheme } from './apply-config-to-theme' -test('Config values can be merged into the theme', ({ expect }) => { +test('Config values can be merged into the theme', () => { let theme = new Theme() let design = buildDesignSystem(theme) diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts index 9e8849513b6a..a4477ec1496c 100644 --- a/packages/tailwindcss/src/compat/config.test.ts +++ b/packages/tailwindcss/src/compat/config.test.ts @@ -1,11 +1,11 @@ -import { describe, test } from 'vitest' +import { describe, expect, test } from 'vitest' import { compile } from '..' import plugin from '../plugin' import { flattenColorPalette } from './flatten-color-palette' const css = String.raw -test('Config files can add content', async ({ expect }) => { +test('Config files can add content', async () => { let input = css` @tailwind utilities; @config "./config.js"; @@ -18,7 +18,7 @@ test('Config files can add content', async ({ expect }) => { expect(compiler.globs).toEqual([{ origin: './config.js', pattern: './file.txt' }]) }) -test('Config files can change dark mode (media)', async ({ expect }) => { +test('Config files can change dark mode (media)', async () => { let input = css` @tailwind utilities; @config "./config.js"; @@ -38,7 +38,7 @@ test('Config files can change dark mode (media)', async ({ expect }) => { `) }) -test('Config files can change dark mode (selector)', async ({ expect }) => { +test('Config files can change dark mode (selector)', async () => { let input = css` @tailwind utilities; @config "./config.js"; @@ -58,7 +58,7 @@ test('Config files can change dark mode (selector)', async ({ expect }) => { `) }) -test('Config files can change dark mode (variant)', async ({ expect }) => { +test('Config files can change dark mode (variant)', async () => { let input = css` @tailwind utilities; @config "./config.js"; @@ -78,7 +78,7 @@ test('Config files can change dark mode (variant)', async ({ expect }) => { `) }) -test('Config files can add plugins', async ({ expect }) => { +test('Config files can add plugins', async () => { let input = css` @tailwind utilities; @config "./config.js"; @@ -106,7 +106,7 @@ test('Config files can add plugins', async ({ expect }) => { `) }) -test('Plugins loaded from config files can contribute to the config', async ({ expect }) => { +test('Plugins loaded from config files can contribute to the config', async () => { let input = css` @tailwind utilities; @config "./config.js"; @@ -132,7 +132,7 @@ test('Plugins loaded from config files can contribute to the config', async ({ e `) }) -test('Config file presets can contribute to the config', async ({ expect }) => { +test('Config file presets can contribute to the config', async () => { let input = css` @tailwind utilities; @config "./config.js"; @@ -158,7 +158,7 @@ test('Config file presets can contribute to the config', async ({ expect }) => { `) }) -test('Config files can affect the theme', async ({ expect }) => { +test('Config files can affect the theme', async () => { let input = css` @tailwind utilities; @config "./config.js"; @@ -197,7 +197,7 @@ test('Config files can affect the theme', async ({ expect }) => { `) }) -test('Variants in CSS overwrite variants from plugins', async ({ expect }) => { +test('Variants in CSS overwrite variants from plugins', async () => { let input = css` @tailwind utilities; @config "./config.js"; @@ -334,7 +334,7 @@ describe('theme callbacks', () => { }) describe('theme overrides order', () => { - test('user theme > js config > default theme', async ({ expect }) => { + test('user theme > js config > default theme', async () => { let input = css` @theme default { --color-red: red; @@ -373,7 +373,7 @@ describe('theme overrides order', () => { `) }) - test('user theme > js config > default theme (with nested object)', async ({ expect }) => { + test('user theme > js config > default theme (with nested object)', async () => { let input = css` @theme default { --color-slate-100: #000100; @@ -498,7 +498,7 @@ describe('theme overrides order', () => { }) describe('default font family compatibility', () => { - test('overriding `fontFamily.sans` sets `--default-font-family`', async ({ expect }) => { + test('overriding `fontFamily.sans` sets `--default-font-family`', async () => { let input = css` @theme default { --default-font-family: var(--font-family-sans); @@ -756,7 +756,7 @@ describe('default font family compatibility', () => { `) }) - test('overriding `fontFamily.mono` sets `--default-mono-font-family`', async ({ expect }) => { + test('overriding `fontFamily.mono` sets `--default-mono-font-family`', async () => { let input = css` @theme default { --default-mono-font-family: var(--font-family-mono); @@ -978,3 +978,89 @@ describe('default font family compatibility', () => { `) }) }) + +test('creates variants for `data`, `supports`, and `aria` theme options at the same level as the core utility ', async () => { + let input = css` + @tailwind utilities; + @config "./config.js"; + ` + + let compiler = await compile(input, { + loadConfig: async () => ({ + theme: { + extend: { + aria: { + polite: 'live="polite"', + }, + supports: { + 'child-combinator': 'selector(h2 > p)', + foo: 'bar', + }, + data: { + checked: 'ui~="checked"', + }, + }, + }, + }), + }) + + expect( + compiler.build([ + 'aria-polite:underline', + 'supports-child-combinator:underline', + 'supports-foo:underline', + 'data-checked:underline', + + // Ensure core variants still work + 'aria-hidden:flex', + 'supports-grid:flex', + 'data-foo:flex', + + // The `print` variant should still be sorted last, even after registering + // the other custom variants. + 'print:flex', + ]), + ).toMatchInlineSnapshot(` + ".aria-polite\\:underline { + &[aria-live="polite"] { + text-decoration-line: underline; + } + } + .aria-hidden\\:flex { + &[aria-hidden="true"] { + display: flex; + } + } + .data-checked\\:underline { + &[data-ui~="checked"] { + text-decoration-line: underline; + } + } + .data-foo\\:flex { + &[data-foo] { + display: flex; + } + } + .supports-child-combinator\\:underline { + @supports selector(h2 > p) { + text-decoration-line: underline; + } + } + .supports-foo\\:underline { + @supports (bar: var(--tw)) { + text-decoration-line: underline; + } + } + .supports-grid\\:flex { + @supports (grid: var(--tw)) { + display: flex; + } + } + .print\\:flex { + @media print { + display: flex; + } + } + " + `) +}) diff --git a/packages/tailwindcss/src/compat/config/resolve-config.test.ts b/packages/tailwindcss/src/compat/config/resolve-config.test.ts index 2ce588f4dd1d..818ff8373d85 100644 --- a/packages/tailwindcss/src/compat/config/resolve-config.test.ts +++ b/packages/tailwindcss/src/compat/config/resolve-config.test.ts @@ -1,9 +1,9 @@ -import { test } from 'vitest' +import { expect, test } from 'vitest' import { buildDesignSystem } from '../../design-system' import { Theme } from '../../theme' import { resolveConfig } from './resolve-config' -test('top level theme keys are replaced', ({ expect }) => { +test('top level theme keys are replaced', () => { let design = buildDesignSystem(new Theme()) let config = resolveConfig(design, [ @@ -52,7 +52,7 @@ test('top level theme keys are replaced', ({ expect }) => { }) }) -test('theme can be extended', ({ expect }) => { +test('theme can be extended', () => { let design = buildDesignSystem(new Theme()) let config = resolveConfig(design, [ @@ -164,7 +164,7 @@ test('theme keys can reference other theme keys using the theme function regardl }) }) -test('theme keys can read from the CSS theme', ({ expect }) => { +test('theme keys can read from the CSS theme', () => { let theme = new Theme() theme.add('--color-green', 'green') diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts index 1a11882f35a9..c4db0385249b 100644 --- a/packages/tailwindcss/src/compat/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -8,7 +8,7 @@ import type { CssInJs, PluginAPI } from './plugin-api' const css = String.raw describe('theme', async () => { - test('plugin theme can contain objects', async ({ expect }) => { + test('plugin theme can contain objects', async () => { let input = css` @tailwind utilities; @plugin "my-plugin"; @@ -68,7 +68,7 @@ describe('theme', async () => { `) }) - test('plugin theme can extend colors', async ({ expect }) => { + test('plugin theme can extend colors', async () => { let input = css` @theme reference { --color-red-500: #ef4444; @@ -208,7 +208,7 @@ describe('theme', async () => { `) }) - test('plugin theme can have opacity modifiers', async ({ expect }) => { + test('plugin theme can have opacity modifiers', async () => { let input = css` @tailwind utilities; @theme { @@ -251,7 +251,7 @@ describe('theme', async () => { " `) }) - test('theme value functions are resolved correctly regardless of order', async ({ expect }) => { + test('theme value functions are resolved correctly regardless of order', async () => { let input = css` @tailwind utilities; @plugin "my-plugin"; @@ -302,7 +302,7 @@ describe('theme', async () => { `) }) - test('plugins can override the default key', async ({ expect }) => { + test('plugins can override the default key', async () => { let input = css` @tailwind utilities; @plugin "my-plugin"; @@ -342,7 +342,7 @@ describe('theme', async () => { `) }) - test('plugins can read CSS theme keys using the old theme key notation', async ({ expect }) => { + test('plugins can read CSS theme keys using the old theme key notation', async () => { let input = css` @tailwind utilities; @plugin "my-plugin"; @@ -397,7 +397,7 @@ describe('theme', async () => { `) }) - test('CSS theme values are merged with JS theme values', async ({ expect }) => { + test('CSS theme values are merged with JS theme values', async () => { let input = css` @tailwind utilities; @plugin "my-plugin"; @@ -448,7 +448,7 @@ describe('theme', async () => { `) }) - test('CSS theme defaults take precedence over JS theme defaults', async ({ expect }) => { + test('CSS theme defaults take precedence over JS theme defaults', async () => { let input = css` @tailwind utilities; @plugin "my-plugin"; @@ -492,7 +492,7 @@ describe('theme', async () => { `) }) - test('CSS theme values take precedence even over non-object JS values', async ({ expect }) => { + test('CSS theme values take precedence even over non-object JS values', async () => { let input = css` @tailwind utilities; @plugin "my-plugin"; @@ -530,7 +530,7 @@ describe('theme', async () => { }) }) - test('all necessary theme keys support bare values', async ({ expect }) => { + test('all necessary theme keys support bare values', async () => { let input = css` @tailwind utilities; @plugin "my-plugin"; @@ -768,7 +768,7 @@ describe('theme', async () => { `) }) - test('theme keys can derive from other theme keys', async ({ expect }) => { + test('theme keys can derive from other theme keys', async () => { let input = css` @tailwind utilities; @plugin "my-plugin"; @@ -834,7 +834,7 @@ describe('theme', async () => { expect(fn).toHaveBeenNthCalledWith(3, 'ease-out') }) - test('nested theme key lookups work even for flattened keys', async ({ expect }) => { + test('nested theme key lookups work even for flattened keys', async () => { let input = css` @tailwind utilities; @plugin "my-plugin"; @@ -889,7 +889,7 @@ describe('theme', async () => { expect(fn).toHaveBeenCalledWith({}) // Present in the resolved config }) - test('Candidates can match multiple utility definitions', async ({ expect }) => { + test('Candidates can match multiple utility definitions', async () => { let input = css` @tailwind utilities; @plugin "my-plugin"; diff --git a/packages/tailwindcss/src/compat/theme-variants.ts b/packages/tailwindcss/src/compat/theme-variants.ts new file mode 100644 index 000000000000..76fafade30e2 --- /dev/null +++ b/packages/tailwindcss/src/compat/theme-variants.ts @@ -0,0 +1,68 @@ +import type { DesignSystem } from '../design-system' +import type { ResolvedConfig } from './config/types' + +export function registerThemeVariantOverrides(config: ResolvedConfig, designSystem: DesignSystem) { + let ariaVariants = config.theme.aria || {} + let supportsVariants = config.theme.supports || {} + let dataVariants = config.theme.data || {} + + if (Object.keys(ariaVariants).length > 0) { + let coreAria = designSystem.variants.get('aria') + let applyFn = coreAria?.applyFn + let compounds = coreAria?.compounds + designSystem.variants.functional( + 'aria', + (ruleNode, variant) => { + let value = variant.value + if (value && value.kind === 'named' && value.value in ariaVariants) { + return applyFn?.(ruleNode, { + ...variant, + value: { kind: 'arbitrary', value: ariaVariants[value.value] as string }, + }) + } + return applyFn?.(ruleNode, variant) + }, + { compounds }, + ) + } + + if (Object.keys(supportsVariants).length > 0) { + let coreSupports = designSystem.variants.get('supports') + let applyFn = coreSupports?.applyFn + let compounds = coreSupports?.compounds + designSystem.variants.functional( + 'supports', + (ruleNode, variant) => { + let value = variant.value + if (value && value.kind === 'named' && value.value in supportsVariants) { + return applyFn?.(ruleNode, { + ...variant, + value: { kind: 'arbitrary', value: supportsVariants[value.value] as string }, + }) + } + return applyFn?.(ruleNode, variant) + }, + { compounds }, + ) + } + + if (Object.keys(dataVariants).length > 0) { + let coreData = designSystem.variants.get('data') + let applyFn = coreData?.applyFn + let compounds = coreData?.compounds + designSystem.variants.functional( + 'data', + (ruleNode, variant) => { + let value = variant.value + if (value && value.kind === 'named' && value.value in dataVariants) { + return applyFn?.(ruleNode, { + ...variant, + value: { kind: 'arbitrary', value: dataVariants[value.value] as string }, + }) + } + return applyFn?.(ruleNode, variant) + }, + { compounds }, + ) + } +} diff --git a/packages/tailwindcss/src/plugin.test.ts b/packages/tailwindcss/src/plugin.test.ts index d04fafd09d7f..5af7ff1d4599 100644 --- a/packages/tailwindcss/src/plugin.test.ts +++ b/packages/tailwindcss/src/plugin.test.ts @@ -1,10 +1,10 @@ -import { test } from 'vitest' +import { expect, test } from 'vitest' import { compile } from '.' import plugin from './plugin' const css = String.raw -test('plugin', async ({ expect }) => { +test('plugin', async () => { let input = css` @plugin "my-plugin"; ` @@ -31,7 +31,7 @@ test('plugin', async ({ expect }) => { `) }) -test('plugin.withOptions', async ({ expect }) => { +test('plugin.withOptions', async () => { let input = css` @plugin "my-plugin"; `