mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
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
This commit is contained in:
parent
b5651e88c3
commit
45d1a1b593
@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Added 'place-items-baseline' utility ([#9507](https://github.com/tailwindlabs/tailwindcss/pull/9507))
|
||||
- Added 'content-baseline' utility ([#9507](https://github.com/tailwindlabs/tailwindcss/pull/9507))
|
||||
- Prepare for container queries setup ([#9526](https://github.com/tailwindlabs/tailwindcss/pull/9526))
|
||||
- Add support for modifiers to `matchUtilities` ([#9541](https://github.com/tailwindlabs/tailwindcss/pull/9541))
|
||||
- Switch to positional argument + object for modifiers ([#9541](https://github.com/tailwindlabs/tailwindcss/pull/9541))
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@ -144,28 +144,27 @@ export let variantPlugins = {
|
||||
}
|
||||
|
||||
let variants = {
|
||||
group: ({ modifier }) =>
|
||||
group: (_, { modifier }) =>
|
||||
modifier ? [`:merge(.group\\/${modifier})`, ' &'] : [`:merge(.group)`, ' &'],
|
||||
peer: ({ modifier }) =>
|
||||
peer: (_, { modifier }) =>
|
||||
modifier ? [`:merge(.peer\\/${modifier})`, ' ~ &'] : [`:merge(.peer)`, ' ~ &'],
|
||||
}
|
||||
|
||||
for (let [name, fn] of Object.entries(variants)) {
|
||||
matchVariant(
|
||||
name,
|
||||
(ctx = {}) => {
|
||||
let { modifier, value = '' } = ctx
|
||||
if (modifier) {
|
||||
(value = '', extra) => {
|
||||
if (extra.modifier) {
|
||||
log.warn(`modifier-${name}-experimental`, [
|
||||
`The ${name} variant modifier feature in Tailwind CSS is currently in preview.`,
|
||||
'Preview features are not covered by semver, and may be improved in breaking ways at any time.',
|
||||
])
|
||||
}
|
||||
|
||||
let result = normalize(typeof value === 'function' ? value(ctx) : value)
|
||||
let result = normalize(typeof value === 'function' ? value(extra) : value)
|
||||
if (!result.includes('&')) result = '&' + result
|
||||
|
||||
let [a, b] = fn({ modifier })
|
||||
let [a, b] = fn('', extra)
|
||||
return result.replace(/&(\S+)?/g, (_, pseudo = '') => a + pseudo + b)
|
||||
},
|
||||
{ values: Object.fromEntries(pseudoVariants) }
|
||||
@ -232,7 +231,7 @@ export let variantPlugins = {
|
||||
supportsVariants: ({ matchVariant, theme }) => {
|
||||
matchVariant(
|
||||
'supports',
|
||||
({ value = '' }) => {
|
||||
(value = '') => {
|
||||
let check = normalize(value)
|
||||
let isRaw = /^\w*\s*\(/.test(check)
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ let featureFlags = {
|
||||
],
|
||||
experimental: [
|
||||
'optimizeUniversalDefaults',
|
||||
'generalizedModifiers',
|
||||
// 'variantGrouping',
|
||||
],
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import selectorParser from 'postcss-selector-parser'
|
||||
import parseObjectStyles from '../util/parseObjectStyles'
|
||||
import isPlainObject from '../util/isPlainObject'
|
||||
import prefixSelector from '../util/prefixSelector'
|
||||
import { updateAllClasses, typeMap } from '../util/pluginUtils'
|
||||
import { updateAllClasses, getMatchingTypes } from '../util/pluginUtils'
|
||||
import log from '../util/log'
|
||||
import * as sharedState from './sharedState'
|
||||
import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSelector'
|
||||
@ -34,13 +34,24 @@ function* candidatePermutations(candidate) {
|
||||
|
||||
while (lastIndex >= 0) {
|
||||
let dashIdx
|
||||
let wasSlash = false
|
||||
|
||||
if (lastIndex === Infinity && candidate.endsWith(']')) {
|
||||
let bracketIdx = candidate.indexOf('[')
|
||||
|
||||
// If character before `[` isn't a dash or a slash, this isn't a dynamic class
|
||||
// eg. string[]
|
||||
dashIdx = ['-', '/'].includes(candidate[bracketIdx - 1]) ? bracketIdx - 1 : -1
|
||||
if (candidate[bracketIdx - 1] === '-') {
|
||||
dashIdx = bracketIdx - 1
|
||||
} else if (candidate[bracketIdx - 1] === '/') {
|
||||
dashIdx = bracketIdx - 1
|
||||
wasSlash = true
|
||||
} else {
|
||||
dashIdx = -1
|
||||
}
|
||||
} else if (lastIndex === Infinity && candidate.includes('/')) {
|
||||
dashIdx = candidate.lastIndexOf('/')
|
||||
wasSlash = true
|
||||
} else {
|
||||
dashIdx = candidate.lastIndexOf('-', lastIndex)
|
||||
}
|
||||
@ -50,11 +61,16 @@ function* candidatePermutations(candidate) {
|
||||
}
|
||||
|
||||
let prefix = candidate.slice(0, dashIdx)
|
||||
let modifier = candidate.slice(dashIdx + 1)
|
||||
|
||||
yield [prefix, modifier]
|
||||
let modifier = candidate.slice(wasSlash ? dashIdx : dashIdx + 1)
|
||||
|
||||
lastIndex = dashIdx - 1
|
||||
|
||||
// TODO: This feels a bit hacky
|
||||
if (prefix === '' || modifier === '/') {
|
||||
continue
|
||||
}
|
||||
|
||||
yield [prefix, modifier]
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,6 +153,10 @@ function applyVariant(variant, matches, context) {
|
||||
if (match) {
|
||||
variant = match[1]
|
||||
args.modifier = match[2]
|
||||
|
||||
if (!flagEnabled(context.tailwindConfig, 'generalizedModifiers')) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -552,16 +572,14 @@ function* resolveMatches(candidate, context, original = candidate) {
|
||||
}
|
||||
|
||||
if (matchesPerPlugin.length > 0) {
|
||||
let matchingTypes = (sort.options?.types ?? [])
|
||||
.map(({ type }) => type)
|
||||
// Only track the types for this plugin that resulted in some result
|
||||
.filter((type) => {
|
||||
return Boolean(
|
||||
typeMap[type](modifier, sort.options, {
|
||||
tailwindConfig: context.tailwindConfig,
|
||||
})
|
||||
)
|
||||
})
|
||||
let matchingTypes = Array.from(
|
||||
getMatchingTypes(
|
||||
sort.options?.types ?? [],
|
||||
modifier,
|
||||
sort.options ?? {},
|
||||
context.tailwindConfig
|
||||
)
|
||||
).map(([_, type]) => type)
|
||||
|
||||
if (matchingTypes.length > 0) {
|
||||
typesByMatches.set(matchesPerPlugin, matchingTypes)
|
||||
|
||||
@ -21,6 +21,7 @@ import isValidArbitraryValue from '../util/isValidArbitraryValue'
|
||||
import { generateRules } from './generateRules'
|
||||
import { hasContentChanged } from './cacheInvalidation.js'
|
||||
import { Offsets } from './offsets.js'
|
||||
import { flagEnabled } from '../featureFlags.js'
|
||||
|
||||
let MATCH_VARIANT = Symbol()
|
||||
|
||||
@ -358,6 +359,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
||||
let defaultOptions = {
|
||||
respectPrefix: true,
|
||||
respectImportant: true,
|
||||
modifiers: false,
|
||||
}
|
||||
|
||||
options = normalizeOptionTypes({ ...defaultOptions, ...options })
|
||||
@ -371,7 +373,12 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
||||
classList.add([prefixedIdentifier, options])
|
||||
|
||||
function wrapped(modifier, { isOnlyPlugin }) {
|
||||
let [value, coercedType] = coerceValue(options.types, modifier, options, tailwindConfig)
|
||||
let [value, coercedType, utilityModifier] = coerceValue(
|
||||
options.types,
|
||||
modifier,
|
||||
options,
|
||||
tailwindConfig
|
||||
)
|
||||
|
||||
if (value === undefined) {
|
||||
return []
|
||||
@ -395,8 +402,22 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
||||
return []
|
||||
}
|
||||
|
||||
let extras = {
|
||||
get modifier() {
|
||||
if (!options.modifiers) {
|
||||
log.warn(`modifier-used-without-options-for-${identifier}`, [
|
||||
'Your plugin must set `modifiers: true` in its options to support modifiers.',
|
||||
])
|
||||
}
|
||||
|
||||
return utilityModifier
|
||||
},
|
||||
}
|
||||
|
||||
let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
|
||||
|
||||
let ruleSets = []
|
||||
.concat(rule(value))
|
||||
.concat(modifiersEnabled ? rule(value, extras) : rule(value))
|
||||
.filter(Boolean)
|
||||
.map((declaration) => ({
|
||||
[nameClass(identifier, modifier)]: declaration,
|
||||
@ -418,6 +439,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
||||
let defaultOptions = {
|
||||
respectPrefix: true,
|
||||
respectImportant: false,
|
||||
modifiers: false,
|
||||
}
|
||||
|
||||
options = normalizeOptionTypes({ ...defaultOptions, ...options })
|
||||
@ -431,7 +453,12 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
||||
classList.add([prefixedIdentifier, options])
|
||||
|
||||
function wrapped(modifier, { isOnlyPlugin }) {
|
||||
let [value, coercedType] = coerceValue(options.types, modifier, options, tailwindConfig)
|
||||
let [value, coercedType, utilityModifier] = coerceValue(
|
||||
options.types,
|
||||
modifier,
|
||||
options,
|
||||
tailwindConfig
|
||||
)
|
||||
|
||||
if (value === undefined) {
|
||||
return []
|
||||
@ -455,8 +482,22 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
||||
return []
|
||||
}
|
||||
|
||||
let extras = {
|
||||
get modifier() {
|
||||
if (!options.modifiers) {
|
||||
log.warn(`modifier-used-without-options-for-${identifier}`, [
|
||||
'Your plugin must set `modifiers: true` in its options to support modifiers.',
|
||||
])
|
||||
}
|
||||
|
||||
return utilityModifier
|
||||
},
|
||||
}
|
||||
|
||||
let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
|
||||
|
||||
let ruleSets = []
|
||||
.concat(rule(value))
|
||||
.concat(modifiersEnabled ? rule(value, extras) : rule(value))
|
||||
.filter(Boolean)
|
||||
.map((declaration) => ({
|
||||
[nameClass(identifier, modifier)]: declaration,
|
||||
@ -522,21 +563,37 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
||||
let id = ++variantIdentifier // A unique identifier that "groups" these variables together.
|
||||
let isSpecial = variant === '@'
|
||||
|
||||
let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
|
||||
|
||||
for (let [key, value] of Object.entries(options?.values ?? {})) {
|
||||
api.addVariant(
|
||||
isSpecial ? `${variant}${key}` : `${variant}-${key}`,
|
||||
Object.assign(({ args, container }) => variantFn({ ...args, container, value }), {
|
||||
[MATCH_VARIANT]: true,
|
||||
}),
|
||||
Object.assign(
|
||||
({ args, container }) =>
|
||||
variantFn(
|
||||
value,
|
||||
modifiersEnabled ? { modifier: args.modifier, container } : { container }
|
||||
),
|
||||
{
|
||||
[MATCH_VARIANT]: true,
|
||||
}
|
||||
),
|
||||
{ ...options, value, id }
|
||||
)
|
||||
}
|
||||
|
||||
api.addVariant(
|
||||
variant,
|
||||
Object.assign(({ args, container }) => variantFn({ ...args, container }), {
|
||||
[MATCH_VARIANT]: true,
|
||||
}),
|
||||
Object.assign(
|
||||
({ args, container }) =>
|
||||
variantFn(
|
||||
args.value,
|
||||
modifiersEnabled ? { modifier: args.modifier, container } : { container }
|
||||
),
|
||||
{
|
||||
[MATCH_VARIANT]: true,
|
||||
}
|
||||
),
|
||||
{ ...options, id }
|
||||
)
|
||||
},
|
||||
|
||||
@ -22,5 +22,9 @@ export function formatClass(classPrefix, key) {
|
||||
return `-${classPrefix}${key}`
|
||||
}
|
||||
|
||||
if (key.startsWith('/')) {
|
||||
return `${classPrefix}${key}`
|
||||
}
|
||||
|
||||
return `${classPrefix}-${key}`
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ import {
|
||||
} from './dataTypes'
|
||||
import negateValue from './negateValue'
|
||||
import { backgroundSize } from './validateFormalSyntax'
|
||||
import { flagEnabled } from '../featureFlags.js'
|
||||
|
||||
export function updateAllClasses(selectors, updateClass) {
|
||||
let parser = selectorParser((selectors) => {
|
||||
@ -86,11 +87,20 @@ function isArbitraryValue(input) {
|
||||
return input.startsWith('[') && input.endsWith(']')
|
||||
}
|
||||
|
||||
function splitAlpha(modifier) {
|
||||
function splitUtilityModifier(modifier) {
|
||||
let slashIdx = modifier.lastIndexOf('/')
|
||||
|
||||
if (slashIdx === -1 || slashIdx === modifier.length - 1) {
|
||||
return [modifier]
|
||||
return [modifier, undefined]
|
||||
}
|
||||
|
||||
let arbitrary = isArbitraryValue(modifier)
|
||||
|
||||
// The modifier could be of the form `[foo]/[bar]`
|
||||
// We want to handle this case properly
|
||||
// without affecting `[foo/bar]`
|
||||
if (arbitrary && !modifier.includes(']/[')) {
|
||||
return [modifier, undefined]
|
||||
}
|
||||
|
||||
return [modifier.slice(0, slashIdx), modifier.slice(slashIdx + 1)]
|
||||
@ -106,12 +116,18 @@ export function parseColorFormat(value) {
|
||||
return value
|
||||
}
|
||||
|
||||
export function asColor(modifier, options = {}, { tailwindConfig = {} } = {}) {
|
||||
if (options.values?.[modifier] !== undefined) {
|
||||
return parseColorFormat(options.values?.[modifier])
|
||||
export function asColor(
|
||||
_,
|
||||
options = {},
|
||||
{ tailwindConfig = {}, utilityModifier, rawModifier } = {}
|
||||
) {
|
||||
if (options.values?.[rawModifier] !== undefined) {
|
||||
return parseColorFormat(options.values?.[rawModifier])
|
||||
}
|
||||
|
||||
let [color, alpha] = splitAlpha(modifier)
|
||||
// TODO: Hoist this up to getMatchingTypes or something
|
||||
// We do this here because we need the alpha value (if any)
|
||||
let [color, alpha] = splitUtilityModifier(rawModifier)
|
||||
|
||||
if (alpha !== undefined) {
|
||||
let normalizedColor =
|
||||
@ -134,7 +150,7 @@ export function asColor(modifier, options = {}, { tailwindConfig = {} } = {}) {
|
||||
return withAlphaValue(normalizedColor, tailwindConfig.theme.opacity[alpha])
|
||||
}
|
||||
|
||||
return asValue(modifier, options, { validate: validateColor })
|
||||
return asValue(rawModifier, options, { rawModifier, utilityModifier, validate: validateColor })
|
||||
}
|
||||
|
||||
export function asLookupValue(modifier, options = {}) {
|
||||
@ -142,8 +158,8 @@ export function asLookupValue(modifier, options = {}) {
|
||||
}
|
||||
|
||||
function guess(validate) {
|
||||
return (modifier, options) => {
|
||||
return asValue(modifier, options, { validate })
|
||||
return (modifier, options, extras) => {
|
||||
return asValue(modifier, options, { ...extras, validate })
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,15 +208,73 @@ export function coerceValue(types, modifier, options, tailwindConfig) {
|
||||
}
|
||||
|
||||
if (value.length > 0 && supportedTypes.includes(explicitType)) {
|
||||
return [asValue(`[${value}]`, options), explicitType]
|
||||
return [asValue(`[${value}]`, options), explicitType, null]
|
||||
}
|
||||
}
|
||||
|
||||
let matches = getMatchingTypes(types, modifier, options, tailwindConfig)
|
||||
|
||||
// Find first matching type
|
||||
for (let { type } of types) {
|
||||
let result = typeMap[type](modifier, options, { tailwindConfig })
|
||||
if (result !== undefined) return [result, type]
|
||||
for (let match of matches) {
|
||||
return match
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{type: string}[]} types
|
||||
* @param {string} rawModifier
|
||||
* @param {any} options
|
||||
* @param {any} tailwindConfig
|
||||
* @returns {Iterator<[value: string, type: string, modifier: string | null]>}
|
||||
*/
|
||||
export function* getMatchingTypes(types, rawModifier, options, tailwindConfig) {
|
||||
let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
|
||||
|
||||
let canUseUtilityModifier =
|
||||
modifiersEnabled &&
|
||||
options.modifiers != null &&
|
||||
(options.modifiers === 'any' || typeof options.modifiers === 'object')
|
||||
|
||||
let [modifier, utilityModifier] = canUseUtilityModifier
|
||||
? splitUtilityModifier(rawModifier)
|
||||
: [rawModifier, undefined]
|
||||
|
||||
if (utilityModifier !== undefined && modifier === '') {
|
||||
modifier = 'DEFAULT'
|
||||
}
|
||||
|
||||
// Check the full value first
|
||||
// TODO: Move to asValue… somehow
|
||||
if (utilityModifier !== undefined) {
|
||||
if (typeof options.modifiers === 'object') {
|
||||
let configValue = options.modifiers?.[utilityModifier] ?? null
|
||||
if (configValue !== null) {
|
||||
utilityModifier = configValue
|
||||
} else if (isArbitraryValue(utilityModifier)) {
|
||||
utilityModifier = utilityModifier.slice(1, -1)
|
||||
}
|
||||
}
|
||||
|
||||
let result = asValue(rawModifier, options, { rawModifier, utilityModifier, tailwindConfig })
|
||||
if (result !== undefined) {
|
||||
yield [result, 'any', null]
|
||||
}
|
||||
}
|
||||
|
||||
for (const { type } of types ?? []) {
|
||||
let result = typeMap[type](modifier, options, {
|
||||
rawModifier,
|
||||
utilityModifier,
|
||||
tailwindConfig,
|
||||
})
|
||||
|
||||
if (result === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
yield [result, type, utilityModifier ?? null]
|
||||
}
|
||||
}
|
||||
|
||||
@ -709,6 +709,9 @@ it('should support supports', () => {
|
||||
|
||||
it('should be possible to use modifiers and arbitrary groups', () => {
|
||||
let config = {
|
||||
experimental: {
|
||||
generalizedModifiers: true,
|
||||
},
|
||||
content: [
|
||||
{
|
||||
raw: html`
|
||||
@ -810,6 +813,9 @@ it('should be possible to use modifiers and arbitrary groups', () => {
|
||||
|
||||
it('should be possible to use modifiers and arbitrary peers', () => {
|
||||
let config = {
|
||||
experimental: {
|
||||
generalizedModifiers: true,
|
||||
},
|
||||
content: [
|
||||
{
|
||||
raw: html`
|
||||
|
||||
177
tests/match-utilities.test.js
Normal file
177
tests/match-utilities.test.js
Normal file
@ -0,0 +1,177 @@
|
||||
import { run, html, css } from './util/run'
|
||||
|
||||
test('match utilities with modifiers', async () => {
|
||||
let config = {
|
||||
experimental: {
|
||||
generalizedModifiers: true,
|
||||
},
|
||||
|
||||
content: [
|
||||
{
|
||||
raw: html`<div class="test test/foo test-1/foo test-2/foo test/[foo] test-1/[foo]"></div> `,
|
||||
},
|
||||
],
|
||||
corePlugins: { preflight: false },
|
||||
|
||||
plugins: [
|
||||
({ matchUtilities }) => {
|
||||
matchUtilities(
|
||||
{
|
||||
test: (value, { modifier }) => ({
|
||||
color: `${value}_${modifier}`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
values: {
|
||||
DEFAULT: 'default',
|
||||
bar: 'bar',
|
||||
'1': 'one',
|
||||
'2': 'two',
|
||||
'1/foo': 'onefoo',
|
||||
},
|
||||
modifiers: 'any',
|
||||
}
|
||||
)
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
let result = await run(input, config)
|
||||
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.test {
|
||||
color: default_null;
|
||||
}
|
||||
.test\/foo {
|
||||
color: default_foo;
|
||||
}
|
||||
.test-1\/foo {
|
||||
color: onefoo_null;
|
||||
}
|
||||
.test-2\/foo {
|
||||
color: two_foo;
|
||||
}
|
||||
.test\/\[foo\] {
|
||||
color: default_[foo];
|
||||
}
|
||||
.test-1\/\[foo\] {
|
||||
color: one_[foo];
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test('match utilities with modifiers in the config', async () => {
|
||||
let config = {
|
||||
experimental: {
|
||||
generalizedModifiers: true,
|
||||
},
|
||||
content: [
|
||||
{
|
||||
raw: html`<div class="test test/foo test-1/foo test/[bar] test-1/[bar]"></div> `,
|
||||
},
|
||||
],
|
||||
corePlugins: { preflight: false },
|
||||
|
||||
plugins: [
|
||||
({ matchUtilities }) => {
|
||||
matchUtilities(
|
||||
{
|
||||
test: (value, { modifier }) => ({
|
||||
color: `${value}_${modifier}`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
values: {
|
||||
DEFAULT: 'default',
|
||||
bar: 'bar',
|
||||
'1': 'one',
|
||||
},
|
||||
modifiers: {
|
||||
foo: 'mewtwo',
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
let result = await run(input, config)
|
||||
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.test {
|
||||
color: default_null;
|
||||
}
|
||||
.test\/foo {
|
||||
color: default_mewtwo;
|
||||
}
|
||||
.test-1\/foo {
|
||||
color: one_mewtwo;
|
||||
}
|
||||
.test\/\[bar\] {
|
||||
color: default_bar;
|
||||
}
|
||||
.test-1\/\[bar\] {
|
||||
color: one_bar;
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test('match utilities can omit utilities by returning null', async () => {
|
||||
let config = {
|
||||
experimental: {
|
||||
generalizedModifiers: true,
|
||||
},
|
||||
content: [
|
||||
{
|
||||
raw: html`<div class="test test/good test/bad"></div> `,
|
||||
},
|
||||
],
|
||||
corePlugins: { preflight: false },
|
||||
|
||||
plugins: [
|
||||
({ matchUtilities }) => {
|
||||
matchUtilities(
|
||||
{
|
||||
test: (value, { modifier }) =>
|
||||
modifier === 'bad'
|
||||
? null
|
||||
: {
|
||||
color: `${value}_${modifier}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
values: {
|
||||
DEFAULT: 'default',
|
||||
bar: 'bar',
|
||||
'1': 'one',
|
||||
},
|
||||
modifiers: 'any',
|
||||
}
|
||||
)
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
let result = await run(input, config)
|
||||
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.test {
|
||||
color: default_null;
|
||||
}
|
||||
.test\/good {
|
||||
color: default_good;
|
||||
}
|
||||
`)
|
||||
})
|
||||
@ -10,7 +10,7 @@ test('partial arbitrary variants', () => {
|
||||
corePlugins: { preflight: false },
|
||||
plugins: [
|
||||
({ matchVariant }) => {
|
||||
matchVariant('potato', ({ value: flavor }) => `.potato-${flavor} &`)
|
||||
matchVariant('potato', (flavor) => `.potato-${flavor} &`)
|
||||
},
|
||||
],
|
||||
}
|
||||
@ -43,7 +43,7 @@ test('partial arbitrary variants with at-rules', () => {
|
||||
corePlugins: { preflight: false },
|
||||
plugins: [
|
||||
({ matchVariant }) => {
|
||||
matchVariant('potato', ({ value: flavor }) => `@media (potato: ${flavor})`)
|
||||
matchVariant('potato', (flavor) => `@media (potato: ${flavor})`)
|
||||
},
|
||||
],
|
||||
}
|
||||
@ -79,7 +79,7 @@ test('partial arbitrary variants with at-rules and placeholder', () => {
|
||||
corePlugins: { preflight: false },
|
||||
plugins: [
|
||||
({ matchVariant }) => {
|
||||
matchVariant('potato', ({ value: flavor }) => `@media (potato: ${flavor}) { &:potato }`)
|
||||
matchVariant('potato', (flavor) => `@media (potato: ${flavor}) { &:potato }`)
|
||||
},
|
||||
],
|
||||
}
|
||||
@ -115,7 +115,7 @@ test('partial arbitrary variants with default values', () => {
|
||||
corePlugins: { preflight: false },
|
||||
plugins: [
|
||||
({ matchVariant }) => {
|
||||
matchVariant('tooltip', ({ value: side }) => `&${side}`, {
|
||||
matchVariant('tooltip', (side) => `&${side}`, {
|
||||
values: {
|
||||
bottom: '[data-location="bottom"]',
|
||||
top: '[data-location="top"]',
|
||||
@ -154,7 +154,7 @@ test('matched variant values maintain the sort order they are registered in', ()
|
||||
corePlugins: { preflight: false },
|
||||
plugins: [
|
||||
({ matchVariant }) => {
|
||||
matchVariant('alphabet', ({ value: side }) => `&${side}`, {
|
||||
matchVariant('alphabet', (side) => `&${side}`, {
|
||||
values: {
|
||||
a: '[data-value="a"]',
|
||||
b: '[data-value="b"]',
|
||||
@ -201,7 +201,7 @@ test('matchVariant can return an array of format strings from the function', ()
|
||||
corePlugins: { preflight: false },
|
||||
plugins: [
|
||||
({ matchVariant }) => {
|
||||
matchVariant('test', ({ value: selector }) =>
|
||||
matchVariant('test', (selector) =>
|
||||
selector.split(',').map((selector) => `&.${selector} > *`)
|
||||
)
|
||||
},
|
||||
@ -243,7 +243,7 @@ it('should be possible to sort variants', () => {
|
||||
corePlugins: { preflight: false },
|
||||
plugins: [
|
||||
({ matchVariant }) => {
|
||||
matchVariant('min', ({ value }) => `@media (min-width: ${value})`, {
|
||||
matchVariant('min', (value) => `@media (min-width: ${value})`, {
|
||||
sort(a, z) {
|
||||
return parseInt(a.value) - parseInt(z.value)
|
||||
},
|
||||
@ -287,7 +287,7 @@ it('should be possible to compare arbitrary variants and hardcoded variants', ()
|
||||
corePlugins: { preflight: false },
|
||||
plugins: [
|
||||
({ matchVariant }) => {
|
||||
matchVariant('min', ({ value }) => `@media (min-width: ${value})`, {
|
||||
matchVariant('min', (value) => `@media (min-width: ${value})`, {
|
||||
values: {
|
||||
example: '600px',
|
||||
},
|
||||
@ -347,13 +347,13 @@ it('should be possible to sort stacked arbitrary variants correctly', () => {
|
||||
corePlugins: { preflight: false },
|
||||
plugins: [
|
||||
({ matchVariant }) => {
|
||||
matchVariant('min', ({ value }) => `@media (min-width: ${value})`, {
|
||||
matchVariant('min', (value) => `@media (min-width: ${value})`, {
|
||||
sort(a, z) {
|
||||
return parseInt(a.value) - parseInt(z.value)
|
||||
},
|
||||
})
|
||||
|
||||
matchVariant('max', ({ value }) => `@media (max-width: ${value})`, {
|
||||
matchVariant('max', (value) => `@media (max-width: ${value})`, {
|
||||
sort(a, z) {
|
||||
return parseInt(z.value) - parseInt(a.value)
|
||||
},
|
||||
@ -412,13 +412,13 @@ it('should maintain sort from other variants, if sort functions of arbitrary var
|
||||
corePlugins: { preflight: false },
|
||||
plugins: [
|
||||
({ matchVariant }) => {
|
||||
matchVariant('min', ({ value }) => `@media (min-width: ${value})`, {
|
||||
matchVariant('min', (value) => `@media (min-width: ${value})`, {
|
||||
sort(a, z) {
|
||||
return parseInt(a.value) - parseInt(z.value)
|
||||
},
|
||||
})
|
||||
|
||||
matchVariant('max', ({ value }) => `@media (max-width: ${value})`, {
|
||||
matchVariant('max', (value) => `@media (max-width: ${value})`, {
|
||||
sort(a, z) {
|
||||
return parseInt(z.value) - parseInt(a.value)
|
||||
},
|
||||
@ -464,12 +464,12 @@ it('should sort arbitrary variants left to right (1)', () => {
|
||||
corePlugins: { preflight: false },
|
||||
plugins: [
|
||||
({ matchVariant }) => {
|
||||
matchVariant('min', ({ value }) => `@media (min-width: ${value})`, {
|
||||
matchVariant('min', (value) => `@media (min-width: ${value})`, {
|
||||
sort(a, z) {
|
||||
return parseInt(a.value) - parseInt(z.value)
|
||||
},
|
||||
})
|
||||
matchVariant('max', ({ value }) => `@media (max-width: ${value})`, {
|
||||
matchVariant('max', (value) => `@media (max-width: ${value})`, {
|
||||
sort(a, z) {
|
||||
return parseInt(z.value) - parseInt(a.value)
|
||||
},
|
||||
@ -532,12 +532,12 @@ it('should sort arbitrary variants left to right (2)', () => {
|
||||
corePlugins: { preflight: false },
|
||||
plugins: [
|
||||
({ matchVariant }) => {
|
||||
matchVariant('min', ({ value }) => `@media (min-width: ${value})`, {
|
||||
matchVariant('min', (value) => `@media (min-width: ${value})`, {
|
||||
sort(a, z) {
|
||||
return parseInt(a.value) - parseInt(z.value)
|
||||
},
|
||||
})
|
||||
matchVariant('max', ({ value }) => `@media (max-width: ${value})`, {
|
||||
matchVariant('max', (value) => `@media (max-width: ${value})`, {
|
||||
sort(a, z) {
|
||||
return parseInt(z.value) - parseInt(a.value)
|
||||
},
|
||||
@ -598,7 +598,7 @@ it('should guarantee that we are not passing values from other variants to the w
|
||||
corePlugins: { preflight: false },
|
||||
plugins: [
|
||||
({ matchVariant }) => {
|
||||
matchVariant('min', ({ value }) => `@media (min-width: ${value})`, {
|
||||
matchVariant('min', (value) => `@media (min-width: ${value})`, {
|
||||
sort(a, z) {
|
||||
let lookup = ['100px', '200px']
|
||||
if (lookup.indexOf(a.value) === -1 || lookup.indexOf(z.value) === -1) {
|
||||
@ -607,7 +607,7 @@ it('should guarantee that we are not passing values from other variants to the w
|
||||
return lookup.indexOf(a.value) - lookup.indexOf(z.value)
|
||||
},
|
||||
})
|
||||
matchVariant('max', ({ value }) => `@media (max-width: ${value})`, {
|
||||
matchVariant('max', (value) => `@media (max-width: ${value})`, {
|
||||
sort(a, z) {
|
||||
let lookup = ['300px', '400px']
|
||||
if (lookup.indexOf(a.value) === -1 || lookup.indexOf(z.value) === -1) {
|
||||
|
||||
41
types/config.d.ts
vendored
41
types/config.d.ts
vendored
@ -12,7 +12,7 @@ interface RecursiveKeyValuePair<K extends keyof any = string, V = string> {
|
||||
[key: string]: V | RecursiveKeyValuePair<K, V>
|
||||
}
|
||||
type ResolvableTo<T> = T | ((utils: PluginUtils) => T)
|
||||
type CSSRuleObject = RecursiveKeyValuePair<string, string | string[]>
|
||||
type CSSRuleObject = RecursiveKeyValuePair<string, null | string | string[]>
|
||||
|
||||
interface PluginUtils {
|
||||
colors: DefaultColors
|
||||
@ -263,13 +263,17 @@ export interface PluginAPI {
|
||||
}>
|
||||
): void
|
||||
// for registering new dynamic utility styles
|
||||
matchUtilities<T>(
|
||||
utilities: KeyValuePair<string, (value: T) => CSSRuleObject>,
|
||||
matchUtilities<T = string, U = string>(
|
||||
utilities: KeyValuePair<
|
||||
string,
|
||||
(value: T | string, extra: { modifier: U | string | null }) => CSSRuleObject | null
|
||||
>,
|
||||
options?: Partial<{
|
||||
respectPrefix: boolean
|
||||
respectImportant: boolean
|
||||
type: ValueType | ValueType[]
|
||||
values: KeyValuePair<string, T>
|
||||
modifiers: 'any' | KeyValuePair<string, U>
|
||||
supportsNegativeValues: boolean
|
||||
}>
|
||||
): void
|
||||
@ -282,13 +286,17 @@ export interface PluginAPI {
|
||||
}>
|
||||
): void
|
||||
// for registering new dynamic component styles
|
||||
matchComponents<T>(
|
||||
components: KeyValuePair<string, (value: T) => CSSRuleObject>,
|
||||
matchComponents<T = string, U = string>(
|
||||
components: KeyValuePair<
|
||||
string,
|
||||
(value: T | string, extra: { modifier: U | string | null }) => CSSRuleObject | null
|
||||
>,
|
||||
options?: Partial<{
|
||||
respectPrefix: boolean
|
||||
respectImportant: boolean
|
||||
type: ValueType | ValueType[]
|
||||
values: KeyValuePair<string, T>
|
||||
modifiers: 'any' | KeyValuePair<string, U>
|
||||
supportsNegativeValues: boolean
|
||||
}>
|
||||
): void
|
||||
@ -296,18 +304,14 @@ export interface PluginAPI {
|
||||
addBase(base: CSSRuleObject | CSSRuleObject[]): void
|
||||
// for registering custom variants
|
||||
addVariant(name: string, definition: string | string[] | (() => string) | (() => string)[]): void
|
||||
matchVariant(
|
||||
matchVariant<T = string>(
|
||||
name: string,
|
||||
cb: (options: { value: string; modifier: string | null }) => string | string[]
|
||||
): void
|
||||
matchVariant<Values extends {}>(
|
||||
name: string,
|
||||
cb: (options: { value: string; modifier: string | null }) => string | string[],
|
||||
options: {
|
||||
values: Values
|
||||
sort(
|
||||
a: { value: keyof Values | string; modifier: string | null },
|
||||
b: { value: keyof Values | string; modifier: string | null }
|
||||
cb: (value: T | string, extra: { modifier: string | null }) => string | string[],
|
||||
options?: {
|
||||
values?: KeyValuePair<string, T>
|
||||
sort?(
|
||||
a: { value: T | string; modifier: string | null },
|
||||
b: { value: T | string; modifier: string | null }
|
||||
): number
|
||||
}
|
||||
): void
|
||||
@ -327,7 +331,10 @@ export type PluginCreator = (api: PluginAPI) => void
|
||||
export type PluginsConfig = (
|
||||
| PluginCreator
|
||||
| { handler: PluginCreator; config?: Partial<Config> }
|
||||
| { (options: any): { handler: PluginCreator; config?: Partial<Config> }; __isOptionsFunction: true }
|
||||
| {
|
||||
(options: any): { handler: PluginCreator; config?: Partial<Config> }
|
||||
__isOptionsFunction: true
|
||||
}
|
||||
)[]
|
||||
|
||||
// Top level config related
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user