From 5788a9753a6437f00697f62ecb64b36fc3ea3aa5 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 6 Oct 2022 19:53:51 +0200 Subject: [PATCH] Add experimental `label`s 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 --- CHANGELOG.md | 1 + src/corePlugins.js | 41 ++++--- src/featureFlags.js | 1 - src/lib/generateRules.js | 22 +++- src/lib/setupContextUtils.js | 22 ++-- tests/arbitrary-variants.test.js | 203 ++++++++++++++++++++++++++++++- tests/match-variants.test.js | 13 -- 7 files changed, 259 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75ce2d203..f044c8035 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/corePlugins.js b/src/corePlugins.js index 352737c70..e6caa7ca3 100644 --- a/src/corePlugins.js +++ b/src/corePlugins.js @@ -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 = '' }) => { diff --git a/src/featureFlags.js b/src/featureFlags.js index a03916c5b..2bfee94d9 100644 --- a/src/featureFlags.js +++ b/src/featureFlags.js @@ -14,7 +14,6 @@ let featureFlags = { ], experimental: [ 'optimizeUniversalDefaults', - 'matchVariant', // 'variantGrouping', ], } diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index a78557f20..d0955de61 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -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), diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index 874b7f705..fb51e06eb 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -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 diff --git a/tests/arbitrary-variants.test.js b/tests/arbitrary-variants.test.js index a947dbe43..a55fe008b 100644 --- a/tests/arbitrary-variants.test.js +++ b/tests/arbitrary-variants.test.js @@ -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` +
+
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+
+
+ `, + }, + ], + 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\:hover .group\-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\: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; + } + `) + }) +}) + +it('should be possible to use labels and arbitrary peers', () => { + let config = { + content: [ + { + raw: html` +
+
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+
+
+ `, + }, + ], + 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\:hover ~ .peer\-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\: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; + } + `) + }) +}) diff --git a/tests/match-variants.test.js b/tests/match-variants.test.js index fce47734e..e881613c5 100644 --- a/tests/match-variants.test.js +++ b/tests/match-variants.test.js @@ -2,7 +2,6 @@ import { run, html, css } from './util/run' test('partial arbitrary variants', () => { let config = { - experimental: { matchVariant: true }, content: [ { raw: html`
`, @@ -36,7 +35,6 @@ test('partial arbitrary variants', () => { test('partial arbitrary variants with at-rules', () => { let config = { - experimental: { matchVariant: true }, content: [ { raw: html`
`, @@ -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`
`, @@ -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`
`, @@ -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`
{ let config = { - experimental: { matchVariant: true }, content: [ { raw: html`
`, @@ -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`