diff --git a/__tests__/resolveConfig.test.js b/__tests__/resolveConfig.test.js index b6c37b1f2995..7295717a65b7 100644 --- a/__tests__/resolveConfig.test.js +++ b/__tests__/resolveConfig.test.js @@ -1738,6 +1738,133 @@ test('user theme extensions take precedence over plugin theme extensions with th }) }) +test('variants can be extended', () => { + const userConfig = { + variants: { + borderColor: ({ after }) => after(['group-focus'], 'hover'), + extend: { + backgroundColor: ['active', 'disabled', 'group-hover'], + }, + }, + } + + const otherConfig = { + variants: { + extend: { + textColor: ['hover', 'focus-within'], + }, + }, + } + + const defaultConfig = { + prefix: '', + important: false, + separator: ':', + theme: {}, + variants: { + borderColor: ['hover', 'focus'], + backgroundColor: ['responsive', 'hover', 'focus'], + textColor: ['responsive', 'focus'], + }, + } + + const result = resolveConfig([userConfig, otherConfig, defaultConfig]) + + expect(result).toMatchObject({ + variants: { + borderColor: ['hover', 'group-focus', 'focus'], + backgroundColor: ['responsive', 'group-hover', 'hover', 'focus', 'active', 'disabled'], + textColor: ['responsive', 'focus-within', 'hover', 'focus'], + }, + }) +}) + +test('variant sort order can be customized', () => { + const userConfig = { + variantOrder: [ + 'disabled', + 'focus', + 'group-hover', + 'focus-within', + 'active', + 'hover', + 'responsive', + ], + variants: { + borderColor: ({ after }) => after(['group-focus'], 'hover'), + extend: { + backgroundColor: ['active', 'disabled', 'group-hover'], + }, + }, + } + + const otherConfig = { + variants: { + extend: { + textColor: ['hover', 'focus-within'], + }, + }, + } + + const defaultConfig = { + prefix: '', + important: false, + separator: ':', + theme: {}, + variants: { + borderColor: ['hover', 'focus'], + backgroundColor: ['responsive', 'hover', 'focus'], + textColor: ['responsive', 'focus'], + }, + } + + const result = resolveConfig([userConfig, otherConfig, defaultConfig]) + + expect(result).toMatchObject({ + variants: { + borderColor: ['hover', 'group-focus', 'focus'], + backgroundColor: ['disabled', 'focus', 'group-hover', 'active', 'hover', 'responsive'], + textColor: ['focus', 'focus-within', 'hover', 'responsive'], + }, + }) +}) + +test('custom variants go to the beginning by default when sort is applied', () => { + const userConfig = { + variants: { + extend: { + backgroundColor: ['active', 'custom-variant-1', 'group-hover', 'custom-variant-2'], + }, + }, + } + + const defaultConfig = { + prefix: '', + important: false, + separator: ':', + theme: {}, + variants: { + backgroundColor: ['responsive', 'hover', 'focus'], + }, + } + + const result = resolveConfig([userConfig, defaultConfig]) + + expect(result).toMatchObject({ + variants: { + backgroundColor: [ + 'responsive', + 'custom-variant-1', + 'custom-variant-2', + 'group-hover', + 'hover', + 'focus', + 'active', + ], + }, + }) +}) + test('variants can be defined as a function', () => { const userConfig = { variants: { diff --git a/src/util/resolveConfig.js b/src/util/resolveConfig.js index b2adea7c1ddb..f682214b175f 100644 --- a/src/util/resolveConfig.js +++ b/src/util/resolveConfig.js @@ -5,10 +5,12 @@ import isUndefined from 'lodash/isUndefined' import defaults from 'lodash/defaults' import map from 'lodash/map' import get from 'lodash/get' +import uniq from 'lodash/uniq' import toPath from 'lodash/toPath' import negateValue from './negateValue' import { corePluginList } from '../corePluginList' import configurePlugins from './configurePlugins' +import defaultConfig from '../../stubs/defaultConfig.stub' const configUtils = { negative(scale) { @@ -39,31 +41,29 @@ function value(valueToResolve, ...args) { return isFunction(valueToResolve) ? valueToResolve(...args) : valueToResolve } -function mergeThemes(themes) { - const theme = (({ extend: _, ...t }) => t)( - themes.reduce((merged, t) => { - return defaults(merged, t) - }, {}) - ) +function collectExtends(items) { + return items.reduce((merged, { extend }) => { + return mergeWith(merged, extend, (mergedValue, extendValue) => { + if (isUndefined(mergedValue)) { + return [extendValue] + } + if (Array.isArray(mergedValue)) { + return [extendValue, ...mergedValue] + } + + return [extendValue, mergedValue] + }) + }, {}) +} + +function mergeThemes(themes) { return { - ...theme, + ...themes.reduce((merged, theme) => defaults(merged, theme), {}), // In order to resolve n config objects, we combine all of their `extend` properties // into arrays instead of objects so they aren't overridden. - extend: themes.reduce((merged, { extend }) => { - return mergeWith(merged, extend, (mergedValue, extendValue) => { - if (isUndefined(mergedValue)) { - return [extendValue] - } - - if (Array.isArray(mergedValue)) { - return [extendValue, ...mergedValue] - } - - return [extendValue, mergedValue] - }) - }, {}), + extend: collectExtends(themes), } } @@ -130,12 +130,8 @@ function extractPluginConfigs(configs) { return allConfigs } -function resolveVariants([firstConfig, ...variantConfigs]) { - if (Array.isArray(firstConfig)) { - return firstConfig - } - - return [firstConfig, ...variantConfigs].reverse().reduce((resolved, variants) => { +function mergeVariants(variants) { + const mergedVariants = variants.reduce((resolved, variants) => { Object.entries(variants || {}).forEach(([plugin, pluginVariants]) => { if (isFunction(pluginVariants)) { resolved[plugin] = pluginVariants({ @@ -187,10 +183,39 @@ function resolveVariants([firstConfig, ...variantConfigs]) { return resolved }, {}) + + return { + ...mergedVariants, + extend: collectExtends(variants), + } +} + +function mergeVariantExtensions({ extend, ...variants }, variantOrder) { + return mergeWith(variants, extend, (variantsValue, extensions) => { + const merged = uniq([...variantsValue, ...extensions].flat()) + + if (extensions.flat().length === 0) { + return merged + } + + return merged.sort((a, z) => variantOrder.indexOf(a) - variantOrder.indexOf(z)) + }) +} + +function resolveVariants([firstConfig, ...variantConfigs], variantOrder) { + // Global variants configuration like `variants: ['hover', 'focus']` + if (Array.isArray(firstConfig)) { + return firstConfig + } + + return mergeVariantExtensions( + mergeVariants([firstConfig, ...variantConfigs].reverse()), + variantOrder + ) } function resolveCorePlugins(corePluginConfigs) { - const result = [...corePluginConfigs].reverse().reduce((resolved, corePluginConfig) => { + const result = [...corePluginConfigs].reduceRight((resolved, corePluginConfig) => { if (isFunction(corePluginConfig)) { return corePluginConfig({ corePlugins: resolved }) } @@ -201,7 +226,7 @@ function resolveCorePlugins(corePluginConfigs) { } function resolvePluginLists(pluginLists) { - const result = [...pluginLists].reverse().reduce((resolved, pluginList) => { + const result = [...pluginLists].reduceRight((resolved, pluginList) => { return [...resolved, ...pluginList] }, []) @@ -209,23 +234,30 @@ function resolvePluginLists(pluginLists) { } export default function resolveConfig(configs) { - const allConfigs = extractPluginConfigs(configs) + const allConfigs = [ + ...extractPluginConfigs(configs), + { + darkMode: false, + prefix: '', + important: false, + separator: ':', + variantOrder: defaultConfig.variantOrder, + }, + ] + const { variantOrder } = allConfigs.find((c) => c.variantOrder) return defaults( { theme: resolveFunctionKeys( mergeExtensions(mergeThemes(map(allConfigs, (t) => get(t, 'theme', {})))) ), - variants: resolveVariants(allConfigs.map((c) => c.variants)), + variants: resolveVariants( + allConfigs.map((c) => get(c, 'variants', {})), + variantOrder + ), corePlugins: resolveCorePlugins(allConfigs.map((c) => c.corePlugins)), plugins: resolvePluginLists(configs.map((c) => get(c, 'plugins', []))), }, - ...allConfigs, - { - darkMode: false, - prefix: '', - important: false, - separator: ':', - } + ...allConfigs ) } diff --git a/stubs/defaultConfig.stub.js b/stubs/defaultConfig.stub.js index f93022f79e79..b4928ed052bf 100644 --- a/stubs/defaultConfig.stub.js +++ b/stubs/defaultConfig.stub.js @@ -668,6 +668,22 @@ module.exports = { }, }, }, + variantOrder: [ + 'first', + 'last', + 'odd', + 'even', + 'visited', + 'checked', + 'group-hover', + 'group-focus', + 'focus-within', + 'hover', + 'focus', + 'focus-visible', + 'active', + 'disabled', + ], variants: { accessibility: ['responsive', 'focus'], alignContent: ['responsive'], diff --git a/stubs/simpleConfig.stub.js b/stubs/simpleConfig.stub.js index 4e246b6ba5d4..db401eb7e7d8 100644 --- a/stubs/simpleConfig.stub.js +++ b/stubs/simpleConfig.stub.js @@ -4,6 +4,8 @@ module.exports = { theme: { extend: {}, }, - variants: {}, + variants: { + extend: {}, + }, plugins: [], }