diff --git a/CHANGELOG.md b/CHANGELOG.md index 19d11212af17..107d0433c8b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/__tests__/resolveConfig.test.js b/__tests__/resolveConfig.test.js index 0354db3c7e51..50e6b9877c1d 100644 --- a/__tests__/resolveConfig.test.js +++ b/__tests__/resolveConfig.test.js @@ -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, + }) +}) diff --git a/src/util/resolveConfig.js b/src/util/resolveConfig.js index 75dac0d41b4c..0af2b1769159 100644 --- a/src/util/resolveConfig.js +++ b/src/util/resolveConfig.js @@ -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 )