diff --git a/CHANGELOG.md b/CHANGELOG.md index 0be64b34e3f0..b350588acd37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - Fix class detection in Slim templates with attached attributes and ID ([#14019](https://github.com/tailwindlabs/tailwindcss/pull/14019)) +- Attribute selectors in `data-*` and `aria-*` modifiers are now wrapped in quotation marks by default, allowing numbers and spaces in them ([#14037])(https://github.com/tailwindlabs/tailwindcss/pull/14037) ## [3.4.6] - 2024-07-16 diff --git a/src/corePlugins.js b/src/corePlugins.js index f3441f4b88a3..8f274153879e 100644 --- a/src/corePlugins.js +++ b/src/corePlugins.js @@ -21,7 +21,7 @@ import { import { formatBoxShadowValue, parseBoxShadowValue } from './util/parseBoxShadowValue' import { removeAlphaVariables } from './util/removeAlphaVariables' import { flagEnabled } from './featureFlags' -import { normalize } from './util/dataTypes' +import { normalize, normalizeAttributeSelectors } from './util/dataTypes' import { INTERNAL_FEATURES } from './lib/setupContextUtils' export let variantPlugins = { @@ -472,41 +472,45 @@ export let variantPlugins = { }, ariaVariants: ({ matchVariant, theme }) => { - matchVariant('aria', (value) => `&[aria-${normalize(value)}]`, { values: theme('aria') ?? {} }) + matchVariant('aria', (value) => `&[aria-${normalizeAttributeSelectors(normalize(value))}]`, { + values: theme('aria') ?? {}, + }) matchVariant( 'group-aria', (value, { modifier }) => modifier - ? `:merge(.group\\/${modifier})[aria-${normalize(value)}] &` - : `:merge(.group)[aria-${normalize(value)}] &`, + ? `:merge(.group\\/${modifier})[aria-${normalizeAttributeSelectors(normalize(value))}] &` + : `:merge(.group)[aria-${normalizeAttributeSelectors(normalize(value))}] &`, { values: theme('aria') ?? {} } ) matchVariant( 'peer-aria', (value, { modifier }) => modifier - ? `:merge(.peer\\/${modifier})[aria-${normalize(value)}] ~ &` - : `:merge(.peer)[aria-${normalize(value)}] ~ &`, + ? `:merge(.peer\\/${modifier})[aria-${normalizeAttributeSelectors(normalize(value))}] ~ &` + : `:merge(.peer)[aria-${normalizeAttributeSelectors(normalize(value))}] ~ &`, { values: theme('aria') ?? {} } ) }, dataVariants: ({ matchVariant, theme }) => { - matchVariant('data', (value) => `&[data-${normalize(value)}]`, { values: theme('data') ?? {} }) + matchVariant('data', (value) => `&[data-${normalizeAttributeSelectors(normalize(value))}]`, { + values: theme('data') ?? {}, + }) matchVariant( 'group-data', (value, { modifier }) => modifier - ? `:merge(.group\\/${modifier})[data-${normalize(value)}] &` - : `:merge(.group)[data-${normalize(value)}] &`, + ? `:merge(.group\\/${modifier})[data-${normalizeAttributeSelectors(normalize(value))}] &` + : `:merge(.group)[data-${normalizeAttributeSelectors(normalize(value))}] &`, { values: theme('data') ?? {} } ) matchVariant( 'peer-data', (value, { modifier }) => modifier - ? `:merge(.peer\\/${modifier})[data-${normalize(value)}] ~ &` - : `:merge(.peer)[data-${normalize(value)}] ~ &`, + ? `:merge(.peer\\/${modifier})[data-${normalizeAttributeSelectors(normalize(value))}] ~ &` + : `:merge(.peer)[data-${normalizeAttributeSelectors(normalize(value))}] ~ &`, { values: theme('data') ?? {} } ) }, diff --git a/src/util/dataTypes.js b/src/util/dataTypes.js index 85bdbd2059de..e1db13754e45 100644 --- a/src/util/dataTypes.js +++ b/src/util/dataTypes.js @@ -81,6 +81,34 @@ export function normalize(value, context = null, isRoot = true) { return value } +export function normalizeAttributeSelectors(value) { + // Wrap values in attribute selectors with quotes + if (value.includes('=')) { + value = value.replace(/(=.*)/g, (_fullMatch, match) => { + 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 +} + /** * Add spaces around operators inside math functions * like calc() that do not follow an operator, '(', or `,`. diff --git a/tests/arbitrary-variants.test.js b/tests/arbitrary-variants.test.js index 0ab9e234c336..c60bb92ad307 100644 --- a/tests/arbitrary-variants.test.js +++ b/tests/arbitrary-variants.test.js @@ -442,6 +442,32 @@ test('keeps escaped underscores with multiple arbitrary variants', () => { }) }) +test('does not add quotes on arbitrary variants', () => { + let config = { + content: [ + { + raw: '
', + }, + ], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind base; + @tailwind components; + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + ${defaults} + .\[\&\[data-foo\=\'1\'\]\+\.bar\]\:underline[data-foo='1']+.bar { + text-decoration-line: underline; + } + `) + }) +}) + test('keeps escaped underscores in arbitrary variants mixed with normal variants', () => { let config = { content: [ @@ -601,6 +627,7 @@ it('should support aria variants', () => {
+
@@ -610,6 +637,8 @@ it('should support aria variants', () => {
+
+
@@ -629,16 +658,19 @@ it('should support aria variants', () => { .aria-checked\:underline[aria-checked='true'], .aria-\[labelledby\=\'a_b\'\]\:underline[aria-labelledby='a b'], .aria-\[sort\=ascending\]\:underline[aria-sort='ascending'], + .aria-\[valuenow\=1\]\:underline[aria-valuenow='1'], .group\/foo[aria-checked='true'] .group-aria-checked\/foo\:underline, .group[aria-checked='true'] .group-aria-checked\:underline, .group[aria-labelledby='a b'] .group-aria-\[labelledby\=\'a_b\'\]\:underline, .group\/foo[aria-sort='ascending'] .group-aria-\[sort\=ascending\]\/foo\:underline, .group[aria-sort='ascending'] .group-aria-\[sort\=ascending\]\:underline, + .group[aria-valuenow='1'] .group-aria-\[valuenow\=1\]\:underline, .peer\/foo[aria-checked='true'] ~ .peer-aria-checked\/foo\:underline, .peer[aria-checked='true'] ~ .peer-aria-checked\:underline, .peer[aria-labelledby='a b'] ~ .peer-aria-\[labelledby\=\'a_b\'\]\:underline, .peer\/foo[aria-sort='ascending'] ~ .peer-aria-\[sort\=ascending\]\/foo\:underline, - .peer[aria-sort='ascending'] ~ .peer-aria-\[sort\=ascending\]\:underline { + .peer[aria-sort='ascending'] ~ .peer-aria-\[sort\=ascending\]\:underline, + .peer[aria-valuenow='1'] ~ .peer-aria-\[valuenow\=1\]\:underline { text-decoration-line: underline; } `) @@ -657,8 +689,11 @@ it('should support data variants', () => { raw: html`
-
+
+
+
+
@@ -667,6 +702,12 @@ it('should support data variants', () => {
+
+
+
+
+
+
@@ -685,15 +726,24 @@ it('should support data variants', () => { .underline, .data-checked\:underline[data-ui~='checked'], .data-\[foo\=\'bar_baz\'\]\:underline[data-foo='bar baz'], + .data-\[id\$\=\'foo\'_s\]\:underline[data-id$='foo' s], + .data-\[id\$\=foo_bar_s\]\:underline[data-id$='foo bar' s], + .data-\[id\=0\]\:underline[data-id='0'], .data-\[position\=top\]\:underline[data-position='top'], .group\/foo[data-ui~='checked'] .group-data-checked\/foo\:underline, .group[data-ui~='checked'] .group-data-checked\:underline, .group[data-foo='bar baz'] .group-data-\[foo\=\'bar_baz\'\]\:underline, + .group[data-id$='foo' s] .group-data-\[id\$\=\'foo\'_s\]\:underline, + .group[data-id$='foo bar' s] .group-data-\[id\$\=foo_bar_s\]\:underline, + .group[data-id='0'] .group-data-\[id\=0\]\:underline, .group\/foo[data-position='top'] .group-data-\[position\=top\]\/foo\:underline, .group[data-position='top'] .group-data-\[position\=top\]\:underline, .peer\/foo[data-ui~='checked'] ~ .peer-data-checked\/foo\:underline, .peer[data-ui~='checked'] ~ .peer-data-checked\:underline, .peer[data-foo='bar baz'] ~ .peer-data-\[foo\=\'bar_baz\'\]\:underline, + .peer[data-id$='foo' s] ~ .peer-data-\[id\$\=\'foo\'_s\]\:underline, + .peer[data-id$='foo bar' s] ~ .peer-data-\[id\$\=foo_bar_s\]\:underline, + .peer[data-id='0'] ~ .peer-data-\[id\=0\]\:underline, .peer\/foo[data-position='top'] ~ .peer-data-\[position\=top\]\/foo\:underline, .peer[data-position='top'] ~ .peer-data-\[position\=top\]\:underline { text-decoration-line: underline; @@ -799,6 +849,7 @@ test('has-* variants with arbitrary values', () => {
+
`, }, @@ -836,6 +887,9 @@ test('has-* variants with arbitrary values', () => { .has-\[h2\]\:has-\[\.banana\]\:hidden:has(.banana):has(h2) { display: none; } + .has-\[\[data-foo\=\'1\'\]\+div\]\:font-bold:has([data-foo='1'] + div) { + font-weight: 700; + } `) }) })