Skip to content

Commit 50bed74

Browse files
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
1 parent 22f9dc8 commit 50bed74

8 files changed

+540
-33
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4949
- Add `matchVariant` API ([#8310](https://github.com/tailwindlabs/tailwindcss/pull/8310))
5050
- Add `prefers-contrast` media query variants ([#8410](https://github.com/tailwindlabs/tailwindcss/pull/8410))
5151
- Experimental support for variant grouping ([#8405](https://github.com/tailwindlabs/tailwindcss/pull/8405))
52+
- Add opacity support when referencing colors with `theme` function ([#8416](https://github.com/tailwindlabs/tailwindcss/pull/8416))
5253

5354
## [3.0.24] - 2022-04-12
5455

src/lib/evaluateTailwindFunctions.js

+18-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import parseValue from 'postcss-value-parser'
55
import { normalizeScreens } from '../util/normalizeScreens'
66
import buildMediaQuery from '../util/buildMediaQuery'
77
import { toPath } from '../util/toPath'
8+
import { withAlphaValue } from '../util/withAlphaVariable'
89

910
function isObject(input) {
1011
return typeof input === 'object' && input !== null
@@ -37,7 +38,7 @@ function listKeys(obj) {
3738
return list(Object.keys(obj))
3839
}
3940

40-
function validatePath(config, path, defaultValue) {
41+
function validatePath(config, path, defaultValue, themeOpts = {}) {
4142
const pathString = Array.isArray(path)
4243
? pathToString(path)
4344
: path.replace(/^['"]+/g, '').replace(/['"]+$/g, '')
@@ -114,7 +115,7 @@ function validatePath(config, path, defaultValue) {
114115

115116
return {
116117
isValid: true,
117-
value: transformThemeValue(themeSection)(value),
118+
value: transformThemeValue(themeSection)(value, themeOpts),
118119
}
119120
}
120121

@@ -160,16 +161,29 @@ let nodeTypePropertyMap = {
160161
export default function ({ tailwindConfig: config }) {
161162
let functions = {
162163
theme: (node, path, ...defaultValue) => {
163-
const { isValid, value, error } = validatePath(
164+
let matches = path.match(/^([^\/\s]+)(?:\s*\/\s*([^\/\s]+))$/)
165+
let alpha = undefined
166+
167+
if (matches) {
168+
path = matches[1]
169+
alpha = matches[2]
170+
}
171+
172+
let { isValid, value, error } = validatePath(
164173
config,
165174
path,
166-
defaultValue.length ? defaultValue : undefined
175+
defaultValue.length ? defaultValue : undefined,
176+
{ opacityValue: alpha }
167177
)
168178

169179
if (!isValid) {
170180
throw node.error(error)
171181
}
172182

183+
if (alpha !== undefined) {
184+
value = withAlphaValue(value, alpha, value)
185+
}
186+
173187
return value
174188
},
175189
screen: (node, screen) => {

src/lib/setupContextUtils.js

+14-5
Original file line numberDiff line numberDiff line change
@@ -221,16 +221,25 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
221221
return context.tailwindConfig.prefix + identifier
222222
}
223223

224+
function resolveThemeValue(path, defaultValue, opts = {}) {
225+
const [pathRoot, ...subPaths] = toPath(path)
226+
const value = getConfigValue(['theme', pathRoot, ...subPaths], defaultValue)
227+
return transformThemeValue(pathRoot)(value, opts)
228+
}
229+
230+
const theme = Object.assign(
231+
(path, defaultValue = undefined) => resolveThemeValue(path, defaultValue),
232+
{
233+
withAlpha: (path, opacityValue) => resolveThemeValue(path, undefined, { opacityValue }),
234+
}
235+
)
236+
224237
let api = {
225238
postcss,
226239
prefix: applyConfiguredPrefix,
227240
e: escapeClassName,
228241
config: getConfigValue,
229-
theme(path, defaultValue) {
230-
const [pathRoot, ...subPaths] = toPath(path)
231-
const value = getConfigValue(['theme', pathRoot, ...subPaths], defaultValue)
232-
return transformThemeValue(pathRoot)(value)
233-
},
242+
theme,
234243
corePlugins: (path) => {
235244
if (Array.isArray(tailwindConfig.corePlugins)) {
236245
return tailwindConfig.corePlugins.includes(path)

src/util/resolveConfig.js

+63-21
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { toPath } from './toPath'
88
import { normalizeConfig } from './normalizeConfig'
99
import isPlainObject from './isPlainObject'
1010
import { cloneDeep } from './cloneDeep'
11+
import { withAlphaValue } from './withAlphaVariable'
1112

1213
function isFunction(input) {
1314
return typeof input === 'function'
@@ -164,40 +165,81 @@ function mergeExtensions({ extend, ...theme }) {
164165
})
165166
}
166167

168+
/**
169+
*
170+
* @param {string} key
171+
* @return {Iterable<string[] & {alpha: string | undefined}>}
172+
*/
173+
function* toPaths(key) {
174+
let path = toPath(key)
175+
176+
if (path.length === 0) {
177+
return
178+
}
179+
180+
yield path
181+
182+
if (Array.isArray(key)) {
183+
return
184+
}
185+
186+
let pattern = /^(.*?)\s*\/\s*([^/]+)$/
187+
let matches = key.match(pattern)
188+
189+
if (matches !== null) {
190+
let [, prefix, alpha] = matches
191+
192+
let newPath = toPath(prefix)
193+
newPath.alpha = alpha
194+
195+
yield newPath
196+
}
197+
}
198+
167199
function resolveFunctionKeys(object) {
200+
// theme('colors.red.500 / 0.5') -> ['colors', 'red', '500 / 0', '5]
201+
168202
const resolvePath = (key, defaultValue) => {
169-
const path = toPath(key)
203+
for (const path of toPaths(key)) {
204+
let index = 0
205+
let val = object
170206

171-
let index = 0
172-
let val = object
207+
while (val !== undefined && val !== null && index < path.length) {
208+
val = val[path[index++]]
173209

174-
while (val !== undefined && val !== null && index < path.length) {
175-
val = val[path[index++]]
176-
val = isFunction(val) ? val(resolvePath, configUtils) : val
177-
}
210+
let shouldResolveAsFn =
211+
isFunction(val) && (path.alpha === undefined || index < path.length - 1)
178212

179-
if (val === undefined) {
180-
return defaultValue
181-
}
213+
val = shouldResolveAsFn ? val(resolvePath, configUtils) : val
214+
}
215+
216+
if (val !== undefined) {
217+
if (path.alpha !== undefined) {
218+
return withAlphaValue(val, path.alpha)
219+
}
220+
221+
if (isPlainObject(val)) {
222+
return cloneDeep(val)
223+
}
182224

183-
if (isPlainObject(val)) {
184-
return cloneDeep(val)
225+
return val
226+
}
185227
}
186228

187-
return val
229+
return defaultValue
188230
}
189231

190-
resolvePath.theme = resolvePath
232+
// colors.red.500/50
191233

192-
for (let key in configUtils) {
193-
resolvePath[key] = configUtils[key]
194-
}
234+
Object.assign(resolvePath, {
235+
theme: resolvePath,
236+
...configUtils,
237+
})
195238

196239
return Object.keys(object).reduce((resolved, key) => {
197-
return {
198-
...resolved,
199-
[key]: isFunction(object[key]) ? object[key](resolvePath, configUtils) : object[key],
200-
}
240+
resolved[key] = isFunction(object[key]) ? object[key](resolvePath, configUtils) : object[key]
241+
242+
return resolved
201243
}, {})
202244
}
203245

src/util/toPath.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Square bracket notation `a[b]` may be used to "escape" dots that would otherwise be interpreted as path separators.
55
*
66
* Example:
7-
* a -> ['a]
7+
* a -> ['a']
88
* a.b.c -> ['a', 'b', 'c']
99
* a[b].c -> ['a', 'b', 'c']
1010
* a[b.c].e.f -> ['a', 'b.c', 'e', 'f']

src/util/transformThemeValue.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,10 @@ export default function transformThemeValue(themeSection) {
4444
}
4545
}
4646

47-
return (value) => {
48-
if (typeof value === 'function') value = value({})
47+
return (value, opts = {}) => {
48+
if (typeof value === 'function') {
49+
value = value(opts)
50+
}
4951

5052
return value
5153
}

0 commit comments

Comments
 (0)