Skip to content

Commit

Permalink
Normalize attribute selectors for data-* and aria-* modifiers (#1…
Browse files Browse the repository at this point in the history
…4040)

Fixes #14026 
See #14037 for the v3
fix

When translating `data-` and `aria-` modifiers with attribute selectors,
we currently do not wrap the target attribute in quotes. This only works
for keywords (purely alphabetic words) but breaks as soon as there are
numbers or things like spaces in the attribute:

```html
<div data-id="foo" class="data-[id=foo]:underline">underlined</div>
<div data-id="f1" class="data-[id=1]:underline">not underlined</div>
<div data-id="foo bar" class="data-[id=foo_bar]:underline">not underlined</div>
```

Since it's fairly common to have attribute selectors with `data-` and
`aria-` modifiers, this PR will now wrap the attribute in quotes unless
these are already wrapped.

| Tailwind Modifier  | CSS Selector |
| ------------- | ------------- |
| `.data-[id=foo]`  | `[data-id='foo']`  |
| `.data-[id='foo']`  | `[data-id='foo']`  |
| `.data-[id=foo_i]`  | `[data-id='foo i']`  |
| `.data-[id='foo'_i]`  | `[data-id='foo' i]`  |
| `.data-[id=123]`  | `[data-id='123']`  |

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
  • Loading branch information
philipp-spiess and RobinMalfait committed Jul 24, 2024
1 parent 6ab2893 commit 300524b
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add missing utilities that exist in v3, such as `resize`, `fill-none`, `accent-none`, `drop-shadow-none`, and negative `hue-rotate` and `backdrop-hue-rotate` utilities ([#13971](https://github.com/tailwindlabs/tailwindcss/pull/13971))
- Don’t allow at-rule-only variants to be compounded ([#14015](https://github.com/tailwindlabs/tailwindcss/pull/14015))
- Ensure compound variants work with variants with multiple selectors ([#14016](https://github.com/tailwindlabs/tailwindcss/pull/14016))
- Attribute selectors in `data-*` and `aria-*` modifiers are now wrapped in quotation marks by default, allowing numbers and spaces in them ([#14040])(https://github.com/tailwindlabs/tailwindcss/pull/14037)

### Added

Expand Down
95 changes: 95 additions & 0 deletions packages/tailwindcss/src/variants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1756,16 +1756,21 @@ test('aria', () => {
run([
'aria-checked:flex',
'aria-[invalid=spelling]:flex',
'aria-[valuenow=1]:flex',

'group-aria-[modal]:flex',
'group-aria-checked:flex',
'group-aria-[valuenow=1]:flex',
'group-aria-[modal]/parent-name:flex',
'group-aria-checked/parent-name:flex',
'group-aria-[valuenow=1]/parent-name:flex',

'peer-aria-[modal]:flex',
'peer-aria-checked:flex',
'peer-aria-[valuenow=1]:flex',
'peer-aria-[modal]/parent-name:flex',
'peer-aria-checked/parent-name:flex',
'peer-aria-[valuenow=1]/parent-name:flex',
]),
).toMatchInlineSnapshot(`
".group-aria-\\[modal\\]\\:flex:is(:where(.group)[aria-modal] *) {
Expand All @@ -1776,6 +1781,10 @@ test('aria', () => {
display: flex;
}
.group-aria-\\[valuenow\\=1\\]\\:flex:is(:where(.group)[aria-valuenow="1"] *) {
display: flex;
}
.group-aria-\\[modal\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[aria-modal] *) {
display: flex;
}
Expand All @@ -1784,6 +1793,10 @@ test('aria', () => {
display: flex;
}
.group-aria-\\[valuenow\\=1\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[aria-valuenow="1"] *) {
display: flex;
}
.peer-aria-\\[modal\\]\\:flex:is(:where(.peer)[aria-modal] ~ *) {
display: flex;
}
Expand All @@ -1792,6 +1805,10 @@ test('aria', () => {
display: flex;
}
.peer-aria-\\[valuenow\\=1\\]\\:flex:is(:where(.peer)[aria-valuenow="1"] ~ *) {
display: flex;
}
.peer-aria-\\[modal\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[aria-modal] ~ *) {
display: flex;
}
Expand All @@ -1800,12 +1817,20 @@ test('aria', () => {
display: flex;
}
.peer-aria-\\[valuenow\\=1\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[aria-valuenow="1"] ~ *) {
display: flex;
}
.aria-checked\\:flex[aria-checked="true"] {
display: flex;
}
.aria-\\[invalid\\=spelling\\]\\:flex[aria-invalid="spelling"] {
display: flex;
}
.aria-\\[valuenow\\=1\\]\\:flex[aria-valuenow="1"] {
display: flex;
}"
`)
expect(run(['aria-checked/foo:flex', 'aria-[invalid=spelling]/foo:flex'])).toEqual('')
Expand All @@ -1816,12 +1841,26 @@ test('data', () => {
run([
'data-disabled:flex',
'data-[potato=salad]:flex',
'data-[foo=1]:flex',
'data-[foo=bar_baz]:flex',
"data-[foo$='bar'_i]:flex",
'data-[foo$=bar_baz_i]:flex',

'group-data-[disabled]:flex',
'group-data-[disabled]/parent-name:flex',
'group-data-[foo=1]:flex',
'group-data-[foo=1]/parent-name:flex',
'group-data-[foo=bar baz]/parent-name:flex',
"group-data-[foo$='bar'_i]/parent-name:flex",
'group-data-[foo$=bar_baz_i]/parent-name:flex',

'peer-data-[disabled]:flex',
'peer-data-[disabled]/parent-name:flex',
'peer-data-[foo=1]:flex',
'peer-data-[foo=1]/parent-name:flex',
'peer-data-[foo=bar baz]/parent-name:flex',
"peer-data-[foo$='bar'_i]/parent-name:flex",
'peer-data-[foo$=bar_baz_i]/parent-name:flex',
]),
).toMatchInlineSnapshot(`
".group-data-\\[disabled\\]\\:flex:is(:where(.group)[data-disabled] *) {
Expand All @@ -1832,6 +1871,26 @@ test('data', () => {
display: flex;
}
.group-data-\\[foo\\=1\\]\\:flex:is(:where(.group)[data-foo="1"] *) {
display: flex;
}
.group-data-\\[foo\\=1\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[data-foo="1"] *) {
display: flex;
}
.group-data-\\[foo\\=bar\\ baz\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[data-foo="bar baz"] *) {
display: flex;
}
.group-data-\\[foo\\$\\=\\'bar\\'_i\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[data-foo$="bar" i] *) {
display: flex;
}
.group-data-\\[foo\\$\\=bar_baz_i\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[data-foo$="bar baz" i] *) {
display: flex;
}
.peer-data-\\[disabled\\]\\:flex:is(:where(.peer)[data-disabled] ~ *) {
display: flex;
}
Expand All @@ -1840,12 +1899,48 @@ test('data', () => {
display: flex;
}
.peer-data-\\[foo\\=1\\]\\:flex:is(:where(.peer)[data-foo="1"] ~ *) {
display: flex;
}
.peer-data-\\[foo\\=1\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[data-foo="1"] ~ *) {
display: flex;
}
.peer-data-\\[foo\\=bar\\ baz\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[data-foo="bar baz"] ~ *) {
display: flex;
}
.peer-data-\\[foo\\$\\=\\'bar\\'_i\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[data-foo$="bar" i] ~ *) {
display: flex;
}
.peer-data-\\[foo\\$\\=bar_baz_i\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[data-foo$="bar baz" i] ~ *) {
display: flex;
}
.data-disabled\\:flex[data-disabled] {
display: flex;
}
.data-\\[potato\\=salad\\]\\:flex[data-potato="salad"] {
display: flex;
}
.data-\\[foo\\=1\\]\\:flex[data-foo="1"] {
display: flex;
}
.data-\\[foo\\=bar_baz\\]\\:flex[data-foo="bar baz"] {
display: flex;
}
.data-\\[foo\\$\\=\\'bar\\'_i\\]\\:flex[data-foo$="bar" i] {
display: flex;
}
.data-\\[foo\\$\\=bar_baz_i\\]\\:flex[data-foo$="bar baz" i] {
display: flex;
}"
`)
expect(run(['data-disabled/foo:flex', 'data-[potato=salad]/foo:flex'])).toEqual('')
Expand Down
32 changes: 30 additions & 2 deletions packages/tailwindcss/src/variants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ export function createVariants(theme: Theme): Variants {
if (!variant.value || variant.modifier) return null

if (variant.value.kind === 'arbitrary') {
ruleNode.nodes = [rule(`&[aria-${variant.value.value}]`, ruleNode.nodes)]
ruleNode.nodes = [rule(`&[aria-${quoteAttributeValue(variant.value.value)}]`, ruleNode.nodes)]
} else {
ruleNode.nodes = [rule(`&[aria-${variant.value.value}="true"]`, ruleNode.nodes)]
}
Expand All @@ -534,7 +534,7 @@ export function createVariants(theme: Theme): Variants {
variants.functional('data', (ruleNode, variant) => {
if (!variant.value || variant.modifier) return null

ruleNode.nodes = [rule(`&[data-${variant.value.value}]`, ruleNode.nodes)]
ruleNode.nodes = [rule(`&[data-${quoteAttributeValue(variant.value.value)}]`, ruleNode.nodes)]
})

variants.functional('nth', (ruleNode, variant) => {
Expand Down Expand Up @@ -904,3 +904,31 @@ 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
}

// 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]}`
}
}

return `="${match.slice(1)}"`
})
}
return value
}

0 comments on commit 300524b

Please sign in to comment.