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
This commit is contained in:
Jordan Pittman 2022-05-25 13:54:30 -04:00 • committed by GitHub
parent 22f9dc8895
commit 50bed74cdc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 542 additions and 35 deletions

View file

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

View file

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

View file

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

View file

@ -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<string[] & {alpha: string | undefined}>}
*/
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)
val = shouldResolveAsFn ? val(resolvePath, configUtils) : val
}
if (val !== undefined) {
if (path.alpha !== undefined) {
return withAlphaValue(val, path.alpha)
}
if (isPlainObject(val)) {
return cloneDeep(val)
}
return val
}
}
if (val === undefined) {
return defaultValue
}
if (isPlainObject(val)) {
return cloneDeep(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
}, {})
}

View file

@ -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']

View file

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

View file

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

View file

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