Skip to content

Commit

Permalink
Support opacity modifiers for colors in JIT (#4348)
Browse files Browse the repository at this point in the history
* Support opacity modifiers for colors in JIT

* Add test for function colors

* Support opacity modifiers for plugins with arbitrary "any" type
  • Loading branch information
adamwathan committed May 14, 2021
1 parent 6be7976 commit 87df93d
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 20 deletions.
4 changes: 2 additions & 2 deletions src/jit/lib/generateRules.js
Expand Up @@ -27,9 +27,9 @@ function* candidatePermutations(candidate, lastIndex = Infinity) {
if (lastIndex === Infinity && candidate.endsWith(']')) {
let bracketIdx = candidate.lastIndexOf('[')

// If character before `[` isn't a dash, this isn't a dynamic class
// If character before `[` isn't a dash or a slash, this isn't a dynamic class
// eg. string[]
dashIdx = candidate[bracketIdx - 1] === '-' ? bracketIdx - 1 : -1
dashIdx = ['-', '/'].includes(candidate[bracketIdx - 1]) ? bracketIdx - 1 : -1
} else {
dashIdx = candidate.lastIndexOf('-', lastIndex)
}
Expand Down
5 changes: 3 additions & 2 deletions src/jit/lib/setupContext.js
Expand Up @@ -541,9 +541,10 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs

function wrapped(modifier) {
let { type = 'any' } = options
let [value, coercedType] = coerceValue(type, modifier, options.values)
type = [].concat(type)
let [value, coercedType] = coerceValue(type, modifier, options.values, tailwindConfig)

if (type !== coercedType || value === undefined) {
if (!type.includes(coercedType) || value === undefined) {
return []
}

Expand Down
2 changes: 1 addition & 1 deletion src/plugins/fill.js
Expand Up @@ -12,7 +12,7 @@ export default function () {
{
values: flattenColorPalette(theme('fill')),
variants: variants('fill'),
type: 'any',
type: ['color', 'any'],
}
)
}
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/gradientColorStops.js
Expand Up @@ -11,7 +11,7 @@ export default function () {
let options = {
values: flattenColorPalette(theme('gradientColorStops')),
variants: variants('gradientColorStops'),
type: 'any',
type: ['color', 'any'],
}

matchUtilities(
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/placeholderColor.js
Expand Up @@ -26,7 +26,7 @@ export default function () {
{
values: flattenColorPalette(theme('placeholderColor')),
variants: variants('placeholderColor'),
type: 'any',
type: ['color', 'any'],
}
)
}
Expand Down
65 changes: 52 additions & 13 deletions src/util/pluginUtils.js
Expand Up @@ -2,6 +2,7 @@ import selectorParser from 'postcss-selector-parser'
import postcss from 'postcss'
import createColor from 'color'
import escapeCommas from './escapeCommas'
import { withAlphaValue } from './withAlphaVariable'

export function updateAllClasses(selectors, updateClass) {
let parser = selectorParser((selectors) => {
Expand Down Expand Up @@ -148,16 +149,50 @@ export function asList(modifier, lookup = {}) {
})
}

export function asColor(modifier, lookup = {}) {
function isArbitraryValue(input) {
return input.startsWith('[') && input.endsWith(']')
}

function splitAlpha(modifier) {
let slashIdx = modifier.lastIndexOf('/')

if (slashIdx === -1 || slashIdx === modifier.length - 1) {
return [modifier]
}

return [modifier.slice(0, slashIdx), modifier.slice(slashIdx + 1)]
}

function isColor(value) {
try {
createColor(value)
return true
} catch (e) {
return false
}
}

export function asColor(modifier, lookup = {}, tailwindConfig = {}) {
if (lookup[modifier] !== undefined) {
return lookup[modifier]
}

let [color, alpha] = splitAlpha(modifier)

if (lookup[color] !== undefined) {
if (isArbitraryValue(alpha)) {
return withAlphaValue(lookup[color], alpha.slice(1, -1))
}

if (tailwindConfig.theme?.opacity?.[alpha] === undefined) {
return undefined
}

return withAlphaValue(lookup[color], tailwindConfig.theme.opacity[alpha])
}

return asValue(modifier, lookup, {
validate: (value) => {
try {
createColor(value)
return true
} catch (e) {
return false
}
},
validate: isColor,
})
}

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

export function coerceValue(type, modifier, values) {
if (modifier.startsWith('[') && modifier.endsWith(']')) {
export function coerceValue(type, modifier, values, tailwindConfig) {
let [scaleType, arbitraryType = scaleType] = [].concat(type)

if (isArbitraryValue(modifier)) {
let [explicitType, value] = splitAtFirst(modifier.slice(1, -1), ':')

if (value.length > 0 && Object.keys(typeMap).includes(explicitType)) {
return [asValue(`[${value}]`, values), explicitType]
return [asValue(`[${value}]`, values, tailwindConfig), explicitType]
}

return [typeMap[arbitraryType](modifier, values, tailwindConfig), arbitraryType]
}

return [typeMap[type](modifier, values), type]
return [typeMap[scaleType](modifier, values, tailwindConfig), scaleType]
}
200 changes: 200 additions & 0 deletions tests/jit/color-opacity-modifiers.test.js
@@ -0,0 +1,200 @@
import postcss from 'postcss'
import path from 'path'
import tailwind from '../../src/jit/index.js'

function run(input, config = {}) {
const { currentTestName } = expect.getState()

return postcss(tailwind(config)).process(input, {
from: `${path.resolve(__filename)}?test=${currentTestName}`,
})
}

test('basic color opacity modifier', async () => {
let config = {
mode: 'jit',
purge: [
{
raw: '<div class="bg-red-500/50"></div>',
},
],
theme: {},
plugins: [],
}

let css = `@tailwind utilities`

return run(css, config).then((result) => {
expect(result.css).toMatchFormattedCss(`
.bg-red-500\\/50 {
background-color: rgba(239, 68, 68, 0.5);
}
`)
})
})

test('colors with slashes are matched first', async () => {
let config = {
mode: 'jit',
purge: [
{
raw: '<div class="bg-red-500/50"></div>',
},
],
theme: {
extend: {
colors: {
'red-500/50': '#ff0000',
},
},
},
plugins: [],
}

let css = `@tailwind utilities`

return run(css, config).then((result) => {
expect(result.css).toMatchFormattedCss(`
.bg-red-500\\/50 {
--tw-bg-opacity: 1;
background-color: rgba(255, 0, 0, var(--tw-bg-opacity));
}
`)
})
})

test('arbitrary color opacity modifier', async () => {
let config = {
mode: 'jit',
purge: [
{
raw: 'bg-red-500/[var(--opacity)]',
},
],
theme: {},
plugins: [],
}

let css = `@tailwind utilities`

return run(css, config).then((result) => {
expect(result.css).toMatchFormattedCss(`
.bg-red-500\\/\\[var\\(--opacity\\)\\] {
background-color: rgba(239, 68, 68, var(--opacity));
}
`)
})
})

test('missing alpha generates nothing', async () => {
let config = {
mode: 'jit',
purge: [
{
raw: '<div class="bg-red-500/"></div>',
},
],
theme: {},
plugins: [],
}

let css = `@tailwind utilities`

return run(css, config).then((result) => {
expect(result.css).toMatchFormattedCss(``)
})
})

test('values not in the opacity config are ignored', async () => {
let config = {
mode: 'jit',
purge: [
{
raw: '<div class="bg-red-500/29"></div>',
},
],
theme: {
opacity: {
0: '0',
25: '0.25',
5: '0.5',
75: '0.75',
100: '1',
},
},
plugins: [],
}

let css = `@tailwind utilities`

return run(css, config).then((result) => {
expect(result.css).toMatchFormattedCss(``)
})
})

test('function colors are supported', async () => {
let config = {
mode: 'jit',
purge: [
{
raw: '<div class="bg-blue/50"></div>',
},
],
theme: {
colors: {
blue: ({ opacityValue }) => {
return `rgba(var(--colors-blue), ${opacityValue})`
},
},
},
plugins: [],
}

let css = `@tailwind utilities`

return run(css, config).then((result) => {
expect(result.css).toMatchFormattedCss(`
.bg-blue\\/50 {
background-color: rgba(var(--colors-blue), 0.5);
}
`)
})
})

test('utilities that support any type are supported', async () => {
let config = {
mode: 'jit',
purge: [
{
raw: `
<div class="from-red-500/50"></div>
<div class="fill-red-500/25"></div>
<div class="placeholder-red-500/75"></div>
`,
},
],
theme: {
extend: {
fill: (theme) => theme('colors'),
},
},
plugins: [],
}

let css = `@tailwind utilities`

return run(css, config).then((result) => {
expect(result.css).toMatchFormattedCss(`
.from-red-500\\/50 {
--tw-gradient-from: rgba(239, 68, 68, 0.5);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(239, 68, 68, 0));
}
.fill-red-500\\/25 {
fill: rgba(239, 68, 68, 0.25);
}
.placeholder-red-500\\/75::placeholder {
color: rgba(239, 68, 68, 0.75);
}
`)
})
})

0 comments on commit 87df93d

Please sign in to comment.