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:
Robin Malfait 2022-10-06 19:53:51 +02:00 committed by GitHub
parent 211c81eef0
commit 5788a9753a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 259 additions and 44 deletions

View File

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

View File

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

View File

@ -14,7 +14,6 @@ let featureFlags = {
],
experimental: [
'optimizeUniversalDefaults',
'matchVariant',
// 'variantGrouping',
],
}

View File

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

View File

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

View File

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

View File

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