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
This commit is contained in:
Adam Wathan 2021-05-14 13:07:56 -04:00 • committed by GitHub
parent 6be797633f
commit 87df93de0a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 260 additions and 20 deletions

View file

@ -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)
}

View file

@ -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 []
}

View file

@ -12,7 +12,7 @@ export default function () {
{
values: flattenColorPalette(theme('fill')),
variants: variants('fill'),
type: 'any',
type: ['color', 'any'],
}
)
}

View file

@ -11,7 +11,7 @@ export default function () {
let options = {
values: flattenColorPalette(theme('gradientColorStops')),
variants: variants('gradientColorStops'),
type: 'any',
type: ['color', 'any'],
}
matchUtilities(

View file

@ -26,7 +26,7 @@ export default function () {
{
values: flattenColorPalette(theme('placeholderColor')),
variants: variants('placeholderColor'),
type: 'any',
type: ['color', 'any'],
}
)
}

View file

@ -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) => {
@ -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,
})
}
@ -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]
}

View file

@ -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);
}
`)
})
})