Make negative values a first-class feature, rather than theme-driven (#5709)

* WIP

* Add failing test for negating default values

* Add dynamic negative value opt-in (#5713)

* Add `supportsNegativeValues` plugin option

* Update `getClassList` to support dynamic negative values

* Add test for using a negative scale value with a plugin that does not support dynamic negative values

* Support dynamic negation of `DEFAULT` values (#5718)

* Add test case

Co-authored-by: Brad Cornes <bradlc41@gmail.com>
This commit is contained in:
Adam Wathan 2021-10-06 13:42:05 -04:00 committed by GitHub
parent b661614265
commit 97062c398b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 493 additions and 139 deletions

View File

@ -529,19 +529,23 @@ export let corePlugins = {
})
},
inset: createUtilityPlugin('inset', [
['inset', ['top', 'right', 'bottom', 'left']],
inset: createUtilityPlugin(
'inset',
[
['inset-x', ['left', 'right']],
['inset-y', ['top', 'bottom']],
['inset', ['top', 'right', 'bottom', 'left']],
[
['inset-x', ['left', 'right']],
['inset-y', ['top', 'bottom']],
],
[
['top', ['top']],
['right', ['right']],
['bottom', ['bottom']],
['left', ['left']],
],
],
[
['top', ['top']],
['right', ['right']],
['bottom', ['bottom']],
['left', ['left']],
],
]),
{ supportsNegativeValues: true }
),
isolation: ({ addUtilities }) => {
addUtilities({
@ -550,8 +554,8 @@ export let corePlugins = {
})
},
zIndex: createUtilityPlugin('zIndex', [['z', ['zIndex']]]),
order: createUtilityPlugin('order'),
zIndex: createUtilityPlugin('zIndex', [['z', ['zIndex']]], { supportsNegativeValues: true }),
order: createUtilityPlugin('order', undefined, { supportsNegativeValues: true }),
gridColumn: createUtilityPlugin('gridColumn', [['col', ['gridColumn']]]),
gridColumnStart: createUtilityPlugin('gridColumnStart', [['col-start', ['gridColumnStart']]]),
gridColumnEnd: createUtilityPlugin('gridColumnEnd', [['col-end', ['gridColumnEnd']]]),
@ -576,19 +580,23 @@ export let corePlugins = {
})
},
margin: createUtilityPlugin('margin', [
['m', ['margin']],
margin: createUtilityPlugin(
'margin',
[
['mx', ['margin-left', 'margin-right']],
['my', ['margin-top', 'margin-bottom']],
['m', ['margin']],
[
['mx', ['margin-left', 'margin-right']],
['my', ['margin-top', 'margin-bottom']],
],
[
['mt', ['margin-top']],
['mr', ['margin-right']],
['mb', ['margin-bottom']],
['ml', ['margin-left']],
],
],
[
['mt', ['margin-top']],
['mr', ['margin-right']],
['mb', ['margin-bottom']],
['ml', ['margin-left']],
],
]),
{ supportsNegativeValues: true }
),
boxSizing: ({ addUtilities }) => {
addUtilities({
@ -653,33 +661,48 @@ export let corePlugins = {
},
transformOrigin: createUtilityPlugin('transformOrigin', [['origin', ['transformOrigin']]]),
translate: createUtilityPlugin('translate', [
translate: createUtilityPlugin(
'translate',
[
[
'translate-x',
[['@defaults transform', {}], '--tw-translate-x', ['transform', 'var(--tw-transform)']],
],
[
'translate-y',
[['@defaults transform', {}], '--tw-translate-y', ['transform', 'var(--tw-transform)']],
[
'translate-x',
[['@defaults transform', {}], '--tw-translate-x', ['transform', 'var(--tw-transform)']],
],
[
'translate-y',
[['@defaults transform', {}], '--tw-translate-y', ['transform', 'var(--tw-transform)']],
],
],
],
]),
rotate: createUtilityPlugin('rotate', [
['rotate', [['@defaults transform', {}], '--tw-rotate', ['transform', 'var(--tw-transform)']]],
]),
skew: createUtilityPlugin('skew', [
{ supportsNegativeValues: true }
),
rotate: createUtilityPlugin(
'rotate',
[
[
'skew-x',
[['@defaults transform', {}], '--tw-skew-x', ['transform', 'var(--tw-transform)']],
],
[
'skew-y',
[['@defaults transform', {}], '--tw-skew-y', ['transform', 'var(--tw-transform)']],
'rotate',
[['@defaults transform', {}], '--tw-rotate', ['transform', 'var(--tw-transform)']],
],
],
]),
{ supportsNegativeValues: true }
),
skew: createUtilityPlugin(
'skew',
[
[
[
'skew-x',
[['@defaults transform', {}], '--tw-skew-x', ['transform', 'var(--tw-transform)']],
],
[
'skew-y',
[['@defaults transform', {}], '--tw-skew-y', ['transform', 'var(--tw-transform)']],
],
],
],
{ supportsNegativeValues: true }
),
scale: createUtilityPlugin('scale', [
[
'scale',
@ -859,19 +882,23 @@ export let corePlugins = {
})
},
scrollMargin: createUtilityPlugin('scrollMargin', [
['scroll-m', ['scroll-margin']],
scrollMargin: createUtilityPlugin(
'scrollMargin',
[
['scroll-mx', ['scroll-margin-left', 'scroll-margin-right']],
['scroll-my', ['scroll-margin-top', 'scroll-margin-bottom']],
['scroll-m', ['scroll-margin']],
[
['scroll-mx', ['scroll-margin-left', 'scroll-margin-right']],
['scroll-my', ['scroll-margin-top', 'scroll-margin-bottom']],
],
[
['scroll-mt', ['scroll-margin-top']],
['scroll-mr', ['scroll-margin-right']],
['scroll-mb', ['scroll-margin-bottom']],
['scroll-ml', ['scroll-margin-left']],
],
],
[
['scroll-mt', ['scroll-margin-top']],
['scroll-mr', ['scroll-margin-right']],
['scroll-mb', ['scroll-margin-bottom']],
['scroll-ml', ['scroll-margin-left']],
],
]),
{ supportsNegativeValues: true }
),
scrollPadding: createUtilityPlugin('scrollPadding', [
['scroll-p', ['scroll-padding']],
@ -1069,7 +1096,7 @@ export let corePlugins = {
}
},
},
{ values: theme('space') }
{ values: theme('space'), supportsNegativeValues: true }
)
addUtilities({
@ -1641,7 +1668,9 @@ export let corePlugins = {
})
},
textIndent: createUtilityPlugin('textIndent', [['indent', ['text-indent']]]),
textIndent: createUtilityPlugin('textIndent', [['indent', ['text-indent']]], {
supportsNegativeValues: true,
}),
verticalAlign: ({ addUtilities, matchUtilities }) => {
addUtilities({
@ -1730,7 +1759,9 @@ export let corePlugins = {
},
lineHeight: createUtilityPlugin('lineHeight', [['leading', ['lineHeight']]]),
letterSpacing: createUtilityPlugin('letterSpacing', [['tracking', ['letterSpacing']]]),
letterSpacing: createUtilityPlugin('letterSpacing', [['tracking', ['letterSpacing']]], {
supportsNegativeValues: true,
}),
textColor: ({ matchUtilities, theme, corePlugins }) => {
matchUtilities(
@ -2099,7 +2130,7 @@ export let corePlugins = {
}
},
},
{ values: theme('hueRotate') }
{ values: theme('hueRotate'), supportsNegativeValues: true }
)
},
@ -2250,7 +2281,7 @@ export let corePlugins = {
}
},
},
{ values: theme('backdropHueRotate') }
{ values: theme('backdropHueRotate'), supportsNegativeValues: true }
)
},

View File

@ -185,6 +185,10 @@ function* resolveMatchedPlugins(classCandidate, context) {
candidatePrefix = twConfigPrefix + candidatePrefix.slice(twConfigPrefixLen + 1)
}
if (negative && context.candidateRuleMap.has(candidatePrefix)) {
yield [context.candidateRuleMap.get(candidatePrefix), '-DEFAULT']
}
for (let [prefix, modifier] of candidatePermutations(candidatePrefix)) {
if (context.candidateRuleMap.has(prefix)) {
yield [context.candidateRuleMap.get(prefix), negative ? `-${modifier}` : modifier]
@ -238,7 +242,7 @@ function* resolveMatches(candidate, context) {
}
}
// Only process static plugins on exact matches
else if (modifier === 'DEFAULT') {
else if (modifier === 'DEFAULT' || modifier === '-DEFAULT') {
let ruleSet = plugin
let [rules, options] = parseRules(ruleSet, context.postCssNodeCache)
for (let rule of rules) {

View File

@ -17,6 +17,7 @@ import * as sharedState from './sharedState'
import { env } from './sharedState'
import { toPath } from '../util/toPath'
import log from '../util/log'
import negateValue from '../util/negateValue'
function insertInto(list, value, { before = [] } = {}) {
before = [].concat(before)
@ -300,7 +301,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
function wrapped(modifier, { isOnlyPlugin }) {
let { type = 'any' } = options
type = [].concat(type)
let [value, coercedType] = coerceValue(type, modifier, options.values, tailwindConfig)
let [value, coercedType] = coerceValue(type, modifier, options, tailwindConfig)
if (value === undefined) {
return []
@ -352,7 +353,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
function wrapped(modifier, { isOnlyPlugin }) {
let { type = 'any' } = options
type = [].concat(type)
let [value, coercedType] = coerceValue(type, modifier, options.values, tailwindConfig)
let [value, coercedType] = coerceValue(type, modifier, options, tailwindConfig)
if (value === undefined) {
return []
@ -670,10 +671,16 @@ function registerPlugins(plugins, context) {
for (let util of classList) {
if (Array.isArray(util)) {
let [utilName, options] = util
let negativeClasses = []
for (let value of Object.keys(options?.values ?? {})) {
output.push(formatClass(utilName, value))
for (let [key, value] of Object.entries(options?.values ?? {})) {
output.push(formatClass(utilName, key))
if (options?.supportsNegativeValues && negateValue(value)) {
negativeClasses.push(formatClass(utilName, `-${key}`))
}
}
output.push(...negativeClasses)
} else {
output.push(util)
}

View File

@ -3,7 +3,7 @@ import transformThemeValue from './transformThemeValue'
export default function createUtilityPlugin(
themeKey,
utilityVariations = [[themeKey, [themeKey]]],
{ filterDefault = false, type = 'any' } = {}
{ filterDefault = false, ...options } = {}
) {
let transformValue = transformThemeValue(themeKey)
return function ({ matchUtilities, theme }) {
@ -24,12 +24,12 @@ export default function createUtilityPlugin(
})
}, {}),
{
...options,
values: filterDefault
? Object.fromEntries(
Object.entries(theme(themeKey) ?? {}).filter(([modifier]) => modifier !== 'DEFAULT')
)
: theme(themeKey),
type,
}
)
}

View File

@ -14,7 +14,7 @@ export function formatClass(classPrefix, key) {
return classPrefix
}
if (key === '-') {
if (key === '-' || key === '-DEFAULT') {
return `-${classPrefix}`
}

View File

@ -1,6 +1,10 @@
export default function (value) {
value = `${value}`
if (value === '0') {
return '0'
}
// Flip sign of numbers
if (/^[+-]?(\d+|\d*\.\d+)(e[+-]?\d+)?(%|\w+)?$/.test(value)) {
return value.replace(/^[+-]?/, (sign) => (sign === '-' ? '' : '-'))
@ -9,6 +13,4 @@ export default function (value) {
if (value.includes('var(') || value.includes('calc(')) {
return `calc(${value} * -1)`
}
return value
}

View File

@ -17,6 +17,7 @@ import {
position,
lineWidth,
} from './dataTypes'
import negateValue from './negateValue'
export function applyStateToMarker(selector, marker, state, join) {
let markerIdx = selector.search(new RegExp(`${marker}[:[]`))
@ -167,18 +168,12 @@ export function transformLastClasses(transformClass, { wrap, withRule } = {}) {
}
}
export function asValue(modifier, lookup = {}, { validate = () => true } = {}) {
let value = lookup[modifier]
if (value !== undefined) {
return value
}
function resolveArbitraryValue(modifier, validate) {
if (!isArbitraryValue(modifier)) {
return undefined
}
value = modifier.slice(1, -1)
let value = modifier.slice(1, -1)
if (!validate(value)) {
return undefined
@ -187,6 +182,38 @@ export function asValue(modifier, lookup = {}, { validate = () => true } = {}) {
return normalize(value)
}
function asNegativeValue(modifier, lookup = {}, validate) {
let positiveValue = lookup[modifier]
if (positiveValue !== undefined) {
return negateValue(positiveValue)
}
if (isArbitraryValue(modifier)) {
let resolved = resolveArbitraryValue(modifier, validate)
if (resolved === undefined) {
return undefined
}
return negateValue(resolved)
}
}
export function asValue(modifier, options = {}, { validate = () => true } = {}) {
let value = options.values?.[modifier]
if (value !== undefined) {
return value
}
if (options.supportsNegativeValues && modifier.startsWith('-')) {
return asNegativeValue(modifier.slice(1), options.values, validate)
}
return resolveArbitraryValue(modifier, validate)
}
function isArbitraryValue(input) {
return input.startsWith('[') && input.endsWith(']')
}
@ -201,16 +228,16 @@ function splitAlpha(modifier) {
return [modifier.slice(0, slashIdx), modifier.slice(slashIdx + 1)]
}
export function asColor(modifier, lookup = {}, tailwindConfig = {}) {
if (lookup[modifier] !== undefined) {
return lookup[modifier]
export function asColor(modifier, options = {}, { tailwindConfig = {} } = {}) {
if (options.values?.[modifier] !== undefined) {
return options.values?.[modifier]
}
let [color, alpha] = splitAlpha(modifier)
if (alpha !== undefined) {
let normalizedColor =
lookup[color] ?? (isArbitraryValue(color) ? color.slice(1, -1) : undefined)
options.values?.[color] ?? (isArbitraryValue(color) ? color.slice(1, -1) : undefined)
if (normalizedColor === undefined) {
return undefined
@ -227,16 +254,16 @@ export function asColor(modifier, lookup = {}, tailwindConfig = {}) {
return withAlphaValue(normalizedColor, tailwindConfig.theme.opacity[alpha])
}
return asValue(modifier, lookup, { validate: validateColor })
return asValue(modifier, options, { validate: validateColor })
}
export function asLookupValue(modifier, lookup = {}) {
return lookup[modifier]
export function asLookupValue(modifier, options = {}) {
return options.values?.[modifier]
}
function guess(validate) {
return (modifier, lookup) => {
return asValue(modifier, lookup, { validate })
return (modifier, options) => {
return asValue(modifier, options, { validate })
}
}
@ -265,7 +292,7 @@ function splitAtFirst(input, delim) {
return [input.slice(0, idx), input.slice(idx + 1)]
}
export function coerceValue(types, modifier, values, tailwindConfig) {
export function coerceValue(types, modifier, options, tailwindConfig) {
if (isArbitraryValue(modifier)) {
let [explicitType, value] = splitAtFirst(modifier.slice(1, -1), ':')
@ -274,13 +301,13 @@ export function coerceValue(types, modifier, values, tailwindConfig) {
}
if (value.length > 0 && supportedTypes.includes(explicitType)) {
return [asValue(`[${value}]`, values, tailwindConfig), explicitType]
return [asValue(`[${value}]`, options), explicitType]
}
}
// Find first matching type
for (let type of [].concat(types)) {
let result = typeMap[type](modifier, values, tailwindConfig)
let result = typeMap[type](modifier, options, { tailwindConfig })
if (result) return [result, type]
}

View File

@ -40,10 +40,16 @@ function mergeWith(target, ...sources) {
const configUtils = {
colors,
negative(scale) {
// TODO: Log that this function isn't really needed anymore?
return Object.keys(scale)
.filter((key) => scale[key] !== '0')
.reduce((negativeScale, key) => {
negativeScale[`-${key}`] = negateValue(scale[key])
let negativeValue = negateValue(scale[key])
if (negativeValue !== undefined) {
negativeScale[`-${key}`] = negativeValue
}
return negativeScale
}, {})
},

View File

@ -257,11 +257,6 @@ module.exports = {
DEFAULT: '100%',
},
hueRotate: {
'-180': '-180deg',
'-90': '-90deg',
'-60': '-60deg',
'-30': '-30deg',
'-15': '-15deg',
0: '0deg',
15: '15deg',
30: '30deg',
@ -510,10 +505,9 @@ module.exports = {
full: '100%',
screen: '100vh',
}),
inset: ({ theme, negative }) => ({
inset: ({ theme }) => ({
auto: 'auto',
...theme('spacing'),
...negative(theme('spacing')),
'1/2': '50%',
'1/3': '33.333333%',
'2/3': '66.666667%',
@ -521,13 +515,6 @@ module.exports = {
'2/4': '50%',
'3/4': '75%',
full: '100%',
'-1/2': '-50%',
'-1/3': '-33.333333%',
'-2/3': '-66.666667%',
'-1/4': '-25%',
'-2/4': '-50%',
'-3/4': '-75%',
'-full': '-100%',
}),
keyframes: {
spin: {
@ -586,10 +573,9 @@ module.exports = {
disc: 'disc',
decimal: 'decimal',
},
margin: ({ theme, negative }) => ({
margin: ({ theme }) => ({
auto: 'auto',
...theme('spacing'),
...negative(theme('spacing')),
}),
maxHeight: ({ theme }) => ({
...theme('spacing'),
@ -705,14 +691,6 @@ module.exports = {
8: '8px',
},
rotate: {
'-180': '-180deg',
'-90': '-90deg',
'-45': '-45deg',
'-12': '-12deg',
'-6': '-6deg',
'-3': '-3deg',
'-2': '-2deg',
'-1': '-1deg',
0: '0deg',
1: '1deg',
2: '2deg',
@ -742,9 +720,8 @@ module.exports = {
125: '1.25',
150: '1.5',
},
scrollMargin: ({ theme, negative }) => ({
scrollMargin: ({ theme }) => ({
...theme('spacing'),
...negative(theme('spacing')),
}),
scrollPadding: ({ theme }) => theme('spacing'),
sepia: {
@ -752,11 +729,6 @@ module.exports = {
DEFAULT: '100%',
},
skew: {
'-12': '-12deg',
'-6': '-6deg',
'-3': '-3deg',
'-2': '-2deg',
'-1': '-1deg',
0: '0deg',
1: '1deg',
2: '2deg',
@ -764,9 +736,8 @@ module.exports = {
6: '6deg',
12: '12deg',
},
space: ({ theme, negative }) => ({
space: ({ theme }) => ({
...theme('spacing'),
...negative(theme('spacing')),
}),
stroke: {
current: 'currentColor',
@ -777,9 +748,8 @@ module.exports = {
2: '2',
},
textColor: ({ theme }) => theme('colors'),
textIndent: ({ theme, negative }) => ({
textIndent: ({ theme }) => ({
...theme('spacing'),
...negative(theme('spacing')),
}),
textOpacity: ({ theme }) => theme('opacity'),
transformOrigin: {
@ -831,9 +801,8 @@ module.exports = {
out: 'cubic-bezier(0, 0, 0.2, 1)',
'in-out': 'cubic-bezier(0.4, 0, 0.2, 1)',
},
translate: ({ theme, negative }) => ({
translate: ({ theme }) => ({
...theme('spacing'),
...negative(theme('spacing')),
'1/2': '50%',
'1/3': '33.333333%',
'2/3': '66.666667%',
@ -841,13 +810,6 @@ module.exports = {
'2/4': '50%',
'3/4': '75%',
full: '100%',
'-1/2': '-50%',
'-1/3': '-33.333333%',
'-2/3': '-66.666667%',
'-1/4': '-25%',
'-2/4': '-50%',
'-3/4': '-75%',
'-full': '-100%',
}),
width: ({ theme }) => ({
auto: 'auto',

View File

@ -15,4 +15,12 @@ it('should generate every possible class, without variants', () => {
// Verify we handle negative values correctly
expect(context.getClassList()).toContain('-inset-1/4')
expect(context.getClassList()).toContain('-m-0')
expect(context.getClassList()).not.toContain('-uppercase')
expect(context.getClassList()).not.toContain('-opacity-50')
expect(
createContext(
resolveConfig({ theme: { extend: { margin: { DEFAULT: '5px' } } } })
).getClassList()
).not.toContain('-m-DEFAULT')
})

View File

@ -8,7 +8,7 @@ test('it negates numeric CSS values', () => {
expect(negateValue('-7ch')).toEqual('7ch')
})
test('it leaves keywords untouched', () => {
expect(negateValue('auto')).toEqual('auto')
expect(negateValue('cover')).toEqual('cover')
test('values that cannot be negated become undefined', () => {
expect(negateValue('auto')).toBeUndefined()
expect(negateValue('cover')).toBeUndefined()
})

View File

@ -0,0 +1,308 @@
import { run, html, css } from './util/run'
test('using a negative prefix with a negative scale value', () => {
let config = {
content: [{ raw: html`<div class="mt-2 -mt-2"></div>` }],
theme: {
margin: {
2: '8px',
'-2': '-4px',
},
},
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.mt-2 {
margin-top: 8px;
}
.-mt-2 {
margin-top: -4px;
}
`)
})
})
test('using a negative scale value with a plugin that does not support dynamic negative values', () => {
let config = {
content: [{ raw: html`<div class="-opacity-50"></div>` }],
theme: {
opacity: {
'-50': '0.5',
},
},
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-opacity-50 {
opacity: 0.5;
}
`)
})
})
test('using a negative prefix without a negative scale value', () => {
let config = {
content: [{ raw: html`<div class="mt-5 -mt-5"></div>` }],
theme: {
margin: {
5: '20px',
},
},
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.mt-5 {
margin-top: 20px;
}
.-mt-5 {
margin-top: -20px;
}
`)
})
})
test('being an asshole', () => {
let config = {
content: [{ raw: html`<div class="-mt-[10px]"></div>` }],
theme: {
margin: {
'-[10px]': '55px',
},
},
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt-\\[10px\\] {
margin-top: 55px;
}
`)
})
})
test('being a real asshole', () => {
let config = {
content: [{ raw: html`<div class="-mt-[10px]"></div>` }],
theme: {
margin: {
'[10px]': '55px',
},
},
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt-\\[10px\\] {
margin-top: -55px;
}
`)
})
})
test('a value that includes a variable', () => {
let config = {
content: [{ raw: html`<div class="mt-5 -mt-5"></div>` }],
theme: {
margin: {
5: 'var(--sizing-5)',
},
},
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.mt-5 {
margin-top: var(--sizing-5);
}
.-mt-5 {
margin-top: calc(var(--sizing-5) * -1);
}
`)
})
})
test('a value that includes a calc', () => {
let config = {
content: [{ raw: html`<div class="mt-5 -mt-5"></div>` }],
theme: {
margin: {
5: 'calc(52px * -3)',
},
},
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.mt-5 {
margin-top: calc(52px * -3);
}
.-mt-5 {
margin-top: calc(calc(52px * -3) * -1);
}
`)
})
})
test('a keyword value', () => {
let config = {
content: [{ raw: html`<div class="mt-auto -mt-auto"></div>` }],
theme: {
margin: {
auto: 'auto',
},
},
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.mt-auto {
margin-top: auto;
}
`)
})
})
test('a zero value', () => {
let config = {
content: [{ raw: html`<div class="mt-0 -mt-0"></div>` }],
theme: {
margin: {
0: '0',
},
},
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.mt-0 {
margin-top: 0;
}
.-mt-0 {
margin-top: 0;
}
`)
})
})
test('a color', () => {
let config = {
content: [{ raw: html`<div class="-bg-red"></div>` }],
theme: {
colors: {
red: 'red',
},
},
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css``)
})
})
test('arbitrary values', () => {
let config = {
content: [{ raw: html`<div class="-mt-[10px]"></div>` }],
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt-\\[10px\\] {
margin-top: -10px;
}
`)
})
})
test('negating a negative scale value', () => {
let config = {
content: [{ raw: html`<div class="-mt-weird"></div>` }],
theme: {
margin: {
weird: '-15px',
},
},
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt-weird {
margin-top: 15px;
}
`)
})
})
test('negating a default value', () => {
let config = {
content: [{ raw: html`<div class="-mt"></div>` }],
theme: {
margin: {
DEFAULT: '15px',
},
},
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.-mt {
margin-top: -15px;
}
`)
})
})
test('using a negative prefix with a negative default scale value', () => {
let config = {
content: [{ raw: html`<div class="mt -mt"></div>` }],
theme: {
margin: {
DEFAULT: '8px',
'-DEFAULT': '-4px',
},
},
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.mt {
margin-top: 8px;
}
.-mt {
margin-top: -4px;
}
`)
})
})
test('negating a default value with a configured prefix', () => {
let config = {
prefix: 'tw-',
content: [{ raw: html`<div class="tw--mt"></div>` }],
theme: {
margin: {
DEFAULT: '15px',
},
},
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css`
.tw--mt {
margin-top: -15px;
}
`)
})
})
test('arbitrary value keywords should be ignored', () => {
let config = {
content: [{ raw: html`<div class="-mt-[auto]"></div>` }],
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchCss(css``)
})
})

View File

@ -1107,12 +1107,11 @@ test('custom properties are multiplied by -1 for negative values', () => {
bar: 'var(--bar, 500px)',
baz: 'calc(50% - 10px)',
qux: '10poops',
'-0': '-0',
'-0': '0',
'-1': '-1px',
'-2': '-2px',
'-3': '-3px',
'-4': '-4px',
'-auto': 'auto',
'-foo': 'calc(var(--foo) * -1)',
'-bar': 'calc(var(--bar, 500px) * -1)',
'-baz': 'calc(calc(50% - 10px) * -1)',