mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Improve addVariant API (#5809)
* fix incorrect comment
Probably messed this up in another PR, so just a bit of cleaning.
* 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 ...`
* add new `format`, `withRule` and `wrap` API for addVariant
* 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?
* 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.
* 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`
* add more variant tests for the new API
* update parallel variants tests to make use of new API
* implement core variants with new API
* simplify/cleanup existing plugin utils
We can get rid of this because we drastically simplified the new
addVariant API.
* 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)')
```
* improve fontVariantNumeric implementation
We will use `@defaults`, so that only the resets are injected for the
utilities we actually use.
* fix typo
* 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 } }')
```
* update changelog
This commit is contained in:
parent
aeaa2a376b
commit
5809c4d07c
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
})
|
||||
|
||||
return `${variantSelector} *::selection`
|
||||
}),
|
||||
transformAllSelectors((selector) => {
|
||||
return updateAllClasses(selector, (className, { withPseudo }) => {
|
||||
return withPseudo(`selection${config('separator')}${className}`, '::selection')
|
||||
})
|
||||
}),
|
||||
])
|
||||
|
||||
addVariant(
|
||||
'file',
|
||||
transformAllSelectors((selector) => {
|
||||
return updateAllClasses(selector, (className, { withPseudo }) => {
|
||||
return withPseudo(`file${config('separator')}${className}`, '::file-selector-button')
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
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: '""' }))
|
||||
}
|
||||
},
|
||||
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: '""' }))
|
||||
}
|
||||
},
|
||||
// TODO: Use `addVariant('after', '*::after')` instead, once `content`
|
||||
// fix is implemented.
|
||||
addVariant('after', ({ format, withRule }) => {
|
||||
format('&::after')
|
||||
|
||||
withRule((rule) => {
|
||||
let foundContent = false
|
||||
rule.walkDecls('content', () => {
|
||||
foundContent = true
|
||||
})
|
||||
|
||||
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',
|
||||
]
|
||||
].map((variant) => (Array.isArray(variant) ? variant : [variant, `:${variant}`]))
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
)
|
||||
for (let [variantName, state] of pseudoVariants) {
|
||||
addVariant(variantName, `&${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
|
||||
}
|
||||
|
||||
return applyStateToMarker(
|
||||
variantSelector,
|
||||
groupMarker,
|
||||
state,
|
||||
(marker, selector) => `${marker} ${selector}`
|
||||
)
|
||||
})
|
||||
)
|
||||
for (let [variantName, state] of pseudoVariants) {
|
||||
addVariant(`group-${variantName}`, `:merge(.group)${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
|
||||
}
|
||||
|
||||
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"] &')
|
||||
})
|
||||
|
||||
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.',
|
||||
])
|
||||
|
||||
format('[dir="rtl"] &')
|
||||
})
|
||||
},
|
||||
|
||||
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(
|
||||
'motion-reduce',
|
||||
transformLastClasses(
|
||||
(className) => {
|
||||
return `motion-reduce${config('separator')}${className}`
|
||||
},
|
||||
{
|
||||
wrap: () =>
|
||||
postcss.atRule({
|
||||
name: 'media',
|
||||
params: '(prefers-reduced-motion: reduce)',
|
||||
}),
|
||||
}
|
||||
)
|
||||
)
|
||||
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}`)
|
||||
}
|
||||
},
|
||||
}
|
||||
@ -1745,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)',
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,7 +113,17 @@ function applyVariant(variant, matches, context) {
|
||||
|
||||
for (let [variantSort, variantFunction] of variantFunctionTuples) {
|
||||
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
|
||||
@ -127,20 +138,80 @@ function applyVariant(variant, matches, context) {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
let ruleWithVariant = variantFunction({
|
||||
container: clone,
|
||||
get container() {
|
||||
prepareBackup()
|
||||
return 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]]
|
||||
// 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,
|
||||
sort: variantSort | meta.sort,
|
||||
collectedFormats: (meta.collectedFormats ?? []).concat(collectedFormats),
|
||||
},
|
||||
clone.nodes[0],
|
||||
]
|
||||
result.push(withOffset)
|
||||
}
|
||||
}
|
||||
@ -323,6 +394,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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -186,7 +215,33 @@ 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
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
return (api) => {
|
||||
for (let fn of fns) {
|
||||
fn(api)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
insertInto(variantList, variantName, options)
|
||||
variantMap.set(variantName, variantFunctions)
|
||||
|
||||
105
src/util/formatVariantSelector.js
Normal file
105
src/util/formatVariantSelector.js
Normal file
@ -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
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
261
tests/format-variant-selector.test.js
Normal file
261
tests/format-variant-selector.test.js
Normal file
@ -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)'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
@ -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),
|
||||
@ -352,7 +350,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 {
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
`)
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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'])
|
||||
},
|
||||
],
|
||||
}
|
||||
@ -42,7 +27,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 +36,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;
|
||||
}
|
||||
`)
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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`
|
||||
<div class="hover:file:bg-pink-600"></div>
|
||||
<div class="file:hover:bg-pink-600"></div>
|
||||
`,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
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` <div class="prose-headings:text-center"></div> `,
|
||||
},
|
||||
],
|
||||
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`
|
||||
<div class="hover:prose-headings:text-center"></div>
|
||||
<div class="prose-headings:hover:text-center"></div>
|
||||
`,
|
||||
},
|
||||
],
|
||||
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`
|
||||
<div class="group-hover:prose-headings:text-center"></div>
|
||||
<div class="prose-headings:group-hover:text-center"></div>
|
||||
`,
|
||||
},
|
||||
],
|
||||
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' }],
|
||||
@ -126,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` <div class="magic:text-center"></div> `,
|
||||
},
|
||||
],
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user