From f9343cdfdbaad492c1123f365602baa99c09ea91 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 16 Oct 2021 23:04:10 +0200 Subject: [PATCH 01/15] fix incorrect comment Probably messed this up in another PR, so just a bit of cleaning. --- src/lib/expandTailwindAtRules.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/expandTailwindAtRules.js b/src/lib/expandTailwindAtRules.js index 081086c35bf4..d25a39f38cd2 100644 --- a/src/lib/expandTailwindAtRules.js +++ b/src/lib/expandTailwindAtRules.js @@ -14,7 +14,7 @@ const PATTERNS = [ /([^<>"'`\s]*\['[^"'`\s]*'\])/.source, // `content-['hello']` but not `content-['hello']']` /([^<>"'`\s]*\["[^"'`\s]*"\])/.source, // `content-["hello"]` but not `content-["hello"]"]` /([^<>"'`\s]*\[[^"'`\s]+\][^<>"'`\s]*)/.source, // `fill-[#bada55]`, `fill-[#bada55]/50` - /([^<>"'`\s]*[^"'`\s:])/.source, // `px-1.5`, `uppercase` but not `uppercase:`].join('|') + /([^<>"'`\s]*[^"'`\s:])/.source, // `px-1.5`, `uppercase` but not `uppercase:` ].join('|') const BROAD_MATCH_GLOBAL_REGEXP = new RegExp(PATTERNS, 'g') const INNER_MATCH_GLOBAL_REGEXP = /[^<>"'`\s.(){}[\]#=%]*[^<>"'`\s.(){}[\]#=%:]/g From 694effdb298efe52d35ecc6f3b5fc7e1095f6d86 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 16 Oct 2021 23:06:31 +0200 Subject: [PATCH 02/15] implement a formatVariantSelector function This will be used to eventually simplify the addVariant API. The idea is that it can take a list of strings that define a certain format. Then it squashes everything to a single format how you would expect it. E.g.: Input: - '&:hover' - '&:focus' - '.dark &' - ':merge(.group):hover &' - ':merge(.group):focus &' Output: - ':merge(.group):focus:hover .dark &:focus:hover' The API here is: - `&`, this means "The parent" or "The previous selector" (you can think of it like if you are using nested selectors) - `:merge(.group)`, this means insert a `.group` if it doesn't exist yet, but if it does exist already, then merge the new value with the old value. This allows us to merge group-focus, group-hover into a single `.group:focus:hover ...` --- src/util/formatVariantSelector.js | 105 +++++++++++ tests/format-variant-selector.test.js | 261 ++++++++++++++++++++++++++ 2 files changed, 366 insertions(+) create mode 100644 src/util/formatVariantSelector.js create mode 100644 tests/format-variant-selector.test.js diff --git a/src/util/formatVariantSelector.js b/src/util/formatVariantSelector.js new file mode 100644 index 000000000000..d24471cc2eb8 --- /dev/null +++ b/src/util/formatVariantSelector.js @@ -0,0 +1,105 @@ +import selectorParser from 'postcss-selector-parser' +import unescape from 'postcss-selector-parser/dist/util/unesc' +import escapeClassName from '../util/escapeClassName' +import prefixSelector from '../util/prefixSelector' + +let MERGE = ':merge' +let PARENT = '&' + +export let selectorFunctions = new Set([MERGE]) + +export function formatVariantSelector(current, ...others) { + for (let other of others) { + let incomingValue = resolveFunctionArgument(other, MERGE) + if (incomingValue !== null) { + let existingValue = resolveFunctionArgument(current, MERGE, incomingValue) + if (existingValue !== null) { + let existingTarget = `${MERGE}(${incomingValue})` + let splitIdx = other.indexOf(existingTarget) + let addition = other.slice(splitIdx + existingTarget.length).split(' ')[0] + + current = current.replace(existingTarget, existingTarget + addition) + continue + } + } + + current = other.replace(PARENT, current) + } + + return current +} + +export function finalizeSelector(format, { selector, candidate, context }) { + let base = candidate.split(context?.tailwindConfig?.separator ?? ':').pop() + + if (context?.tailwindConfig?.prefix) { + format = prefixSelector(context.tailwindConfig.prefix, format) + } + + format = format.replace(PARENT, `.${escapeClassName(candidate)}`) + + // Normalize escaped classes, e.g.: + // + // The idea would be to replace the escaped `base` in the selector with the + // `format`. However, in css you can escape the same selector in a few + // different ways. This would result in different strings and therefore we + // can't replace it properly. + // + // base: bg-[rgb(255,0,0)] + // base in selector: bg-\\[rgb\\(255\\,0\\,0\\)\\] + // escaped base: bg-\\[rgb\\(255\\2c 0\\2c 0\\)\\] + // + selector = selectorParser((selectors) => { + return selectors.walkClasses((node) => { + if (node.raws && node.value.includes(base)) { + node.raws.value = escapeClassName(unescape(node.raws.value)) + } + + return node + }) + }).processSync(selector) + + // We can safely replace the escaped base now, since the `base` section is + // now in a normalized escaped value. + selector = selector.replace(`.${escapeClassName(base)}`, format) + + // Remove unnecessary pseudo selectors that we used as placeholders + return selectorParser((selectors) => { + return selectors.map((selector) => { + selector.walkPseudos((p) => { + if (selectorFunctions.has(p.value)) { + p.replaceWith(p.nodes) + } + + return p + }) + + return selector + }) + }).processSync(selector) +} + +function resolveFunctionArgument(haystack, needle, arg) { + let startIdx = haystack.indexOf(arg ? `${needle}(${arg})` : needle) + if (startIdx === -1) return null + + // Start inside the `(` + startIdx += needle.length + 1 + + let target = '' + let count = 0 + + for (let char of haystack.slice(startIdx)) { + if (char !== '(' && char !== ')') { + target += char + } else if (char === '(') { + target += char + count++ + } else if (char === ')') { + if (--count < 0) break // unbalanced + target += char + } + } + + return target +} diff --git a/tests/format-variant-selector.test.js b/tests/format-variant-selector.test.js new file mode 100644 index 000000000000..a32ae502501e --- /dev/null +++ b/tests/format-variant-selector.test.js @@ -0,0 +1,261 @@ +import { formatVariantSelector, finalizeSelector } from '../src/util/formatVariantSelector' + +it('should be possible to add a simple variant to a simple selector', () => { + let selector = '.text-center' + let candidate = 'hover:text-center' + + let variants = ['&:hover'] + + expect(finalizeSelector(formatVariantSelector(...variants), { selector, candidate })).toEqual( + '.hover\\:text-center:hover' + ) +}) + +it('should be possible to add a multiple simple variants to a simple selector', () => { + let selector = '.text-center' + let candidate = 'focus:hover:text-center' + + let variants = ['&:hover', '&:focus'] + + expect(finalizeSelector(formatVariantSelector(...variants), { selector, candidate })).toEqual( + '.focus\\:hover\\:text-center:hover:focus' + ) +}) + +it('should be possible to add a simple variant to a selector containing escaped parts', () => { + let selector = '.bg-\\[rgba\\(0\\,0\\,0\\)\\]' + let candidate = 'hover:bg-[rgba(0,0,0)]' + + let variants = ['&:hover'] + + expect(finalizeSelector(formatVariantSelector(...variants), { selector, candidate })).toEqual( + '.hover\\:bg-\\[rgba\\(0\\2c 0\\2c 0\\)\\]:hover' + ) +}) + +it('should be possible to add a simple variant to a selector containing escaped parts (escape is slightly different)', () => { + let selector = '.bg-\\[rgba\\(0\\2c 0\\2c 0\\)\\]' + let candidate = 'hover:bg-[rgba(0,0,0)]' + + let variants = ['&:hover'] + + expect(finalizeSelector(formatVariantSelector(...variants), { selector, candidate })).toEqual( + '.hover\\:bg-\\[rgba\\(0\\2c 0\\2c 0\\)\\]:hover' + ) +}) + +it('should be possible to add a simple variant to a more complex selector', () => { + let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])' + let candidate = 'hover:space-x-4' + + let variants = ['&:hover'] + + expect(finalizeSelector(formatVariantSelector(...variants), { selector, candidate })).toEqual( + '.hover\\:space-x-4:hover > :not([hidden]) ~ :not([hidden])' + ) +}) + +it('should be possible to add multiple simple variants to a more complex selector', () => { + let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])' + let candidate = 'disabled:focus:hover:space-x-4' + + let variants = ['&:hover', '&:focus', '&:disabled'] + + expect(finalizeSelector(formatVariantSelector(...variants), { selector, candidate })).toEqual( + '.disabled\\:focus\\:hover\\:space-x-4:hover:focus:disabled > :not([hidden]) ~ :not([hidden])' + ) +}) + +it('should be possible to add a single merge variant to a simple selector', () => { + let selector = '.text-center' + let candidate = 'group-hover:text-center' + + let variants = [':merge(.group):hover &'] + + expect(finalizeSelector(formatVariantSelector(...variants), { selector, candidate })).toEqual( + '.group:hover .group-hover\\:text-center' + ) +}) + +it('should be possible to add multiple merge variants to a simple selector', () => { + let selector = '.text-center' + let candidate = 'group-focus:group-hover:text-center' + + let variants = [':merge(.group):hover &', ':merge(.group):focus &'] + + expect(finalizeSelector(formatVariantSelector(...variants), { selector, candidate })).toEqual( + '.group:focus:hover .group-focus\\:group-hover\\:text-center' + ) +}) + +it('should be possible to add a single merge variant to a more complex selector', () => { + let selector = '.space-x-4 ~ :not([hidden]) ~ :not([hidden])' + let candidate = 'group-hover:space-x-4' + + let variants = [':merge(.group):hover &'] + + expect(finalizeSelector(formatVariantSelector(...variants), { selector, candidate })).toEqual( + '.group:hover .group-hover\\:space-x-4 ~ :not([hidden]) ~ :not([hidden])' + ) +}) + +it('should be possible to add multiple merge variants to a more complex selector', () => { + let selector = '.space-x-4 ~ :not([hidden]) ~ :not([hidden])' + let candidate = 'group-focus:group-hover:space-x-4' + + let variants = [':merge(.group):hover &', ':merge(.group):focus &'] + + expect(finalizeSelector(formatVariantSelector(...variants), { selector, candidate })).toEqual( + '.group:focus:hover .group-focus\\:group-hover\\:space-x-4 ~ :not([hidden]) ~ :not([hidden])' + ) +}) + +it('should be possible to add multiple unique merge variants to a simple selector', () => { + let selector = '.text-center' + let candidate = 'peer-focus:group-hover:text-center' + + let variants = [':merge(.group):hover &', ':merge(.peer):focus ~ &'] + + expect(finalizeSelector(formatVariantSelector(...variants), { selector, candidate })).toEqual( + '.peer:focus ~ .group:hover .peer-focus\\:group-hover\\:text-center' + ) +}) + +it('should be possible to add multiple unique merge variants to a simple selector', () => { + let selector = '.text-center' + let candidate = 'group-hover:peer-focus:text-center' + + let variants = [':merge(.peer):focus ~ &', ':merge(.group):hover &'] + + expect(finalizeSelector(formatVariantSelector(...variants), { selector, candidate })).toEqual( + '.group:hover .peer:focus ~ .group-hover\\:peer-focus\\:text-center' + ) +}) + +it('should be possible to use multiple :merge() calls with different "arguments"', () => { + let result = '&' + result = formatVariantSelector(result, ':merge(.group):hover &') + expect(result).toEqual(':merge(.group):hover &') + + result = formatVariantSelector(result, ':merge(.peer):hover ~ &') + expect(result).toEqual(':merge(.peer):hover ~ :merge(.group):hover &') + + result = formatVariantSelector(result, ':merge(.group):focus &') + expect(result).toEqual(':merge(.peer):hover ~ :merge(.group):focus:hover &') + + result = formatVariantSelector(result, ':merge(.peer):focus ~ &') + expect(result).toEqual(':merge(.peer):focus:hover ~ :merge(.group):focus:hover &') +}) + +it('group hover and prose headings combination', () => { + let selector = '.text-center' + let candidate = 'group-hover:prose-headings:text-center' + let variants = [ + ':where(&) :is(h1, h2, h3, h4)', // Prose Headings + ':merge(.group):hover &', // Group Hover + ] + + expect(finalizeSelector(formatVariantSelector(...variants), { selector, candidate })).toEqual( + '.group:hover :where(.group-hover\\:prose-headings\\:text-center) :is(h1, h2, h3, h4)' + ) +}) + +it('group hover and prose headings combination flipped', () => { + let selector = '.text-center' + let candidate = 'prose-headings:group-hover:text-center' + let variants = [ + ':merge(.group):hover &', // Group Hover + ':where(&) :is(h1, h2, h3, h4)', // Prose Headings + ] + + expect(finalizeSelector(formatVariantSelector(...variants), { selector, candidate })).toEqual( + ':where(.group:hover .prose-headings\\:group-hover\\:text-center) :is(h1, h2, h3, h4)' + ) +}) + +it('should be possible to handle a complex utility', () => { + let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])' + let candidate = 'peer-disabled:peer-first-child:group-hover:group-focus:focus:hover:space-x-4' + let variants = [ + '&:hover', // Hover + '&:focus', // Focus + ':merge(.group):focus &', // Group focus + ':merge(.group):hover &', // Group hover + ':merge(.peer):first-child ~ &', // Peer first-child + ':merge(.peer):disabled ~ &', // Peer disabled + ] + + expect(finalizeSelector(formatVariantSelector(...variants), { selector, candidate })).toEqual( + '.peer:disabled:first-child ~ .group:hover:focus .peer-disabled\\:peer-first-child\\:group-hover\\:group-focus\\:focus\\:hover\\:space-x-4:hover:focus > :not([hidden]) ~ :not([hidden])' + ) +}) + +describe('real examples', () => { + it('example a', () => { + let selector = '.placeholder-red-500::placeholder' + let candidate = 'hover:placeholder-red-500' + + let variants = ['&:hover'] + + expect(finalizeSelector(formatVariantSelector(...variants), { selector, candidate })).toEqual( + '.hover\\:placeholder-red-500:hover::placeholder' + ) + }) + + it('example b', () => { + let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])' + let candidate = 'group-hover:hover:space-x-4' + + let variants = ['&:hover', ':merge(.group):hover &'] + + expect(finalizeSelector(formatVariantSelector(...variants), { selector, candidate })).toEqual( + '.group:hover .group-hover\\:hover\\:space-x-4:hover > :not([hidden]) ~ :not([hidden])' + ) + }) + + it('should work for group-hover and class dark mode combinations', () => { + let selector = '.text-center' + let candidate = 'dark:group-hover:text-center' + + let variants = [':merge(.group):hover &', '.dark &'] + + expect(finalizeSelector(formatVariantSelector(...variants), { selector, candidate })).toEqual( + '.dark .group:hover .dark\\:group-hover\\:text-center' + ) + }) + + it('should work for group-hover and class dark mode combinations (reversed)', () => { + let selector = '.text-center' + let candidate = 'group-hover:dark:text-center' + + let variants = ['.dark &', ':merge(.group):hover &'] + + expect(finalizeSelector(formatVariantSelector(...variants), { selector, candidate })).toEqual( + '.group:hover .dark .group-hover\\:dark\\:text-center' + ) + }) + + describe('prose-headings', () => { + it('should be possible to use hover:prose-headings:text-center', () => { + let selector = '.text-center' + let candidate = 'hover:prose-headings:text-center' + + let variants = [':where(&) :is(h1, h2, h3, h4)', '&:hover'] + + expect(finalizeSelector(formatVariantSelector(...variants), { selector, candidate })).toEqual( + ':where(.hover\\:prose-headings\\:text-center) :is(h1, h2, h3, h4):hover' + ) + }) + + it('should be possible to use prose-headings:hover:text-center', () => { + let selector = '.text-center' + let candidate = 'prose-headings:hover:text-center' + + let variants = ['&:hover', ':where(&) :is(h1, h2, h3, h4)'] + + expect(finalizeSelector(formatVariantSelector(...variants), { selector, candidate })).toEqual( + ':where(.prose-headings\\:hover\\:text-center:hover) :is(h1, h2, h3, h4)' + ) + }) + }) +}) From 757d50e323968dae81beb14eda4965571200a109 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 16 Oct 2021 23:37:23 +0200 Subject: [PATCH 03/15] add new `format`, `withRule` and `wrap` API for addVariant --- src/lib/generateRules.js | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index a6763c5e45e3..ae2e154efef6 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -5,6 +5,7 @@ import isPlainObject from '../util/isPlainObject' import prefixSelector from '../util/prefixSelector' import { updateAllClasses } from '../util/pluginUtils' import log from '../util/log' +import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSelector' let classNameParser = selectorParser((selectors) => { return selectors.first.filter(({ type }) => type === 'class').pop().value @@ -112,6 +113,8 @@ function applyVariant(variant, matches, context) { for (let [variantSort, variantFunction] of variantFunctionTuples) { let clone = container.clone() + let collectedFormats = [] + function modifySelectors(modifierFunction) { clone.each((rule) => { if (rule.type !== 'rule') { @@ -134,13 +137,32 @@ function applyVariant(variant, matches, context) { container: clone, separator: context.tailwindConfig.separator, modifySelectors, + wrap(wrapper) { + let nodes = clone.nodes + clone.removeAll() + wrapper.append(nodes) + clone.append(wrapper) + }, + withRule(modify) { + clone.walkRules(modify) + }, + format(selectorFormat) { + collectedFormats.push(selectorFormat) + }, }) if (ruleWithVariant === null) { continue } - let withOffset = [{ ...meta, sort: variantSort | meta.sort }, clone.nodes[0]] + let withOffset = [ + { + ...meta, + sort: variantSort | meta.sort, + collectedFormats: (meta.collectedFormats ?? []).concat(collectedFormats), + }, + clone.nodes[0], + ] result.push(withOffset) } } @@ -323,6 +345,22 @@ function* resolveMatches(candidate, context) { } for (let match of matches) { + // Apply final format selector + if (match[0].collectedFormats) { + let finalFormat = formatVariantSelector('&', ...match[0].collectedFormats) + let container = postcss.root({ nodes: [match[1].clone()] }) + container.walkRules((rule) => { + if (inKeyframes(rule)) return + + rule.selector = finalizeSelector(finalFormat, { + selector: rule.selector, + candidate, + context, + }) + }) + match[1] = container.nodes[0] + } + yield match } } From 758f767b4588d6d00f9eb203cd3cd07a2a71dbb3 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 16 Oct 2021 23:38:14 +0200 Subject: [PATCH 04/15] implement backwards compatibility This will ensure that the backwards compatibility for `modifySelectors` and direct mutations to the `container` will still work. We will try to capture the changes made to the `rule.selector`, we will also "backup" the existing selector. This allows us to diff the old and new selectors and determine what actually happened. Once we know this, we can restore the selector to the "old" selector and add the diffed string e.g.: `.foo &`, to the `collectedFormats` as if you called `format()` directly. This is a bunch of extra work, but it allows us to be backwards compatible. In the future we could also warn if you are using `modifySelectors`, but it is going to be a little bit tricky, because usually that's implemented by plugin authors and therefore you don't have direct control over this. Maybe we can figure out the plugin this is used in and change the warning somehow? --- src/lib/generateRules.js | 51 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index ae2e154efef6..a6d013a32342 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -115,7 +115,15 @@ function applyVariant(variant, matches, context) { let clone = container.clone() let collectedFormats = [] + let originals = new Map() + + function prepareBackup() { + if (originals.size > 0) return // Already prepared, chicken out + clone.walkRules((rule) => originals.set(rule, rule.selector)) + } + function modifySelectors(modifierFunction) { + prepareBackup() clone.each((rule) => { if (rule.type !== 'rule') { return @@ -130,11 +138,15 @@ function applyVariant(variant, matches, context) { }) }) }) + return clone } let ruleWithVariant = variantFunction({ - container: clone, + get container() { + prepareBackup() + return clone + }, separator: context.tailwindConfig.separator, modifySelectors, wrap(wrapper) { @@ -155,6 +167,43 @@ function applyVariant(variant, matches, context) { continue } + // We filled the `originals`, therefore we assume that somebody touched + // `container` or `modifySelectors`. Let's see if they did, so that we + // can restore the selectors, and collect the format strings. + if (originals.size > 0) { + clone.walkRules((rule) => { + if (!originals.has(rule)) return + let before = originals.get(rule) + if (before === rule.selector) return // No mutation happened + + 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 = `${variant}${context.tailwindConfig.separator}${classNode.value}` + }) + }).processSync(before) + + // 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 & + collectedFormats.push(modified.replace(rebuiltBase, '&')) + rule.selector = before + }) + } + let withOffset = [ { ...meta, From 7e9e155ef20040c6bc497ebfce7c7c1c11732ec6 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 16 Oct 2021 23:44:03 +0200 Subject: [PATCH 05/15] fix incorrect test This was clearly a bug, keyframes should not include escaped variants at all. The reason this is here in the first place is because the nodes in a keyframe are also "rule" nodes. --- tests/custom-plugins.test.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/custom-plugins.test.js b/tests/custom-plugins.test.js index 78ad7ce01dd5..5fd8afd6236a 100644 --- a/tests/custom-plugins.test.js +++ b/tests/custom-plugins.test.js @@ -1521,7 +1521,7 @@ test('keyframes are not escaped', () => { } return run('@tailwind utilities', config).then((result) => { - expect(result.css).toMatchFormattedCss(` + expect(result.css).toMatchFormattedCss(css` @keyframes abc { 25.001% { color: black; @@ -1534,10 +1534,11 @@ test('keyframes are not escaped', () => { @media (min-width: 768px) { @keyframes def { - 25.md\\:001\\% { + 25.001% { color: black; } } + .md\\:foo-\\[def\\] { animation: def 1s infinite; } From 0a4a064b6beebc7ee80423398d8d3dbaf5254f4c Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 16 Oct 2021 23:45:47 +0200 Subject: [PATCH 06/15] swap the order of pseudo states The current implementation had a strange side effect, that resulted in incorrect class definitions. When you are combining the `:hover` and `:focus` event, then there is no difference between `:hover:focus` and `:focus:hover`. However, when you use `:hover::file-selector-button` or `::file-selector-button:hover`, then there is a big difference. In the first place, you can hover over the full file input to apply changes to the `File selector button`. In the second scenario you have to hover over the `File selector button` itself to apply changes. You can think of it as function calls: - focus(hover(text-center)) What you would expect is something like this: `.focus\:hover\:text-center:hover:focus`, where `hover` is on the inside, and `focus` is on the outside. However in the current implementation this is implemented as `.focus\:hover\:text-cener:focus:hover` --- tests/important-modifier-prefix.test.css | 2 +- tests/important-modifier.test.css | 2 +- tests/kitchen-sink.test.css | 6 +++--- tests/layer-at-rules.test.js | 2 +- tests/match-components.test.js | 4 ++-- tests/parallel-variants.test.js | 4 ++-- tests/resolve-defaults-at-rules.test.js | 6 +++--- tests/variants.test.css | 6 +++--- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/important-modifier-prefix.test.css b/tests/important-modifier-prefix.test.css index 8bf95f968c1e..17096025b112 100644 --- a/tests/important-modifier-prefix.test.css +++ b/tests/important-modifier-prefix.test.css @@ -38,7 +38,7 @@ } } @media (min-width: 1280px) { - .xl\:focus\:disabled\:\!tw-float-right:focus:disabled { + .xl\:focus\:disabled\:\!tw-float-right:disabled:focus { float: right !important; } } diff --git a/tests/important-modifier.test.css b/tests/important-modifier.test.css index fffcd3175978..11eef31f2f8f 100644 --- a/tests/important-modifier.test.css +++ b/tests/important-modifier.test.css @@ -38,7 +38,7 @@ } } @media (min-width: 1280px) { - .xl\:focus\:disabled\:\!float-right:focus:disabled { + .xl\:focus\:disabled\:\!float-right:disabled:focus { float: right !important; } } diff --git a/tests/kitchen-sink.test.css b/tests/kitchen-sink.test.css index b4a2e3fd447e..6f32b2117872 100644 --- a/tests/kitchen-sink.test.css +++ b/tests/kitchen-sink.test.css @@ -23,7 +23,7 @@ .apply-test:hover { font-weight: 700; } -.apply-test:focus:hover { +.apply-test:hover:focus { font-weight: 700; } @media (min-width: 640px) { @@ -31,7 +31,7 @@ --tw-bg-opacity: 1; background-color: rgb(34 197 94 / var(--tw-bg-opacity)); } - .apply-test:focus:nth-child(even) { + .apply-test:nth-child(even):focus { --tw-bg-opacity: 1; background-color: rgb(251 207 232 / var(--tw-bg-opacity)); } @@ -352,7 +352,7 @@ div { --tw-ring-opacity: 1; --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity)); } -.focus\:hover\:font-light:focus:hover { +.focus\:hover\:font-light:hover:focus { font-weight: 300; } .disabled\:font-bold:disabled { diff --git a/tests/layer-at-rules.test.js b/tests/layer-at-rules.test.js index 2cb7c11b7bdb..270fa1988b4f 100644 --- a/tests/layer-at-rules.test.js +++ b/tests/layer-at-rules.test.js @@ -43,7 +43,7 @@ test('custom user-land utilities', () => { .hover\\:align-banana:hover { text-align: banana; } - .focus\\:hover\\:align-chocolate:focus:hover { + .focus\\:hover\\:align-chocolate:hover:focus { text-align: chocolate; } `) diff --git a/tests/match-components.test.js b/tests/match-components.test.js index 5390a502d049..9641dadca9d3 100644 --- a/tests/match-components.test.js +++ b/tests/match-components.test.js @@ -79,12 +79,12 @@ it('should be possible to matchComponents', () => { color: #f0f; } - .hover\\:card-\\[\\#f0f\\]:hover .hover\\:card-header:hover { + .hover\\:card-\\[\\#f0f\\]:hover .card-header { border-top-width: 3px; border-top-color: #f0f; } - .hover\\:card-\\[\\#f0f\\]:hover .hover\\:card-footer:hover { + .hover\\:card-\\[\\#f0f\\]:hover .card-footer { border-bottom-width: 3px; border-bottom-color: #f0f; } diff --git a/tests/parallel-variants.test.js b/tests/parallel-variants.test.js index 2f80046336c9..c54de0ad16ca 100644 --- a/tests/parallel-variants.test.js +++ b/tests/parallel-variants.test.js @@ -42,7 +42,7 @@ test('basic parallel variants', async () => { .test\\:font-medium *::test { font-weight: 500; } - .hover\\:test\\:font-black:hover *::test { + .hover\\:test\\:font-black *::test:hover { font-weight: 900; } .test\\:font-bold::test { @@ -51,7 +51,7 @@ test('basic parallel variants', async () => { .test\\:font-medium::test { font-weight: 500; } - .hover\\:test\\:font-black:hover::test { + .hover\\:test\\:font-black::test:hover { font-weight: 900; } `) diff --git a/tests/resolve-defaults-at-rules.test.js b/tests/resolve-defaults-at-rules.test.js index e8831950245b..1d93aa9b7a8d 100644 --- a/tests/resolve-defaults-at-rules.test.js +++ b/tests/resolve-defaults-at-rules.test.js @@ -84,7 +84,7 @@ test('with pseudo-class variants', async () => { --tw-rotate: 3deg; transform: var(--tw-transform); } - .hover\\:focus\\:skew-y-6:hover:focus { + .hover\\:focus\\:skew-y-6:focus:hover { --tw-skew-y: 6deg; transform: var(--tw-transform); } @@ -252,12 +252,12 @@ test('with multi-class pseudo-element and pseudo-class variants', async () => { scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } /* --- */ - .group:hover .group-hover\\:hover\\:before\\:scale-x-110:hover::before { + .group:hover .group-hover\\:hover\\:before\\:scale-x-110::before:hover { content: ''; --tw-scale-x: 1.1; transform: var(--tw-transform); } - .peer:focus ~ .peer-focus\\:focus\\:after\\:rotate-3:focus::after { + .peer:focus ~ .peer-focus\\:focus\\:after\\:rotate-3::after:focus { content: ''; --tw-rotate: 3deg; transform: var(--tw-transform); diff --git a/tests/variants.test.css b/tests/variants.test.css index 5a844fabdd49..9814996d56e0 100644 --- a/tests/variants.test.css +++ b/tests/variants.test.css @@ -313,11 +313,11 @@ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } -.file\:hover\:bg-blue-600::file-selector-button:hover { +.file\:hover\:bg-blue-600:hover::file-selector-button { --tw-bg-opacity: 1; background-color: rgb(37 99 235 / var(--tw-bg-opacity)); } -.open\:hover\:bg-red-200[open]:hover { +.open\:hover\:bg-red-200:hover[open] { --tw-bg-opacity: 1; background-color: rgb(254 202 202 / var(--tw-bg-opacity)); } @@ -326,7 +326,7 @@ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } -.focus\:hover\:shadow-md:focus:hover { +.focus\:hover\:shadow-md:hover:focus { --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -1px rgb(0 0 0 / 0.06); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); From e936a00a9bf4f4ddadfc815d318a7757a4085c3f Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 16 Oct 2021 23:50:47 +0200 Subject: [PATCH 07/15] add more variant tests for the new API --- tests/variants.test.js | 120 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 1 deletion(-) diff --git a/tests/variants.test.js b/tests/variants.test.js index b0edb5f762e0..cba5b8f13538 100644 --- a/tests/variants.test.js +++ b/tests/variants.test.js @@ -1,7 +1,7 @@ import fs from 'fs' import path from 'path' -import { run, css } from './util/run' +import { run, css, html } from './util/run' test('variants', () => { let config = { @@ -24,6 +24,124 @@ test('variants', () => { }) }) +test('order matters and produces different behaviour', () => { + let config = { + content: [ + { + raw: html` +
+
+ `, + }, + ], + } + + return run('@tailwind utilities', config).then((result) => { + return expect(result.css).toMatchFormattedCss(css` + .hover\\:file\\:bg-pink-600::file-selector-button:hover { + --tw-bg-opacity: 1; + background-color: rgb(219 39 119 / var(--tw-bg-opacity)); + } + + .file\\:hover\\:bg-pink-600:hover::file-selector-button { + --tw-bg-opacity: 1; + background-color: rgb(219 39 119 / var(--tw-bg-opacity)); + } + `) + }) +}) + +describe('custom advanced variants', () => { + test('prose-headings usage on its own', () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + plugins: [ + function ({ addVariant }) { + addVariant('prose-headings', ({ format }) => { + return format(':where(&) :is(h1, h2, h3, h4)') + }) + }, + ], + } + + return run('@tailwind components;@tailwind utilities', config).then((result) => { + return expect(result.css).toMatchFormattedCss(css` + :where(.prose-headings\\:text-center) :is(h1, h2, h3, h4) { + text-align: center; + } + `) + }) + }) + + test('prose-headings with another "simple" variant', () => { + let config = { + content: [ + { + raw: html` +
+
+ `, + }, + ], + plugins: [ + function ({ addVariant }) { + addVariant('prose-headings', ({ format }) => { + return format(':where(&) :is(h1, h2, h3, h4)') + }) + }, + ], + } + + return run('@tailwind components;@tailwind utilities', config).then((result) => { + return expect(result.css).toMatchFormattedCss(css` + :where(.hover\\:prose-headings\\:text-center) :is(h1, h2, h3, h4):hover { + text-align: center; + } + + :where(.prose-headings\\:hover\\:text-center:hover) :is(h1, h2, h3, h4) { + text-align: center; + } + `) + }) + }) + + test('prose-headings with another "complex" variant', () => { + let config = { + content: [ + { + raw: html` +
+
+ `, + }, + ], + plugins: [ + function ({ addVariant }) { + addVariant('prose-headings', ({ format }) => { + return format(':where(&) :is(h1, h2, h3, h4)') + }) + }, + ], + } + + return run('@tailwind utilities', config).then((result) => { + return expect(result.css).toMatchFormattedCss(css` + .group:hover :where(.group-hover\\:prose-headings\\:text-center) :is(h1, h2, h3, h4) { + text-align: center; + } + + :where(.group:hover .prose-headings\\:group-hover\\:text-center) :is(h1, h2, h3, h4) { + text-align: center; + } + `) + }) + }) +}) + test('stacked peer variants', async () => { let config = { content: [{ raw: 'peer-disabled:peer-focus:peer-hover:border-blue-500' }], From c5ec2386d07068bab00c9eeb86fce699ab5f27ae Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 16 Oct 2021 23:51:00 +0200 Subject: [PATCH 08/15] update parallel variants tests to make use of new API --- tests/parallel-variants.test.js | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/tests/parallel-variants.test.js b/tests/parallel-variants.test.js index c54de0ad16ca..ea9cd99647f2 100644 --- a/tests/parallel-variants.test.js +++ b/tests/parallel-variants.test.js @@ -1,5 +1,3 @@ -import { transformAllSelectors, updateAllClasses } from '../src/util/pluginUtils.js' - import { run, html, css } from './util/run' test('basic parallel variants', async () => { @@ -12,21 +10,8 @@ test('basic parallel variants', async () => { }, ], plugins: [ - function test({ addVariant, config }) { - addVariant('test', [ - transformAllSelectors((selector) => { - let variantSelector = updateAllClasses(selector, (className) => { - return `test${config('separator')}${className}` - }) - - return `${variantSelector} *::test` - }), - transformAllSelectors((selector) => { - return updateAllClasses(selector, (className, { withPseudo }) => { - return withPseudo(`test${config('separator')}${className}`, '::test') - }) - }), - ]) + function test({ addVariant }) { + addVariant('test', ['& *::test', '&::test']) }, ], } From 4cb8137860ea4f2f4d4b48d77f78c8c1dbc0e444 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 16 Oct 2021 23:51:40 +0200 Subject: [PATCH 09/15] implement core variants with new API --- src/corePlugins.js | 327 +++++++++------------------------------------ 1 file changed, 64 insertions(+), 263 deletions(-) diff --git a/src/corePlugins.js b/src/corePlugins.js index 3cb91990f53b..f4929d27f554 100644 --- a/src/corePlugins.js +++ b/src/corePlugins.js @@ -3,129 +3,60 @@ import * as path from 'path' import postcss from 'postcss' import createUtilityPlugin from './util/createUtilityPlugin' import buildMediaQuery from './util/buildMediaQuery' -import prefixSelector from './util/prefixSelector' import parseAnimationValue from './util/parseAnimationValue' import flattenColorPalette from './util/flattenColorPalette' import withAlphaVariable, { withAlphaValue } from './util/withAlphaVariable' import toColorValue from './util/toColorValue' import isPlainObject from './util/isPlainObject' import transformThemeValue from './util/transformThemeValue' -import { - applyStateToMarker, - updateLastClasses, - updateAllClasses, - transformAllSelectors, - transformAllClasses, - transformLastClasses, -} from './util/pluginUtils' import { version as tailwindVersion } from '../package.json' import log from './util/log' export let variantPlugins = { - pseudoElementVariants: ({ config, addVariant }) => { - addVariant( - 'first-letter', - transformAllSelectors((selector) => { - return updateAllClasses(selector, (className, { withPseudo }) => { - return withPseudo(`first-letter${config('separator')}${className}`, '::first-letter') - }) - }) - ) + pseudoElementVariants: ({ addVariant }) => { + addVariant('first-letter', '&::first-letter') + addVariant('first-line', '&::first-line') - addVariant( - 'first-line', - transformAllSelectors((selector) => { - return updateAllClasses(selector, (className, { withPseudo }) => { - return withPseudo(`first-line${config('separator')}${className}`, '::first-line') - }) - }) - ) + addVariant('marker', ['& *::marker', '&::marker']) + addVariant('selection', ['& *::selection', '&::selection']) - addVariant('marker', [ - transformAllSelectors((selector) => { - let variantSelector = updateAllClasses(selector, (className) => { - return `marker${config('separator')}${className}` - }) + addVariant('file', '&::file-selector-button') - return `${variantSelector} *::marker` - }), - transformAllSelectors((selector) => { - return updateAllClasses(selector, (className, { withPseudo }) => { - return withPseudo(`marker${config('separator')}${className}`, '::marker') - }) - }), - ]) + // TODO: Use `addVariant('before', '*::before')` instead, once `content` + // fix is implemented. + addVariant('before', ({ format, withRule }) => { + format('&::before') - addVariant('selection', [ - transformAllSelectors((selector) => { - let variantSelector = updateAllClasses(selector, (className) => { - return `selection${config('separator')}${className}` + withRule((rule) => { + let foundContent = false + rule.walkDecls('content', () => { + foundContent = true }) + if (!foundContent) { + rule.prepend(postcss.decl({ prop: 'content', value: '""' })) + } + }) + }) - return `${variantSelector} *::selection` - }), - transformAllSelectors((selector) => { - return updateAllClasses(selector, (className, { withPseudo }) => { - return withPseudo(`selection${config('separator')}${className}`, '::selection') - }) - }), - ]) + // TODO: Use `addVariant('after', '*::after')` instead, once `content` + // fix is implemented. + addVariant('after', ({ format, withRule }) => { + format('&::after') - addVariant( - 'file', - transformAllSelectors((selector) => { - return updateAllClasses(selector, (className, { withPseudo }) => { - return withPseudo(`file${config('separator')}${className}`, '::file-selector-button') + withRule((rule) => { + let foundContent = false + rule.walkDecls('content', () => { + foundContent = true }) - }) - ) - addVariant( - 'before', - transformAllSelectors( - (selector) => { - return updateAllClasses(selector, (className, { withPseudo }) => { - return withPseudo(`before${config('separator')}${className}`, '::before') - }) - }, - { - withRule: (rule) => { - let foundContent = false - rule.walkDecls('content', () => { - foundContent = true - }) - if (!foundContent) { - rule.prepend(postcss.decl({ prop: 'content', value: '""' })) - } - }, - } - ) - ) - - addVariant( - 'after', - transformAllSelectors( - (selector) => { - return updateAllClasses(selector, (className, { withPseudo }) => { - return withPseudo(`after${config('separator')}${className}`, '::after') - }) - }, - { - withRule: (rule) => { - let foundContent = false - rule.walkDecls('content', () => { - foundContent = true - }) - if (!foundContent) { - rule.prepend(postcss.decl({ prop: 'content', value: '""' })) - } - }, + if (!foundContent) { + rule.prepend(postcss.decl({ prop: 'content', value: '""' })) } - ) - ) + }) + }) }, - pseudoClassVariants: ({ config, addVariant }) => { + pseudoClassVariants: ({ addVariant }) => { let pseudoVariants = [ // Positional ['first', ':first-child'], @@ -165,137 +96,44 @@ export let variantPlugins = { 'focus-visible', 'active', 'disabled', - ] - - for (let variant of pseudoVariants) { - let [variantName, state] = Array.isArray(variant) ? variant : [variant, `:${variant}`] - - addVariant( - variantName, - transformAllClasses((className, { withAttr, withPseudo }) => { - if (state.startsWith(':')) { - return withPseudo(`${variantName}${config('separator')}${className}`, state) - } else if (state.startsWith('[')) { - return withAttr(`${variantName}${config('separator')}${className}`, state) - } - }) - ) - } - - let groupMarker = prefixSelector(config('prefix'), '.group') - for (let variant of pseudoVariants) { - let [variantName, state] = Array.isArray(variant) ? variant : [variant, `:${variant}`] - let groupVariantName = `group-${variantName}` - - addVariant( - groupVariantName, - transformAllSelectors((selector) => { - let variantSelector = updateAllClasses(selector, (className) => { - if (`.${className}` === groupMarker) return className - return `${groupVariantName}${config('separator')}${className}` - }) - - if (variantSelector === selector) { - return null - } + ].map((variant) => (Array.isArray(variant) ? variant : [variant, `:${variant}`])) - return applyStateToMarker( - variantSelector, - groupMarker, - state, - (marker, selector) => `${marker} ${selector}` - ) - }) - ) + for (let [variantName, state] of pseudoVariants) { + addVariant(variantName, `&${state}`) } - let peerMarker = prefixSelector(config('prefix'), '.peer') - for (let variant of pseudoVariants) { - let [variantName, state] = Array.isArray(variant) ? variant : [variant, `:${variant}`] - let peerVariantName = `peer-${variantName}` - - addVariant( - peerVariantName, - transformAllSelectors((selector) => { - let variantSelector = updateAllClasses(selector, (className) => { - if (`.${className}` === peerMarker) return className - return `${peerVariantName}${config('separator')}${className}` - }) - - if (variantSelector === selector) { - return null - } + for (let [variantName, state] of pseudoVariants) { + addVariant(`group-${variantName}`, `:merge(.group)${state} &`) + } - return applyStateToMarker(variantSelector, peerMarker, state, (marker, selector) => - selector.trim().startsWith('~') ? `${marker}${selector}` : `${marker} ~ ${selector}` - ) - }) - ) + for (let [variantName, state] of pseudoVariants) { + addVariant(`peer-${variantName}`, `:merge(.peer)${state} ~ &`) } }, - directionVariants: ({ config, addVariant }) => { - addVariant( - 'ltr', - transformAllSelectors((selector) => { - log.warn('rtl-experimental', [ - 'The RTL features in Tailwind CSS are currently in preview.', - 'Preview features are not covered by semver, and may be improved in breaking ways at any time.', - ]) - return `[dir="ltr"] ${updateAllClasses( - selector, - (className) => `ltr${config('separator')}${className}` - )}` - }) - ) + directionVariants: ({ addVariant }) => { + addVariant('ltr', ({ format }) => { + log.warn('rtl-experimental', [ + 'The RTL features in Tailwind CSS are currently in preview.', + 'Preview features are not covered by semver, and may be improved in breaking ways at any time.', + ]) - addVariant( - 'rtl', - transformAllSelectors((selector) => { - log.warn('rtl-experimental', [ - 'The RTL features in Tailwind CSS are currently in preview.', - 'Preview features are not covered by semver, and may be improved in breaking ways at any time.', - ]) - return `[dir="rtl"] ${updateAllClasses( - selector, - (className) => `rtl${config('separator')}${className}` - )}` - }) - ) - }, + format('[dir="ltr"] &') + }) - reducedMotionVariants: ({ config, addVariant }) => { - addVariant( - 'motion-safe', - transformLastClasses( - (className) => { - return `motion-safe${config('separator')}${className}` - }, - { - wrap: () => - postcss.atRule({ - name: 'media', - params: '(prefers-reduced-motion: no-preference)', - }), - } - ) - ) + addVariant('rtl', ({ format }) => { + log.warn('rtl-experimental', [ + 'The RTL features in Tailwind CSS are currently in preview.', + 'Preview features are not covered by semver, and may be improved in breaking ways at any time.', + ]) - addVariant( - 'motion-reduce', - transformLastClasses( - (className) => { - return `motion-reduce${config('separator')}${className}` - }, - { - wrap: () => - postcss.atRule({ - name: 'media', - params: '(prefers-reduced-motion: reduce)', - }), - } - ) - ) + format('[dir="rtl"] &') + }) + }, + + reducedMotionVariants: ({ addVariant }) => { + addVariant('motion-safe', '@media (prefers-reduced-motion: no-preference)') + addVariant('motion-reduce', '@media (prefers-reduced-motion: reduce)') }, darkVariants: ({ config, addVariant }) => { @@ -309,55 +147,18 @@ export let variantPlugins = { } if (mode === 'class') { - addVariant( - 'dark', - transformAllSelectors((selector) => { - let variantSelector = updateLastClasses(selector, (className) => { - return `dark${config('separator')}${className}` - }) - - if (variantSelector === selector) { - return null - } - - let darkSelector = prefixSelector(config('prefix'), `.dark`) - - return `${darkSelector} ${variantSelector}` - }) - ) + addVariant('dark', '.dark &') } else if (mode === 'media') { - addVariant( - 'dark', - transformLastClasses( - (className) => { - return `dark${config('separator')}${className}` - }, - { - wrap: () => - postcss.atRule({ - name: 'media', - params: '(prefers-color-scheme: dark)', - }), - } - ) - ) + addVariant('dark', '@media (prefers-color-scheme: dark)') } }, - screenVariants: ({ config, theme, addVariant }) => { + screenVariants: ({ theme, addVariant }) => { for (let screen in theme('screens')) { let size = theme('screens')[screen] let query = buildMediaQuery(size) - addVariant( - screen, - transformLastClasses( - (className) => { - return `${screen}${config('separator')}${className}` - }, - { wrap: () => postcss.atRule({ name: 'media', params: query }) } - ) - ) + addVariant(screen, `@media ${query}`) } }, } From 25c8000f33ea354b803534ba816f6330b962df97 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 16 Oct 2021 23:52:03 +0200 Subject: [PATCH 10/15] simplify/cleanup existing plugin utils We can get rid of this because we drastically simplified the new addVariant API. --- src/util/pluginUtils.js | 136 +--------------------------------------- 1 file changed, 1 insertion(+), 135 deletions(-) diff --git a/src/util/pluginUtils.js b/src/util/pluginUtils.js index ef1cf2aaee0a..6d8db4a21e14 100644 --- a/src/util/pluginUtils.js +++ b/src/util/pluginUtils.js @@ -1,7 +1,6 @@ import selectorParser from 'postcss-selector-parser' import escapeCommas from './escapeCommas' import { withAlphaValue } from './withAlphaVariable' -import isKeyframeRule from './isKeyframeRule' import { normalize, length, @@ -19,34 +18,10 @@ import { } from './dataTypes' import negateValue from './negateValue' -export function applyStateToMarker(selector, marker, state, join) { - let markerIdx = selector.search(new RegExp(`${marker}[:[]`)) - - if (markerIdx === -1) { - return join(marker + state, selector) - } - - let markerSelector = selector.slice(markerIdx, selector.indexOf(' ', markerIdx)) - - return join( - marker + state + markerSelector.slice(markerIdx + marker.length), - selector.replace(markerSelector, '') - ) -} - export function updateAllClasses(selectors, updateClass) { let parser = selectorParser((selectors) => { selectors.walkClasses((sel) => { - let updatedClass = updateClass(sel.value, { - withAttr(className, attr) { - sel.parent.insertAfter(sel, selectorParser.attribute({ attribute: attr.slice(1, -1) })) - return className - }, - withPseudo(className, pseudo) { - sel.parent.insertAfter(sel, selectorParser.pseudo({ value: pseudo })) - return className - }, - }) + let updatedClass = updateClass(sel.value) sel.value = updatedClass if (sel.raws && sel.raws.value) { sel.raws.value = escapeCommas(sel.raws.value) @@ -59,115 +34,6 @@ export function updateAllClasses(selectors, updateClass) { return result } -export function updateLastClasses(selectors, updateClass) { - let parser = selectorParser((selectors) => { - selectors.each((sel) => { - let lastClass = sel.filter(({ type }) => type === 'class').pop() - - if (lastClass === undefined) { - return - } - - let updatedClass = updateClass(lastClass.value, { - withPseudo(className, pseudo) { - lastClass.parent.insertAfter(lastClass, selectorParser.pseudo({ value: `${pseudo}` })) - return className - }, - }) - lastClass.value = updatedClass - if (lastClass.raws && lastClass.raws.value) { - lastClass.raws.value = escapeCommas(lastClass.raws.value) - } - }) - }) - let result = parser.processSync(selectors) - - return result -} - -function splitByNotEscapedCommas(str) { - let chunks = [] - let currentChunk = '' - for (let i = 0; i < str.length; i++) { - if (str[i] === ',' && str[i - 1] !== '\\') { - chunks.push(currentChunk) - currentChunk = '' - } else { - currentChunk += str[i] - } - } - chunks.push(currentChunk) - return chunks -} - -export function transformAllSelectors(transformSelector, { wrap, withRule } = {}) { - return ({ container }) => { - container.walkRules((rule) => { - if (isKeyframeRule(rule)) { - return rule - } - let transformed = splitByNotEscapedCommas(rule.selector).map(transformSelector).join(',') - rule.selector = transformed - if (withRule) { - withRule(rule) - } - return rule - }) - - if (wrap) { - let wrapper = wrap() - let nodes = container.nodes - container.removeAll() - wrapper.append(nodes) - container.append(wrapper) - } - } -} - -export function transformAllClasses(transformClass, { wrap, withRule } = {}) { - return ({ container }) => { - container.walkRules((rule) => { - let selector = rule.selector - let variantSelector = updateAllClasses(selector, transformClass) - rule.selector = variantSelector - if (withRule) { - withRule(rule) - } - return rule - }) - - if (wrap) { - let wrapper = wrap() - let nodes = container.nodes - container.removeAll() - wrapper.append(nodes) - container.append(wrapper) - } - } -} - -export function transformLastClasses(transformClass, { wrap, withRule } = {}) { - return ({ container }) => { - container.walkRules((rule) => { - let selector = rule.selector - let variantSelector = updateLastClasses(selector, transformClass) - rule.selector = variantSelector - if (withRule) { - withRule(rule) - } - return rule - }) - - if (wrap) { - let wrapper = wrap() - let nodes = container.nodes - container.removeAll() - wrapper.append(nodes) - container.append(wrapper) - } - } -} - function resolveArbitraryValue(modifier, validate) { if (!isArbitraryValue(modifier)) { return undefined From b81ceaa67bb6e1acbf6923f49ac6eade548a75b6 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 16 Oct 2021 23:52:27 +0200 Subject: [PATCH 11/15] add addVariant shorthand signature The current API looks like this: ```js addVariant('name', ({ format, wrap }) => { // Wrap in an atRule wrap(postcss.atRule({ name: 'media', params: '(prefers-reduced-motion: reduce)' })) // "Mutate" the selector, for example prepend `.dark` format('.dark &') }) ``` It is also pretty common to have this: ```js addVariant('name', ({ format }) => format('.dark &')) ``` So we simplified this to: ```js addVariant('name', '.dark &') ``` It is also pretty common to have this: ```js addVariant('name', ({ wrap }) => wrap(postcss.atRule({ name: 'media', params: '(prefers-reduced-motion: reduce)' }))) ``` So we simplified this to: ```js addVariant('name', '@media (prefers-reduced-motion: reduce)') ``` --- src/lib/setupContextUtils.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index 53a9851ce07a..645c984a16f5 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -186,7 +186,18 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs return { addVariant(variantName, variantFunctions, options = {}) { - variantFunctions = [].concat(variantFunctions) + variantFunctions = [].concat(variantFunctions).map((variantFunction) => { + if (typeof variantFunction !== 'string') { + return variantFunction + } + + if (!variantFunction.startsWith('@')) { + return ({ format }) => format(variantFunction) + } + + let [, name, params] = /@(.*?) (\(.*\))/g.exec(variantFunction) + return ({ wrap }) => wrap(postcss.atRule({ name, params })) + }) insertInto(variantList, variantName, options) variantMap.set(variantName, variantFunctions) From 634dd2f0476a0c6adda8de64167f8dbca15dc993 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 16 Oct 2021 15:21:08 +0200 Subject: [PATCH 12/15] improve fontVariantNumeric implementation We will use `@defaults`, so that only the resets are injected for the utilities we actually use. --- src/corePlugins.js | 67 +++++++++++++++++++++++++++---------- tests/apply.test.css | 33 +++++++----------- tests/basic-usage.test.css | 12 +++---- tests/kitchen-sink.test.css | 43 +++++++----------------- tests/raw-content.test.css | 12 +++---- 5 files changed, 84 insertions(+), 83 deletions(-) diff --git a/src/corePlugins.js b/src/corePlugins.js index f4929d27f554..aea6f31229c3 100644 --- a/src/corePlugins.js +++ b/src/corePlugins.js @@ -1546,25 +1546,56 @@ export let corePlugins = { fontVariantNumeric: ({ addUtilities }) => { addUtilities({ - '.ordinal, .slashed-zero, .lining-nums, .oldstyle-nums, .proportional-nums, .tabular-nums, .diagonal-fractions, .stacked-fractions': - { - '--tw-ordinal': 'var(--tw-empty,/*!*/ /*!*/)', - '--tw-slashed-zero': 'var(--tw-empty,/*!*/ /*!*/)', - '--tw-numeric-figure': 'var(--tw-empty,/*!*/ /*!*/)', - '--tw-numeric-spacing': 'var(--tw-empty,/*!*/ /*!*/)', - '--tw-numeric-fraction': 'var(--tw-empty,/*!*/ /*!*/)', - 'font-variant-numeric': - 'var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)', - }, + '@defaults font-variant-numeric': { + '--tw-ordinal': 'var(--tw-empty,/*!*/ /*!*/)', + '--tw-slashed-zero': 'var(--tw-empty,/*!*/ /*!*/)', + '--tw-numeric-figure': 'var(--tw-empty,/*!*/ /*!*/)', + '--tw-numeric-spacing': 'var(--tw-empty,/*!*/ /*!*/)', + '--tw-numeric-fraction': 'var(--tw-empty,/*!*/ /*!*/)', + '--tw-font-variant-numeric': + 'var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)', + }, '.normal-nums': { 'font-variant-numeric': 'normal' }, - '.ordinal': { '--tw-ordinal': 'ordinal' }, - '.slashed-zero': { '--tw-slashed-zero': 'slashed-zero' }, - '.lining-nums': { '--tw-numeric-figure': 'lining-nums' }, - '.oldstyle-nums': { '--tw-numeric-figure': 'oldstyle-nums' }, - '.proportional-nums': { '--tw-numeric-spacing': 'proportional-nums' }, - '.tabular-nums': { '--tw-numeric-spacing': 'tabular-nums' }, - '.diagonal-fractions': { '--tw-numeric-fraction': 'diagonal-fractions' }, - '.stacked-fractions': { '--tw-numeric-fraction': 'stacked-fractions' }, + '.ordinal': { + '@defaults font-variant-numeric': {}, + '--tw-ordinal': 'ordinal', + 'font-variant-numeric': 'var(--tw-font-variant-numeric)', + }, + '.slashed-zero': { + '@defaults font-variant-numeric': {}, + '--tw-slashed-zero': 'slashed-zero', + 'font-variant-numeric': 'var(--tw-font-variant-numeric)', + }, + '.lining-nums': { + '@defaults font-variant-numeric': {}, + '--tw-numeric-figure': 'lining-nums', + 'font-variant-numeric': 'var(--tw-font-variant-numeric)', + }, + '.oldstyle-nums': { + '@defaults font-variant-numeric': {}, + '--tw-numeric-figure': 'oldstyle-nums', + 'font-variant-numeric': 'var(--tw-font-variant-numeric)', + }, + '.proportional-nums': { + '@defaults font-variant-numeric': {}, + '--tw-numeric-spacing': 'proportional-nums', + 'font-variant-numeric': 'var(--tw-font-variant-numeric)', + }, + '.tabular-nums': { + '@defaults font-variant-numeric': {}, + '--tw-numeric-spacing': 'tabular-nums', + 'font-variant-numeric': 'var(--tw-font-variant-numeric)', + }, + '.diagonal-fractions': { + '@defaults font-variant-numeric': {}, + '--tw-numeric-fraction': 'diagonal-fractions', + 'font-variant-numeric': 'var(--tw-font-variant-numeric)', + }, + '.stacked-fractions': { + '@defaults font-variant-numeric': {}, + '--tw-numeric-fraction': 'stacked-fractions', + 'font-variant-numeric': 'var(--tw-font-variant-numeric)', + }, }) }, diff --git a/tests/apply.test.css b/tests/apply.test.css index 1eef7c6e26f3..08bcbf5b8ef0 100644 --- a/tests/apply.test.css +++ b/tests/apply.test.css @@ -126,22 +126,10 @@ } /* TODO: This works but the generated CSS is unnecessarily verbose. */ .complex-utilities { - --tw-ordinal: var(--tw-empty, /*!*/ /*!*/); - --tw-slashed-zero: var(--tw-empty, /*!*/ /*!*/); - --tw-numeric-figure: var(--tw-empty, /*!*/ /*!*/); - --tw-numeric-spacing: var(--tw-empty, /*!*/ /*!*/); - --tw-numeric-fraction: var(--tw-empty, /*!*/ /*!*/); - font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) - var(--tw-numeric-spacing) var(--tw-numeric-fraction); - --tw-ordinal: var(--tw-empty, /*!*/ /*!*/); - --tw-slashed-zero: var(--tw-empty, /*!*/ /*!*/); - --tw-numeric-figure: var(--tw-empty, /*!*/ /*!*/); - --tw-numeric-spacing: var(--tw-empty, /*!*/ /*!*/); - --tw-numeric-fraction: var(--tw-empty, /*!*/ /*!*/); - font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) - var(--tw-numeric-spacing) var(--tw-numeric-fraction); --tw-ordinal: ordinal; + font-variant-numeric: var(--tw-font-variant-numeric); --tw-numeric-spacing: tabular-nums; + font-variant-numeric: var(--tw-font-variant-numeric); --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -2px rgb(0 0 0 / 0.05); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); @@ -152,14 +140,8 @@ var(--tw-shadow); } .complex-utilities:focus { - --tw-ordinal: var(--tw-empty, /*!*/ /*!*/); - --tw-slashed-zero: var(--tw-empty, /*!*/ /*!*/); - --tw-numeric-figure: var(--tw-empty, /*!*/ /*!*/); - --tw-numeric-spacing: var(--tw-empty, /*!*/ /*!*/); - --tw-numeric-fraction: var(--tw-empty, /*!*/ /*!*/); - font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) - var(--tw-numeric-spacing) var(--tw-numeric-fraction); --tw-numeric-fraction: diagonal-fractions; + font-variant-numeric: var(--tw-font-variant-numeric); } .basic-nesting-parent { .basic-nesting-child { @@ -332,6 +314,15 @@ h2 { .important-modifier-variant:hover { border-radius: 0.375rem !important; } +.complex-utilities { + --tw-ordinal: var(--tw-empty, /*!*/ /*!*/); + --tw-slashed-zero: var(--tw-empty, /*!*/ /*!*/); + --tw-numeric-figure: var(--tw-empty, /*!*/ /*!*/); + --tw-numeric-spacing: var(--tw-empty, /*!*/ /*!*/); + --tw-numeric-fraction: var(--tw-empty, /*!*/ /*!*/); + --tw-font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) + var(--tw-numeric-spacing) var(--tw-numeric-fraction); +} @keyframes spin { to { transform: rotate(360deg); diff --git a/tests/basic-usage.test.css b/tests/basic-usage.test.css index aeff4c30ab7e..535560916bba 100644 --- a/tests/basic-usage.test.css +++ b/tests/basic-usage.test.css @@ -730,29 +730,27 @@ font-style: normal; } .ordinal, -.slashed-zero, -.lining-nums, -.oldstyle-nums, -.proportional-nums, .tabular-nums, -.diagonal-fractions, -.stacked-fractions { +.diagonal-fractions { --tw-ordinal: var(--tw-empty, /*!*/ /*!*/); --tw-slashed-zero: var(--tw-empty, /*!*/ /*!*/); --tw-numeric-figure: var(--tw-empty, /*!*/ /*!*/); --tw-numeric-spacing: var(--tw-empty, /*!*/ /*!*/); --tw-numeric-fraction: var(--tw-empty, /*!*/ /*!*/); - font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) + --tw-font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction); } .ordinal { --tw-ordinal: ordinal; + font-variant-numeric: var(--tw-font-variant-numeric); } .tabular-nums { --tw-numeric-spacing: tabular-nums; + font-variant-numeric: var(--tw-font-variant-numeric); } .diagonal-fractions { --tw-numeric-fraction: diagonal-fractions; + font-variant-numeric: var(--tw-font-variant-numeric); } .leading-relaxed { line-height: 1.625; diff --git a/tests/kitchen-sink.test.css b/tests/kitchen-sink.test.css index 6f32b2117872..7fb1e7de0b89 100644 --- a/tests/kitchen-sink.test.css +++ b/tests/kitchen-sink.test.css @@ -198,22 +198,10 @@ div { } } .test-apply-font-variant { - --tw-ordinal: var(--tw-empty, /*!*/ /*!*/); - --tw-slashed-zero: var(--tw-empty, /*!*/ /*!*/); - --tw-numeric-figure: var(--tw-empty, /*!*/ /*!*/); - --tw-numeric-spacing: var(--tw-empty, /*!*/ /*!*/); - --tw-numeric-fraction: var(--tw-empty, /*!*/ /*!*/); - font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) - var(--tw-numeric-spacing) var(--tw-numeric-fraction); - --tw-ordinal: var(--tw-empty, /*!*/ /*!*/); - --tw-slashed-zero: var(--tw-empty, /*!*/ /*!*/); - --tw-numeric-figure: var(--tw-empty, /*!*/ /*!*/); - --tw-numeric-spacing: var(--tw-empty, /*!*/ /*!*/); - --tw-numeric-fraction: var(--tw-empty, /*!*/ /*!*/); - font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) - var(--tw-numeric-spacing) var(--tw-numeric-fraction); --tw-ordinal: ordinal; + font-variant-numeric: var(--tw-font-variant-numeric); --tw-numeric-spacing: tabular-nums; + font-variant-numeric: var(--tw-font-variant-numeric); } .custom-component { background: #123456; @@ -267,6 +255,16 @@ div { .font-medium { font-weight: 500; } +.test-apply-font-variant, +.sm\:tabular-nums { + --tw-ordinal: var(--tw-empty, /*!*/ /*!*/); + --tw-slashed-zero: var(--tw-empty, /*!*/ /*!*/); + --tw-numeric-figure: var(--tw-empty, /*!*/ /*!*/); + --tw-numeric-spacing: var(--tw-empty, /*!*/ /*!*/); + --tw-numeric-fraction: var(--tw-empty, /*!*/ /*!*/); + --tw-font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) + var(--tw-numeric-spacing) var(--tw-numeric-fraction); +} .shadow-sm { --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), @@ -427,24 +425,9 @@ div { .sm\:text-center { text-align: center; } - .sm\:ordinal, - .sm\:slashed-zero, - .sm\:lining-nums, - .sm\:oldstyle-nums, - .sm\:proportional-nums, - .sm\:tabular-nums, - .sm\:diagonal-fractions, - .sm\:stacked-fractions { - --tw-ordinal: var(--tw-empty, /*!*/ /*!*/); - --tw-slashed-zero: var(--tw-empty, /*!*/ /*!*/); - --tw-numeric-figure: var(--tw-empty, /*!*/ /*!*/); - --tw-numeric-spacing: var(--tw-empty, /*!*/ /*!*/); - --tw-numeric-fraction: var(--tw-empty, /*!*/ /*!*/); - font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) - var(--tw-numeric-spacing) var(--tw-numeric-fraction); - } .sm\:tabular-nums { --tw-numeric-spacing: tabular-nums; + font-variant-numeric: var(--tw-font-variant-numeric); } .sm\:custom-util { background: #abcdef; diff --git a/tests/raw-content.test.css b/tests/raw-content.test.css index 75ea33b87906..79c3cb495749 100644 --- a/tests/raw-content.test.css +++ b/tests/raw-content.test.css @@ -513,29 +513,27 @@ font-style: normal; } .ordinal, -.slashed-zero, -.lining-nums, -.oldstyle-nums, -.proportional-nums, .tabular-nums, -.diagonal-fractions, -.stacked-fractions { +.diagonal-fractions { --tw-ordinal: var(--tw-empty, /*!*/ /*!*/); --tw-slashed-zero: var(--tw-empty, /*!*/ /*!*/); --tw-numeric-figure: var(--tw-empty, /*!*/ /*!*/); --tw-numeric-spacing: var(--tw-empty, /*!*/ /*!*/); --tw-numeric-fraction: var(--tw-empty, /*!*/ /*!*/); - font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) + --tw-font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction); } .ordinal { --tw-ordinal: ordinal; + font-variant-numeric: var(--tw-font-variant-numeric); } .tabular-nums { --tw-numeric-spacing: tabular-nums; + font-variant-numeric: var(--tw-font-variant-numeric); } .diagonal-fractions { --tw-numeric-fraction: diagonal-fractions; + font-variant-numeric: var(--tw-font-variant-numeric); } .leading-relaxed { line-height: 1.625; From f764948fca48f0a59ba83a4a645ec843b72e25dd Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 16 Oct 2021 23:03:11 +0200 Subject: [PATCH 13/15] fix typo --- tests/arbitrary-values.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/arbitrary-values.test.js b/tests/arbitrary-values.test.js index cde5846382d6..7b0b455bf5c8 100644 --- a/tests/arbitrary-values.test.js +++ b/tests/arbitrary-values.test.js @@ -198,7 +198,7 @@ it('should not convert escaped underscores with spaces', () => { }) }) -it('should warn and not generate if arbitrary values are ambigu', () => { +it('should warn and not generate if arbitrary values are ambiguous', () => { // If we don't protect against this, then `bg-[200px_100px]` would both // generate the background-size as well as the background-position utilities. let config = { From e36abdd581391513d1cc470a75e67bd09cb1c40e Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sun, 17 Oct 2021 12:11:48 +0200 Subject: [PATCH 14/15] allow for nested addVariant shorthand This will allow to write something like: ```js addVariant('name', ` @supports (hover: hover) { @media (print) { &:hover } } `) // Or as a one-liner addVariant('name', '@supports (hover: hover) { @media (print) { &:hover } }') ``` --- src/lib/setupContextUtils.js | 54 ++++++++++++++++++++++++++++++++---- tests/variants.test.js | 27 ++++++++++++++++++ 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index 645c984a16f5..8935e2e0f5b8 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -19,6 +19,35 @@ import { toPath } from '../util/toPath' import log from '../util/log' import negateValue from '../util/negateValue' +function parseVariantFormatString(input) { + if (input.includes('{')) { + if (!isBalanced(input)) throw new Error(`Your { and } are unbalanced.`) + + return input + .split(/{(.*)}/gim) + .flatMap((line) => parseVariantFormatString(line)) + .filter(Boolean) + } + + return [input.trim()] +} + +function isBalanced(input) { + let count = 0 + + for (let char of input) { + if (char === '{') { + count++ + } else if (char === '}') { + if (--count < 0) { + return false // unbalanced + } + } + } + + return count === 0 +} + function insertInto(list, value, { before = [] } = {}) { before = [].concat(before) @@ -191,12 +220,27 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs return variantFunction } - if (!variantFunction.startsWith('@')) { - return ({ format }) => format(variantFunction) - } + variantFunction = variantFunction + .replace(/\n+/g, '') + .replace(/\s{1,}/g, ' ') + .trim() + + let fns = parseVariantFormatString(variantFunction) + .map((str) => { + if (!str.startsWith('@')) { + return ({ format }) => format(str) + } + + let [, name, params] = /@(.*?) (\(.*\))/g.exec(str) + return ({ wrap }) => wrap(postcss.atRule({ name, params })) + }) + .reverse() - let [, name, params] = /@(.*?) (\(.*\))/g.exec(variantFunction) - return ({ wrap }) => wrap(postcss.atRule({ name, params })) + return (api) => { + for (let fn of fns) { + fn(api) + } + } }) insertInto(variantList, variantName, options) diff --git a/tests/variants.test.js b/tests/variants.test.js index cba5b8f13538..f38c3919d879 100644 --- a/tests/variants.test.js +++ b/tests/variants.test.js @@ -244,3 +244,30 @@ it('should properly handle keyframes with multiple variants', async () => { } `) }) + +test('custom addVariant with nested media & format shorthand', () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + plugins: [ + function ({ addVariant }) { + addVariant('magic', '@supports (hover: hover) { @media (print) { &:disabled } }') + }, + ], + } + + return run('@tailwind components;@tailwind utilities', config).then((result) => { + return expect(result.css).toMatchFormattedCss(css` + @supports (hover: hover) { + @media (print) { + .magic\\:text-center:disabled { + text-align: center; + } + } + } + `) + }) +}) From fb05ee29cc02a89bb6554b5633735c1b9b9f98e1 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 18 Oct 2021 11:22:52 +0200 Subject: [PATCH 15/15] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4156c040fb19..e2a7a270721b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Don't use pointer cursor on disabled buttons by default ([#5772](https://github.com/tailwindlabs/tailwindcss/pull/5772)) +- Improve `addVariant` API ([#5809](https://github.com/tailwindlabs/tailwindcss/pull/5809)) ### Added