Skip to content

Commit

Permalink
breaking: rename custom values to arbitrary values everywhere
Browse files Browse the repository at this point in the history
Tailwind is using the term "arbitrary values" in its v3 documentation.
To stay consistent, I need to name them arbitrary values as well.
  • Loading branch information
dcastil committed Dec 10, 2021
1 parent 014a011 commit adc3c02
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 150 deletions.
36 changes: 18 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ twMerge('hover:p-2 hover:p-4') // → 'hover:p-4'
twMerge('hover:focus:p-2 focus:hover:p-4') // → 'focus:hover:p-4'
```

### Supports custom values
### Supports arbitrary values

```ts
twMerge('bg-black bg-[color:var(--mystery-var)]') // → 'bg-[color:var(--mystery-var)]'
Expand Down Expand Up @@ -187,13 +187,13 @@ E.g. here is the overflow class group which results in the classes `overflow-aut
const overflowClassGroup = [{ overflow: ['auto', 'hidden', 'visible', 'scroll'] }]
```

Sometimes it isn't possible to enumerate all elements in a class group. Think of a Tailwind class which allows custom values. In this scenario you can use a validator function which takes a _class part_ and returns a boolean indicating whether a class is part of a class group.
Sometimes it isn't possible to enumerate all elements in a class group. Think of a Tailwind class which allows arbitrary values. In this scenario you can use a validator function which takes a _class part_ and returns a boolean indicating whether a class is part of a class group.

E.g. here is the fill class group.

```ts
const isCustomValue = (classPart: string) => /^\[.+\]$/.test(classPart)
const fillClassGroup = [{ fill: ['current', isCustomValue] }]
const isArbitraryValue = (classPart: string) => /^\[.+\]$/.test(classPart)
const fillClassGroup = [{ fill: ['current', isArbitraryValue] }]
```

Because the function is under the `fill` key, it will only get called for classes which start with `fill-`. Also, the function only gets passed the part of the class name which comes after `fill-`, this way you can use the same function in multiple class groups. tailwind-merge exports its own [validators](#validators), so you don't need to recreate them.
Expand Down Expand Up @@ -500,14 +500,14 @@ const customTwMerge = createTailwindMerge(getDefaultConfig, (config) =>
```ts
interface Validators {
isLength(classPart: string): boolean
isCustomLength(classPart: string): boolean
isArbitraryLength(classPart: string): boolean
isInteger(classPart: string): boolean
isCustomValue(classPart: string): boolean
isArbitraryValue(classPart: string): boolean
isTshirtSize(classPart: string): boolean
isCustomSize(classPart: string): boolean
isCustomPosition(classPart: string): boolean
isCustomUrl(classPart: string): boolean
isCustomWeight(classPart: string): boolean
isArbitrarySize(classPart: string): boolean
isArbitraryPosition(classPart: string): boolean
isArbitraryUrl(classPart: string): boolean
isArbitraryWeight(classPart: string): boolean
isAny(classPart: string): boolean
}
```
Expand All @@ -520,15 +520,15 @@ const paddingClassGroup = [{ p: [validators.isLength] }]

A brief summary for each validator:

- `isLength` checks whether a class part is a number (`3`, `1.5`), a fraction (`3/4`), a custom length (`[3%]`, `[4px]`, `[length:var(--my-var)]`), or one of the strings `px`, `full` or `screen`.
- `isCustomLength` checks for custom length values (`[3%]`, `[4px]`, `[length:var(--my-var)]`).
- `isInteger` checks for integer values (`3`) and custom integer values (`[3]`).
- `isCustomValue` checks whether the class part is enclosed in brackets (`[something]`)
- `isLength` checks whether a class part is a number (`3`, `1.5`), a fraction (`3/4`), a arbitrary length (`[3%]`, `[4px]`, `[length:var(--my-var)]`), or one of the strings `px`, `full` or `screen`.
- `isArbitraryLength` checks for arbitrary length values (`[3%]`, `[4px]`, `[length:var(--my-var)]`).
- `isInteger` checks for integer values (`3`) and arbitrary integer values (`[3]`).
- `isArbitraryValue` checks whether the class part is enclosed in brackets (`[something]`)
- `isTshirtSize`checks whether class part is a T-shirt size (`sm`, `xl`), optionally with a preceding number (`2xl`).
- `isCustomSize` checks whether class part is custom value which starts with with `size:` (`[size:200px_100px]`) which is necessary for background-size classNames.
- `isCustomPosition` checks whether class part is custom value which starts with with `position:` (`[position:200px_100px]`) which is necessary for background-position classNames.
- `isCustomUrl` checks whether class part is custom value which starts with `url:` or `url(` (`[url('/path-to-image.png')]`, `url:var(--maybe-a-url-at-runtime)]`) which is necessary for background-image classNames.
- `isCustomWeight` checks whether class part is custom value whcih starts with `weight:` or is a number (`[weight:var(--value)]`, `[450]`) which is necessary for font-weight classNames.
- `isArbitrarySize` checks whether class part is arbitrary value which starts with with `size:` (`[size:200px_100px]`) which is necessary for background-size classNames.
- `isArbitraryPosition` checks whether class part is arbitrary value which starts with with `position:` (`[position:200px_100px]`) which is necessary for background-position classNames.
- `isArbitraryUrl` checks whether class part is arbitrary value which starts with `url:` or `url(` (`[url('/path-to-image.png')]`, `url:var(--maybe-a-url-at-runtime)]`) which is necessary for background-image classNames.
- `isArbitraryWeight` checks whether class part is arbitrary value whcih starts with `weight:` or is a number (`[weight:var(--value)]`, `[450]`) which is necessary for font-weight classNames.
- `isAny` always returns true. Be careful with this validator as it might match unwanted classes. I use it primarily to match colors or when I'm ceertain there are no other class groups in a namespace.

### `Config`
Expand Down
54 changes: 27 additions & 27 deletions src/default-config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { fromTheme } from './from-theme'
import {
isAny,
isCustomLength,
isCustomPosition,
isCustomSize,
isCustomUrl,
isCustomValue,
isCustomWeight,
isArbitraryLength,
isArbitraryPosition,
isArbitrarySize,
isArbitraryUrl,
isArbitraryValue,
isArbitraryWeight,
isInteger,
isLength,
isTshirtSize,
Expand Down Expand Up @@ -88,10 +88,10 @@ export function getDefaultConfig() {
theme: {
colors: [isAny],
spacing: [isLength],
blur: ['none', '', isTshirtSize, isCustomLength],
blur: ['none', '', isTshirtSize, isArbitraryLength],
brightness: [isInteger],
borderColor: [colors],
borderRadius: ['none', '', 'full', isTshirtSize, isCustomLength],
borderRadius: ['none', '', 'full', isTshirtSize, isArbitraryLength],
borderWidth: getLengthWithEmpty(),
contrast: [isInteger],
grayscale: getZeroAndEmpty(),
Expand All @@ -116,7 +116,7 @@ export function getDefaultConfig() {
* Aspect Ratio
* @see https://tailwindcss.com/docs/aspect-ratio
*/
aspect: [{ aspect: ['auto', 'square', 'video', isCustomValue] }],
aspect: [{ aspect: ['auto', 'square', 'video', isArbitraryValue] }],
/**
* Container
* @see https://tailwindcss.com/docs/container
Expand Down Expand Up @@ -304,7 +304,7 @@ export function getDefaultConfig() {
* Flex
* @see https://tailwindcss.com/docs/flex
*/
flex: [{ flex: ['1', 'auto', 'initial', 'none', isCustomValue] }],
flex: [{ flex: ['1', 'auto', 'initial', 'none', isArbitraryValue] }],
/**
* Flex Grow
* @see https://tailwindcss.com/docs/flex-grow
Expand Down Expand Up @@ -369,12 +369,12 @@ export function getDefaultConfig() {
* Grid Auto Columns
* @see https://tailwindcss.com/docs/grid-auto-columns
*/
'auto-cols': [{ 'auto-cols': ['auto', 'min', 'max', 'fr', isCustomValue] }],
'auto-cols': [{ 'auto-cols': ['auto', 'min', 'max', 'fr', isArbitraryValue] }],
/**
* Grid Auto Rows
* @see https://tailwindcss.com/docs/grid-auto-rows
*/
'auto-rows': [{ 'auto-rows': ['auto', 'min', 'max', 'fr', isCustomValue] }],
'auto-rows': [{ 'auto-rows': ['auto', 'min', 'max', 'fr', isArbitraryValue] }],
/**
* Gap
* @see https://tailwindcss.com/docs/gap
Expand Down Expand Up @@ -553,7 +553,7 @@ export function getDefaultConfig() {
'prose',
{ screen: [isTshirtSize] },
isTshirtSize,
isCustomLength,
isArbitraryLength,
],
},
],
Expand All @@ -577,7 +577,7 @@ export function getDefaultConfig() {
* Font Size
* @see https://tailwindcss.com/docs/font-size
*/
'font-size': [{ text: ['base', isTshirtSize, isCustomLength] }],
'font-size': [{ text: ['base', isTshirtSize, isArbitraryLength] }],
/**
* Font Smoothing
* @see https://tailwindcss.com/docs/font-smoothing
Expand All @@ -604,7 +604,7 @@ export function getDefaultConfig() {
'bold',
'extrabold',
'black',
isCustomWeight,
isArbitraryWeight,
],
},
],
Expand Down Expand Up @@ -656,7 +656,7 @@ export function getDefaultConfig() {
'wide',
'wider',
'widest',
isCustomLength,
isArbitraryLength,
],
},
],
Expand All @@ -671,7 +671,7 @@ export function getDefaultConfig() {
* List Style Type
* @see https://tailwindcss.com/docs/list-style-type
*/
'list-style-type': [{ list: ['none', 'disc', 'decimal', isCustomValue] }],
'list-style-type': [{ list: ['none', 'disc', 'decimal', isArbitraryValue] }],
/**
* List Style Position
* @see https://tailwindcss.com/docs/list-style-position
Expand Down Expand Up @@ -757,7 +757,7 @@ export function getDefaultConfig() {
'text-bottom',
'sub',
'super',
isCustomLength,
isArbitraryLength,
],
},
],
Expand Down Expand Up @@ -796,7 +796,7 @@ export function getDefaultConfig() {
* Background Position
* @see https://tailwindcss.com/docs/background-position
*/
'bg-position': [{ bg: [...getPositions(), isCustomPosition] }],
'bg-position': [{ bg: [...getPositions(), isArbitraryPosition] }],
/**
* Background Repeat
* @see https://tailwindcss.com/docs/background-repeat
Expand All @@ -806,7 +806,7 @@ export function getDefaultConfig() {
* Background Size
* @see https://tailwindcss.com/docs/background-size
*/
'bg-size': [{ bg: ['auto', 'cover', 'contain', isCustomSize] }],
'bg-size': [{ bg: ['auto', 'cover', 'contain', isArbitrarySize] }],
/**
* Background Image
* @see https://tailwindcss.com/docs/background-image
Expand All @@ -816,7 +816,7 @@ export function getDefaultConfig() {
bg: [
'none',
{ 'gradient-to': ['t', 'tr', 'r', 'br', 'b', 'bl', 'l', 'tl'] },
isCustomUrl,
isArbitraryUrl,
],
},
],
Expand Down Expand Up @@ -1184,7 +1184,7 @@ export function getDefaultConfig() {
'opacity',
'shadow',
'transform',
isCustomValue,
isArbitraryValue,
],
},
],
Expand All @@ -1197,7 +1197,7 @@ export function getDefaultConfig() {
* Transition Timing Function
* @see https://tailwindcss.com/docs/transition-timing-function
*/
ease: [{ ease: ['linear', 'in', 'out', 'in-out', isCustomValue] }],
ease: [{ ease: ['linear', 'in', 'out', 'in-out', isArbitraryValue] }],
/**
* Transition Delay
* @see https://tailwindcss.com/docs/transition-delay
Expand All @@ -1207,7 +1207,7 @@ export function getDefaultConfig() {
* Animation
* @see https://tailwindcss.com/docs/animation
*/
animate: [{ animate: ['none', 'spin', 'ping', 'pulse', 'bounce', isCustomValue] }],
animate: [{ animate: ['none', 'spin', 'ping', 'pulse', 'bounce', isArbitraryValue] }],
// Transforms
/**
* Transform
Expand Down Expand Up @@ -1327,7 +1327,7 @@ export function getDefaultConfig() {
'nwse-resize',
'zoom-in',
'zoom-out',
isCustomValue,
isArbitraryValue,
],
},
],
Expand Down Expand Up @@ -1476,7 +1476,7 @@ export function getDefaultConfig() {
* @see https://tailwindcss.com/docs/will-change
*/
'will-change': [
{ 'will-change': ['auto', 'scroll', 'contents', 'transform', isCustomValue] },
{ 'will-change': ['auto', 'scroll', 'contents', 'transform', isArbitraryValue] },
],
// SVG
/**
Expand Down Expand Up @@ -1505,7 +1505,7 @@ export function getDefaultConfig() {
* Content
* @see https://tailwindcss.com/docs/just-in-time-mode#content-utilities
*/
content: [{ content: [isCustomValue] }],
content: [{ content: [isArbitraryValue] }],
/**
* Caret Color
* @see https://tailwindcss.com/docs/just-in-time-mode#caret-color-utilities
Expand Down
2 changes: 1 addition & 1 deletion src/merge-classlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ConfigUtils } from './config-utils'

const SPLIT_CLASSES_REGEX = /\s+/
const IMPORTANT_MODIFIER = '!'
// Regex is needed so we don't match against colons in labels for custom values like `text-[color:var(--mystery-var)]`
// Regex is needed so we don't match against colons in labels for arbitrary values like `text-[color:var(--mystery-var)]`
// I'd prefer to use a negative lookbehind for all supported labels, but lookbheinds don't have good browser support yet. More info: https://caniuse.com/js-regexp-lookbehind
const PREFIX_SEPARATOR_REGEX = /:(?![^[]*\])/
const PREFIX_SEPARATOR = ':'
Expand Down
50 changes: 26 additions & 24 deletions src/validators.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const customValueRegex = /^\[(.+)\]$/
const arbitraryValueRegex = /^\[(.+)\]$/
const fractionRegex = /^\d+\/\d+$/
const stringLengths = new Set(['px', 'full', 'screen'])
const tshirtUnitRegex = /^(\d+)?(xs|sm|md|lg|xl)$/
Expand All @@ -9,58 +9,60 @@ export function isLength(classPart: string) {
!Number.isNaN(Number(classPart)) ||
stringLengths.has(classPart) ||
fractionRegex.test(classPart) ||
isCustomLength(classPart)
isArbitraryLength(classPart)
)
}

export function isCustomLength(classPart: string) {
const customValue = customValueRegex.exec(classPart)?.[1]
export function isArbitraryLength(classPart: string) {
const arbitraryValue = arbitraryValueRegex.exec(classPart)?.[1]

if (customValue) {
return customValue.startsWith('length:') || lengthUnitRegex.test(customValue)
if (arbitraryValue) {
return arbitraryValue.startsWith('length:') || lengthUnitRegex.test(arbitraryValue)
}

return false
}

export function isCustomSize(classPart: string) {
const customValue = customValueRegex.exec(classPart)?.[1]
export function isArbitrarySize(classPart: string) {
const arbitraryValue = arbitraryValueRegex.exec(classPart)?.[1]

return customValue ? customValue.startsWith('size:') : false
return arbitraryValue ? arbitraryValue.startsWith('size:') : false
}

export function isCustomPosition(classPart: string) {
const customValue = customValueRegex.exec(classPart)?.[1]
export function isArbitraryPosition(classPart: string) {
const arbitraryValue = arbitraryValueRegex.exec(classPart)?.[1]

return customValue ? customValue.startsWith('position:') : false
return arbitraryValue ? arbitraryValue.startsWith('position:') : false
}

export function isCustomUrl(classPart: string) {
const customValue = customValueRegex.exec(classPart)?.[1]
export function isArbitraryUrl(classPart: string) {
const arbitraryValue = arbitraryValueRegex.exec(classPart)?.[1]

return customValue ? customValue.startsWith('url(') || customValue.startsWith('url:') : false
return arbitraryValue
? arbitraryValue.startsWith('url(') || arbitraryValue.startsWith('url:')
: false
}

export function isCustomWeight(classPart: string) {
const customValue = customValueRegex.exec(classPart)?.[1]
export function isArbitraryWeight(classPart: string) {
const arbitraryValue = arbitraryValueRegex.exec(classPart)?.[1]

return customValue
? !Number.isNaN(Number(customValue)) || customValue.startsWith('weight:')
return arbitraryValue
? !Number.isNaN(Number(arbitraryValue)) || arbitraryValue.startsWith('weight:')
: false
}

export function isInteger(classPart: string) {
const customValue = customValueRegex.exec(classPart)?.[1]
const arbitraryValue = arbitraryValueRegex.exec(classPart)?.[1]

if (customValue) {
return Number.isInteger(Number(customValue))
if (arbitraryValue) {
return Number.isInteger(Number(arbitraryValue))
}

return Number.isInteger(Number(classPart))
}

export function isCustomValue(classPart: string) {
return customValueRegex.test(classPart)
export function isArbitraryValue(classPart: string) {
return arbitraryValueRegex.test(classPart)
}

export function isAny() {
Expand Down
10 changes: 6 additions & 4 deletions tests/custom-values.test.ts → tests/arbitrary-values.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { twMerge } from '../src'

test('handles custom length conflicts correctly', () => {
test('handles arbitrary length conflicts correctly', () => {
expect(twMerge('m-[2px] m-[10px]')).toBe('m-[10px]')
expect(twMerge('my-[2px] m-[10rem]')).toBe('m-[10rem]')
expect(twMerge('cursor-pointer cursor-[grab]')).toBe('cursor-[grab]')
expect(twMerge('m-[2px] m-[calc(100%-var(--custom))]')).toBe('m-[calc(100%-var(--custom))]')
expect(twMerge('m-[2px] m-[calc(100%-var(--arbitrary))]')).toBe(
'm-[calc(100%-var(--arbitrary))]'
)
expect(twMerge('m-[2px] m-[length:var(--mystery-var)]')).toBe('m-[length:var(--mystery-var)]')
})

test('handles custom length conflicts with labels and prefixes correctly', () => {
test('handles arbitrary length conflicts with labels and prefixes correctly', () => {
expect(twMerge('hover:m-[2px] hover:m-[length:var(--c)]')).toBe('hover:m-[length:var(--c)]')
expect(twMerge('hover:focus:m-[2px] focus:hover:m-[length:var(--c)]')).toBe(
'focus:hover:m-[length:var(--c)]'
)
})

test('handles complex custom value conflicts correctly', () => {
test('handles complex arbitrary value conflicts correctly', () => {
expect(twMerge('grid-rows-[1fr,auto] grid-rows-2')).toBe('grid-rows-2')
expect(twMerge('grid-rows-[repeat(20,minmax(0,1fr))] grid-rows-3')).toBe('grid-rows-3')
})
Loading

0 comments on commit adc3c02

Please sign in to comment.