diff --git a/src/jit/lib/generateRules.js b/src/jit/lib/generateRules.js index 027ad707ce5f..916a64181e57 100644 --- a/src/jit/lib/generateRules.js +++ b/src/jit/lib/generateRules.js @@ -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) } diff --git a/src/jit/lib/setupContext.js b/src/jit/lib/setupContext.js index b0e01d3a2f1a..3ea367b8ac45 100644 --- a/src/jit/lib/setupContext.js +++ b/src/jit/lib/setupContext.js @@ -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 [] } diff --git a/src/plugins/fill.js b/src/plugins/fill.js index 311524f9d968..7778ece9156c 100644 --- a/src/plugins/fill.js +++ b/src/plugins/fill.js @@ -12,7 +12,7 @@ export default function () { { values: flattenColorPalette(theme('fill')), variants: variants('fill'), - type: 'any', + type: ['color', 'any'], } ) } diff --git a/src/plugins/gradientColorStops.js b/src/plugins/gradientColorStops.js index 38d7d39a767d..e1ebba1364ec 100644 --- a/src/plugins/gradientColorStops.js +++ b/src/plugins/gradientColorStops.js @@ -11,7 +11,7 @@ export default function () { let options = { values: flattenColorPalette(theme('gradientColorStops')), variants: variants('gradientColorStops'), - type: 'any', + type: ['color', 'any'], } matchUtilities( diff --git a/src/plugins/placeholderColor.js b/src/plugins/placeholderColor.js index 66e6e27a5152..412d3647de8a 100644 --- a/src/plugins/placeholderColor.js +++ b/src/plugins/placeholderColor.js @@ -26,7 +26,7 @@ export default function () { { values: flattenColorPalette(theme('placeholderColor')), variants: variants('placeholderColor'), - type: 'any', + type: ['color', 'any'], } ) } diff --git a/src/util/pluginUtils.js b/src/util/pluginUtils.js index acc9e4a9200a..a37c6e98d7dc 100644 --- a/src/util/pluginUtils.js +++ b/src/util/pluginUtils.js @@ -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] } diff --git a/tests/jit/color-opacity-modifiers.test.js b/tests/jit/color-opacity-modifiers.test.js new file mode 100644 index 000000000000..7955a758f091 --- /dev/null +++ b/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: '
', + }, + ], + 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: '
', + }, + ], + 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: '
', + }, + ], + 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: '
', + }, + ], + 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: '
', + }, + ], + 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: ` +
+
+
+ `, + }, + ], + 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); + } + `) + }) +})