From 50bed74cdc5c0b6f55f141aa580d1c314d477db0 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 25 May 2022 13:54:30 -0400 Subject: [PATCH] Support alpha values for `theme()` function (#8416) * Fix typo * Support alpha modifier for theme color values * Eliminate redundant object creation in resolveFunctionKeys Building an object of N keys incrementally using Object.reduce + splat results in N intermediate objects. We should just create one object and assign each key. * Switch to inline theme values in theme fn in config * Add test case And fix typos that were definitely not there * Update changelog --- CHANGELOG.md | 1 + src/lib/evaluateTailwindFunctions.js | 22 ++- src/lib/setupContextUtils.js | 19 +- src/util/resolveConfig.js | 84 ++++++--- src/util/toPath.js | 2 +- src/util/transformThemeValue.js | 6 +- tests/evaluateTailwindFunctions.test.js | 207 +++++++++++++++++++++ tests/opacity.test.js | 232 ++++++++++++++++++++++++ 8 files changed, 540 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index edc653a0531a..23420e8ea65a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `matchVariant` API ([#8310](https://github.com/tailwindlabs/tailwindcss/pull/8310)) - Add `prefers-contrast` media query variants ([#8410](https://github.com/tailwindlabs/tailwindcss/pull/8410)) - Experimental support for variant grouping ([#8405](https://github.com/tailwindlabs/tailwindcss/pull/8405)) +- Add opacity support when referencing colors with `theme` function ([#8416](https://github.com/tailwindlabs/tailwindcss/pull/8416)) ## [3.0.24] - 2022-04-12 diff --git a/src/lib/evaluateTailwindFunctions.js b/src/lib/evaluateTailwindFunctions.js index fbe366a93197..bfeea5a52009 100644 --- a/src/lib/evaluateTailwindFunctions.js +++ b/src/lib/evaluateTailwindFunctions.js @@ -5,6 +5,7 @@ import parseValue from 'postcss-value-parser' import { normalizeScreens } from '../util/normalizeScreens' import buildMediaQuery from '../util/buildMediaQuery' import { toPath } from '../util/toPath' +import { withAlphaValue } from '../util/withAlphaVariable' function isObject(input) { return typeof input === 'object' && input !== null @@ -37,7 +38,7 @@ function listKeys(obj) { return list(Object.keys(obj)) } -function validatePath(config, path, defaultValue) { +function validatePath(config, path, defaultValue, themeOpts = {}) { const pathString = Array.isArray(path) ? pathToString(path) : path.replace(/^['"]+/g, '').replace(/['"]+$/g, '') @@ -114,7 +115,7 @@ function validatePath(config, path, defaultValue) { return { isValid: true, - value: transformThemeValue(themeSection)(value), + value: transformThemeValue(themeSection)(value, themeOpts), } } @@ -160,16 +161,29 @@ let nodeTypePropertyMap = { export default function ({ tailwindConfig: config }) { let functions = { theme: (node, path, ...defaultValue) => { - const { isValid, value, error } = validatePath( + let matches = path.match(/^([^\/\s]+)(?:\s*\/\s*([^\/\s]+))$/) + let alpha = undefined + + if (matches) { + path = matches[1] + alpha = matches[2] + } + + let { isValid, value, error } = validatePath( config, path, - defaultValue.length ? defaultValue : undefined + defaultValue.length ? defaultValue : undefined, + { opacityValue: alpha } ) if (!isValid) { throw node.error(error) } + if (alpha !== undefined) { + value = withAlphaValue(value, alpha, value) + } + return value }, screen: (node, screen) => { diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index d03007c38805..5f0a3203ede7 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -221,16 +221,25 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs return context.tailwindConfig.prefix + identifier } + function resolveThemeValue(path, defaultValue, opts = {}) { + const [pathRoot, ...subPaths] = toPath(path) + const value = getConfigValue(['theme', pathRoot, ...subPaths], defaultValue) + return transformThemeValue(pathRoot)(value, opts) + } + + const theme = Object.assign( + (path, defaultValue = undefined) => resolveThemeValue(path, defaultValue), + { + withAlpha: (path, opacityValue) => resolveThemeValue(path, undefined, { opacityValue }), + } + ) + let api = { postcss, prefix: applyConfiguredPrefix, e: escapeClassName, config: getConfigValue, - theme(path, defaultValue) { - const [pathRoot, ...subPaths] = toPath(path) - const value = getConfigValue(['theme', pathRoot, ...subPaths], defaultValue) - return transformThemeValue(pathRoot)(value) - }, + theme, corePlugins: (path) => { if (Array.isArray(tailwindConfig.corePlugins)) { return tailwindConfig.corePlugins.includes(path) diff --git a/src/util/resolveConfig.js b/src/util/resolveConfig.js index ad9c22529881..f4808cf687f5 100644 --- a/src/util/resolveConfig.js +++ b/src/util/resolveConfig.js @@ -8,6 +8,7 @@ import { toPath } from './toPath' import { normalizeConfig } from './normalizeConfig' import isPlainObject from './isPlainObject' import { cloneDeep } from './cloneDeep' +import { withAlphaValue } from './withAlphaVariable' function isFunction(input) { return typeof input === 'function' @@ -164,40 +165,81 @@ function mergeExtensions({ extend, ...theme }) { }) } +/** + * + * @param {string} key + * @return {Iterable} + */ +function* toPaths(key) { + let path = toPath(key) + + if (path.length === 0) { + return + } + + yield path + + if (Array.isArray(key)) { + return + } + + let pattern = /^(.*?)\s*\/\s*([^/]+)$/ + let matches = key.match(pattern) + + if (matches !== null) { + let [, prefix, alpha] = matches + + let newPath = toPath(prefix) + newPath.alpha = alpha + + yield newPath + } +} + function resolveFunctionKeys(object) { + // theme('colors.red.500 / 0.5') -> ['colors', 'red', '500 / 0', '5] + const resolvePath = (key, defaultValue) => { - const path = toPath(key) + for (const path of toPaths(key)) { + let index = 0 + let val = object - let index = 0 - let val = object + while (val !== undefined && val !== null && index < path.length) { + val = val[path[index++]] - while (val !== undefined && val !== null && index < path.length) { - val = val[path[index++]] - val = isFunction(val) ? val(resolvePath, configUtils) : val - } + let shouldResolveAsFn = + isFunction(val) && (path.alpha === undefined || index < path.length - 1) - if (val === undefined) { - return defaultValue - } + val = shouldResolveAsFn ? val(resolvePath, configUtils) : val + } + + if (val !== undefined) { + if (path.alpha !== undefined) { + return withAlphaValue(val, path.alpha) + } + + if (isPlainObject(val)) { + return cloneDeep(val) + } - if (isPlainObject(val)) { - return cloneDeep(val) + return val + } } - return val + return defaultValue } - resolvePath.theme = resolvePath + // colors.red.500/50 - for (let key in configUtils) { - resolvePath[key] = configUtils[key] - } + Object.assign(resolvePath, { + theme: resolvePath, + ...configUtils, + }) return Object.keys(object).reduce((resolved, key) => { - return { - ...resolved, - [key]: isFunction(object[key]) ? object[key](resolvePath, configUtils) : object[key], - } + resolved[key] = isFunction(object[key]) ? object[key](resolvePath, configUtils) : object[key] + + return resolved }, {}) } diff --git a/src/util/toPath.js b/src/util/toPath.js index bf9d1333c371..6dce9249a3da 100644 --- a/src/util/toPath.js +++ b/src/util/toPath.js @@ -4,7 +4,7 @@ * Square bracket notation `a[b]` may be used to "escape" dots that would otherwise be interpreted as path separators. * * Example: - * a -> ['a] + * a -> ['a'] * a.b.c -> ['a', 'b', 'c'] * a[b].c -> ['a', 'b', 'c'] * a[b.c].e.f -> ['a', 'b.c', 'e', 'f'] diff --git a/src/util/transformThemeValue.js b/src/util/transformThemeValue.js index e6a8b8735317..30bd99cadfb0 100644 --- a/src/util/transformThemeValue.js +++ b/src/util/transformThemeValue.js @@ -44,8 +44,10 @@ export default function transformThemeValue(themeSection) { } } - return (value) => { - if (typeof value === 'function') value = value({}) + return (value, opts = {}) => { + if (typeof value === 'function') { + value = value(opts) + } return value } diff --git a/tests/evaluateTailwindFunctions.test.js b/tests/evaluateTailwindFunctions.test.js index 1cd07607b885..ecadb6edcb51 100644 --- a/tests/evaluateTailwindFunctions.test.js +++ b/tests/evaluateTailwindFunctions.test.js @@ -1,11 +1,16 @@ import postcss from 'postcss' import plugin from '../src/lib/evaluateTailwindFunctions' +import tailwind from '../src/index' import { css } from './util/run' function run(input, opts = {}) { return postcss([plugin({ tailwindConfig: opts })]).process(input, { from: undefined }) } +function runFull(input, config) { + return postcss([tailwind(config)]).process(input, { from: undefined }) +} + test('it looks up values in the theme using dot notation', () => { let input = css` .banana { @@ -817,3 +822,205 @@ test('screen arguments can be quoted', () => { expect(result.warnings().length).toBe(0) }) }) + +test('Theme function can extract alpha values for colors (1)', () => { + let input = css` + .foo { + color: theme(colors.blue.500 / 50%); + } + ` + + let output = css` + .foo { + color: rgb(59 130 246 / 50%); + } + ` + + return run(input, { + theme: { + colors: { blue: { 500: '#3b82f6' } }, + }, + }).then((result) => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('Theme function can extract alpha values for colors (2)', () => { + let input = css` + .foo { + color: theme(colors.blue.500 / 0.5); + } + ` + + let output = css` + .foo { + color: rgb(59 130 246 / 0.5); + } + ` + + return run(input, { + theme: { + colors: { blue: { 500: '#3b82f6' } }, + }, + }).then((result) => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('Theme function can extract alpha values for colors (3)', () => { + let input = css` + .foo { + color: theme(colors.blue.500 / var(--my-alpha)); + } + ` + + let output = css` + .foo { + color: rgb(59 130 246 / var(--my-alpha)); + } + ` + + return run(input, { + theme: { + colors: { blue: { 500: '#3b82f6' } }, + }, + }).then((result) => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('Theme function can extract alpha values for colors (4)', () => { + let input = css` + .foo { + color: theme(colors.blue.500 / 50%); + } + ` + + let output = css` + .foo { + color: hsl(217 91% 60% / 50%); + } + ` + + return run(input, { + theme: { + colors: { + blue: { 500: 'hsl(217, 91%, 60%)' }, + }, + }, + }).then((result) => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('Theme function can extract alpha values for colors (5)', () => { + let input = css` + .foo { + color: theme(colors.blue.500 / 0.5); + } + ` + + let output = css` + .foo { + color: hsl(217 91% 60% / 0.5); + } + ` + + return run(input, { + theme: { + colors: { + blue: { 500: 'hsl(217, 91%, 60%)' }, + }, + }, + }).then((result) => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('Theme function can extract alpha values for colors (6)', () => { + let input = css` + .foo { + color: theme(colors.blue.500 / var(--my-alpha)); + } + ` + + let output = css` + .foo { + color: hsl(217 91% 60% / var(--my-alpha)); + } + ` + + return run(input, { + theme: { + colors: { + blue: { 500: 'hsl(217, 91%, 60%)' }, + }, + }, + }).then((result) => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('Theme function can extract alpha values for colors (7)', () => { + let input = css` + .foo { + color: theme(colors.blue.500 / var(--my-alpha)); + } + ` + + let output = css` + .foo { + color: rgb(var(--foo) / var(--my-alpha)); + } + ` + + return runFull(input, { + theme: { + colors: ({ rgb }) => ({ + blue: { + 500: rgb('--foo'), + }, + }), + }, + }).then((result) => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('Theme function can extract alpha values for colors (8)', () => { + let input = css` + .foo { + color: theme(colors.blue.500 / theme(opacity.myalpha)); + } + ` + + let output = css` + .foo { + color: rgb(var(--foo) / 50%); + } + ` + + return runFull(input, { + theme: { + colors: ({ rgb }) => ({ + blue: { + 500: rgb('--foo'), + }, + }), + + opacity: { + myalpha: '50%', + }, + }, + }).then((result) => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) diff --git a/tests/opacity.test.js b/tests/opacity.test.js index cd18c479627a..18b43ec01a00 100644 --- a/tests/opacity.test.js +++ b/tests/opacity.test.js @@ -427,3 +427,235 @@ it('the hsl helper throws when not passing custom properties', () => { 'The hsl() helper requires a custom property name to be passed as the first argument.' ) }) + +test('Theme function in JS can apply alpha values to colors (1)', () => { + let input = css` + @tailwind utilities; + ` + + let output = css` + .text-foo { + color: rgb(59 130 246 / 50%); + } + ` + + return run(input, { + content: [{ raw: html`text-foo` }], + corePlugins: { textOpacity: false }, + theme: { + colors: { blue: { 500: '#3b82f6' } }, + extend: { + textColor: ({ theme }) => ({ + foo: theme('colors.blue.500 / 50%'), + }), + }, + }, + }).then((result) => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('Theme function in JS can apply alpha values to colors (2)', () => { + let input = css` + @tailwind utilities; + ` + + let output = css` + .text-foo { + color: rgb(59 130 246 / 0.5); + } + ` + + return run(input, { + content: [{ raw: html`text-foo` }], + corePlugins: { textOpacity: false }, + theme: { + colors: { blue: { 500: '#3b82f6' } }, + extend: { + textColor: ({ theme }) => ({ + foo: theme('colors.blue.500 / 0.5'), + }), + }, + }, + }).then((result) => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('Theme function in JS can apply alpha values to colors (3)', () => { + let input = css` + @tailwind utilities; + ` + + let output = css` + .text-foo { + color: rgb(59 130 246 / var(--my-alpha)); + } + ` + + return run(input, { + content: [{ raw: html`text-foo` }], + corePlugins: { textOpacity: false }, + theme: { + colors: { blue: { 500: '#3b82f6' } }, + extend: { + textColor: ({ theme }) => ({ + foo: theme('colors.blue.500 / var(--my-alpha)'), + }), + }, + }, + }).then((result) => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('Theme function in JS can apply alpha values to colors (4)', () => { + let input = css` + @tailwind utilities; + ` + + let output = css` + .text-foo { + color: hsl(217 91% 60% / 50%); + } + ` + + return run(input, { + content: [{ raw: html`text-foo` }], + corePlugins: { textOpacity: false }, + theme: { + colors: { blue: { 500: 'hsl(217, 91%, 60%)' } }, + extend: { + textColor: ({ theme }) => ({ + foo: theme('colors.blue.500 / 50%'), + }), + }, + }, + }).then((result) => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('Theme function in JS can apply alpha values to colors (5)', () => { + let input = css` + @tailwind utilities; + ` + + let output = css` + .text-foo { + color: hsl(217 91% 60% / 0.5); + } + ` + + return run(input, { + content: [{ raw: html`text-foo` }], + corePlugins: { textOpacity: false }, + theme: { + colors: { blue: { 500: 'hsl(217, 91%, 60%)' } }, + extend: { + textColor: ({ theme }) => ({ + foo: theme('colors.blue.500 / 0.5'), + }), + }, + }, + }).then((result) => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('Theme function in JS can apply alpha values to colors (6)', () => { + let input = css` + @tailwind utilities; + ` + + let output = css` + .text-foo { + color: hsl(217 91% 60% / var(--my-alpha)); + } + ` + + return run(input, { + content: [{ raw: html`text-foo` }], + corePlugins: { textOpacity: false }, + theme: { + colors: { blue: { 500: 'hsl(217, 91%, 60%)' } }, + extend: { + textColor: ({ theme }) => ({ + foo: theme('colors.blue.500 / var(--my-alpha)'), + }), + }, + }, + }).then((result) => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('Theme function in JS can apply alpha values to colors (7)', () => { + let input = css` + @tailwind utilities; + ` + + let output = css` + .text-foo { + color: rgb(var(--foo) / var(--my-alpha)); + } + ` + + return run(input, { + content: [{ raw: html`text-foo` }], + corePlugins: { textOpacity: false }, + theme: { + colors: ({ rgb }) => ({ + blue: { + 500: rgb('--foo'), + }, + }), + extend: { + textColor: ({ theme }) => ({ + foo: theme('colors.blue.500 / var(--my-alpha)'), + }), + }, + }, + }).then((result) => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('Theme function prefers existing values in config', () => { + let input = css` + @tailwind utilities; + ` + + let output = css` + .text-foo { + color: purple; + } + ` + + return run(input, { + content: [{ raw: html`text-foo` }], + corePlugins: { textOpacity: false }, + theme: { + colors: { + blue: { + '500 / 50%': 'purple', + }, + }, + extend: { + textColor: ({ theme }) => ({ + foo: theme('colors.blue.500 / 50%'), + }), + }, + }, + }).then((result) => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +})