Support defining variants as functions for easier extending (#2309)

* Support defining variants as functions for easier extending

* Fix style

* Remove commented code

* Add 'without' helper to variant function API

* Update changelog
This commit is contained in:
Adam Wathan 2020-09-04 10:59:02 -04:00 committed by GitHub
parent ff013c5e9c
commit 476950ce40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 126 additions and 8 deletions

View File

@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- New `layers` mode for `purge` ([#2288](https://github.com/tailwindlabs/tailwindcss/pull/2288))
- New `font-variant-numeric` utilities ([#2305](https://github.com/tailwindlabs/tailwindcss/pull/2305))
- New `place-items`, `place-content`, `place-self`, `justify-items`, and `justify-self` utilities ([#2306](https://github.com/tailwindlabs/tailwindcss/pull/2306))
- Support configuring variants as functions ([#2309](https://github.com/tailwindlabs/tailwindcss/pull/2309))
### Deprecated

View File

@ -1736,3 +1736,65 @@ test('user theme extensions take precedence over plugin theme extensions with th
plugins: userConfig.plugins,
})
})
test('variants can be defined as a function', () => {
const userConfig = {
variants: {
backgroundColor: ({ variants }) => [...variants('backgroundColor'), 'disabled'],
padding: ({ before }) => before(['active']),
float: ({ before }) => before(['disabled'], 'focus'),
margin: ({ before }) => before(['hover'], 'focus'),
borderWidth: ({ after }) => after(['active']),
backgroundImage: ({ after }) => after(['disabled'], 'hover'),
opacity: ({ after }) => after(['hover'], 'focus'),
rotate: ({ without }) => without(['hover']),
cursor: ({ before, after, without }) =>
without(['responsive'], before(['checked'], 'hover', after(['hover'], 'focus'))),
},
}
const otherConfig = {
variants: {
backgroundColor: ({ variants }) => [...variants('backgroundColor'), 'active'],
},
}
const defaultConfig = {
prefix: '',
important: false,
separator: ':',
theme: {},
variants: {
backgroundColor: ['responsive', 'hover', 'focus'],
padding: ['responsive', 'focus'],
float: ['responsive', 'hover', 'focus'],
margin: ['responsive'],
borderWidth: ['responsive', 'focus'],
backgroundImage: ['responsive', 'hover', 'focus'],
opacity: ['responsive'],
rotate: ['responsive', 'hover', 'focus'],
cursor: ['responsive', 'focus'],
},
}
const result = resolveConfig([userConfig, otherConfig, defaultConfig])
expect(result).toEqual({
prefix: '',
important: false,
separator: ':',
theme: {},
variants: {
backgroundColor: ['responsive', 'hover', 'focus', 'active', 'disabled'],
padding: ['active', 'responsive', 'focus'],
float: ['responsive', 'hover', 'disabled', 'focus'],
margin: ['responsive', 'hover'],
borderWidth: ['responsive', 'focus', 'active'],
backgroundImage: ['responsive', 'hover', 'disabled', 'focus'],
opacity: ['hover', 'responsive'],
rotate: ['responsive', 'focus'],
cursor: ['focus', 'checked', 'hover'],
},
plugins: userConfig.plugins,
})
})

View File

@ -83,7 +83,7 @@ function mergeExtensions({ extend, ...theme }) {
}
function resolveFunctionKeys(object) {
const resolveThemePath = (key, defaultValue) => {
const resolvePath = (key, defaultValue) => {
const path = toPath(key)
let index = 0
@ -91,7 +91,7 @@ function resolveFunctionKeys(object) {
while (val !== undefined && val !== null && index < path.length) {
val = val[path[index++]]
val = isFunction(val) ? val(resolveThemePath, configUtils) : val
val = isFunction(val) ? val(resolvePath, configUtils) : val
}
return val === undefined ? defaultValue : val
@ -100,7 +100,7 @@ function resolveFunctionKeys(object) {
return Object.keys(object).reduce((resolved, key) => {
return {
...resolved,
[key]: isFunction(object[key]) ? object[key](resolveThemePath, configUtils) : object[key],
[key]: isFunction(object[key]) ? object[key](resolvePath, configUtils) : object[key],
}
}, {})
}
@ -128,6 +128,65 @@ function extractPluginConfigs(configs) {
return allConfigs
}
function resolveVariants([firstConfig, ...variantConfigs]) {
if (Array.isArray(firstConfig)) {
return firstConfig
}
return [firstConfig, ...variantConfigs].reverse().reduce((resolved, variants) => {
Object.entries(variants || {}).forEach(([plugin, pluginVariants]) => {
if (isFunction(pluginVariants)) {
resolved[plugin] = pluginVariants({
variants(path) {
return get(resolved, path, [])
},
before(toInsert, variant, existingPluginVariants = get(resolved, plugin, [])) {
if (variant === undefined) {
return [...toInsert, ...existingPluginVariants]
}
const index = existingPluginVariants.indexOf(variant)
if (index === -1) {
return [...existingPluginVariants, ...toInsert]
}
return [
...existingPluginVariants.slice(0, index),
...toInsert,
...existingPluginVariants.slice(index),
]
},
after(toInsert, variant, existingPluginVariants = get(resolved, plugin, [])) {
if (variant === undefined) {
return [...existingPluginVariants, ...toInsert]
}
const index = existingPluginVariants.indexOf(variant)
if (index === -1) {
return [...toInsert, ...existingPluginVariants]
}
return [
...existingPluginVariants.slice(0, index + 1),
...toInsert,
...existingPluginVariants.slice(index + 1),
]
},
without(toRemove, existingPluginVariants = get(resolved, plugin, [])) {
return existingPluginVariants.filter(v => !toRemove.includes(v))
},
})
} else {
resolved[plugin] = pluginVariants
}
})
return resolved
}, {})
}
export default function resolveConfig(configs) {
const allConfigs = extractPluginConfigs(configs)
@ -136,11 +195,7 @@ export default function resolveConfig(configs) {
theme: resolveFunctionKeys(
mergeExtensions(mergeThemes(map(allConfigs, t => get(t, 'theme', {}))))
),
variants: (firstVariants => {
return Array.isArray(firstVariants)
? firstVariants
: defaults({}, ...map(allConfigs, 'variants'))
})(defaults({}, ...map(allConfigs)).variants),
variants: resolveVariants(allConfigs.map(c => c.variants)),
},
...allConfigs
)