Skip to content

Commit b7c4d25

Browse files
Ensure existing spaces in attribute selectors are valid (tailwindlabs#14703)
This PR fixes an issue where spaces in a selector generated invalid CSS. Lightning CSS will throw those incorrect lines of CSS out, but if you are in an environment where Lightning CSS doesn't run then invalid CSS is generated. Given this input: ```html data-[foo_=_"true"]:flex ``` This will be generated: ```css .data-\[foo_\=_\"true\"\]\:flex[data-foo=""true] { display: flex; } ``` With this PR in place, the generated CSS will now be: ```css .data-\[foo_\=_\"true\"\]\:flex[data-foo="true"] { display: flex; } ``` --------- Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
1 parent 5c1bfd3 commit b7c4d25

File tree

3 files changed

+55
-23
lines changed

3 files changed

+55
-23
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- _Upgrade (experimental)_: Migrate `plugins` with options to CSS ([#14700](https://github.com/tailwindlabs/tailwindcss/pull/14700))
1313

14+
### Fixed
15+
16+
- Allow spaces spaces around operators in attribute selector variants ([#14703](https://github.com/tailwindlabs/tailwindcss/pull/14703))
17+
1418
### Changed
1519

1620
- _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))

packages/tailwindcss/src/variants.test.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1985,6 +1985,7 @@ test('aria', async () => {
19851985
'aria-checked:flex',
19861986
'aria-[invalid=spelling]:flex',
19871987
'aria-[valuenow=1]:flex',
1988+
'aria-[valuenow_=_"1"]:flex',
19881989

19891990
'group-aria-[modal]:flex',
19901991
'group-aria-checked:flex',
@@ -2059,6 +2060,10 @@ test('aria', async () => {
20592060
20602061
.aria-\\[valuenow\\=1\\]\\:flex[aria-valuenow="1"] {
20612062
display: flex;
2063+
}
2064+
2065+
.aria-\\[valuenow_\\=_\\"1\\"\\]\\:flex[aria-valuenow="1"] {
2066+
display: flex;
20622067
}"
20632068
`)
20642069
expect(await run(['aria-checked/foo:flex', 'aria-[invalid=spelling]/foo:flex'])).toEqual('')
@@ -2069,6 +2074,9 @@ test('data', async () => {
20692074
await run([
20702075
'data-disabled:flex',
20712076
'data-[potato=salad]:flex',
2077+
'data-[potato_=_"salad"]:flex',
2078+
'data-[potato_^=_"salad"]:flex',
2079+
'data-[potato="^_="]:flex',
20722080
'data-[foo=1]:flex',
20732081
'data-[foo=bar_baz]:flex',
20742082
"data-[foo$='bar'_i]:flex",
@@ -2155,6 +2163,18 @@ test('data', async () => {
21552163
display: flex;
21562164
}
21572165
2166+
.data-\\[potato_\\=_\\"salad\\"\\]\\:flex[data-potato="salad"] {
2167+
display: flex;
2168+
}
2169+
2170+
.data-\\[potato_\\^\\=_\\"salad\\"\\]\\:flex[data-potato^="salad"] {
2171+
display: flex;
2172+
}
2173+
2174+
.data-\\[potato\\=\\"\\^_\\=\\"\\]\\:flex[data-potato="^ ="] {
2175+
display: flex;
2176+
}
2177+
21582178
.data-\\[foo\\=1\\]\\:flex[data-foo="1"] {
21592179
display: flex;
21602180
}
@@ -2171,7 +2191,13 @@ test('data', async () => {
21712191
display: flex;
21722192
}"
21732193
`)
2174-
expect(await run(['data-disabled/foo:flex', 'data-[potato=salad]/foo:flex'])).toEqual('')
2194+
expect(
2195+
await run([
2196+
'data-[foo_^_=_"bar"]:flex', // Can't have spaces between `^` and `=`
2197+
'data-disabled/foo:flex',
2198+
'data-[potato=salad]/foo:flex',
2199+
]),
2200+
).toEqual('')
21752201
})
21762202

21772203
test('portrait', async () => {

packages/tailwindcss/src/variants.ts

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -898,32 +898,34 @@ export function createVariants(theme: Theme): Variants {
898898
return variants
899899
}
900900

901-
function quoteAttributeValue(value: string) {
902-
if (value.includes('=')) {
903-
value = value.replace(/(=.*)/g, (_fullMatch, match) => {
904-
// If the value is already quoted, skip.
905-
if (match[1] === "'" || match[1] === '"') {
906-
return match
907-
}
901+
function quoteAttributeValue(input: string) {
902+
if (input.includes('=')) {
903+
let [attribute, ...after] = segment(input, '=')
904+
let value = after.join('=').trim()
905+
906+
// If the value is already quoted, skip.
907+
if (value[0] === "'" || value[0] === '"') {
908+
return input
909+
}
908910

909-
// Handle regex flags on unescaped values
910-
if (match.length > 2) {
911-
let trailingCharacter = match[match.length - 1]
912-
if (
913-
match[match.length - 2] === ' ' &&
914-
(trailingCharacter === 'i' ||
915-
trailingCharacter === 'I' ||
916-
trailingCharacter === 's' ||
917-
trailingCharacter === 'S')
918-
) {
919-
return `="${match.slice(1, -2)}" ${match[match.length - 1]}`
920-
}
911+
// Handle case sensitivity flags on unescaped values
912+
if (value.length > 1) {
913+
let trailingCharacter = value[value.length - 1]
914+
if (
915+
value[value.length - 2] === ' ' &&
916+
(trailingCharacter === 'i' ||
917+
trailingCharacter === 'I' ||
918+
trailingCharacter === 's' ||
919+
trailingCharacter === 'S')
920+
) {
921+
return `${attribute}="${value.slice(0, -2)}" ${trailingCharacter}`
921922
}
923+
}
922924

923-
return `="${match.slice(1)}"`
924-
})
925+
return `${attribute}="${value}"`
925926
}
926-
return value
927+
928+
return input
927929
}
928930

929931
export function substituteAtSlot(ast: AstNode[], nodes: AstNode[]) {

0 commit comments

Comments
 (0)