mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Add experimental labels for variants (#9456)
* add ability to add a `label` This could be used for named groups or named container queries in the future. * expose `container` to `matchVariant` Ideally we don't have to do this. But since we will be implementing `group` and `peer` using the `matchVariant` API, we do require it for the `visited` state. * implement `group` and `peer` using the `matchVariant` API * remove feature flag for `matchVariant` * update changelog
This commit is contained in:
parent
211c81eef0
commit
5788a9753a
@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Add `fill-none` and `stroke-none` utilities by default ([#9403](https://github.com/tailwindlabs/tailwindcss/pull/9403))
|
||||
- Support `sort` function in `matchVariant` ([#9423](https://github.com/tailwindlabs/tailwindcss/pull/9423))
|
||||
- Implement the `supports` variant ([#9453](https://github.com/tailwindlabs/tailwindcss/pull/9453))
|
||||
- Add experimental `label`s for variants ([#9456](https://github.com/tailwindlabs/tailwindcss/pull/9456))
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@ -75,7 +75,7 @@ export let variantPlugins = {
|
||||
})
|
||||
},
|
||||
|
||||
pseudoClassVariants: ({ addVariant, config }) => {
|
||||
pseudoClassVariants: ({ addVariant, matchVariant, config }) => {
|
||||
let pseudoVariants = [
|
||||
// Positional
|
||||
['first', '&:first-child'],
|
||||
@ -143,20 +143,33 @@ export let variantPlugins = {
|
||||
})
|
||||
}
|
||||
|
||||
for (let [variantName, state] of pseudoVariants) {
|
||||
addVariant(`group-${variantName}`, (ctx) => {
|
||||
let result = typeof state === 'function' ? state(ctx) : state
|
||||
|
||||
return result.replace(/&(\S+)/, ':merge(.group)$1 &')
|
||||
})
|
||||
let variants = {
|
||||
group: ({ label }) =>
|
||||
label ? [`:merge(.group\\<${label}\\>)`, ' &'] : [`:merge(.group)`, ' &'],
|
||||
peer: ({ label }) =>
|
||||
label ? [`:merge(.peer\\<${label}\\>)`, ' ~ &'] : [`:merge(.peer)`, ' ~ &'],
|
||||
}
|
||||
|
||||
for (let [variantName, state] of pseudoVariants) {
|
||||
addVariant(`peer-${variantName}`, (ctx) => {
|
||||
let result = typeof state === 'function' ? state(ctx) : state
|
||||
for (let [name, fn] of Object.entries(variants)) {
|
||||
matchVariant(
|
||||
name,
|
||||
(ctx = {}) => {
|
||||
let { label, value = '' } = ctx
|
||||
if (label) {
|
||||
log.warn(`labelled-${name}-experimental`, [
|
||||
`The labelled ${name} feature in Tailwind CSS is currently in preview.`,
|
||||
'Preview features are not covered by semver, and may be improved in breaking ways at any time.',
|
||||
])
|
||||
}
|
||||
|
||||
return result.replace(/&(\S+)/, ':merge(.peer)$1 ~ &')
|
||||
})
|
||||
let result = normalize(typeof value === 'function' ? value(ctx) : value)
|
||||
if (!result.includes('&')) result = '&' + result
|
||||
|
||||
let [a, b] = fn({ label })
|
||||
return result.replace(/&(\S+)?/g, (_, pseudo = '') => a + pseudo + b)
|
||||
},
|
||||
{ values: Object.fromEntries(pseudoVariants) }
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@ -216,9 +229,7 @@ export let variantPlugins = {
|
||||
}
|
||||
},
|
||||
|
||||
supportsVariants: ({ matchVariant, theme, config }) => {
|
||||
if (!flagEnabled(config(), 'matchVariant')) return
|
||||
|
||||
supportsVariants: ({ matchVariant, theme }) => {
|
||||
matchVariant(
|
||||
'supports',
|
||||
({ value = '' }) => {
|
||||
|
||||
@ -14,7 +14,6 @@ let featureFlags = {
|
||||
],
|
||||
experimental: [
|
||||
'optimizeUniversalDefaults',
|
||||
'matchVariant',
|
||||
// 'variantGrouping',
|
||||
],
|
||||
}
|
||||
|
||||
@ -128,12 +128,24 @@ function applyVariant(variant, matches, context) {
|
||||
return matches
|
||||
}
|
||||
|
||||
let args
|
||||
let args = {}
|
||||
|
||||
// Find partial arbitrary variants
|
||||
// Retrieve "label"
|
||||
{
|
||||
let match = /^[\w-]+\<(.*)\>-/g.exec(variant)
|
||||
if (match) {
|
||||
variant = variant.replace(`<${match[1]}>`, '')
|
||||
args.label = match[1]
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve "arbitrary value"
|
||||
if (variant.endsWith(']') && !variant.startsWith('[')) {
|
||||
args = variant.slice(variant.lastIndexOf('[') + 1, -1)
|
||||
variant = variant.slice(0, variant.indexOf(args) - 1 /* - */ - 1 /* [ */)
|
||||
let match = /-?\[(.*)\]/g.exec(variant)
|
||||
if (match) {
|
||||
variant = variant.replace(match[0], '')
|
||||
args.value = match[1]
|
||||
}
|
||||
}
|
||||
|
||||
// Register arbitrary variants
|
||||
@ -297,7 +309,7 @@ function applyVariant(variant, matches, context) {
|
||||
sort: context.offsets.applyVariantOffset(
|
||||
meta.sort,
|
||||
variantSort,
|
||||
Object.assign({ value: args }, context.variantOptions.get(variant))
|
||||
Object.assign(args, context.variantOptions.get(variant))
|
||||
),
|
||||
collectedFormats: (meta.collectedFormats ?? []).concat(collectedFormats),
|
||||
isArbitraryVariant: isArbitraryValue(variant),
|
||||
|
||||
@ -4,7 +4,6 @@ import postcss from 'postcss'
|
||||
import dlv from 'dlv'
|
||||
import selectorParser from 'postcss-selector-parser'
|
||||
|
||||
import { flagEnabled } from '../featureFlags.js'
|
||||
import transformThemeValue from '../util/transformThemeValue'
|
||||
import parseObjectStyles from '../util/parseObjectStyles'
|
||||
import prefixSelector from '../util/prefixSelector'
|
||||
@ -256,6 +255,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
||||
}
|
||||
)
|
||||
|
||||
let variantIdentifier = 0
|
||||
let api = {
|
||||
postcss,
|
||||
prefix: applyConfiguredPrefix,
|
||||
@ -518,23 +518,27 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
||||
variantMap.set(variantName, variantFunctions)
|
||||
context.variantOptions.set(variantName, options)
|
||||
},
|
||||
}
|
||||
|
||||
if (flagEnabled(tailwindConfig, 'matchVariant')) {
|
||||
let variantIdentifier = 0
|
||||
api.matchVariant = function (variant, variantFn, options) {
|
||||
matchVariant(variant, variantFn, options) {
|
||||
let id = ++variantIdentifier // A unique identifier that "groups" these variables together.
|
||||
|
||||
for (let [key, value] of Object.entries(options?.values ?? {})) {
|
||||
api.addVariant(`${variant}-${key}`, variantFn({ value }), { ...options, value, id })
|
||||
api.addVariant(
|
||||
`${variant}-${key}`,
|
||||
Object.assign(({ args, container }) => variantFn({ ...args, container, value }), {
|
||||
[MATCH_VARIANT]: true,
|
||||
}),
|
||||
{ ...options, value, id }
|
||||
)
|
||||
}
|
||||
|
||||
api.addVariant(
|
||||
variant,
|
||||
Object.assign(({ args }) => variantFn({ value: args }), { [MATCH_VARIANT]: true }),
|
||||
Object.assign(({ args, container }) => variantFn({ ...args, container }), {
|
||||
[MATCH_VARIANT]: true,
|
||||
}),
|
||||
{ ...options, id }
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return api
|
||||
|
||||
@ -618,7 +618,6 @@ test('classes in the same arbitrary variant should not be prefixed', () => {
|
||||
|
||||
it('should support supports', () => {
|
||||
let config = {
|
||||
experimental: { matchVariant: true },
|
||||
theme: {
|
||||
supports: {
|
||||
grid: 'display: grid',
|
||||
@ -707,3 +706,205 @@ it('should support supports', () => {
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
it('should be possible to use labels and arbitrary groups', () => {
|
||||
let config = {
|
||||
content: [
|
||||
{
|
||||
raw: html`
|
||||
<div>
|
||||
<div class="group">
|
||||
<!-- Default group usage -->
|
||||
<div class="group-hover:underline"></div>
|
||||
|
||||
<!-- Arbitrary variants with pseudo class for group -->
|
||||
<!-- With & -->
|
||||
<div class="group-[&:focus]:underline"></div>
|
||||
<!-- Without & -->
|
||||
<div class="group-[:hover]:underline"></div>
|
||||
|
||||
<!-- Arbitrary variants with attributes selectors for group -->
|
||||
<!-- With & -->
|
||||
<div class="group-[&[data-open]]:underline"></div>
|
||||
<!-- Without & -->
|
||||
<div class="group-[[data-open]]:underline"></div>
|
||||
|
||||
<!-- Arbitrary variants with other selectors -->
|
||||
<!-- With & -->
|
||||
<div class="group-[.in-foo_&]:underline"></div>
|
||||
<!-- Without & -->
|
||||
<div class="group-[.in-foo]:underline"></div>
|
||||
</div>
|
||||
|
||||
<!-- The same as above, but with labels -->
|
||||
<div class="group<foo>">
|
||||
<div class="group<foo>-hover:underline"></div>
|
||||
|
||||
<div class="group<foo>-[&:focus]:underline"></div>
|
||||
<div class="group<foo>-[:hover]:underline"></div>
|
||||
|
||||
<div class="group<foo>-[&[data-open]]:underline"></div>
|
||||
<div class="group<foo>-[[data-open]]:underline"></div>
|
||||
|
||||
<div class="group<foo>-[.in-foo_&]:underline"></div>
|
||||
<div class="group<foo>-[.in-foo]:underline"></div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
},
|
||||
],
|
||||
corePlugins: { preflight: false },
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
return run(input, config).then((result) => {
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.group:hover .group-hover\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.group\<foo\>:hover .group\<foo\>-hover\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.group:focus .group-\[\&\:focus\]\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.group:hover .group-\[\:hover\]\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.group[data-open] .group-\[\&\[data-open\]\]\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.group[data-open] .group-\[\[data-open\]\]\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.in-foo .group .group-\[\.in-foo_\&\]\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.group.in-foo .group-\[\.in-foo\]\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.group\<foo\>:focus .group\<foo\>-\[\&\:focus\]\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.group\<foo\>:hover .group\<foo\>-\[\:hover\]\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.group\<foo\>[data-open] .group\<foo\>-\[\&\[data-open\]\]\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.group\<foo\>[data-open] .group\<foo\>-\[\[data-open\]\]\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.in-foo .group\<foo\> .group\<foo\>-\[\.in-foo_\&\]\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.group\<foo\>.in-foo .group\<foo\>-\[\.in-foo\]\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
it('should be possible to use labels and arbitrary peers', () => {
|
||||
let config = {
|
||||
content: [
|
||||
{
|
||||
raw: html`
|
||||
<div>
|
||||
<div class="peer">
|
||||
<!-- Default peer usage -->
|
||||
<div class="peer-hover:underline"></div>
|
||||
|
||||
<!-- Arbitrary variants with pseudo class for peer -->
|
||||
<!-- With & -->
|
||||
<div class="peer-[&:focus]:underline"></div>
|
||||
<!-- Without & -->
|
||||
<div class="peer-[:hover]:underline"></div>
|
||||
|
||||
<!-- Arbitrary variants with attributes selectors for peer -->
|
||||
<!-- With & -->
|
||||
<div class="peer-[&[data-open]]:underline"></div>
|
||||
<!-- Without & -->
|
||||
<div class="peer-[[data-open]]:underline"></div>
|
||||
|
||||
<!-- Arbitrary variants with other selectors -->
|
||||
<!-- With & -->
|
||||
<div class="peer-[.in-foo_&]:underline"></div>
|
||||
<!-- Without & -->
|
||||
<div class="peer-[.in-foo]:underline"></div>
|
||||
</div>
|
||||
|
||||
<!-- The same as above, but with labels -->
|
||||
<div class="peer<foo>">
|
||||
<div class="peer<foo>-hover:underline"></div>
|
||||
|
||||
<div class="peer<foo>-[&:focus]:underline"></div>
|
||||
<div class="peer<foo>-[:hover]:underline"></div>
|
||||
|
||||
<div class="peer<foo>-[&[data-open]]:underline"></div>
|
||||
<div class="peer<foo>-[[data-open]]:underline"></div>
|
||||
|
||||
<div class="peer<foo>-[.in-foo_&]:underline"></div>
|
||||
<div class="peer<foo>-[.in-foo]:underline"></div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
},
|
||||
],
|
||||
corePlugins: { preflight: false },
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
return run(input, config).then((result) => {
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.peer:hover ~ .peer-hover\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.peer\<foo\>:hover ~ .peer\<foo\>-hover\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.peer:focus ~ .peer-\[\&\:focus\]\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.peer:hover ~ .peer-\[\:hover\]\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.peer[data-open] ~ .peer-\[\&\[data-open\]\]\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.peer[data-open] ~ .peer-\[\[data-open\]\]\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.in-foo .peer ~ .peer-\[\.in-foo_\&\]\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.peer.in-foo ~ .peer-\[\.in-foo\]\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.peer\<foo\>:focus ~ .peer\<foo\>-\[\&\:focus\]\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.peer\<foo\>:hover ~ .peer\<foo\>-\[\:hover\]\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.peer\<foo\>[data-open] ~ .peer\<foo\>-\[\&\[data-open\]\]\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.peer\<foo\>[data-open] ~ .peer\<foo\>-\[\[data-open\]\]\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.in-foo .peer\<foo\> ~ .peer\<foo\>-\[\.in-foo_\&\]\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.peer\<foo\>.in-foo ~ .peer\<foo\>-\[\.in-foo\]\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
@ -2,7 +2,6 @@ import { run, html, css } from './util/run'
|
||||
|
||||
test('partial arbitrary variants', () => {
|
||||
let config = {
|
||||
experimental: { matchVariant: true },
|
||||
content: [
|
||||
{
|
||||
raw: html`<div class="potato-[yellow]:bg-yellow-200 potato-[baked]:w-3"></div> `,
|
||||
@ -36,7 +35,6 @@ test('partial arbitrary variants', () => {
|
||||
|
||||
test('partial arbitrary variants with at-rules', () => {
|
||||
let config = {
|
||||
experimental: { matchVariant: true },
|
||||
content: [
|
||||
{
|
||||
raw: html`<div class="potato-[yellow]:bg-yellow-200 potato-[baked]:w-3"></div> `,
|
||||
@ -73,7 +71,6 @@ test('partial arbitrary variants with at-rules', () => {
|
||||
|
||||
test('partial arbitrary variants with at-rules and placeholder', () => {
|
||||
let config = {
|
||||
experimental: { matchVariant: true },
|
||||
content: [
|
||||
{
|
||||
raw: html`<div class="potato-[yellow]:bg-yellow-200 potato-[baked]:w-3"></div> `,
|
||||
@ -110,7 +107,6 @@ test('partial arbitrary variants with at-rules and placeholder', () => {
|
||||
|
||||
test('partial arbitrary variants with default values', () => {
|
||||
let config = {
|
||||
experimental: { matchVariant: true },
|
||||
content: [
|
||||
{
|
||||
raw: html`<div class="tooltip-bottom:mt-2 tooltip-top:mb-2"></div>`,
|
||||
@ -148,7 +144,6 @@ test('partial arbitrary variants with default values', () => {
|
||||
|
||||
test('matched variant values maintain the sort order they are registered in', () => {
|
||||
let config = {
|
||||
experimental: { matchVariant: true },
|
||||
content: [
|
||||
{
|
||||
raw: html`<div
|
||||
@ -198,7 +193,6 @@ test('matched variant values maintain the sort order they are registered in', ()
|
||||
|
||||
test('matchVariant can return an array of format strings from the function', () => {
|
||||
let config = {
|
||||
experimental: { matchVariant: true },
|
||||
content: [
|
||||
{
|
||||
raw: html`<div class="test-[a,b,c]:underline"></div>`,
|
||||
@ -237,7 +231,6 @@ test('matchVariant can return an array of format strings from the function', ()
|
||||
|
||||
it('should be possible to sort variants', () => {
|
||||
let config = {
|
||||
experimental: { matchVariant: true },
|
||||
content: [
|
||||
{
|
||||
raw: html`
|
||||
@ -282,7 +275,6 @@ it('should be possible to sort variants', () => {
|
||||
|
||||
it('should be possible to compare arbitrary variants and hardcoded variants', () => {
|
||||
let config = {
|
||||
experimental: { matchVariant: true },
|
||||
content: [
|
||||
{
|
||||
raw: html`
|
||||
@ -336,7 +328,6 @@ it('should be possible to compare arbitrary variants and hardcoded variants', ()
|
||||
|
||||
it('should be possible to sort stacked arbitrary variants correctly', () => {
|
||||
let config = {
|
||||
experimental: { matchVariant: true },
|
||||
content: [
|
||||
{
|
||||
raw: html`
|
||||
@ -408,7 +399,6 @@ it('should be possible to sort stacked arbitrary variants correctly', () => {
|
||||
|
||||
it('should maintain sort from other variants, if sort functions of arbitrary variants return 0', () => {
|
||||
let config = {
|
||||
experimental: { matchVariant: true },
|
||||
content: [
|
||||
{
|
||||
raw: html`
|
||||
@ -459,7 +449,6 @@ it('should maintain sort from other variants, if sort functions of arbitrary var
|
||||
|
||||
it('should sort arbitrary variants left to right (1)', () => {
|
||||
let config = {
|
||||
experimental: { matchVariant: true },
|
||||
content: [
|
||||
{
|
||||
raw: html`
|
||||
@ -528,7 +517,6 @@ it('should sort arbitrary variants left to right (1)', () => {
|
||||
|
||||
it('should sort arbitrary variants left to right (2)', () => {
|
||||
let config = {
|
||||
experimental: { matchVariant: true },
|
||||
content: [
|
||||
{
|
||||
raw: html`
|
||||
@ -595,7 +583,6 @@ it('should sort arbitrary variants left to right (2)', () => {
|
||||
|
||||
it('should guarantee that we are not passing values from other variants to the wrong function', () => {
|
||||
let config = {
|
||||
experimental: { matchVariant: true },
|
||||
content: [
|
||||
{
|
||||
raw: html`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user