diff --git a/CHANGELOG.md b/CHANGELOG.md index de378c91af76..63b4dc2dad6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - _Upgrade (experimental)_: Migrate `plugins` with options to CSS ([#14700](https://github.com/tailwindlabs/tailwindcss/pull/14700)) +### Fixed + +- Allow spaces spaces around operators in attribute selector variants ([#14703](https://github.com/tailwindlabs/tailwindcss/pull/14703)) + ### Changed - _Upgrade (experimental)_: Don't create `@source` rules for `content` paths that are already covered by automatic source detection ([#14714](https://github.com/tailwindlabs/tailwindcss/pull/14714)) diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index f43945f890fd..97e358ef0117 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -1985,6 +1985,7 @@ test('aria', async () => { 'aria-checked:flex', 'aria-[invalid=spelling]:flex', 'aria-[valuenow=1]:flex', + 'aria-[valuenow_=_"1"]:flex', 'group-aria-[modal]:flex', 'group-aria-checked:flex', @@ -2059,6 +2060,10 @@ test('aria', async () => { .aria-\\[valuenow\\=1\\]\\:flex[aria-valuenow="1"] { display: flex; + } + + .aria-\\[valuenow_\\=_\\"1\\"\\]\\:flex[aria-valuenow="1"] { + display: flex; }" `) expect(await run(['aria-checked/foo:flex', 'aria-[invalid=spelling]/foo:flex'])).toEqual('') @@ -2069,6 +2074,9 @@ test('data', async () => { await run([ 'data-disabled:flex', 'data-[potato=salad]:flex', + 'data-[potato_=_"salad"]:flex', + 'data-[potato_^=_"salad"]:flex', + 'data-[potato="^_="]:flex', 'data-[foo=1]:flex', 'data-[foo=bar_baz]:flex', "data-[foo$='bar'_i]:flex", @@ -2155,6 +2163,18 @@ test('data', async () => { display: flex; } + .data-\\[potato_\\=_\\"salad\\"\\]\\:flex[data-potato="salad"] { + display: flex; + } + + .data-\\[potato_\\^\\=_\\"salad\\"\\]\\:flex[data-potato^="salad"] { + display: flex; + } + + .data-\\[potato\\=\\"\\^_\\=\\"\\]\\:flex[data-potato="^ ="] { + display: flex; + } + .data-\\[foo\\=1\\]\\:flex[data-foo="1"] { display: flex; } @@ -2171,7 +2191,13 @@ test('data', async () => { display: flex; }" `) - expect(await run(['data-disabled/foo:flex', 'data-[potato=salad]/foo:flex'])).toEqual('') + expect( + await run([ + 'data-[foo_^_=_"bar"]:flex', // Can't have spaces between `^` and `=` + 'data-disabled/foo:flex', + 'data-[potato=salad]/foo:flex', + ]), + ).toEqual('') }) test('portrait', async () => { diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 693222860020..4999439e6a7a 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -898,32 +898,34 @@ export function createVariants(theme: Theme): Variants { return variants } -function quoteAttributeValue(value: string) { - if (value.includes('=')) { - value = value.replace(/(=.*)/g, (_fullMatch, match) => { - // If the value is already quoted, skip. - if (match[1] === "'" || match[1] === '"') { - return match - } +function quoteAttributeValue(input: string) { + if (input.includes('=')) { + let [attribute, ...after] = segment(input, '=') + let value = after.join('=').trim() + + // If the value is already quoted, skip. + if (value[0] === "'" || value[0] === '"') { + return input + } - // Handle regex flags on unescaped values - if (match.length > 2) { - let trailingCharacter = match[match.length - 1] - if ( - match[match.length - 2] === ' ' && - (trailingCharacter === 'i' || - trailingCharacter === 'I' || - trailingCharacter === 's' || - trailingCharacter === 'S') - ) { - return `="${match.slice(1, -2)}" ${match[match.length - 1]}` - } + // Handle case sensitivity flags on unescaped values + if (value.length > 1) { + let trailingCharacter = value[value.length - 1] + if ( + value[value.length - 2] === ' ' && + (trailingCharacter === 'i' || + trailingCharacter === 'I' || + trailingCharacter === 's' || + trailingCharacter === 'S') + ) { + return `${attribute}="${value.slice(0, -2)}" ${trailingCharacter}` } + } - return `="${match.slice(1)}"` - }) + return `${attribute}="${value}"` } - return value + + return input } export function substituteAtSlot(ast: AstNode[], nodes: AstNode[]) {