Skip to content

Commit 476950c

Browse files
authored
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
1 parent ff013c5 commit 476950c

File tree

3 files changed

+126
-8
lines changed

3 files changed

+126
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- New `layers` mode for `purge` ([#2288](https://github.com/tailwindlabs/tailwindcss/pull/2288))
1515
- New `font-variant-numeric` utilities ([#2305](https://github.com/tailwindlabs/tailwindcss/pull/2305))
1616
- New `place-items`, `place-content`, `place-self`, `justify-items`, and `justify-self` utilities ([#2306](https://github.com/tailwindlabs/tailwindcss/pull/2306))
17+
- Support configuring variants as functions ([#2309](https://github.com/tailwindlabs/tailwindcss/pull/2309))
1718

1819
### Deprecated
1920

__tests__/resolveConfig.test.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1736,3 +1736,65 @@ test('user theme extensions take precedence over plugin theme extensions with th
17361736
plugins: userConfig.plugins,
17371737
})
17381738
})
1739+
1740+
test('variants can be defined as a function', () => {
1741+
const userConfig = {
1742+
variants: {
1743+
backgroundColor: ({ variants }) => [...variants('backgroundColor'), 'disabled'],
1744+
padding: ({ before }) => before(['active']),
1745+
float: ({ before }) => before(['disabled'], 'focus'),
1746+
margin: ({ before }) => before(['hover'], 'focus'),
1747+
borderWidth: ({ after }) => after(['active']),
1748+
backgroundImage: ({ after }) => after(['disabled'], 'hover'),
1749+
opacity: ({ after }) => after(['hover'], 'focus'),
1750+
rotate: ({ without }) => without(['hover']),
1751+
cursor: ({ before, after, without }) =>
1752+
without(['responsive'], before(['checked'], 'hover', after(['hover'], 'focus'))),
1753+
},
1754+
}
1755+
1756+
const otherConfig = {
1757+
variants: {
1758+
backgroundColor: ({ variants }) => [...variants('backgroundColor'), 'active'],
1759+
},
1760+
}
1761+
1762+
const defaultConfig = {
1763+
prefix: '',
1764+
important: false,
1765+
separator: ':',
1766+
theme: {},
1767+
variants: {
1768+
backgroundColor: ['responsive', 'hover', 'focus'],
1769+
padding: ['responsive', 'focus'],
1770+
float: ['responsive', 'hover', 'focus'],
1771+
margin: ['responsive'],
1772+
borderWidth: ['responsive', 'focus'],
1773+
backgroundImage: ['responsive', 'hover', 'focus'],
1774+
opacity: ['responsive'],
1775+
rotate: ['responsive', 'hover', 'focus'],
1776+
cursor: ['responsive', 'focus'],
1777+
},
1778+
}
1779+
1780+
const result = resolveConfig([userConfig, otherConfig, defaultConfig])
1781+
1782+
expect(result).toEqual({
1783+
prefix: '',
1784+
important: false,
1785+
separator: ':',
1786+
theme: {},
1787+
variants: {
1788+
backgroundColor: ['responsive', 'hover', 'focus', 'active', 'disabled'],
1789+
padding: ['active', 'responsive', 'focus'],
1790+
float: ['responsive', 'hover', 'disabled', 'focus'],
1791+
margin: ['responsive', 'hover'],
1792+
borderWidth: ['responsive', 'focus', 'active'],
1793+
backgroundImage: ['responsive', 'hover', 'disabled', 'focus'],
1794+
opacity: ['hover', 'responsive'],
1795+
rotate: ['responsive', 'focus'],
1796+
cursor: ['focus', 'checked', 'hover'],
1797+
},
1798+
plugins: userConfig.plugins,
1799+
})
1800+
})

src/util/resolveConfig.js

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,15 @@ function mergeExtensions({ extend, ...theme }) {
8383
}
8484

8585
function resolveFunctionKeys(object) {
86-
const resolveThemePath = (key, defaultValue) => {
86+
const resolvePath = (key, defaultValue) => {
8787
const path = toPath(key)
8888

8989
let index = 0
9090
let val = object
9191

9292
while (val !== undefined && val !== null && index < path.length) {
9393
val = val[path[index++]]
94-
val = isFunction(val) ? val(resolveThemePath, configUtils) : val
94+
val = isFunction(val) ? val(resolvePath, configUtils) : val
9595
}
9696

9797
return val === undefined ? defaultValue : val
@@ -100,7 +100,7 @@ function resolveFunctionKeys(object) {
100100
return Object.keys(object).reduce((resolved, key) => {
101101
return {
102102
...resolved,
103-
[key]: isFunction(object[key]) ? object[key](resolveThemePath, configUtils) : object[key],
103+
[key]: isFunction(object[key]) ? object[key](resolvePath, configUtils) : object[key],
104104
}
105105
}, {})
106106
}
@@ -128,6 +128,65 @@ function extractPluginConfigs(configs) {
128128
return allConfigs
129129
}
130130

131+
function resolveVariants([firstConfig, ...variantConfigs]) {
132+
if (Array.isArray(firstConfig)) {
133+
return firstConfig
134+
}
135+
136+
return [firstConfig, ...variantConfigs].reverse().reduce((resolved, variants) => {
137+
Object.entries(variants || {}).forEach(([plugin, pluginVariants]) => {
138+
if (isFunction(pluginVariants)) {
139+
resolved[plugin] = pluginVariants({
140+
variants(path) {
141+
return get(resolved, path, [])
142+
},
143+
before(toInsert, variant, existingPluginVariants = get(resolved, plugin, [])) {
144+
if (variant === undefined) {
145+
return [...toInsert, ...existingPluginVariants]
146+
}
147+
148+
const index = existingPluginVariants.indexOf(variant)
149+
150+
if (index === -1) {
151+
return [...existingPluginVariants, ...toInsert]
152+
}
153+
154+
return [
155+
...existingPluginVariants.slice(0, index),
156+
...toInsert,
157+
...existingPluginVariants.slice(index),
158+
]
159+
},
160+
after(toInsert, variant, existingPluginVariants = get(resolved, plugin, [])) {
161+
if (variant === undefined) {
162+
return [...existingPluginVariants, ...toInsert]
163+
}
164+
165+
const index = existingPluginVariants.indexOf(variant)
166+
167+
if (index === -1) {
168+
return [...toInsert, ...existingPluginVariants]
169+
}
170+
171+
return [
172+
...existingPluginVariants.slice(0, index + 1),
173+
...toInsert,
174+
...existingPluginVariants.slice(index + 1),
175+
]
176+
},
177+
without(toRemove, existingPluginVariants = get(resolved, plugin, [])) {
178+
return existingPluginVariants.filter(v => !toRemove.includes(v))
179+
},
180+
})
181+
} else {
182+
resolved[plugin] = pluginVariants
183+
}
184+
})
185+
186+
return resolved
187+
}, {})
188+
}
189+
131190
export default function resolveConfig(configs) {
132191
const allConfigs = extractPluginConfigs(configs)
133192

@@ -136,11 +195,7 @@ export default function resolveConfig(configs) {
136195
theme: resolveFunctionKeys(
137196
mergeExtensions(mergeThemes(map(allConfigs, t => get(t, 'theme', {}))))
138197
),
139-
variants: (firstVariants => {
140-
return Array.isArray(firstVariants)
141-
? firstVariants
142-
: defaults({}, ...map(allConfigs, 'variants'))
143-
})(defaults({}, ...map(allConfigs)).variants),
198+
variants: resolveVariants(allConfigs.map(c => c.variants)),
144199
},
145200
...allConfigs
146201
)

0 commit comments

Comments
 (0)