diff --git a/CHANGELOG.md b/CHANGELOG.md index 971ad96296d0..08a0a8282f1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - _Experimental_: add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901)) - Allow using `@variant` with stacked variants (e.g. `@variant hover:focus { … }`) ([#19996](https://github.com/tailwindlabs/tailwindcss/pull/19996)) - Allow using `@variant` with compound variants (e.g. `@variant hover, focus { … }`) ([#19996](https://github.com/tailwindlabs/tailwindcss/pull/19996)) +- Support `--default(…)` in `--value(…)` and `--modifier(…)` for functional `@utility` definitions ([#19989](https://github.com/tailwindlabs/tailwindcss/pull/19989)) ### Fixed diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index 37c5945f6d53..7046663b94e0 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -29574,6 +29574,134 @@ describe('custom utilities', () => { `) }) + test('functional utilities can use `--default(…)` in `--value(…)`', async () => { + let input = css` + @utility tab-* { + tab-size: --value(integer, --default(4)); + } + + @tailwind utilities; + ` + + expect(await compileCss(input, ['tab', 'tab-123'])).toMatchInlineSnapshot(` + ".tab { + tab-size: 4; + } + + .tab-123 { + tab-size: 123; + }" + `) + + expect(await compileCss(input, ['tab-foo'])).toEqual('') + }) + + test('functional utilities can use `--default(…)` in complex expressions', async () => { + let input = css` + @utility tab-* { + tab-size: calc(--value(integer, --default(4)) * 2); + } + + @tailwind utilities; + ` + + expect(await compileCss(input, ['tab', 'tab-123'])).toMatchInlineSnapshot(` + ".tab { + tab-size: 8; + } + + .tab-123 { + tab-size: 246; + }" + `) + + expect(await compileCss(input, ['tab-foo'])).toEqual('') + }) + + test('functional utilities can use `--default(…)` with `--modifier(…)`', async () => { + let input = css` + @utility tab-* { + tab-size: --value(integer, --default(4)); + line-height: --modifier(integer); + } + + @tailwind utilities; + ` + + expect(await compileCss(input, ['tab', 'tab/25'])).toMatchInlineSnapshot(` + ".tab\\/25 { + tab-size: 4; + line-height: 25; + } + + .tab { + tab-size: 4; + }" + `) + + expect(await compileCss(input, ['tab/foo'])).toEqual('') + }) + + test('functional utilities can use `--default(…)` in `--modifier(…)`', async () => { + let input = css` + @utility tab-* { + tab-size: --value(integer); + line-height: --modifier(integer, --default(1)); + } + + @tailwind utilities; + ` + + expect(await compileCss(input, ['tab-123', 'tab-123/25'])).toMatchInlineSnapshot(` + ".tab-123 { + tab-size: 123; + line-height: 1; + } + + .tab-123\\/25 { + tab-size: 123; + line-height: 25; + }" + `) + + expect(await compileCss(input, ['tab-123/foo'])).toEqual('') + }) + + test('functional utilities can use `--default(…)` in `--value(…)` and `--modifier(…)`', async () => { + let input = css` + @utility tab-* { + tab-size: --value(integer, --default(12)); + line-height: --modifier(integer, --default(34)); + } + + @tailwind utilities; + ` + + expect(await compileCss(input, ['tab', 'tab/1', 'tab-1', 'tab-1/1'])).toMatchInlineSnapshot(` + ".tab { + tab-size: 12; + line-height: 34; + } + + .tab-1 { + tab-size: 1; + line-height: 34; + } + + .tab-1\\/1 { + tab-size: 1; + line-height: 1; + } + + .tab\\/1 { + tab-size: 12; + line-height: 1; + }" + `) + + expect(await compileCss(input, ['tab-123/foo'])).toEqual('') + }) + test('modifiers', async () => { let input = css` @theme reference { diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index 2aa25fc2547f..d5389631acb0 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -5956,6 +5956,8 @@ export function createCssUtility(node: AtRule) { // - `--value(number)` resolves a bare value of type number // - `--value([number])` resolves an arbitrary value of type number // - `--value(--color)` resolves a theme value in the `color` namespace + // - `--value(--default(4))` resolves to a default value when only the + // root of the functional utility was used. // - `--value(number, [number])` resolves a bare value of type number or an // arbitrary value of type number in order. // @@ -6069,7 +6071,7 @@ export function createCssUtility(node: AtRule) { arg = arg.replace(/(-\*){2,}/g, '-*') // Ensure trailing `-*` exists if `-*` isn't present yet - if (arg[0] === '-' && arg[1] === '-' && !arg.includes('-*')) { + if (arg[0] === '-' && arg[1] === '-' && !arg.includes('(') && !arg.includes('-*')) { arg += '-*' } @@ -6135,9 +6137,8 @@ export function createCssUtility(node: AtRule) { let value = candidate.value let modifier = candidate.modifier - // A value is required for functional utilities, if you want to accept - // just `tab-size`, you'd have to use a static utility. - if (value === null) return + // Functional CSS utilities must resolve at least one `--value(…)`. + // Use `--default(…)` inside `--value(…)` for the omitted-value case. // Whether `--value(…)` was used let usedValueFn = false @@ -6197,30 +6198,21 @@ export function createCssUtility(node: AtRule) { } // Drop the declaration in case we couldn't resolve the value - usedValueFn ||= false shouldRemoveDeclaration = true return WalkAction.Stop } // Modifier function, e.g.: `--modifier(integer)` else if (fnNode.value === '--modifier') { - // If there is no modifier present in the candidate, then the - // declaration can be removed. - if (modifier === null) { - shouldRemoveDeclaration = true - return WalkAction.Stop - } - usedModifierFn = true - let replacement = resolveValueFunction(modifier, fnNode, designSystem) - if (replacement) { + let resolved = resolveValueFunction(modifier, fnNode, designSystem) + if (resolved) { resolvedModifierFn = true - return WalkAction.ReplaceSkip(replacement.nodes) + return WalkAction.ReplaceSkip(resolved.nodes) } // Drop the declaration in case we couldn't resolve the value - usedModifierFn ||= false shouldRemoveDeclaration = true return WalkAction.Stop } @@ -6238,7 +6230,7 @@ export function createCssUtility(node: AtRule) { if (!usedValueFn || !resolvedValueFn) return null // Used `--modifier(…)` but nothing resolved - if (usedModifierFn && !resolvedModifierFn) return null + if (usedModifierFn && !resolvedModifierFn && modifier !== null) return null // Resolved `--value(ratio)` and `--modifier(…)`, which is invalid if (resolvedRatioValue && resolvedModifierFn) return null @@ -6309,13 +6301,23 @@ export function createCssUtility(node: AtRule) { } function resolveValueFunction( - value: NonNullable< + value: | Extract['value'] - | Extract['modifier'] - >, + | Extract['modifier'], fn: ValueParser.ValueFunctionNode, designSystem: DesignSystem, ): { nodes: ValueParser.ValueAstNode[]; ratio?: boolean } | undefined { + // No value provided, we can try `--default(…)` + if (value === null) { + for (let arg of fn.nodes) { + // Resolve default value, e.g.: `--default(…)` + if (arg.kind === 'function' && arg.value === '--default') { + return { nodes: arg.nodes } + } + } + return + } + for (let arg of fn.nodes) { // Resolve literal value, e.g.: `--modifier('closest-side')` if (