diff --git a/docs/configuration.md b/docs/configuration.md index f587ef25..90dc02a9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -42,8 +42,11 @@ const tailwindMergeConfig = { // Conflicts between class groups are defined here }, conflictingClassGroupModifiers: { - // Conflicts between postfox modifier of a class group and another class group are defined here + // Conflicts between postfix modifier of a class group and another class group are defined here }, + ignoredVariants: [ + // Variants that should be ignored in classes + ], } ``` @@ -157,6 +160,12 @@ In the Tailwind config you can modify theme scales. tailwind-merge follows the s If you modified one of these theme scales in your Tailwind config, you can add all your keys right here and tailwind-merge will take care of the rest. If you modified other theme scales, you need to figure out the class group to modify in the [default config](./api-reference.md#getdefaultconfig). +### Ignored variants + +Tailwind supports variants that don't modify the selector, which some third-party plugins use to modify utilities. +If you or one of your Tailwind plugins uses variants like this, you can add them to `ignoredVariants` so +that tailwind-merge ignores them when merging classes. + ### Extending the tailwind-merge config If you only need to slightly modify the default tailwind-merge config, [`extendTailwindMerge`](./api-reference.md#extendtailwindmerge) is the easiest way to extend the config. You provide it a `configExtension` object which gets [merged](./api-reference.md#mergeconfigs) with the default config. Therefore, all keys here are optional. diff --git a/src/lib/default-config.ts b/src/lib/default-config.ts index 45c921d9..e01779bd 100644 --- a/src/lib/default-config.ts +++ b/src/lib/default-config.ts @@ -1861,5 +1861,6 @@ export function getDefaultConfig() { conflictingClassGroupModifiers: { 'font-size': ['leading'], }, + ignoredVariants: [], } as const satisfies Config } diff --git a/src/lib/merge-configs.ts b/src/lib/merge-configs.ts index df2d765f..bbe9a1f3 100644 --- a/src/lib/merge-configs.ts +++ b/src/lib/merge-configs.ts @@ -46,10 +46,13 @@ function overrideProperty( } function overrideConfigProperties( - baseObject: Partial>, - overrideObject: Partial> | undefined, + baseObject: Partial> | unknown[], + overrideObject: Partial> | unknown[] | undefined, ) { - if (overrideObject) { + if (!overrideObject) return + if (Array.isArray(baseObject) && Array.isArray(overrideObject)) { + baseObject.splice(0, Infinity, ...overrideObject) + } else if (!Array.isArray(baseObject) && !Array.isArray(overrideObject)) { for (const key in overrideObject) { overrideProperty(baseObject, key, overrideObject[key]) } @@ -57,10 +60,13 @@ function overrideConfigProperties( } function mergeConfigProperties( - baseObject: Partial>, - mergeObject: Partial> | undefined, + baseObject: Partial> | unknown[], + mergeObject: Partial> | unknown[] | undefined, ) { - if (mergeObject) { + if (!mergeObject) return + if (Array.isArray(baseObject) && Array.isArray(mergeObject)) { + baseObject.push(...mergeObject) + } else if (!Array.isArray(baseObject) && !Array.isArray(mergeObject)) { for (const key in mergeObject) { const mergeValue = mergeObject[key] diff --git a/src/lib/modifier-utils.ts b/src/lib/modifier-utils.ts index 7709422d..94005554 100644 --- a/src/lib/modifier-utils.ts +++ b/src/lib/modifier-utils.ts @@ -3,14 +3,14 @@ import { GenericConfig } from './types' export const IMPORTANT_MODIFIER = '!' export function createSplitModifiers(config: GenericConfig) { - const separator = config.separator + const { separator, ignoredVariants } = config const isSeparatorSingleCharacter = separator.length === 1 const firstSeparatorCharacter = separator[0] const separatorLength = separator.length // splitModifiers inspired by https://github.com/tailwindlabs/tailwindcss/blob/v3.2.2/src/util/splitAtTopLevelOnly.js return function splitModifiers(className: string) { - const modifiers = [] + const modifiers: string[] = [] let bracketDepth = 0 let modifierStart = 0 @@ -25,7 +25,13 @@ export function createSplitModifiers(config: GenericConfig) { (isSeparatorSingleCharacter || className.slice(index, index + separatorLength) === separator) ) { - modifiers.push(className.slice(modifierStart, index)) + const modifier = className.slice(modifierStart, index) + if ( + !ignoredVariants.some((v) => + typeof v === 'string' ? v === modifier : v(modifier), + ) + ) + modifiers.push(modifier) modifierStart = index + separatorLength continue } diff --git a/src/lib/types.ts b/src/lib/types.ts index 54ed0876..eef30a07 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -19,10 +19,6 @@ interface ConfigStatic { * @see https://tailwindcss.com/docs/configuration#separator */ separator: string - /** - * Theme scales used in classGroups. - * The keys are the same as in the Tailwind config but the values are sometimes defined more broadly. - */ } interface ConfigGroups { @@ -57,6 +53,11 @@ interface ConfigGroups> > + /** + * Variants that should be ignored in classes. + * @example ['no-op', (variant) => variant.startsWith('ignore')] + */ + ignoredVariants: (string | ClassValidator)[] } export interface ConfigExtension diff --git a/tests/create-tailwind-merge.test.ts b/tests/create-tailwind-merge.test.ts index 1c34716e..6be39d7a 100644 --- a/tests/create-tailwind-merge.test.ts +++ b/tests/create-tailwind-merge.test.ts @@ -15,6 +15,7 @@ test('createTailwindMerge works with single config function', () => { otherKey: ['fooKey', 'fooKey2'], }, conflictingClassGroupModifiers: {}, + ignoredVariants: [], })) expect(tailwindMerge('')).toBe('') @@ -56,6 +57,7 @@ test('createTailwindMerge works with multiple config functions', () => { otherKey: ['fooKey', 'fooKey2'], }, conflictingClassGroupModifiers: {}, + ignoredVariants: [], }), (config) => ({ ...config, diff --git a/tests/merge-configs.test.ts b/tests/merge-configs.test.ts index 5e98ceb0..9eca091d 100644 --- a/tests/merge-configs.test.ts +++ b/tests/merge-configs.test.ts @@ -24,6 +24,7 @@ test('mergeConfigs has correct behavior', () => { hello: ['world'], toOverride: ['groupToOverride-2'], }, + ignoredVariants: ['noop-variant'], }, { separator: '-', @@ -42,6 +43,7 @@ test('mergeConfigs has correct behavior', () => { conflictingClassGroupModifiers: { toOverride: ['overridden-2'], }, + ignoredVariants: ['noop-variant2'], }, extend: { classGroups: { @@ -57,6 +59,7 @@ test('mergeConfigs has correct behavior', () => { conflictingClassGroupModifiers: { hello: ['world2'], }, + ignoredVariants: ['noop-variant3'], }, }, ), @@ -85,5 +88,6 @@ test('mergeConfigs has correct behavior', () => { hello: ['world', 'world2'], toOverride: ['overridden-2'], }, + ignoredVariants: ['noop-variant2', 'noop-variant3'], }) }) diff --git a/tests/modifiers.test.ts b/tests/modifiers.test.ts index 18152566..3461f04f 100644 --- a/tests/modifiers.test.ts +++ b/tests/modifiers.test.ts @@ -1,4 +1,4 @@ -import { createTailwindMerge, twMerge } from '../src' +import { createTailwindMerge, extendTailwindMerge, twMerge } from '../src' test('conflicts across prefix modifiers', () => { expect(twMerge('hover:block hover:inline')).toBe('hover:inline') @@ -9,6 +9,18 @@ test('conflicts across prefix modifiers', () => { expect(twMerge('focus-within:inline focus-within:block')).toBe('focus-within:block') }) +test('ignored variants', () => { + const tailwindMerge = extendTailwindMerge({ + extend: { + ignoredVariants: ['custom-variant', (variant) => variant.startsWith('ignore')], + }, + }) + expect(tailwindMerge('hover:block hover:custom-variant:inline')).toBe( + 'hover:custom-variant:inline', + ) + expect(tailwindMerge('hover:block hover:ignored:inline')).toBe('hover:ignored:inline') +}) + test('conflicts across postfix modifiers', () => { expect(twMerge('text-lg/7 text-lg/8')).toBe('text-lg/8') expect(twMerge('text-lg/none leading-9')).toBe('text-lg/none leading-9') @@ -28,6 +40,7 @@ test('conflicts across postfix modifiers', () => { conflictingClassGroupModifiers: { baz: ['bar'], }, + ignoredVariants: [], })) expect(customTwMerge('foo-1/2 foo-2/3')).toBe('foo-2/3') diff --git a/tests/public-api.test.ts b/tests/public-api.test.ts index 2fa33cad..b20e7a3d 100644 --- a/tests/public-api.test.ts +++ b/tests/public-api.test.ts @@ -94,6 +94,7 @@ test('createTailwindMerge() has correct inputs and outputs', () => { classGroups: {}, conflictingClassGroups: {}, conflictingClassGroupModifiers: {}, + ignoredVariants: [], })), ).toStrictEqual(expect.any(Function)) @@ -111,6 +112,7 @@ test('createTailwindMerge() has correct inputs and outputs', () => { otherKey: ['fooKey', 'fooKey2'], }, conflictingClassGroupModifiers: {}, + ignoredVariants: [], })) expect(tailwindMerge).toStrictEqual(expect.any(Function)) @@ -173,6 +175,7 @@ test('mergeConfigs has correct inputs and outputs', () => { }, conflictingClassGroups: {}, conflictingClassGroupModifiers: {}, + ignoredVariants: [], }, {}, ), diff --git a/tests/type-generics.test.ts b/tests/type-generics.test.ts index 0ff25baa..eac717d1 100644 --- a/tests/type-generics.test.ts +++ b/tests/type-generics.test.ts @@ -117,6 +117,7 @@ test('mergeConfigs type generics work correctly', () => { hello: ['world'], toOverride: ['groupToOverride-2'], }, + ignoredVariants: [], }, { separator: '-',