Skip to content

Commit 87df93d

Browse files
authored
Support opacity modifiers for colors in JIT (#4348)
* Support opacity modifiers for colors in JIT * Add test for function colors * Support opacity modifiers for plugins with arbitrary "any" type
1 parent 6be7976 commit 87df93d

7 files changed

+260
-20
lines changed

src/jit/lib/generateRules.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ function* candidatePermutations(candidate, lastIndex = Infinity) {
2727
if (lastIndex === Infinity && candidate.endsWith(']')) {
2828
let bracketIdx = candidate.lastIndexOf('[')
2929

30-
// If character before `[` isn't a dash, this isn't a dynamic class
30+
// If character before `[` isn't a dash or a slash, this isn't a dynamic class
3131
// eg. string[]
32-
dashIdx = candidate[bracketIdx - 1] === '-' ? bracketIdx - 1 : -1
32+
dashIdx = ['-', '/'].includes(candidate[bracketIdx - 1]) ? bracketIdx - 1 : -1
3333
} else {
3434
dashIdx = candidate.lastIndexOf('-', lastIndex)
3535
}

src/jit/lib/setupContext.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -541,9 +541,10 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
541541

542542
function wrapped(modifier) {
543543
let { type = 'any' } = options
544-
let [value, coercedType] = coerceValue(type, modifier, options.values)
544+
type = [].concat(type)
545+
let [value, coercedType] = coerceValue(type, modifier, options.values, tailwindConfig)
545546

546-
if (type !== coercedType || value === undefined) {
547+
if (!type.includes(coercedType) || value === undefined) {
547548
return []
548549
}
549550

src/plugins/fill.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default function () {
1212
{
1313
values: flattenColorPalette(theme('fill')),
1414
variants: variants('fill'),
15-
type: 'any',
15+
type: ['color', 'any'],
1616
}
1717
)
1818
}

src/plugins/gradientColorStops.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export default function () {
1111
let options = {
1212
values: flattenColorPalette(theme('gradientColorStops')),
1313
variants: variants('gradientColorStops'),
14-
type: 'any',
14+
type: ['color', 'any'],
1515
}
1616

1717
matchUtilities(

src/plugins/placeholderColor.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default function () {
2626
{
2727
values: flattenColorPalette(theme('placeholderColor')),
2828
variants: variants('placeholderColor'),
29-
type: 'any',
29+
type: ['color', 'any'],
3030
}
3131
)
3232
}

src/util/pluginUtils.js

+52-13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import selectorParser from 'postcss-selector-parser'
22
import postcss from 'postcss'
33
import createColor from 'color'
44
import escapeCommas from './escapeCommas'
5+
import { withAlphaValue } from './withAlphaVariable'
56

67
export function updateAllClasses(selectors, updateClass) {
78
let parser = selectorParser((selectors) => {
@@ -148,16 +149,50 @@ export function asList(modifier, lookup = {}) {
148149
})
149150
}
150151

151-
export function asColor(modifier, lookup = {}) {
152+
function isArbitraryValue(input) {
153+
return input.startsWith('[') && input.endsWith(']')
154+
}
155+
156+
function splitAlpha(modifier) {
157+
let slashIdx = modifier.lastIndexOf('/')
158+
159+
if (slashIdx === -1 || slashIdx === modifier.length - 1) {
160+
return [modifier]
161+
}
162+
163+
return [modifier.slice(0, slashIdx), modifier.slice(slashIdx + 1)]
164+
}
165+
166+
function isColor(value) {
167+
try {
168+
createColor(value)
169+
return true
170+
} catch (e) {
171+
return false
172+
}
173+
}
174+
175+
export function asColor(modifier, lookup = {}, tailwindConfig = {}) {
176+
if (lookup[modifier] !== undefined) {
177+
return lookup[modifier]
178+
}
179+
180+
let [color, alpha] = splitAlpha(modifier)
181+
182+
if (lookup[color] !== undefined) {
183+
if (isArbitraryValue(alpha)) {
184+
return withAlphaValue(lookup[color], alpha.slice(1, -1))
185+
}
186+
187+
if (tailwindConfig.theme?.opacity?.[alpha] === undefined) {
188+
return undefined
189+
}
190+
191+
return withAlphaValue(lookup[color], tailwindConfig.theme.opacity[alpha])
192+
}
193+
152194
return asValue(modifier, lookup, {
153-
validate: (value) => {
154-
try {
155-
createColor(value)
156-
return true
157-
} catch (e) {
158-
return false
159-
}
160-
},
195+
validate: isColor,
161196
})
162197
}
163198

@@ -208,14 +243,18 @@ function splitAtFirst(input, delim) {
208243
return (([first, ...rest]) => [first, rest.join(delim)])(input.split(delim))
209244
}
210245

211-
export function coerceValue(type, modifier, values) {
212-
if (modifier.startsWith('[') && modifier.endsWith(']')) {
246+
export function coerceValue(type, modifier, values, tailwindConfig) {
247+
let [scaleType, arbitraryType = scaleType] = [].concat(type)
248+
249+
if (isArbitraryValue(modifier)) {
213250
let [explicitType, value] = splitAtFirst(modifier.slice(1, -1), ':')
214251

215252
if (value.length > 0 && Object.keys(typeMap).includes(explicitType)) {
216-
return [asValue(`[${value}]`, values), explicitType]
253+
return [asValue(`[${value}]`, values, tailwindConfig), explicitType]
217254
}
255+
256+
return [typeMap[arbitraryType](modifier, values, tailwindConfig), arbitraryType]
218257
}
219258

220-
return [typeMap[type](modifier, values), type]
259+
return [typeMap[scaleType](modifier, values, tailwindConfig), scaleType]
221260
}
+200
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import postcss from 'postcss'
2+
import path from 'path'
3+
import tailwind from '../../src/jit/index.js'
4+
5+
function run(input, config = {}) {
6+
const { currentTestName } = expect.getState()
7+
8+
return postcss(tailwind(config)).process(input, {
9+
from: `${path.resolve(__filename)}?test=${currentTestName}`,
10+
})
11+
}
12+
13+
test('basic color opacity modifier', async () => {
14+
let config = {
15+
mode: 'jit',
16+
purge: [
17+
{
18+
raw: '<div class="bg-red-500/50"></div>',
19+
},
20+
],
21+
theme: {},
22+
plugins: [],
23+
}
24+
25+
let css = `@tailwind utilities`
26+
27+
return run(css, config).then((result) => {
28+
expect(result.css).toMatchFormattedCss(`
29+
.bg-red-500\\/50 {
30+
background-color: rgba(239, 68, 68, 0.5);
31+
}
32+
`)
33+
})
34+
})
35+
36+
test('colors with slashes are matched first', async () => {
37+
let config = {
38+
mode: 'jit',
39+
purge: [
40+
{
41+
raw: '<div class="bg-red-500/50"></div>',
42+
},
43+
],
44+
theme: {
45+
extend: {
46+
colors: {
47+
'red-500/50': '#ff0000',
48+
},
49+
},
50+
},
51+
plugins: [],
52+
}
53+
54+
let css = `@tailwind utilities`
55+
56+
return run(css, config).then((result) => {
57+
expect(result.css).toMatchFormattedCss(`
58+
.bg-red-500\\/50 {
59+
--tw-bg-opacity: 1;
60+
background-color: rgba(255, 0, 0, var(--tw-bg-opacity));
61+
}
62+
`)
63+
})
64+
})
65+
66+
test('arbitrary color opacity modifier', async () => {
67+
let config = {
68+
mode: 'jit',
69+
purge: [
70+
{
71+
raw: 'bg-red-500/[var(--opacity)]',
72+
},
73+
],
74+
theme: {},
75+
plugins: [],
76+
}
77+
78+
let css = `@tailwind utilities`
79+
80+
return run(css, config).then((result) => {
81+
expect(result.css).toMatchFormattedCss(`
82+
.bg-red-500\\/\\[var\\(--opacity\\)\\] {
83+
background-color: rgba(239, 68, 68, var(--opacity));
84+
}
85+
`)
86+
})
87+
})
88+
89+
test('missing alpha generates nothing', async () => {
90+
let config = {
91+
mode: 'jit',
92+
purge: [
93+
{
94+
raw: '<div class="bg-red-500/"></div>',
95+
},
96+
],
97+
theme: {},
98+
plugins: [],
99+
}
100+
101+
let css = `@tailwind utilities`
102+
103+
return run(css, config).then((result) => {
104+
expect(result.css).toMatchFormattedCss(``)
105+
})
106+
})
107+
108+
test('values not in the opacity config are ignored', async () => {
109+
let config = {
110+
mode: 'jit',
111+
purge: [
112+
{
113+
raw: '<div class="bg-red-500/29"></div>',
114+
},
115+
],
116+
theme: {
117+
opacity: {
118+
0: '0',
119+
25: '0.25',
120+
5: '0.5',
121+
75: '0.75',
122+
100: '1',
123+
},
124+
},
125+
plugins: [],
126+
}
127+
128+
let css = `@tailwind utilities`
129+
130+
return run(css, config).then((result) => {
131+
expect(result.css).toMatchFormattedCss(``)
132+
})
133+
})
134+
135+
test('function colors are supported', async () => {
136+
let config = {
137+
mode: 'jit',
138+
purge: [
139+
{
140+
raw: '<div class="bg-blue/50"></div>',
141+
},
142+
],
143+
theme: {
144+
colors: {
145+
blue: ({ opacityValue }) => {
146+
return `rgba(var(--colors-blue), ${opacityValue})`
147+
},
148+
},
149+
},
150+
plugins: [],
151+
}
152+
153+
let css = `@tailwind utilities`
154+
155+
return run(css, config).then((result) => {
156+
expect(result.css).toMatchFormattedCss(`
157+
.bg-blue\\/50 {
158+
background-color: rgba(var(--colors-blue), 0.5);
159+
}
160+
`)
161+
})
162+
})
163+
164+
test('utilities that support any type are supported', async () => {
165+
let config = {
166+
mode: 'jit',
167+
purge: [
168+
{
169+
raw: `
170+
<div class="from-red-500/50"></div>
171+
<div class="fill-red-500/25"></div>
172+
<div class="placeholder-red-500/75"></div>
173+
`,
174+
},
175+
],
176+
theme: {
177+
extend: {
178+
fill: (theme) => theme('colors'),
179+
},
180+
},
181+
plugins: [],
182+
}
183+
184+
let css = `@tailwind utilities`
185+
186+
return run(css, config).then((result) => {
187+
expect(result.css).toMatchFormattedCss(`
188+
.from-red-500\\/50 {
189+
--tw-gradient-from: rgba(239, 68, 68, 0.5);
190+
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(239, 68, 68, 0));
191+
}
192+
.fill-red-500\\/25 {
193+
fill: rgba(239, 68, 68, 0.25);
194+
}
195+
.placeholder-red-500\\/75::placeholder {
196+
color: rgba(239, 68, 68, 0.75);
197+
}
198+
`)
199+
})
200+
})

0 commit comments

Comments
 (0)