mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Expose context.getVariants for intellisense (#9505)
* add `context.getVariants` * use `modifier` instead of `label` * handle `modifySelectors` version * use reference * reverse engineer manual format strings if container was touched * use new positional API for `matchVariant` * update changelog
This commit is contained in:
parent
b7d5a2f247
commit
bc004455bc
@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Add aria variants ([#9557](https://github.com/tailwindlabs/tailwindcss/pull/9557))
|
||||
- Add `data-*` variants ([#9559](https://github.com/tailwindlabs/tailwindcss/pull/9559))
|
||||
- Upgrade to `postcss-nested` v6.0 ([#9546](https://github.com/tailwindlabs/tailwindcss/pull/9546))
|
||||
- Expose `context.getVariants` for intellisense ([#9505](https://github.com/tailwindlabs/tailwindcss/pull/9505))
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@ -375,7 +375,7 @@ export let variantPlugins = {
|
||||
check = `(${check})`
|
||||
}
|
||||
|
||||
return `@supports ${check} `
|
||||
return `@supports ${check}`
|
||||
},
|
||||
{ values: theme('supports') ?? {} }
|
||||
)
|
||||
|
||||
@ -18,7 +18,7 @@ let classNameParser = selectorParser((selectors) => {
|
||||
return selectors.first.filter(({ type }) => type === 'class').pop().value
|
||||
})
|
||||
|
||||
function getClassNameFromSelector(selector) {
|
||||
export function getClassNameFromSelector(selector) {
|
||||
return classNameParser.transformSync(selector)
|
||||
}
|
||||
|
||||
|
||||
@ -18,12 +18,21 @@ import { toPath } from '../util/toPath'
|
||||
import log from '../util/log'
|
||||
import negateValue from '../util/negateValue'
|
||||
import isValidArbitraryValue from '../util/isValidArbitraryValue'
|
||||
import { generateRules } from './generateRules'
|
||||
import { generateRules, getClassNameFromSelector } from './generateRules'
|
||||
import { hasContentChanged } from './cacheInvalidation.js'
|
||||
import { Offsets } from './offsets.js'
|
||||
import { flagEnabled } from '../featureFlags.js'
|
||||
import { finalizeSelector, formatVariantSelector } from '../util/formatVariantSelector'
|
||||
|
||||
let MATCH_VARIANT = Symbol()
|
||||
const VARIANT_TYPES = {
|
||||
AddVariant: Symbol.for('ADD_VARIANT'),
|
||||
MatchVariant: Symbol.for('MATCH_VARIANT'),
|
||||
}
|
||||
|
||||
const VARIANT_INFO = {
|
||||
Base: 1 << 0,
|
||||
Dynamic: 1 << 1,
|
||||
}
|
||||
|
||||
function prefix(context, selector) {
|
||||
let prefix = context.tailwindConfig.prefix
|
||||
@ -524,7 +533,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
||||
let result = variantFunction(
|
||||
Object.assign(
|
||||
{ modifySelectors, container, separator },
|
||||
variantFunction[MATCH_VARIANT] && { args, wrap, format }
|
||||
options.type === VARIANT_TYPES.MatchVariant && { args, wrap, format }
|
||||
)
|
||||
)
|
||||
|
||||
@ -570,33 +579,34 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
||||
for (let [key, value] of Object.entries(options?.values ?? {})) {
|
||||
api.addVariant(
|
||||
isSpecial ? `${variant}${key}` : `${variant}-${key}`,
|
||||
Object.assign(
|
||||
({ args, container }) =>
|
||||
variantFn(
|
||||
value,
|
||||
modifiersEnabled ? { modifier: args.modifier, container } : { container }
|
||||
),
|
||||
{
|
||||
[MATCH_VARIANT]: true,
|
||||
}
|
||||
),
|
||||
{ ...options, value, id }
|
||||
({ args, container }) =>
|
||||
variantFn(
|
||||
value,
|
||||
modifiersEnabled ? { modifier: args.modifier, container } : { container }
|
||||
),
|
||||
{
|
||||
...options,
|
||||
value,
|
||||
id,
|
||||
type: VARIANT_TYPES.MatchVariant,
|
||||
variantInfo: VARIANT_INFO.Base,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
api.addVariant(
|
||||
variant,
|
||||
Object.assign(
|
||||
({ args, container }) =>
|
||||
variantFn(
|
||||
args.value,
|
||||
modifiersEnabled ? { modifier: args.modifier, container } : { container }
|
||||
),
|
||||
{
|
||||
[MATCH_VARIANT]: true,
|
||||
}
|
||||
),
|
||||
{ ...options, id }
|
||||
({ args, container }) =>
|
||||
variantFn(
|
||||
args.value,
|
||||
modifiersEnabled ? { modifier: args.modifier, container } : { container }
|
||||
),
|
||||
{
|
||||
...options,
|
||||
id,
|
||||
type: VARIANT_TYPES.MatchVariant,
|
||||
variantInfo: VARIANT_INFO.Dynamic,
|
||||
}
|
||||
)
|
||||
},
|
||||
}
|
||||
@ -948,6 +958,137 @@ function registerPlugins(plugins, context) {
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
// Generate a list of available variants with meta information of the type of variant.
|
||||
context.getVariants = function getVariants() {
|
||||
let result = []
|
||||
for (let [name, options] of context.variantOptions.entries()) {
|
||||
if (options.variantInfo === VARIANT_INFO.Base) continue
|
||||
|
||||
result.push({
|
||||
name,
|
||||
isArbitrary: options.type === Symbol.for('MATCH_VARIANT'),
|
||||
values: Object.keys(options.values ?? {}),
|
||||
selectors({ modifier, value } = {}) {
|
||||
let candidate = '__TAILWIND_PLACEHOLDER__'
|
||||
|
||||
let rule = postcss.rule({ selector: `.${candidate}` })
|
||||
let container = postcss.root({ nodes: [rule.clone()] })
|
||||
|
||||
let before = container.toString()
|
||||
|
||||
let fns = (context.variantMap.get(name) ?? []).flatMap(([_, fn]) => fn)
|
||||
let formatStrings = []
|
||||
for (let fn of fns) {
|
||||
let localFormatStrings = []
|
||||
|
||||
let api = {
|
||||
args: { modifier, value: options.values?.[value] ?? value },
|
||||
separator: context.tailwindConfig.separator,
|
||||
modifySelectors(modifierFunction) {
|
||||
// Run the modifierFunction over each rule
|
||||
container.each((rule) => {
|
||||
if (rule.type !== 'rule') {
|
||||
return
|
||||
}
|
||||
|
||||
rule.selectors = rule.selectors.map((selector) => {
|
||||
return modifierFunction({
|
||||
get className() {
|
||||
return getClassNameFromSelector(selector)
|
||||
},
|
||||
selector,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return container
|
||||
},
|
||||
format(str) {
|
||||
localFormatStrings.push(str)
|
||||
},
|
||||
wrap(wrapper) {
|
||||
localFormatStrings.push(`@${wrapper.name} ${wrapper.params} { & }`)
|
||||
},
|
||||
container,
|
||||
}
|
||||
|
||||
let ruleWithVariant = fn(api)
|
||||
if (localFormatStrings.length > 0) {
|
||||
formatStrings.push(localFormatStrings)
|
||||
}
|
||||
|
||||
if (Array.isArray(ruleWithVariant)) {
|
||||
for (let variantFunction of ruleWithVariant) {
|
||||
localFormatStrings = []
|
||||
variantFunction(api)
|
||||
formatStrings.push(localFormatStrings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse engineer the result of the `container`
|
||||
let manualFormatStrings = []
|
||||
let after = container.toString()
|
||||
|
||||
if (before !== after) {
|
||||
// Figure out all selectors
|
||||
container.walkRules((rule) => {
|
||||
let modified = rule.selector
|
||||
|
||||
// Rebuild the base selector, this is what plugin authors would do
|
||||
// as well. E.g.: `${variant}${separator}${className}`.
|
||||
// However, plugin authors probably also prepend or append certain
|
||||
// classes, pseudos, ids, ...
|
||||
let rebuiltBase = selectorParser((selectors) => {
|
||||
selectors.walkClasses((classNode) => {
|
||||
classNode.value = `${name}${context.tailwindConfig.separator}${classNode.value}`
|
||||
})
|
||||
}).processSync(modified)
|
||||
|
||||
// Now that we know the original selector, the new selector, and
|
||||
// the rebuild part in between, we can replace the part that plugin
|
||||
// authors need to rebuild with `&`, and eventually store it in the
|
||||
// collectedFormats. Similar to what `format('...')` would do.
|
||||
//
|
||||
// E.g.:
|
||||
// variant: foo
|
||||
// selector: .markdown > p
|
||||
// modified (by plugin): .foo .foo\\:markdown > p
|
||||
// rebuiltBase (internal): .foo\\:markdown > p
|
||||
// format: .foo &
|
||||
manualFormatStrings.push(modified.replace(rebuiltBase, '&').replace(candidate, '&'))
|
||||
})
|
||||
|
||||
// Figure out all atrules
|
||||
container.walkAtRules((atrule) => {
|
||||
manualFormatStrings.push(`@${atrule.name} (${atrule.params}) { & }`)
|
||||
})
|
||||
}
|
||||
|
||||
let result = formatStrings.map((formatString) =>
|
||||
finalizeSelector(formatVariantSelector('&', ...formatString), {
|
||||
selector: `.${candidate}`,
|
||||
candidate,
|
||||
context,
|
||||
isArbitraryVariant: !(value in (options.values ?? {})),
|
||||
})
|
||||
.replace(`.${candidate}`, '&')
|
||||
.replace('{ & }', '')
|
||||
.trim()
|
||||
)
|
||||
|
||||
if (manualFormatStrings.length > 0) {
|
||||
result.push(formatVariantSelector('&', ...manualFormatStrings))
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
140
tests/getVariants.test.js
Normal file
140
tests/getVariants.test.js
Normal file
@ -0,0 +1,140 @@
|
||||
import postcss from 'postcss'
|
||||
import selectorParser from 'postcss-selector-parser'
|
||||
import resolveConfig from '../src/public/resolve-config'
|
||||
import { createContext } from '../src/lib/setupContextUtils'
|
||||
|
||||
it('should return a list of variants with meta information about the variant', () => {
|
||||
let config = {}
|
||||
let context = createContext(resolveConfig(config))
|
||||
|
||||
let variants = context.getVariants()
|
||||
|
||||
expect(variants).toContainEqual({
|
||||
name: 'hover',
|
||||
isArbitrary: false,
|
||||
values: [],
|
||||
selectors: expect.any(Function),
|
||||
})
|
||||
|
||||
expect(variants).toContainEqual({
|
||||
name: 'group',
|
||||
isArbitrary: true,
|
||||
values: expect.any(Array),
|
||||
selectors: expect.any(Function),
|
||||
})
|
||||
|
||||
// `group-hover` now belongs to the `group` variant. The information exposed for the `group`
|
||||
// variant is all you need.
|
||||
expect(variants.find((v) => v.name === 'group-hover')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should provide selectors for simple variants', () => {
|
||||
let config = {}
|
||||
let context = createContext(resolveConfig(config))
|
||||
|
||||
let variants = context.getVariants()
|
||||
|
||||
let variant = variants.find((v) => v.name === 'hover')
|
||||
expect(variant.selectors()).toEqual(['&:hover'])
|
||||
})
|
||||
|
||||
it('should provide selectors for parallel variants', () => {
|
||||
let config = {}
|
||||
let context = createContext(resolveConfig(config))
|
||||
|
||||
let variants = context.getVariants()
|
||||
|
||||
let variant = variants.find((v) => v.name === 'marker')
|
||||
expect(variant.selectors()).toEqual(['& *::marker', '&::marker'])
|
||||
})
|
||||
|
||||
it('should provide selectors for complex matchVariant variants like `group`', () => {
|
||||
let config = {}
|
||||
let context = createContext(resolveConfig(config))
|
||||
|
||||
let variants = context.getVariants()
|
||||
|
||||
let variant = variants.find((v) => v.name === 'group')
|
||||
expect(variant.selectors()).toEqual(['.group &'])
|
||||
expect(variant.selectors({})).toEqual(['.group &'])
|
||||
expect(variant.selectors({ value: 'hover' })).toEqual(['.group:hover &'])
|
||||
expect(variant.selectors({ value: '.foo_&' })).toEqual(['.foo .group &'])
|
||||
expect(variant.selectors({ modifier: 'foo', value: 'hover' })).toEqual(['.group\\/foo:hover &'])
|
||||
expect(variant.selectors({ modifier: 'foo', value: '.foo_&' })).toEqual(['.foo .group\\/foo &'])
|
||||
})
|
||||
|
||||
it('should provide selectors for variants with atrules', () => {
|
||||
let config = {}
|
||||
let context = createContext(resolveConfig(config))
|
||||
|
||||
let variants = context.getVariants()
|
||||
|
||||
let variant = variants.find((v) => v.name === 'supports')
|
||||
expect(variant.selectors({ value: 'display:grid' })).toEqual(['@supports (display:grid)'])
|
||||
expect(variant.selectors({ value: 'aspect-ratio' })).toEqual([
|
||||
'@supports (aspect-ratio: var(--tw))',
|
||||
])
|
||||
})
|
||||
|
||||
it('should provide selectors for custom plugins that do a combination of parallel variants with modifiers with arbitrary values and with atrules', () => {
|
||||
let config = {
|
||||
plugins: [
|
||||
function ({ matchVariant }) {
|
||||
matchVariant('foo', (value, { modifier }) => {
|
||||
return [
|
||||
`
|
||||
@supports (foo: ${modifier}) {
|
||||
@media (width <= 400px) {
|
||||
&:hover
|
||||
}
|
||||
}
|
||||
`,
|
||||
`.${modifier}\\/${value} &:focus`,
|
||||
]
|
||||
})
|
||||
},
|
||||
],
|
||||
}
|
||||
let context = createContext(resolveConfig(config))
|
||||
|
||||
let variants = context.getVariants()
|
||||
|
||||
let variant = variants.find((v) => v.name === 'foo')
|
||||
expect(variant.selectors({ modifier: 'bar', value: 'baz' })).toEqual([
|
||||
'@supports (foo: bar) { @media (width <= 400px) { &:hover } }',
|
||||
'.bar\\/baz &:focus',
|
||||
])
|
||||
})
|
||||
|
||||
it('should work for plugins that still use the modifySelectors API', () => {
|
||||
let config = {
|
||||
plugins: [
|
||||
function ({ addVariant }) {
|
||||
addVariant('foo', ({ modifySelectors, container }) => {
|
||||
// Manually mutating the selector
|
||||
modifySelectors(({ selector }) => {
|
||||
return selectorParser((selectors) => {
|
||||
selectors.walkClasses((classNode) => {
|
||||
classNode.value = `foo:${classNode.value}`
|
||||
classNode.parent.insertBefore(classNode, selectorParser().astSync(`.foo `))
|
||||
})
|
||||
}).processSync(selector)
|
||||
})
|
||||
|
||||
// Manually wrap in supports query
|
||||
let wrapper = postcss.atRule({ name: 'supports', params: 'display: grid' })
|
||||
let nodes = container.nodes
|
||||
container.removeAll()
|
||||
wrapper.append(nodes)
|
||||
container.append(wrapper)
|
||||
})
|
||||
},
|
||||
],
|
||||
}
|
||||
let context = createContext(resolveConfig(config))
|
||||
|
||||
let variants = context.getVariants()
|
||||
|
||||
let variant = variants.find((v) => v.name === 'foo')
|
||||
expect(variant.selectors({})).toEqual(['@supports (display: grid) { .foo .foo\\:& }'])
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user