Don't space out and/or/not inside function calls in supports-[…] variants (#20420)
## Summary The `supports-[…]` variant works around a Chrome bug where `@supports (a)or(b)` is invalid by spacing out the `and`, `or`, and `not` keywords. However, the replacement is applied to the entire value, including the inside of function calls, where these words can be part of a selector. For example, `supports-[selector(a:not(.foo))]:flex` generates: ```css @supports selector(a: not (.foo)) ``` The selector `a: not (.foo)` is unparsable, so a condition that is true in every browser silently becomes false and the utility never applies. The same happens to class names like `.and` or `.or` inside `selector(…)`. This PR only spaces out the keywords at the condition level: parens preceded by an identifier (other than the keywords themselves) start a function call, and everything inside is left as-is. The Chrome workaround still applies to the condition itself, e.g. `supports-[(display:grid)or(display:flex)]` still becomes `@supports (display: grid) or (display: flex)`. ## Test plan - Added a test covering `selector(a:not(.foo))`, class names `.and`/`.or` inside `selector(…)`, the Chrome `(a)or(b)` workaround, and a top-level `not(…)` condition. - `pnpm vitest run packages/tailwindcss/src/variants.test.ts` — 102 passed. --------- Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
parent
7a7f386648
commit
90f8ff41c8
3 changed files with 76 additions and 1 deletions
|
|
@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- `@tailwindcss/vite` no longer forces a full page reload for external files (e.g.: `.php` files) ([#20414](https://github.com/tailwindlabs/tailwindcss/issues/20414))
|
||||
- Canonicalization: don't merge utilities that reference different theme variables set to CSS-wide keywords like `unset` ([#20417](https://github.com/tailwindlabs/tailwindcss/pull/20417))
|
||||
- Don't generate utilities when a modifier is used that would otherwise be silently ignored (e.g. `rounded-sm/[5]`, `shadow-sm/foo`, `stroke-2/50`) ([#20419](https://github.com/tailwindlabs/tailwindcss/pull/20419))
|
||||
- Only normalize top-level `and`, `or`, and `not` keywords in `supports-[…]` variants (e.g. `selector(a: not (.foo))` → `selector(a:not(.foo))`) ([#20420](https://github.com/tailwindlabs/tailwindcss/pull/20420))
|
||||
|
||||
## [4.3.3] - 2026-07-16
|
||||
|
||||
|
|
|
|||
|
|
@ -1435,6 +1435,17 @@ test('supports', async () => {
|
|||
'supports-[font-tech(color-COLRv1)]:flex',
|
||||
'supports-[var(--test)]:flex',
|
||||
'supports-[--test]:flex',
|
||||
|
||||
// Only top-level and/or/not should have spaces around them. We should
|
||||
// ignore and/or/not inside of `selector(…)`
|
||||
'supports-[selector(a:not(.foo))]:flex',
|
||||
'supports-[selector(a:is(.and,.or))]:flex',
|
||||
'supports-[(display:grid)or(display:flex)]:grid',
|
||||
'supports-[not(display:grid)]:flex',
|
||||
'supports-[foo-not(display:grid)]:flex',
|
||||
'supports-[selector([data-foo="("])or(display:grid)]:flex',
|
||||
'supports-[selector(.foo\\(bar)or(display:grid)]:flex',
|
||||
'supports-[((display:grid)or(display:flex))]:grid',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1444,12 +1455,24 @@ test('supports', async () => {
|
|||
}
|
||||
}
|
||||
|
||||
@supports (display: grid) or (display: flex) {
|
||||
.supports-\\[\\(\\(display\\:grid\\)or\\(display\\:flex\\)\\)\\]\\:grid {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (display: grid) and font-format(opentype) {
|
||||
.supports-\\[\\(display\\:grid\\)_and_font-format\\(opentype\\)\\]\\:grid {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (display: grid) or (display: flex) {
|
||||
.supports-\\[\\(display\\:grid\\)or\\(display\\:flex\\)\\]\\:grid {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (--test: var(--tw)) {
|
||||
.supports-\\[--test\\]\\:flex {
|
||||
display: flex;
|
||||
|
|
@ -1474,12 +1497,48 @@ test('supports', async () => {
|
|||
}
|
||||
}
|
||||
|
||||
@supports foo-not(display:grid) {
|
||||
.supports-\\[foo-not\\(display\\:grid\\)\\]\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@supports not (display: grid) {
|
||||
.supports-\\[not\\(display\\:grid\\)\\]\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@supports selector(.foo\\(bar) or (display: grid) {
|
||||
.supports-\\[selector\\(\\.foo\\\\\\(bar\\)or\\(display\\:grid\\)\\]\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@supports selector(A > B) {
|
||||
.supports-\\[selector\\(A_\\>_B\\)\\]\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@supports selector([data-foo="("]) or (display: grid) {
|
||||
.supports-\\[selector\\(\\[data-foo\\=\\"\\(\\"\\]\\)or\\(display\\:grid\\)\\]\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@supports selector(a:is(.and,.or)) {
|
||||
.supports-\\[selector\\(a\\:is\\(\\.and\\,\\.or\\)\\)\\]\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@supports selector(a:not(.foo)) {
|
||||
.supports-\\[selector\\(a\\:not\\(\\.foo\\)\\)\\]\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@supports var(--test) {
|
||||
.supports-\\[var\\(--test\\)\\]\\:flex {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -938,7 +938,22 @@ export function createVariants(theme: Theme): Variants {
|
|||
if (/^[\w-]*\s*\(/.test(value)) {
|
||||
// Chrome has a bug where `(condition1)or(condition2)` is not valid, but
|
||||
// `(condition1) or (condition2)` is supported.
|
||||
let query = value.replace(/\b(and|or|not)\b/g, ' $1 ')
|
||||
let changed = false
|
||||
let ast = ValueParser.parse(value)
|
||||
walk(ast, (node) => {
|
||||
if (node.kind !== 'function') return
|
||||
|
||||
// Leave selectors as-is, they could contain `selector(a:not(b))`, and
|
||||
// in this case we don't want the space around the `not`.
|
||||
if (node.value === 'selector') return WalkAction.Skip
|
||||
|
||||
if (node.value === 'and' || node.value === 'or' || node.value === 'not') {
|
||||
changed = true
|
||||
node.value = ` ${node.value} `
|
||||
}
|
||||
})
|
||||
|
||||
let query = changed ? ValueParser.toCss(ast) : value
|
||||
|
||||
ruleNode.nodes = [atRule('@supports', query, ruleNode.nodes)]
|
||||
return
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue