Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
80 changes: 80 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,23 @@ test('data', () => {
run([
'data-disabled:flex',
'data-[potato=salad]:flex',
'data-[foo=1]:flex',
'data-[foo=bar baz]:flex',
"data-[foo$='bar' 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",

'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",
]),
).toMatchInlineSnapshot(`
".group-data-\\[disabled\\]\\:flex:is(:where(.group)[data-disabled] *) {
Expand All @@ -1832,6 +1868,22 @@ 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;
}

.peer-data-\\[disabled\\]\\:flex:is(:where(.peer)[data-disabled] ~ *) {
display: flex;
}
Expand All @@ -1840,12 +1892,40 @@ 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;
}

.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;
}"
`)
expect(run(['data-disabled/foo:flex', 'data-[potato=salad]/foo:flex'])).toEqual('')
Expand Down
21 changes: 19 additions & 2 deletions packages/tailwindcss/src/variants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,9 @@ 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-${normalizeAttributeSelectors(variant.value.value)}]`, ruleNode.nodes),
]
} else {
ruleNode.nodes = [rule(`&[aria-${variant.value.value}="true"]`, ruleNode.nodes)]
}
Expand All @@ -534,7 +536,9 @@ 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-${normalizeAttributeSelectors(variant.value.value)}]`, ruleNode.nodes),
]
})

variants.functional('nth', (ruleNode, variant) => {
Expand Down Expand Up @@ -904,3 +908,16 @@ export function createVariants(theme: Theme): Variants {

return variants
}

function normalizeAttributeSelectors(value: string) {
// Wrap values in attribute selectors with quotes
if (value.includes('=')) {
value = value.replace(/(=.*)/g, (_fullMatch, match) => {
if (match[1] === "'" || match[1] === '"') {
return match
}
return `="${match.slice(1)}"`
})
}
return value
}