Fix container names with hyphens (#17628)

Fixes #17614.

Candidate parsing for variants only account for the root `@` if there no
hyphens. It seems like the current logic assumes if it *does* have a
hyphen, then it would be one of `@min` or `@max`. However, with:

```css
@theme {
  --container-foo-bar: 1440px;
}
```
Then `@foo-bar` should be valid. However, we only check for `@foo-bar`
and `@foo` as roots, but never `@`. This PR adds a check for `@` at the
very end after iterating through root permutations.

---------

Co-authored-by: Philipp Spiess <hello@philippspiess.com>
This commit is contained in:
Justin Wong 2025-04-11 11:28:25 +01:00 • committed by GitHub
parent cdecb55702
commit 3ab7f12563
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 83 additions and 11 deletions

View file

@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ensure `color-mix(…)` polyfills do not cause used CSS variables to be removed ([#17555](https://github.com/tailwindlabs/tailwindcss/pull/17555))
- Ensure the `color-mix(…)` polyfill creates fallbacks for theme variables that reference other theme variables ([#17562](https://github.com/tailwindlabs/tailwindcss/pull/17562))
- Fix brace expansion in `@source inline('z-{10..0}')` with range going down ([#17591](https://github.com/tailwindlabs/tailwindcss/pull/17591))
- Ensure container query variant names can contain hyphens ([#17628](https://github.com/tailwindlabs/tailwindcss/pull/17628))
## [4.1.3] - 2025-04-04

View file

@ -1294,6 +1294,36 @@ it('should parse a functional variant starting with @', () => {
`)
})
it('should parse a functional variant starting with @ that has a hyphen', () => {
let utilities = new Utilities()
utilities.static('flex', () => [])
let variants = new Variants()
variants.functional('@', () => {})
expect(run('@foo-bar:flex', { utilities, variants })).toMatchInlineSnapshot(`
[
{
"important": false,
"kind": "static",
"raw": "@foo-bar:flex",
"root": "flex",
"variants": [
{
"kind": "functional",
"modifier": null,
"root": "@",
"value": {
"kind": "named",
"value": "foo-bar",
},
},
],
},
]
`)
})
it('should parse a functional variant starting with @ and a modifier', () => {
let utilities = new Utilities()
utilities.static('flex', () => [])

View file

@ -745,14 +745,6 @@ function* findRoots(input: string, exists: (input: string) => boolean): Iterable
// Otherwise test every permutation of the input by iteratively removing
// everything after the last dash.
let idx = input.lastIndexOf('-')
if (idx === -1) {
// Variants starting with `@` are special because they don't need a `-`
// after the `@` (E.g.: `@-lg` should be written as `@lg`).
if (input[0] === '@' && exists('@')) {
yield ['@', input.slice(1)]
}
return
}
// Determine the root and value by testing permutations of the incoming input.
//
@ -761,7 +753,7 @@ function* findRoots(input: string, exists: (input: string) => boolean): Iterable
// `bg-red-500` -> No match
// `bg-red` -> No match
// `bg` -> Match
do {
while (idx > 0) {
let maybeRoot = input.slice(0, idx)
if (exists(maybeRoot)) {
@ -776,5 +768,11 @@ function* findRoots(input: string, exists: (input: string) => boolean): Iterable
}
idx = input.lastIndexOf('-', idx - 1)
} while (idx > 0)
}
// Try '@' variant after permutations. This allows things like `@max` of `@max-foo-bar`
// to match before looking for `@`.
if (input[0] === '@' && exists('@')) {
yield ['@', input.slice(1)]
}
}

View file

@ -2074,6 +2074,7 @@ test('container queries', async () => {
css`
@theme {
--container-lg: 1024px;
--container-foo-bar: 1440px;
}
@tailwind utilities;
`,
@ -2082,20 +2083,38 @@ test('container queries', async () => {
'@lg/name:flex',
'@[123px]:flex',
'@[456px]/name:flex',
'@foo-bar:flex',
'@foo-bar/name:flex',
'@min-lg:flex',
'@min-lg/name:flex',
'@min-[123px]:flex',
'@min-[456px]/name:flex',
'@min-foo-bar:flex',
'@min-foo-bar/name:flex',
'@max-lg:flex',
'@max-lg/name:flex',
'@max-[123px]:flex',
'@max-[456px]/name:flex',
'@max-foo-bar:flex',
'@max-foo-bar/name:flex',
],
),
).toMatchInlineSnapshot(`
"@container name not (min-width: 1024px) {
"@container name not (min-width: 1440px) {
.\\@max-foo-bar\\/name\\:flex {
display: flex;
}
}
@container not (min-width: 1440px) {
.\\@max-foo-bar\\:flex {
display: flex;
}
}
@container name not (min-width: 1024px) {
.\\@max-lg\\/name\\:flex {
display: flex;
}
@ -2153,6 +2172,30 @@ test('container queries', async () => {
.\\@min-lg\\:flex {
display: flex;
}
}
@container name (min-width: 1440px) {
.\\@foo-bar\\/name\\:flex {
display: flex;
}
}
@container (min-width: 1440px) {
.\\@foo-bar\\:flex {
display: flex;
}
}
@container name (min-width: 1440px) {
.\\@min-foo-bar\\/name\\:flex {
display: flex;
}
}
@container (min-width: 1440px) {
.\\@min-foo-bar\\:flex {
display: flex;
}
}"
`)
})