Discard invalid variants and utilities with modifiers (#13977)

* ensure that static utilities do not take a `modifier`

* do not allow multiple segments for now

Right now, `bg-red-1/2/3` should not parse

* add tests for variants that don't accept a modifier

* ensure static variants do not accept a modifier

* do not accept a modifier for some variants

* add tests for utilities that don't accept a modifier

* do not accept a modifier for some utilities

* update changelog

* re-add sorting related test
This commit is contained in:
Robin Malfait 2024-07-10 15:56:33 +02:00 committed by GitHub
parent de48a76b6d
commit 6c0c6a5941
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 2159 additions and 195 deletions

View File

@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Discard invalid classes such as `bg-red-[#000]` ([#13970](https://github.com/tailwindlabs/tailwindcss/pull/13970))
- Fix parsing body-less at-rule without terminating semicolon ([#13978](https://github.com/tailwindlabs/tailwindcss/pull/13978))
- Ensure opacity modifier with variables work with `color-mix()` ([#13972](https://github.com/tailwindlabs/tailwindcss/pull/13972))
- Discard invalid `variants` and `utilities` with modifiers ([#13977](https://github.com/tailwindlabs/tailwindcss/pull/13977))
## [4.0.0-alpha.17] - 2024-07-04

View File

@ -445,6 +445,27 @@ it('should not parse a partial utility', () => {
expect(run('bg-', { utilities })).toMatchInlineSnapshot(`null`)
})
it('should not parse static utilities with a modifier', () => {
let utilities = new Utilities()
utilities.static('flex', () => [])
expect(run('flex/foo', { utilities })).toMatchInlineSnapshot(`null`)
})
it('should not parse static utilities with multiple modifiers', () => {
let utilities = new Utilities()
utilities.static('flex', () => [])
expect(run('flex/foo/bar', { utilities })).toMatchInlineSnapshot(`null`)
})
it('should not parse functional utilities with multiple modifiers', () => {
let utilities = new Utilities()
utilities.functional('bg', () => [])
expect(run('bg-red-1/2/3', { utilities })).toMatchInlineSnapshot(`null`)
})
it('should parse a utility with an arbitrary value', () => {
let utilities = new Utilities()
utilities.functional('bg', () => [])

View File

@ -258,7 +258,14 @@ export function parseCandidate(input: string, designSystem: DesignSystem): Candi
// ^^^^^^^^^^ -> Base without modifier
// ^^ -> Modifier segment
// ```
let [baseWithoutModifier, modifierSegment = null] = segment(base, '/')
let [baseWithoutModifier, modifierSegment = null, additionalModifier] = segment(base, '/')
// If there's more than one modifier, the utility is invalid.
//
// E.g.:
//
// - `bg-red-500/50/50`
if (additionalModifier) return null
// Arbitrary properties
if (baseWithoutModifier[0] === '[') {
@ -373,8 +380,12 @@ export function parseCandidate(input: string, designSystem: DesignSystem): Candi
let kind = designSystem.utilities.kind(root)
if (kind === 'static') {
// Static utilities do not have a value
if (value !== null) return null
// Static utilities do not have a modifier
if (modifierSegment !== null) return null
return {
kind: 'static',
root,
@ -557,8 +568,12 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia
switch (designSystem.variants.kind(root)) {
case 'static': {
// Static variants do not have a value
if (value !== null) return null
// Static variants do not have a modifier
if (modifier !== null) return null
return {
kind: 'static',
root,

File diff suppressed because it is too large Load Diff

View File

@ -317,12 +317,15 @@ export function createUtilities(theme: Theme) {
let value: string | null = null
if (!candidate.value) {
if (candidate.modifier) return
// If the candidate has no value segment (like `rounded`), use the
// `defaultValue` (for candidates like `grow` that have no theme values)
// or a bare theme value (like `--radius` for `rounded`). No utility
// will ever support both of these.
value = desc.defaultValue ?? theme.resolve(null, desc.themeKeys ?? [])
} else if (candidate.value.kind === 'arbitrary') {
if (candidate.modifier) return
value = candidate.value.value
} else {
value = theme.resolve(
@ -333,6 +336,8 @@ export function createUtilities(theme: Theme) {
// Automatically handle things like `w-1/2` without requiring `1/2` to
// exist as a theme value.
if (value === null && desc.supportsFractions && candidate.value.fraction) {
let [lhs, rhs] = segment(candidate.value.fraction, '/')
if (!Number.isInteger(Number(lhs)) || !Number.isInteger(Number(rhs))) return
value = `calc(${candidate.value.fraction} * 100%)`
}
@ -340,6 +345,7 @@ export function createUtilities(theme: Theme) {
// use the bare candidate value as the value.
if (value === null && desc.handleBareValue) {
value = desc.handleBareValue(candidate.value)
if (!value?.includes('/') && candidate.modifier) return
}
}
@ -893,8 +899,7 @@ export function createUtilities(theme: Theme) {
handleBareValue: ({ fraction }) => {
if (fraction === null) return null
let [lhs, rhs] = segment(fraction, '/')
if (!Number.isInteger(Number(lhs))) return null
if (!Number.isInteger(Number(rhs))) return null
if (!Number.isInteger(Number(lhs)) || !Number.isInteger(Number(rhs))) return null
return fraction
},
handle: (value) => [decl('aspect-ratio', value)],
@ -1058,18 +1063,23 @@ export function createUtilities(theme: Theme) {
if (candidate.negative) return
if (!candidate.value) {
if (candidate.modifier) return
return [decl('display', 'flex')]
}
if (candidate.value.kind === 'arbitrary') {
if (candidate.modifier) return
return [decl('flex', candidate.value.value)]
}
if (candidate.value.fraction) {
let [lhs, rhs] = segment(candidate.value.fraction, '/')
if (!Number.isInteger(Number(lhs)) || !Number.isInteger(Number(rhs))) return
return [decl('flex', `calc(${candidate.value.fraction} * 100%)`)]
}
if (Number.isInteger(Number(candidate.value.value))) {
if (candidate.modifier) return
return [decl('flex', candidate.value.value)]
}
})
@ -1325,7 +1335,8 @@ export function createUtilities(theme: Theme) {
*/
staticUtility('scale-none', [['scale', 'none']])
utilities.functional('scale', (candidate) => {
if (!candidate.value) return
if (!candidate.value || candidate.modifier) return
let value
if (candidate.value.kind === 'arbitrary') {
value = candidate.value.value
@ -1402,7 +1413,8 @@ export function createUtilities(theme: Theme) {
*/
staticUtility('rotate-none', [['rotate', 'none']])
utilities.functional('rotate', (candidate) => {
if (!candidate.value) return
if (!candidate.value || candidate.modifier) return
let value
if (candidate.value.kind === 'arbitrary') {
value = candidate.value.value
@ -1556,7 +1568,7 @@ export function createUtilities(theme: Theme) {
* @css `transform`
*/
utilities.functional('transform', (candidate) => {
if (candidate.negative) return
if (candidate.negative || candidate.modifier) return
let value: string | null = null
if (!candidate.value) {
@ -2238,6 +2250,7 @@ export function createUtilities(theme: Theme) {
if (candidate.negative) return
if (!candidate.value) {
if (candidate.modifier) return
let value = theme.get(['--default-border-width']) ?? '1px'
let decls = desc.width(value)
if (!decls) return
@ -2252,6 +2265,7 @@ export function createUtilities(theme: Theme) {
switch (type) {
case 'line-width':
case 'length': {
if (candidate.modifier) return
let decls = desc.width(value)
if (!decls) return
return [borderProperties(), ...decls]
@ -2275,6 +2289,7 @@ export function createUtilities(theme: Theme) {
// `border-width` property
{
if (candidate.modifier) return
let value = theme.resolve(candidate.value.value, ['--border-width'])
if (value) {
let decls = desc.width(value)
@ -2510,7 +2525,7 @@ export function createUtilities(theme: Theme) {
}
utilities.functional('bg-linear', (candidate) => {
if (!candidate.value) return
if (!candidate.value || candidate.modifier) return
if (candidate.value.kind === 'arbitrary') {
let value: string | null = candidate.value.value
@ -2552,15 +2567,18 @@ export function createUtilities(theme: Theme) {
switch (type) {
case 'percentage':
case 'position': {
if (candidate.modifier) return
return [decl('background-position', value)]
}
case 'bg-size':
case 'length':
case 'size': {
if (candidate.modifier) return
return [decl('background-size', value)]
}
case 'image':
case 'url': {
if (candidate.modifier) return
return [decl('background-image', value)]
}
default: {
@ -2582,6 +2600,7 @@ export function createUtilities(theme: Theme) {
// `background-image` property
{
if (candidate.modifier) return
let value = theme.resolve(candidate.value.value, ['--background-image'])
if (value) {
return [decl('background-image', value)]
@ -2635,6 +2654,7 @@ export function createUtilities(theme: Theme) {
switch (type) {
case 'length':
case 'percentage': {
if (candidate.modifier) return
return desc.position(value)
}
default: {
@ -2656,6 +2676,7 @@ export function createUtilities(theme: Theme) {
// Known values: Positions
{
if (candidate.modifier) return
let value = theme.resolve(candidate.value.value, ['--gradient-color-stop-positions'])
if (value) {
return desc.position(value)
@ -2808,6 +2829,7 @@ export function createUtilities(theme: Theme) {
case 'number':
case 'length':
case 'percentage': {
if (candidate.modifier) return
return [decl('stroke-width', value)]
}
default: {
@ -2937,7 +2959,7 @@ export function createUtilities(theme: Theme) {
})
utilities.functional('font', (candidate) => {
if (candidate.negative || !candidate.value) return
if (candidate.negative || !candidate.value || candidate.modifier) return
if (candidate.value.kind === 'arbitrary') {
let value = candidate.value.value
@ -3105,6 +3127,7 @@ export function createUtilities(theme: Theme) {
switch (type) {
case 'length':
case 'percentage': {
if (candidate.modifier) return
return [decl('text-decoration-thickness', value)]
}
default: {
@ -3120,10 +3143,12 @@ export function createUtilities(theme: Theme) {
{
let value = theme.resolve(candidate.value.value, ['--text-decoration-thickness'])
if (value) {
if (candidate.modifier) return
return [decl('text-decoration-thickness', value)]
}
if (!Number.isNaN(Number(candidate.value.value))) {
if (candidate.modifier) return
return [decl('text-decoration-thickness', `${candidate.value.value}px`)]
}
}
@ -3210,7 +3235,7 @@ export function createUtilities(theme: Theme) {
}
utilities.functional('filter', (candidate) => {
if (candidate.negative) return
if (candidate.negative || candidate.modifier) return
if (candidate.value === null) {
return [filterProperties(), decl('filter', cssFilterValue)]
@ -3227,7 +3252,7 @@ export function createUtilities(theme: Theme) {
})
utilities.functional('backdrop-filter', (candidate) => {
if (candidate.negative) return
if (candidate.negative || candidate.modifier) return
if (candidate.value === null) {
return [
@ -3671,6 +3696,9 @@ export function createUtilities(theme: Theme) {
// This utility doesn't support negative values.
if (candidate.negative) return
// This utility doesn't support modifiers.
if (candidate.modifier) return
// This utility doesn't support `DEFAULT` values.
if (!candidate.value) return
@ -3912,6 +3940,7 @@ export function createUtilities(theme: Theme) {
if (candidate.negative) return
if (candidate.value === null) {
if (candidate.modifier) return
return [
outlineProperties(),
decl('outline-style', 'var(--tw-outline-style)'),
@ -3929,6 +3958,7 @@ export function createUtilities(theme: Theme) {
case 'length':
case 'number':
case 'percentage': {
if (candidate.modifier) return
return [
outlineProperties(),
decl('outline-style', 'var(--tw-outline-style)'),
@ -3954,6 +3984,7 @@ export function createUtilities(theme: Theme) {
// `outline-width` property
{
if (candidate.modifier) return
let value = theme.resolve(candidate.value.value, ['--outline-width'])
if (value) {
return [
@ -4205,6 +4236,7 @@ export function createUtilities(theme: Theme) {
switch (candidate.value.value) {
case 'none':
if (candidate.modifier) return
return [
boxShadowProperties(),
decl('--tw-shadow', nullShadow),
@ -4217,6 +4249,7 @@ export function createUtilities(theme: Theme) {
{
let value = theme.get([`--shadow-${candidate.value.value}`])
if (value) {
if (candidate.modifier) return
return [
boxShadowProperties(),
decl('--tw-shadow', value),
@ -4302,6 +4335,7 @@ export function createUtilities(theme: Theme) {
switch (candidate.value.value) {
case 'none':
if (candidate.modifier) return
return [
boxShadowProperties(),
decl('--tw-inset-shadow', nullShadow),
@ -4315,6 +4349,7 @@ export function createUtilities(theme: Theme) {
let value = theme.get([`--inset-shadow-${candidate.value.value}`])
if (value) {
if (candidate.modifier) return
return [
boxShadowProperties(),
decl('--tw-inset-shadow', value),
@ -4364,6 +4399,7 @@ export function createUtilities(theme: Theme) {
if (candidate.negative) return
if (!candidate.value) {
if (candidate.modifier) return
let value = theme.get(['--default-ring-width']) ?? '1px'
return [
boxShadowProperties(),
@ -4378,6 +4414,7 @@ export function createUtilities(theme: Theme) {
switch (type) {
case 'length': {
if (candidate.modifier) return
return [
boxShadowProperties(),
decl('--tw-ring-shadow', ringShadowValue(value)),
@ -4403,6 +4440,7 @@ export function createUtilities(theme: Theme) {
// Ring width
{
if (candidate.modifier) return
let value = theme.resolve(candidate.value.value, ['--ring-width'])
if (value === null && !Number.isNaN(Number(candidate.value.value))) {
value = `${candidate.value.value}px`
@ -4438,6 +4476,7 @@ export function createUtilities(theme: Theme) {
if (candidate.negative) return
if (!candidate.value) {
if (candidate.modifier) return
return [
boxShadowProperties(),
decl('--tw-inset-ring-shadow', insetRingShadowValue('1px')),
@ -4451,6 +4490,7 @@ export function createUtilities(theme: Theme) {
switch (type) {
case 'length': {
if (candidate.modifier) return
return [
boxShadowProperties(),
decl('--tw-inset-ring-shadow', insetRingShadowValue(value)),
@ -4476,6 +4516,7 @@ export function createUtilities(theme: Theme) {
// Ring width
{
if (candidate.modifier) return
let value = theme.resolve(candidate.value.value, ['--ring-width'])
if (value === null && !Number.isNaN(Number(candidate.value.value))) {
value = `${candidate.value.value}px`
@ -4515,6 +4556,7 @@ export function createUtilities(theme: Theme) {
switch (type) {
case 'length': {
if (candidate.modifier) return
return [
decl('--tw-ring-offset-width', value),
decl('--tw-ring-offset-shadow', ringOffsetShadowValue),
@ -4533,11 +4575,13 @@ export function createUtilities(theme: Theme) {
{
let value = theme.resolve(candidate.value.value, ['--ring-offset-width'])
if (value) {
if (candidate.modifier) return
return [
decl('--tw-ring-offset-width', value),
decl('--tw-ring-offset-shadow', ringOffsetShadowValue),
]
} else if (!Number.isNaN(Number(candidate.value.value))) {
if (candidate.modifier) return
return [
decl('--tw-ring-offset-width', `${candidate.value.value}px`),
decl('--tw-ring-offset-shadow', ringOffsetShadowValue),

View File

@ -9,14 +9,16 @@ test('force', () => {
display: flex;
}"
`)
expect(run(['force/foo:flex'])).toEqual('')
})
test('*', () => {
expect(run(['*:flex'])).toMatchInlineSnapshot(`
".\\*\\:flex > * {
display: flex;
}"
`)
".\\*\\:flex > * {
display: flex;
}"
`)
expect(run(['*/foo:flex'])).toEqual('')
})
test('first-letter', () => {
@ -25,6 +27,7 @@ test('first-letter', () => {
display: flex;
}"
`)
expect(run(['first-letter/foo:flex'])).toEqual('')
})
test('first-line', () => {
@ -33,6 +36,7 @@ test('first-line', () => {
display: flex;
}"
`)
expect(run(['first-line/foo:flex'])).toEqual('')
})
test('marker', () => {
@ -41,6 +45,7 @@ test('marker', () => {
display: flex;
}"
`)
expect(run(['marker/foo:flex'])).toEqual('')
})
test('selection', () => {
@ -49,6 +54,7 @@ test('selection', () => {
display: flex;
}"
`)
expect(run(['selection/foo:flex'])).toEqual('')
})
test('file', () => {
@ -57,6 +63,7 @@ test('file', () => {
display: flex;
}"
`)
expect(run(['file/foo:flex'])).toEqual('')
})
test('placeholder', () => {
@ -65,6 +72,7 @@ test('placeholder', () => {
display: flex;
}"
`)
expect(run(['placeholder/foo:flex'])).toEqual('')
})
test('backdrop', () => {
@ -73,6 +81,7 @@ test('backdrop', () => {
display: flex;
}"
`)
expect(run(['backdrop/foo:flex'])).toEqual('')
})
test('before', () => {
@ -96,6 +105,7 @@ test('before', () => {
initial-value: "";
}"
`)
expect(run(['before/foo:flex'])).toEqual('')
})
test('after', () => {
@ -119,6 +129,7 @@ test('after', () => {
initial-value: "";
}"
`)
expect(run(['after/foo:flex'])).toEqual('')
})
test('first', () => {
@ -135,6 +146,7 @@ test('first', () => {
display: flex;
}"
`)
expect(run(['first/foo:flex'])).toEqual('')
})
test('last', () => {
@ -151,6 +163,7 @@ test('last', () => {
display: flex;
}"
`)
expect(run(['last/foo:flex'])).toEqual('')
})
test('only', () => {
@ -167,6 +180,7 @@ test('only', () => {
display: flex;
}"
`)
expect(run(['only/foo:flex'])).toEqual('')
})
test('odd', () => {
@ -183,6 +197,7 @@ test('odd', () => {
display: flex;
}"
`)
expect(run(['odd/foo:flex'])).toEqual('')
})
test('even', () => {
@ -199,6 +214,7 @@ test('even', () => {
display: flex;
}"
`)
expect(run(['even/foo:flex'])).toEqual('')
})
test('first-of-type', () => {
@ -216,6 +232,7 @@ test('first-of-type', () => {
display: flex;
}"
`)
expect(run(['first-of-type/foo:flex'])).toEqual('')
})
test('last-of-type', () => {
@ -233,6 +250,7 @@ test('last-of-type', () => {
display: flex;
}"
`)
expect(run(['last-of-type/foo:flex'])).toEqual('')
})
test('only-of-type', () => {
@ -250,6 +268,7 @@ test('only-of-type', () => {
display: flex;
}"
`)
expect(run(['only-of-type/foo:flex'])).toEqual('')
})
test('visited', () => {
@ -266,6 +285,7 @@ test('visited', () => {
display: flex;
}"
`)
expect(run(['visited/foo:flex'])).toEqual('')
})
test('target', () => {
@ -282,6 +302,7 @@ test('target', () => {
display: flex;
}"
`)
expect(run(['target/foo:flex'])).toEqual('')
})
test('open', () => {
@ -298,6 +319,7 @@ test('open', () => {
display: flex;
}"
`)
expect(run(['open/foo:flex'])).toEqual('')
})
test('default', () => {
@ -314,6 +336,7 @@ test('default', () => {
display: flex;
}"
`)
expect(run(['default/foo:flex'])).toEqual('')
})
test('checked', () => {
@ -330,6 +353,7 @@ test('checked', () => {
display: flex;
}"
`)
expect(run(['checked/foo:flex'])).toEqual('')
})
test('indeterminate', () => {
@ -347,6 +371,7 @@ test('indeterminate', () => {
display: flex;
}"
`)
expect(run(['indeterminate/foo:flex'])).toEqual('')
})
test('placeholder-shown', () => {
@ -365,6 +390,7 @@ test('placeholder-shown', () => {
display: flex;
}"
`)
expect(run(['placeholder-shown/foo:flex'])).toEqual('')
})
test('autofill', () => {
@ -382,6 +408,7 @@ test('autofill', () => {
display: flex;
}"
`)
expect(run(['autofill/foo:flex'])).toEqual('')
})
test('optional', () => {
@ -399,6 +426,7 @@ test('optional', () => {
display: flex;
}"
`)
expect(run(['optional/foo:flex'])).toEqual('')
})
test('required', () => {
@ -416,6 +444,7 @@ test('required', () => {
display: flex;
}"
`)
expect(run(['required/foo:flex'])).toEqual('')
})
test('valid', () => {
@ -432,6 +461,7 @@ test('valid', () => {
display: flex;
}"
`)
expect(run(['valid/foo:flex'])).toEqual('')
})
test('invalid', () => {
@ -448,6 +478,7 @@ test('invalid', () => {
display: flex;
}"
`)
expect(run(['invalid/foo:flex'])).toEqual('')
})
test('in-range', () => {
@ -465,6 +496,7 @@ test('in-range', () => {
display: flex;
}"
`)
expect(run(['in-range/foo:flex'])).toEqual('')
})
test('out-of-range', () => {
@ -482,6 +514,7 @@ test('out-of-range', () => {
display: flex;
}"
`)
expect(run(['out-of-range/foo:flex'])).toEqual('')
})
test('read-only', () => {
@ -499,6 +532,7 @@ test('read-only', () => {
display: flex;
}"
`)
expect(run(['read-only/foo:flex'])).toEqual('')
})
test('empty', () => {
@ -515,6 +549,7 @@ test('empty', () => {
display: flex;
}"
`)
expect(run(['empty/foo:flex'])).toEqual('')
})
test('focus-within', () => {
@ -532,6 +567,7 @@ test('focus-within', () => {
display: flex;
}"
`)
expect(run(['focus-within/foo:flex'])).toEqual('')
})
test('hover', () => {
@ -548,6 +584,7 @@ test('hover', () => {
display: flex;
}"
`)
expect(run(['hover/foo:flex'])).toEqual('')
})
test('focus', () => {
@ -564,9 +601,10 @@ test('focus', () => {
display: flex;
}"
`)
expect(run(['focus/foo:flex'])).toEqual('')
})
test('group-hover group-focus', () => {
test('group-hover group-focus sorting', () => {
expect(run(['group-hover:flex', 'group-focus:flex'])).toMatchInlineSnapshot(`
".group-hover\\:flex:is(:where(.group):hover *) {
display: flex;
@ -602,6 +640,7 @@ test('focus-visible', () => {
display: flex;
}"
`)
expect(run(['focus-visible/foo:flex'])).toEqual('')
})
test('active', () => {
@ -618,6 +657,7 @@ test('active', () => {
display: flex;
}"
`)
expect(run(['active/foo:flex'])).toEqual('')
})
test('enabled', () => {
@ -634,6 +674,7 @@ test('enabled', () => {
display: flex;
}"
`)
expect(run(['enabled/foo:flex'])).toEqual('')
})
test('disabled', () => {
@ -651,6 +692,7 @@ test('disabled', () => {
display: flex;
}"
`)
expect(run(['disabled/foo:flex'])).toEqual('')
})
test('group-[...]', () => {
@ -777,6 +819,7 @@ test('ltr', () => {
display: flex;
}"
`)
expect(run(['ltr/foo:flex'])).toEqual('')
})
test('rtl', () => {
@ -785,6 +828,7 @@ test('rtl', () => {
display: flex;
}"
`)
expect(run(['rtl/foo:flex'])).toEqual('')
})
test('motion-safe', () => {
@ -795,6 +839,7 @@ test('motion-safe', () => {
}
}"
`)
expect(run(['motion-safe/foo:flex'])).toEqual('')
})
test('motion-reduce', () => {
@ -805,6 +850,7 @@ test('motion-reduce', () => {
}
}"
`)
expect(run(['motion-reduce/foo:flex'])).toEqual('')
})
test('dark', () => {
@ -815,6 +861,7 @@ test('dark', () => {
}
}"
`)
expect(run(['dark/foo:flex'])).toEqual('')
})
test('starting', () => {
@ -825,6 +872,7 @@ test('starting', () => {
}
}"
`)
expect(run(['starting/foo:flex'])).toEqual('')
})
test('print', () => {
@ -835,6 +883,7 @@ test('print', () => {
}
}"
`)
expect(run(['print/foo:flex'])).toEqual('')
})
test('default breakpoints', () => {
@ -892,6 +941,22 @@ test('default breakpoints', () => {
}
}"
`)
expect(
compileCss(
css`
@theme reference {
/* Breakpoints */
--breakpoint-sm: 640px;
--breakpoint-md: 768px;
--breakpoint-lg: 1024px;
--breakpoint-xl: 1280px;
--breakpoint-2xl: 1536px;
}
@tailwind utilities;
`,
['sm/foo:flex', 'md/foo:flex', 'lg/foo:flex', 'xl/foo:flex', '2xl/foo:flex'],
),
).toEqual('')
})
test('custom breakpoint', () => {
@ -957,6 +1022,20 @@ test('max-*', () => {
}
}"
`)
expect(
compileCss(
css`
@theme reference {
/* Explicitly ordered in a strange way */
--breakpoint-sm: 640px;
--breakpoint-lg: 1024px;
--breakpoint-md: 768px;
}
@tailwind utilities;
`,
['max-lg/foo:flex', 'max-sm/foo:flex', 'max-md/foo:flex'],
),
).toEqual('')
})
test('min-*', () => {
@ -998,6 +1077,20 @@ test('min-*', () => {
}
}"
`)
expect(
compileCss(
css`
@theme reference {
/* Explicitly ordered in a strange way */
--breakpoint-sm: 640px;
--breakpoint-lg: 1024px;
--breakpoint-md: 768px;
}
@tailwind utilities;
`,
['min-lg/foo:flex', 'min-sm/foo:flex', 'min-md/foo:flex'],
),
).toEqual('')
})
test('sorting stacked min-* and max-* variants', () => {
@ -1356,6 +1449,17 @@ test('supports', () => {
}
}"
`)
expect(
run([
'supports-gap/foo:grid',
'supports-[display:grid]/foo:flex',
'supports-[selector(A_>_B)]/foo:flex',
'supports-[font-format(opentype)]/foo:grid',
'supports-[(display:grid)_and_font-format(opentype)]/foo:grid',
'supports-[font-tech(color-COLRv1)]/foo:flex',
'supports-[--test]/foo:flex',
]),
).toEqual('')
})
test('not', () => {
@ -1400,6 +1504,8 @@ test('not', () => {
display: flex;
}"
`)
expect(run(['not-[:checked]/foo:flex'])).toEqual('')
})
test('has', () => {
@ -1444,6 +1550,7 @@ test('has', () => {
display: flex;
}"
`)
expect(run(['has-[:checked]/foo:flex'])).toEqual('')
})
test('aria', () => {
@ -1503,6 +1610,7 @@ test('aria', () => {
display: flex;
}"
`)
expect(run(['aria-checked/foo:flex', 'aria-[invalid=spelling]/foo:flex'])).toEqual('')
})
test('data', () => {
@ -1542,6 +1650,7 @@ test('data', () => {
display: flex;
}"
`)
expect(run(['data-disabled/foo:flex', 'data-[potato=salad]/foo:flex'])).toEqual('')
})
test('portrait', () => {
@ -1552,6 +1661,7 @@ test('portrait', () => {
}
}"
`)
expect(run(['portrait/foo:flex'])).toEqual('')
})
test('landscape', () => {
@ -1562,6 +1672,7 @@ test('landscape', () => {
}
}"
`)
expect(run(['landscape/foo:flex'])).toEqual('')
})
test('contrast-more', () => {
@ -1572,6 +1683,7 @@ test('contrast-more', () => {
}
}"
`)
expect(run(['contrast-more/foo:flex'])).toEqual('')
})
test('contrast-less', () => {
@ -1582,6 +1694,7 @@ test('contrast-less', () => {
}
}"
`)
expect(run(['contrast-less/foo:flex'])).toEqual('')
})
test('forced-colors', () => {
@ -1592,6 +1705,7 @@ test('forced-colors', () => {
}
}"
`)
expect(run(['forced-colors/foo:flex'])).toEqual('')
})
test('nth', () => {
@ -1653,6 +1767,20 @@ test('nth', () => {
expect(
run(['nth-foo:flex', 'nth-of-type-foo:flex', 'nth-last-foo:flex', 'nth-last-of-type-foo:flex']),
).toEqual('')
expect(
run([
'nth-3/foo:flex',
'nth-[2n+1]/foo:flex',
'nth-[2n+1_of_.foo]/foo:flex',
'nth-last-3/foo:flex',
'nth-last-[2n+1]/foo:flex',
'nth-last-[2n+1_of_.foo]/foo:flex',
'nth-of-type-3/foo:flex',
'nth-of-type-[2n+1]/foo:flex',
'nth-last-of-type-3/foo:flex',
'nth-last-of-type-[2n+1]/foo:flex',
]),
).toEqual('')
})
test('container queries', () => {

View File

@ -178,7 +178,8 @@ export function createVariants(theme: Theme): Variants {
variants.static('force', () => {}, { compounds: false })
staticVariant('*', ['& > *'], { compounds: false })
variants.compound('not', (ruleNode) => {
variants.compound('not', (ruleNode, variant) => {
if (variant.modifier) return null
ruleNode.selector = `&:not(${ruleNode.selector.replace('&', '*')})`
})
@ -336,7 +337,8 @@ export function createVariants(theme: Theme): Variants {
staticVariant(key, [value])
}
variants.compound('has', (ruleNode) => {
variants.compound('has', (ruleNode, variant) => {
if (variant.modifier) return null
ruleNode.selector = `&:has(${ruleNode.selector.replace('&', '*')})`
})
@ -347,7 +349,8 @@ export function createVariants(theme: Theme): Variants {
})
variants.functional('aria', (ruleNode, variant) => {
if (variant.value === null) return null
if (!variant.value || variant.modifier) return null
if (variant.value.kind === 'arbitrary') {
ruleNode.nodes = [rule(`&[aria-${variant.value.value}]`, ruleNode.nodes)]
} else {
@ -368,13 +371,13 @@ export function createVariants(theme: Theme): Variants {
])
variants.functional('data', (ruleNode, variant) => {
if (variant.value === null) return null
if (!variant.value || variant.modifier) return null
ruleNode.nodes = [rule(`&[data-${variant.value.value}]`, ruleNode.nodes)]
})
variants.functional('nth', (ruleNode, variant) => {
if (variant.value === null) return null
if (!variant.value || variant.modifier) return null
// Only numeric bare values are allowed
if (variant.value.kind === 'named' && Number.isNaN(Number(variant.value.value))) return null
@ -383,7 +386,7 @@ export function createVariants(theme: Theme): Variants {
})
variants.functional('nth-last', (ruleNode, variant) => {
if (variant.value === null) return null
if (!variant.value || variant.modifier) return null
// Only numeric bare values are allowed
if (variant.value.kind === 'named' && Number.isNaN(Number(variant.value.value))) return null
@ -392,7 +395,7 @@ export function createVariants(theme: Theme): Variants {
})
variants.functional('nth-of-type', (ruleNode, variant) => {
if (variant.value === null) return null
if (!variant.value || variant.modifier) return null
// Only numeric bare values are allowed
if (variant.value.kind === 'named' && Number.isNaN(Number(variant.value.value))) return null
@ -401,7 +404,7 @@ export function createVariants(theme: Theme): Variants {
})
variants.functional('nth-last-of-type', (ruleNode, variant) => {
if (variant.value === null) return null
if (!variant.value || variant.modifier) return null
// Only numeric bare values are allowed
if (variant.value.kind === 'named' && Number.isNaN(Number(variant.value.value))) return null
@ -412,7 +415,7 @@ export function createVariants(theme: Theme): Variants {
variants.functional(
'supports',
(ruleNode, variant) => {
if (variant.value === null) return null
if (!variant.value || variant.modifier) return null
let value = variant.value.value
if (value === null) return null
@ -540,7 +543,7 @@ export function createVariants(theme: Theme): Variants {
}
case 'functional': {
if (variant.value === null) return null
if (!variant.value || variant.modifier) return null
let value: string | null = null
@ -567,6 +570,7 @@ export function createVariants(theme: Theme): Variants {
variants.functional(
'max',
(ruleNode, variant) => {
if (variant.modifier) return null
let value = resolvedBreakpoints.get(variant)
if (value === null) return null
@ -601,6 +605,7 @@ export function createVariants(theme: Theme): Variants {
variants.functional(
'min',
(ruleNode, variant) => {
if (variant.modifier) return null
let value = resolvedBreakpoints.get(variant)
if (value === null) return null