Support --default(…) in --value(…) and --modifier(…) to support fallback values (#19989)
This PR adds a new `--default(…)` option that can be used inside
`--value(…)` or `--modifier(…)` such that functional utilities without
an explicit value/modifier can still be defined as a functional utility.
It would also allow you to use a functional utility without a value and
_with_ a modifier, e.g.: `shadow/50`.
---
This allows us to re-implement functional utilities with a default value
in CSS using `@utility`.
Used the explicit `--default(…)` argument of `--value(…)` for a few
reasons.
1. It's explicit about being a falllback value. If you have `@utility
foo-*`, then you want to be able to use `foo`, but `foo-bad` should not
compile.
2. When `--value(…)` is used in (complex) property values (think a bunch
of `calc(…)` expressions), then we don't need a separate property for
this.
One of the ideas was to have a literal fallback:
```css
@utility tab-* {
tab-size: 4;
tab-size: --value(number);
}
```
For `tab`, this would compile to:
```css
.tab {
tab-size: 4;
}
```
For `tab-123`, this would compile to:
```css
.tab {
tab-size: 4;
tab-size: 123;
}
```
Getting rid of the `tab-size: 4` would be an option, but it's a common
pattern in real CSS for fallback values (think hex background color,
over a more modern `oklch` color).
For `tab-foo`, this would compile to:
```css
.tab {
tab-size: 4;
}
```
Which means that we have an infinite amount classes that would result in
the same class, which is bad. We could special case this one because the
internal `value` would still be `null`, but it might be too confusing.
This syntax without the `--default(…)` also means repetition of certain
properties. Add `--modifier(…)` to the mix, and there is even more
repetition going on.
Another option to consider is that the default fallback is just another
option in the `--value(…, 4)`, but if a default fallback is a keyword,
then there is a chance that this might conflict with actual keywords we
interpret.
Main motivation is to be able to re-implement utilities such as
`shadow/50` purely in CSS. It's also something we support in the JS
based APIs, but not in the CSS based one, so while it's a "new" feature,
it's more like a missing feature right now, and often a reason for
people to use the JS based APIs instead.
For consistency reasons, this is also implemented for `--modifier(…)`
such that you can use a default value there. E.g. when re-implementing
`text-sm` where a default `line-height` is set without the explicit use
of a modifier.
Fixes: https://github.com/tailwindlabs/tailwindcss/issues/16824
## Test plan
1. Added a handful of new tests to make sure this functionality works
2. Existing tests still pass
This commit is contained in:
parent
0f6f7d480f
commit
08cad84bbe
3 changed files with 151 additions and 20 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<Candidate, { kind: 'functional' }>['value']
|
||||
| Extract<Candidate, { kind: 'functional' }>['modifier']
|
||||
>,
|
||||
| Extract<Candidate, { kind: 'functional' }>['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 (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue