Skip to content

Commit

Permalink
Support defining variants as functions for easier extending (#2309)
Browse files Browse the repository at this point in the history
* Support defining variants as functions for easier extending

* Fix style

* Remove commented code

* Add 'without' helper to variant function API

* Update changelog
  • Loading branch information
adamwathan authored Sep 4, 2020
1 parent ff013c5 commit 476950c
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 8 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
62 changes: 62 additions & 0 deletions __tests__/resolveConfig.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
})
71 changes: 63 additions & 8 deletions src/util/resolveConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,15 @@ function mergeExtensions({ extend, ...theme }) {
}

function resolveFunctionKeys(object) {
const resolveThemePath = (key, defaultValue) => {
const resolvePath = (key, defaultValue) => {
const path = toPath(key)

let index = 0
let val = 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
Expand All @@ -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],
}
}, {})
}
Expand Down Expand Up @@ -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)

Expand All @@ -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
)
Expand Down

0 comments on commit 476950c

Please sign in to comment.