Skip to content

Commit 45d1a1b

Browse files
Add generalized modifier support to matchUtilities (#9541)
* Change `matchVariant` API to use positional arguments * Fix CS wip * Change match variant wrap modifier in an object Needed for compat w/ some group and peer plugins * Add modifier support to matchUtilities * refactor * Hoist utility modifier splitting * Rename fn * refactor * Add support for generic utility modifiers * Fix CS * wip * update types * Warn when using modifiers without the option * Allow modifiers to be a config object * Make sure we can return null from matchUtilities to omit rules * Feature flag generalized modifiers We’re putting a flag for modifiers in front of matchVariant and matchUtilities * cleanup * Update changelog * Properly flag variants using modifiers * Fix test
1 parent b5651e8 commit 45d1a1b

11 files changed

+426
-81
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2626
- Added 'place-items-baseline' utility ([#9507](https://github.com/tailwindlabs/tailwindcss/pull/9507))
2727
- Added 'content-baseline' utility ([#9507](https://github.com/tailwindlabs/tailwindcss/pull/9507))
2828
- Prepare for container queries setup ([#9526](https://github.com/tailwindlabs/tailwindcss/pull/9526))
29+
- Add support for modifiers to `matchUtilities` ([#9541](https://github.com/tailwindlabs/tailwindcss/pull/9541))
30+
- Switch to positional argument + object for modifiers ([#9541](https://github.com/tailwindlabs/tailwindcss/pull/9541))
2931

3032
### Fixed
3133

src/corePlugins.js

+7-8
Original file line numberDiff line numberDiff line change
@@ -144,28 +144,27 @@ export let variantPlugins = {
144144
}
145145

146146
let variants = {
147-
group: ({ modifier }) =>
147+
group: (_, { modifier }) =>
148148
modifier ? [`:merge(.group\\/${modifier})`, ' &'] : [`:merge(.group)`, ' &'],
149-
peer: ({ modifier }) =>
149+
peer: (_, { modifier }) =>
150150
modifier ? [`:merge(.peer\\/${modifier})`, ' ~ &'] : [`:merge(.peer)`, ' ~ &'],
151151
}
152152

153153
for (let [name, fn] of Object.entries(variants)) {
154154
matchVariant(
155155
name,
156-
(ctx = {}) => {
157-
let { modifier, value = '' } = ctx
158-
if (modifier) {
156+
(value = '', extra) => {
157+
if (extra.modifier) {
159158
log.warn(`modifier-${name}-experimental`, [
160159
`The ${name} variant modifier feature in Tailwind CSS is currently in preview.`,
161160
'Preview features are not covered by semver, and may be improved in breaking ways at any time.',
162161
])
163162
}
164163

165-
let result = normalize(typeof value === 'function' ? value(ctx) : value)
164+
let result = normalize(typeof value === 'function' ? value(extra) : value)
166165
if (!result.includes('&')) result = '&' + result
167166

168-
let [a, b] = fn({ modifier })
167+
let [a, b] = fn('', extra)
169168
return result.replace(/&(\S+)?/g, (_, pseudo = '') => a + pseudo + b)
170169
},
171170
{ values: Object.fromEntries(pseudoVariants) }
@@ -232,7 +231,7 @@ export let variantPlugins = {
232231
supportsVariants: ({ matchVariant, theme }) => {
233232
matchVariant(
234233
'supports',
235-
({ value = '' }) => {
234+
(value = '') => {
236235
let check = normalize(value)
237236
let isRaw = /^\w*\s*\(/.test(check)
238237

src/featureFlags.js

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ let featureFlags = {
1414
],
1515
experimental: [
1616
'optimizeUniversalDefaults',
17+
'generalizedModifiers',
1718
// 'variantGrouping',
1819
],
1920
}

src/lib/generateRules.js

+33-15
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import selectorParser from 'postcss-selector-parser'
33
import parseObjectStyles from '../util/parseObjectStyles'
44
import isPlainObject from '../util/isPlainObject'
55
import prefixSelector from '../util/prefixSelector'
6-
import { updateAllClasses, typeMap } from '../util/pluginUtils'
6+
import { updateAllClasses, getMatchingTypes } from '../util/pluginUtils'
77
import log from '../util/log'
88
import * as sharedState from './sharedState'
99
import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSelector'
@@ -34,13 +34,24 @@ function* candidatePermutations(candidate) {
3434

3535
while (lastIndex >= 0) {
3636
let dashIdx
37+
let wasSlash = false
3738

3839
if (lastIndex === Infinity && candidate.endsWith(']')) {
3940
let bracketIdx = candidate.indexOf('[')
4041

4142
// If character before `[` isn't a dash or a slash, this isn't a dynamic class
4243
// eg. string[]
43-
dashIdx = ['-', '/'].includes(candidate[bracketIdx - 1]) ? bracketIdx - 1 : -1
44+
if (candidate[bracketIdx - 1] === '-') {
45+
dashIdx = bracketIdx - 1
46+
} else if (candidate[bracketIdx - 1] === '/') {
47+
dashIdx = bracketIdx - 1
48+
wasSlash = true
49+
} else {
50+
dashIdx = -1
51+
}
52+
} else if (lastIndex === Infinity && candidate.includes('/')) {
53+
dashIdx = candidate.lastIndexOf('/')
54+
wasSlash = true
4455
} else {
4556
dashIdx = candidate.lastIndexOf('-', lastIndex)
4657
}
@@ -50,11 +61,16 @@ function* candidatePermutations(candidate) {
5061
}
5162

5263
let prefix = candidate.slice(0, dashIdx)
53-
let modifier = candidate.slice(dashIdx + 1)
54-
55-
yield [prefix, modifier]
64+
let modifier = candidate.slice(wasSlash ? dashIdx : dashIdx + 1)
5665

5766
lastIndex = dashIdx - 1
67+
68+
// TODO: This feels a bit hacky
69+
if (prefix === '' || modifier === '/') {
70+
continue
71+
}
72+
73+
yield [prefix, modifier]
5874
}
5975
}
6076

@@ -137,6 +153,10 @@ function applyVariant(variant, matches, context) {
137153
if (match) {
138154
variant = match[1]
139155
args.modifier = match[2]
156+
157+
if (!flagEnabled(context.tailwindConfig, 'generalizedModifiers')) {
158+
return []
159+
}
140160
}
141161
}
142162

@@ -552,16 +572,14 @@ function* resolveMatches(candidate, context, original = candidate) {
552572
}
553573

554574
if (matchesPerPlugin.length > 0) {
555-
let matchingTypes = (sort.options?.types ?? [])
556-
.map(({ type }) => type)
557-
// Only track the types for this plugin that resulted in some result
558-
.filter((type) => {
559-
return Boolean(
560-
typeMap[type](modifier, sort.options, {
561-
tailwindConfig: context.tailwindConfig,
562-
})
563-
)
564-
})
575+
let matchingTypes = Array.from(
576+
getMatchingTypes(
577+
sort.options?.types ?? [],
578+
modifier,
579+
sort.options ?? {},
580+
context.tailwindConfig
581+
)
582+
).map(([_, type]) => type)
565583

566584
if (matchingTypes.length > 0) {
567585
typesByMatches.set(matchesPerPlugin, matchingTypes)

src/lib/setupContextUtils.js

+67-10
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import isValidArbitraryValue from '../util/isValidArbitraryValue'
2121
import { generateRules } from './generateRules'
2222
import { hasContentChanged } from './cacheInvalidation.js'
2323
import { Offsets } from './offsets.js'
24+
import { flagEnabled } from '../featureFlags.js'
2425

2526
let MATCH_VARIANT = Symbol()
2627

@@ -358,6 +359,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
358359
let defaultOptions = {
359360
respectPrefix: true,
360361
respectImportant: true,
362+
modifiers: false,
361363
}
362364

363365
options = normalizeOptionTypes({ ...defaultOptions, ...options })
@@ -371,7 +373,12 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
371373
classList.add([prefixedIdentifier, options])
372374

373375
function wrapped(modifier, { isOnlyPlugin }) {
374-
let [value, coercedType] = coerceValue(options.types, modifier, options, tailwindConfig)
376+
let [value, coercedType, utilityModifier] = coerceValue(
377+
options.types,
378+
modifier,
379+
options,
380+
tailwindConfig
381+
)
375382

376383
if (value === undefined) {
377384
return []
@@ -395,8 +402,22 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
395402
return []
396403
}
397404

405+
let extras = {
406+
get modifier() {
407+
if (!options.modifiers) {
408+
log.warn(`modifier-used-without-options-for-${identifier}`, [
409+
'Your plugin must set `modifiers: true` in its options to support modifiers.',
410+
])
411+
}
412+
413+
return utilityModifier
414+
},
415+
}
416+
417+
let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
418+
398419
let ruleSets = []
399-
.concat(rule(value))
420+
.concat(modifiersEnabled ? rule(value, extras) : rule(value))
400421
.filter(Boolean)
401422
.map((declaration) => ({
402423
[nameClass(identifier, modifier)]: declaration,
@@ -418,6 +439,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
418439
let defaultOptions = {
419440
respectPrefix: true,
420441
respectImportant: false,
442+
modifiers: false,
421443
}
422444

423445
options = normalizeOptionTypes({ ...defaultOptions, ...options })
@@ -431,7 +453,12 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
431453
classList.add([prefixedIdentifier, options])
432454

433455
function wrapped(modifier, { isOnlyPlugin }) {
434-
let [value, coercedType] = coerceValue(options.types, modifier, options, tailwindConfig)
456+
let [value, coercedType, utilityModifier] = coerceValue(
457+
options.types,
458+
modifier,
459+
options,
460+
tailwindConfig
461+
)
435462

436463
if (value === undefined) {
437464
return []
@@ -455,8 +482,22 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
455482
return []
456483
}
457484

485+
let extras = {
486+
get modifier() {
487+
if (!options.modifiers) {
488+
log.warn(`modifier-used-without-options-for-${identifier}`, [
489+
'Your plugin must set `modifiers: true` in its options to support modifiers.',
490+
])
491+
}
492+
493+
return utilityModifier
494+
},
495+
}
496+
497+
let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
498+
458499
let ruleSets = []
459-
.concat(rule(value))
500+
.concat(modifiersEnabled ? rule(value, extras) : rule(value))
460501
.filter(Boolean)
461502
.map((declaration) => ({
462503
[nameClass(identifier, modifier)]: declaration,
@@ -522,21 +563,37 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
522563
let id = ++variantIdentifier // A unique identifier that "groups" these variables together.
523564
let isSpecial = variant === '@'
524565

566+
let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
567+
525568
for (let [key, value] of Object.entries(options?.values ?? {})) {
526569
api.addVariant(
527570
isSpecial ? `${variant}${key}` : `${variant}-${key}`,
528-
Object.assign(({ args, container }) => variantFn({ ...args, container, value }), {
529-
[MATCH_VARIANT]: true,
530-
}),
571+
Object.assign(
572+
({ args, container }) =>
573+
variantFn(
574+
value,
575+
modifiersEnabled ? { modifier: args.modifier, container } : { container }
576+
),
577+
{
578+
[MATCH_VARIANT]: true,
579+
}
580+
),
531581
{ ...options, value, id }
532582
)
533583
}
534584

535585
api.addVariant(
536586
variant,
537-
Object.assign(({ args, container }) => variantFn({ ...args, container }), {
538-
[MATCH_VARIANT]: true,
539-
}),
587+
Object.assign(
588+
({ args, container }) =>
589+
variantFn(
590+
args.value,
591+
modifiersEnabled ? { modifier: args.modifier, container } : { container }
592+
),
593+
{
594+
[MATCH_VARIANT]: true,
595+
}
596+
),
540597
{ ...options, id }
541598
)
542599
},

src/util/nameClass.js

+4
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,9 @@ export function formatClass(classPrefix, key) {
2222
return `-${classPrefix}${key}`
2323
}
2424

25+
if (key.startsWith('/')) {
26+
return `${classPrefix}${key}`
27+
}
28+
2529
return `${classPrefix}-${key}`
2630
}

0 commit comments

Comments
 (0)