From f2ebb8eb826ad1bf14a93926452bdfbb713029d3 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 14 Oct 2024 14:08:41 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20`var(=E2=80=A6)`=20as=20the=20opacity=20v?= =?UTF-8?q?alue=20inside=20the=20`theme(=E2=80=A6)`=20function=20(#14653)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inside the `theme(…)` function, we can use the `/` character for applying an opacity. For example `theme(colors.red.500 / 50%)` will apply a 50% opacity to the `colors.red.500` value. However, if you used a variable instead of the hardcoded `50%` value, then this was not parsed correctly. E.g.: `theme(colors.red.500 / var(--opacity))` _If_ we have this exact syntax (with the spaces), then it parses, but some information is lost: ```html
``` Results in: ```css .bg-\[theme\(colors\.red\.500_\/_var\(--opacity\)\)\] { background-color: color-mix(in srgb, #ef4444 calc(var * 100%), transparent); } ``` Notice that the `var(--opacity)` is just parsed as `var`, and the `--opacity` is lost. Additionally, if we drop the spaces, then it doesn't parse at all: ```html
``` Results in: ```css ``` This means that we have to handle 2 issues to make this work: 1. We have to properly handle the `/` character as a proper separator. 2. If we have sub-functions, we have to make sure to print them in full (instead of only the very first node (`var` in this case)). --- CHANGELOG.md | 1 + .../tailwindcss/src/css-functions.test.ts | 64 +++++++++++++++++++ packages/tailwindcss/src/css-functions.ts | 2 +- packages/tailwindcss/src/value-parser.test.ts | 14 ++++ packages/tailwindcss/src/value-parser.ts | 2 + 5 files changed, 82 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b049c4cf7..1c2c86eb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Pass options when using `addComponents` and `matchComponents` ([#14590](https://github.com/tailwindlabs/tailwindcss/pull/14590)) - Ensure `boxShadow` and `animation` theme keys in JS config files are accessible under `--shadow-*` and `--animate-*` using the `theme()` function ([#14642](https://github.com/tailwindlabs/tailwindcss/pull/14642)) - Ensure all theme keys with new names are also accessible under their old names when using the `theme()` function with the legacy dot notation syntax ([#14642](https://github.com/tailwindlabs/tailwindcss/pull/14642)) +- Ensure `var(…)` can be used as the opacity value inside the `theme([path] / [modifier])` function ([#14653](https://github.com/tailwindlabs/tailwindcss/pull/14653)) - _Upgrade (experimental)_: Ensure CSS before a layer stays unlayered when running codemods ([#14596](https://github.com/tailwindlabs/tailwindcss/pull/14596)) - _Upgrade (experimental)_: Resolve issues where some prefixed candidates were not properly migrated ([#14600](https://github.com/tailwindlabs/tailwindcss/pull/14600)) diff --git a/packages/tailwindcss/src/css-functions.test.ts b/packages/tailwindcss/src/css-functions.test.ts index cb54f3406..a2df1994f 100644 --- a/packages/tailwindcss/src/css-functions.test.ts +++ b/packages/tailwindcss/src/css-functions.test.ts @@ -136,6 +136,27 @@ describe('theme function', () => { `) }) + test('theme(colors.red.500/75%)', async () => { + expect( + await compileCss(css` + @theme { + --color-red-500: #f00; + } + .red { + color: theme(colors.red.500/75%); + } + `), + ).toMatchInlineSnapshot(` + ":root { + --color-red-500: red; + } + + .red { + color: #ff0000bf; + }" + `) + }) + test('theme(colors.red.500 / 75%)', async () => { expect( await compileCss(css` @@ -178,6 +199,49 @@ describe('theme function', () => { `) }) + test('theme(colors.red.500/var(--opacity))', async () => { + expect( + await compileCss(css` + @theme { + --color-red-500: #f00; + } + .red { + color: theme(colors.red.500/var(--opacity)); + } + `), + ).toMatchInlineSnapshot(` + ":root { + --color-red-500: red; + } + + .red { + color: color-mix(in srgb, red calc(var(--opacity) * 100%), transparent); + }" + `) + }) + + test('theme(colors.red.500/var(--opacity,50%))', async () => { + expect( + await compileCss(css` + @theme { + --color-red-500: #f00; + } + .red { + /* prettier-ignore */ + color: theme(colors.red.500/var(--opacity,50%)); + } + `), + ).toMatchInlineSnapshot(` + ":root { + --color-red-500: red; + } + + .red { + color: color-mix(in srgb, red calc(var(--opacity, 50%) * 100%), transparent); + }" + `) + }) + test('theme(spacing.12)', async () => { expect( await compileCss(css` diff --git a/packages/tailwindcss/src/css-functions.ts b/packages/tailwindcss/src/css-functions.ts index 5decddca1..80cbd8fbe 100644 --- a/packages/tailwindcss/src/css-functions.ts +++ b/packages/tailwindcss/src/css-functions.ts @@ -68,7 +68,7 @@ export function substituteFunctionsInValue( if (node.nodes[i].value.includes(',')) { break } - path += node.nodes[i].value + path += ValueParser.toCss([node.nodes[i]]) skipUntilIndex = i + 1 } diff --git a/packages/tailwindcss/src/value-parser.test.ts b/packages/tailwindcss/src/value-parser.test.ts index b90033fbc..2814b2e9e 100644 --- a/packages/tailwindcss/src/value-parser.test.ts +++ b/packages/tailwindcss/src/value-parser.test.ts @@ -66,6 +66,20 @@ describe('parse', () => { ]) }) + it('should parse a function with nested arguments separated by `/`', () => { + expect(parse('theme(colors.red.500/var(--opacity))')).toEqual([ + { + kind: 'function', + value: 'theme', + nodes: [ + { kind: 'word', value: 'colors.red.500' }, + { kind: 'separator', value: '/' }, + { kind: 'function', value: 'var', nodes: [{ kind: 'word', value: '--opacity' }] }, + ], + }, + ]) + }) + it('should handle calculations', () => { expect(parse('calc((1 + 2) * 3)')).toEqual([ { diff --git a/packages/tailwindcss/src/value-parser.ts b/packages/tailwindcss/src/value-parser.ts index e941a9664..852ba6ed0 100644 --- a/packages/tailwindcss/src/value-parser.ts +++ b/packages/tailwindcss/src/value-parser.ts @@ -114,6 +114,7 @@ const SPACE = 0x20 const LESS_THAN = 0x3c const GREATER_THAN = 0x3e const EQUALS = 0x3d +const SLASH = 0x2f export function parse(input: string) { input = input.replaceAll('\r\n', '\n') @@ -143,6 +144,7 @@ export function parse(input: string) { case COLON: case COMMA: case SPACE: + case SLASH: case LESS_THAN: case GREATER_THAN: case EQUALS: {