Restore old behavior for class dark mode, add new selector and variant options for dark mode (#12717)

* Add dark mode variant option

* Tweak warning messages

* Add legacy dark mode option

* wip

* Use `class` for legacy behavior, `selector` for new behavior

* Add simplified failing apply/where test case

* Switch to `where` list, apply changes to `dir` variants

* Don’t let `:where`, `:is:`, or `:has` be attached to pseudo elements

* Updating tests...

* Finish updating tests

* Remove `variant` dark mode strategy

* Update types

* Update comments

* Update changelog

* Revert "Remove `variant` dark mode strategy"

This reverts commit 185250438ccb2f61ba876d4676823c1807891122.

* Add variant back to types

* wip

* Update comments

* Update tests

* Rename variable

* Update changelog

* Update changelog

* Update changelog

* Fix CS

---------

Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
This commit is contained in:
Jordan Pittman 2024-01-05 14:39:34 -05:00
parent 78fedd5cc0
commit 3fb57e55ab
20 changed files with 446 additions and 134 deletions

View File

@ -11,6 +11,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Don't remove keyframe stops when using important utilities ([#12639](https://github.com/tailwindlabs/tailwindcss/pull/12639))
- Don't add spaces to gradients and grid track names when followed by `calc()` ([#12704](https://github.com/tailwindlabs/tailwindcss/pull/12704))
- Restore old behavior for `class` dark mode strategy ([#12717](https://github.com/tailwindlabs/tailwindcss/pull/12717))
### Added
- Add new `selector` and `variant` strategies for dark mode ([#12717](https://github.com/tailwindlabs/tailwindcss/pull/12717))
### Changed
- Support `rtl` and `ltr` variants on same element as `dir` attribute ([#12717](https://github.com/tailwindlabs/tailwindcss/pull/12717))
## [3.4.0] - 2023-12-19

View File

@ -207,8 +207,8 @@ export let variantPlugins = {
},
directionVariants: ({ addVariant }) => {
addVariant('ltr', ':is(:where([dir="ltr"]) &)')
addVariant('rtl', ':is(:where([dir="rtl"]) &)')
addVariant('ltr', '&:where([dir="ltr"], [dir="ltr"] *)')
addVariant('rtl', '&:where([dir="rtl"], [dir="rtl"] *)')
},
reducedMotionVariants: ({ addVariant }) => {
@ -217,7 +217,7 @@ export let variantPlugins = {
},
darkVariants: ({ config, addVariant }) => {
let [mode, className = '.dark'] = [].concat(config('darkMode', 'media'))
let [mode, selector = '.dark'] = [].concat(config('darkMode', 'media'))
if (mode === false) {
mode = 'media'
@ -228,10 +228,49 @@ export let variantPlugins = {
])
}
if (mode === 'class') {
addVariant('dark', `:is(:where(${className}) &)`)
if (mode === 'variant') {
let formats
if (Array.isArray(selector)) {
formats = selector
} else if (typeof selector === 'function') {
formats = selector
} else if (typeof selector === 'string') {
formats = [selector]
}
// TODO: We could also add these warnings if the user passes a function that returns string | string[]
// But this is an advanced enough use case that it's probably not necessary
if (Array.isArray(formats)) {
for (let format of formats) {
if (format === '.dark') {
mode = false
log.warn('darkmode-variant-without-selector', [
'When using `variant` for `darkMode`, you must provide a selector.',
'Example: `darkMode: ["variant", ".your-selector &"]`',
])
} else if (!format.includes('&')) {
mode = false
log.warn('darkmode-variant-without-ampersand', [
'When using `variant` for `darkMode`, your selector must contain `&`.',
'Example `darkMode: ["variant", ".your-selector &"]`',
])
}
}
}
selector = formats
}
if (mode === 'selector') {
// New preferred behavior
addVariant('dark', `&:where(${selector}, ${selector} *)`)
} else if (mode === 'media') {
addVariant('dark', '@media (prefers-color-scheme: dark)')
} else if (mode === 'variant') {
addVariant('dark', selector)
} else if (mode === 'class') {
// Old behavior
addVariant('dark', `:is(${selector} &)`)
}
},

View File

@ -767,14 +767,35 @@ function resolvePlugins(context, root) {
variantPlugins['supportsVariants'],
variantPlugins['reducedMotionVariants'],
variantPlugins['prefersContrastVariants'],
variantPlugins['printVariant'],
variantPlugins['screenVariants'],
variantPlugins['orientationVariants'],
variantPlugins['directionVariants'],
variantPlugins['darkVariants'],
variantPlugins['forcedColorsVariants'],
variantPlugins['printVariant'],
]
// This is a compatibility fix for the pre 3.4 dark mode behavior
// `class` retains the old behavior, but `selector` keeps the new behavior
let isLegacyDarkMode =
context.tailwindConfig.darkMode === 'class' ||
(Array.isArray(context.tailwindConfig.darkMode) &&
context.tailwindConfig.darkMode[0] === 'class')
if (isLegacyDarkMode) {
afterVariants = [
variantPlugins['supportsVariants'],
variantPlugins['reducedMotionVariants'],
variantPlugins['prefersContrastVariants'],
variantPlugins['darkVariants'],
variantPlugins['screenVariants'],
variantPlugins['orientationVariants'],
variantPlugins['directionVariants'],
variantPlugins['forcedColorsVariants'],
variantPlugins['printVariant'],
]
}
return [...corePluginList, ...beforeVariants, ...userPlugins, ...afterVariants, ...layerPlugins]
}

View File

@ -60,6 +60,10 @@ let elementProperties = {
':first-letter': ['terminal', 'jumpable'],
':first-line': ['terminal', 'jumpable'],
':where': [],
':is': [],
':has': [],
// The default value is used when the pseudo-element is not recognized
// Because it's not recognized, we don't know if it's terminal or not
// So we assume it can be moved AND can have user-action pseudo classes attached to it

View File

@ -35,7 +35,7 @@ crosscheck(({ stable, oxide }) => {
test('@apply', () => {
let config = {
darkMode: 'class',
darkMode: 'selector',
content: [{ raw: sharedHtml }],
}
@ -216,14 +216,14 @@ crosscheck(({ stable, oxide }) => {
text-align: left;
}
}
:is(:where(.dark) .apply-dark-variant) {
.apply-dark-variant:where(.dark, .dark *) {
text-align: center;
}
:is(:where(.dark) .apply-dark-variant:hover) {
.apply-dark-variant:hover:where(.dark, .dark *) {
text-align: right;
}
@media (min-width: 1024px) {
:is(:where(.dark) .apply-dark-variant) {
.apply-dark-variant:where(.dark, .dark *) {
text-align: left;
}
}
@ -513,14 +513,14 @@ crosscheck(({ stable, oxide }) => {
text-align: left;
}
}
:is(:where(.dark) .apply-dark-variant) {
.apply-dark-variant:where(.dark, .dark *) {
text-align: center;
}
:is(:where(.dark) .apply-dark-variant:hover) {
.apply-dark-variant:hover:where(.dark, .dark *) {
text-align: right;
}
@media (min-width: 1024px) {
:is(:where(.dark) .apply-dark-variant) {
.apply-dark-variant:where(.dark, .dark *) {
text-align: left;
}
}
@ -755,7 +755,7 @@ crosscheck(({ stable, oxide }) => {
test('@apply error with unknown utility', async () => {
let config = {
darkMode: 'class',
darkMode: 'selector',
content: [{ raw: sharedHtml }],
}
@ -775,7 +775,7 @@ crosscheck(({ stable, oxide }) => {
test('@apply error with nested @screen', async () => {
let config = {
darkMode: 'class',
darkMode: 'selector',
content: [{ raw: sharedHtml }],
}
@ -799,7 +799,7 @@ crosscheck(({ stable, oxide }) => {
test('@apply error with nested @anyatrulehere', async () => {
let config = {
darkMode: 'class',
darkMode: 'selector',
content: [{ raw: sharedHtml }],
}
@ -823,7 +823,7 @@ crosscheck(({ stable, oxide }) => {
test('@apply error when using .group utility', async () => {
let config = {
darkMode: 'class',
darkMode: 'selector',
content: [{ raw: '<div class="foo"></div>' }],
}
@ -846,7 +846,7 @@ crosscheck(({ stable, oxide }) => {
test('@apply error when using a prefixed .group utility', async () => {
let config = {
prefix: 'tw-',
darkMode: 'class',
darkMode: 'selector',
content: [{ raw: html`<div class="foo"></div>` }],
}
@ -868,7 +868,7 @@ crosscheck(({ stable, oxide }) => {
test('@apply error when using .peer utility', async () => {
let config = {
darkMode: 'class',
darkMode: 'selector',
content: [{ raw: '<div class="foo"></div>' }],
}
@ -891,7 +891,7 @@ crosscheck(({ stable, oxide }) => {
test('@apply error when using a prefixed .peer utility', async () => {
let config = {
prefix: 'tw-',
darkMode: 'class',
darkMode: 'selector',
content: [{ raw: html`<div class="foo"></div>` }],
}
@ -2360,7 +2360,7 @@ crosscheck(({ stable, oxide }) => {
it('pseudo elements inside apply are moved outside of :is() or :has()', () => {
let config = {
darkMode: 'class',
darkMode: 'selector',
content: [
{
raw: html` <div class="foo bar baz qux steve bob"></div> `,
@ -2404,18 +2404,18 @@ crosscheck(({ stable, oxide }) => {
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
:is(:where(.dark) .foo)::before,
:is(:where([dir='rtl']) :is(:where(.dark) .bar))::before,
:is(:where([dir='rtl']) :is(:where(.dark) .baz:hover))::before {
.foo:where(.dark, .dark *)::before,
.bar:where(.dark, .dark *):where([dir='rtl'], [dir='rtl'] *)::before,
.baz:hover:where(.dark, .dark *):where([dir='rtl'], [dir='rtl'] *)::before {
background-color: #000;
}
:is(:where([dir='rtl']) :is(:where(.dark) .qux))::file-selector-button:hover {
.qux:where(.dark, .dark *):where([dir='rtl'], [dir='rtl'] *)::file-selector-button:hover {
background-color: #000;
}
:is(:where([dir='rtl']) :is(:where(.dark) .steve):hover):before {
.steve:where(.dark, .dark *):hover:where([dir='rtl'], [dir='rtl'] *):before {
background-color: #000;
}
:is(:where([dir='rtl']) :is(:where(.dark) .bob))::file-selector-button:hover {
.bob:where(.dark, .dark *):hover:where([dir='rtl'], [dir='rtl'] *)::file-selector-button {
background-color: #000;
}
:has([dir='rtl'] .foo:hover):before {
@ -2430,7 +2430,7 @@ crosscheck(({ stable, oxide }) => {
stable.test('::ng-deep, ::deep, ::v-deep pseudo elements are left alone', () => {
let config = {
darkMode: 'class',
darkMode: 'selector',
content: [
{
raw: html` <div class="foo bar"></div> `,

View File

@ -3,7 +3,7 @@ import { crosscheck, run, html, css } from './util/run'
crosscheck(() => {
test('custom separator', () => {
let config = {
darkMode: 'class',
darkMode: 'selector',
content: [
{
raw: html`
@ -33,10 +33,10 @@ crosscheck(() => {
text-align: right;
}
}
:is(:where([dir='rtl']) .rtl_active_text-center:active) {
.rtl_active_text-center:active:where([dir='rtl'], [dir='rtl'] *) {
text-align: center;
}
:is(:where(.dark) .dark_focus_text-left:focus) {
.dark_focus_text-left:focus:where(.dark, .dark *) {
text-align: left;
}
`)
@ -45,7 +45,7 @@ crosscheck(() => {
test('dash is not supported', () => {
let config = {
darkMode: 'class',
darkMode: 'selector',
content: [{ raw: 'lg-hover-font-bold' }],
separator: '-',
}

View File

@ -1,6 +1,6 @@
import { crosscheck, run, html, css, defaults } from './util/run'
crosscheck(() => {
crosscheck(({ oxide, stable }) => {
it('should be possible to use the darkMode "class" mode', () => {
let config = {
darkMode: 'class',
@ -17,7 +17,7 @@ crosscheck(() => {
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
:is(:where(.dark) .dark\:font-bold) {
:is(.dark .dark\:font-bold) {
font-weight: 700;
}
`)
@ -40,7 +40,7 @@ crosscheck(() => {
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
:is(:where(.test-dark) .dark\:font-bold) {
:is(.test-dark .dark\:font-bold) {
font-weight: 700;
}
`)
@ -120,4 +120,202 @@ crosscheck(() => {
`)
})
})
it('should support the deprecated `class` dark mode behavior', () => {
let config = {
darkMode: 'class',
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
:is(.dark .dark\:font-bold) {
font-weight: 700;
}
`)
})
})
it('should support custom classes with deprecated `class` dark mode', () => {
let config = {
darkMode: ['class', '.my-dark'],
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
:is(.my-dark .dark\:font-bold) {
font-weight: 700;
}
`)
})
})
it('should use legacy sorting when using `darkMode: class`', () => {
let config = {
darkMode: 'class',
content: [
{
raw: html`<div class="dark:text-green-100 hover:text-green-200 lg:text-green-300"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind utilities;
`
return run(input, config).then((result) => {
stable.expect(result.css).toMatchFormattedCss(css`
.hover\:text-green-200:hover {
--tw-text-opacity: 1;
color: rgb(187 247 208 / var(--tw-text-opacity));
}
:is(.dark .dark\:text-green-100) {
--tw-text-opacity: 1;
color: rgb(220 252 231 / var(--tw-text-opacity));
}
@media (min-width: 1024px) {
.lg\:text-green-300 {
--tw-text-opacity: 1;
color: rgb(134 239 172 / var(--tw-text-opacity));
}
}
`)
oxide.expect(result.css).toMatchFormattedCss(css`
.hover\:text-green-200:hover {
color: #bbf7d0;
}
:is(.dark .dark\:text-green-100) {
color: #dcfce7;
}
@media (min-width: 1024px) {
.lg\:text-green-300 {
color: #86efac;
}
}
`)
})
})
it('should use modern sorting otherwise', () => {
let config = {
darkMode: 'selector',
content: [
{
raw: html`<div class="dark:text-green-100 hover:text-green-200 lg:text-green-300"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind utilities;
`
return run(input, config).then((result) => {
stable.expect(result.css).toMatchFormattedCss(css`
.hover\:text-green-200:hover {
--tw-text-opacity: 1;
color: rgb(187 247 208 / var(--tw-text-opacity));
}
@media (min-width: 1024px) {
.lg\:text-green-300 {
--tw-text-opacity: 1;
color: rgb(134 239 172 / var(--tw-text-opacity));
}
}
.dark\:text-green-100:where(.dark, .dark *) {
--tw-text-opacity: 1;
color: rgb(220 252 231 / var(--tw-text-opacity));
}
`)
oxide.expect(result.css).toMatchFormattedCss(css`
.hover\:text-green-200:hover {
color: #bbf7d0;
}
@media (min-width: 1024px) {
.lg\:text-green-300 {
color: #86efac;
}
}
.dark\:text-green-100:where(.dark, .dark *) {
color: #dcfce7;
}
`)
})
})
it('should allow customization of the dark mode variant', () => {
let config = {
darkMode: ['variant', '&:not(.light *)'],
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.dark\:font-bold:not(.light *) {
font-weight: 700;
}
`)
})
})
it('should support parallel selectors for the dark mode variant', () => {
let config = {
darkMode: ['variant', ['&:not(.light *)', '&:not(.extralight *)']],
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.dark\:font-bold:not(.light *),
.dark\:font-bold:not(.extralight *) {
font-weight: 700;
}
`)
})
})
it('should support fn selectors for the dark mode variant', () => {
let config = {
darkMode: ['variant', () => ['&:not(.light *)', '&:not(.extralight *)']],
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.dark\:font-bold:not(.light *),
.dark\:font-bold:not(.extralight *) {
font-weight: 700;
}
`)
})
})
})

View File

@ -8,7 +8,7 @@ crosscheck(() => {
test('important boolean', () => {
let config = {
important: true,
darkMode: 'class',
darkMode: 'selector',
content: [
{
raw: html`
@ -148,10 +148,10 @@ crosscheck(() => {
text-align: right !important;
}
}
:is(:where([dir='rtl']) .rtl\:active\:text-center:active) {
.rtl\:active\:text-center:active:where([dir='rtl'], [dir='rtl'] *) {
text-align: center !important;
}
:is(:where(.dark) .dark\:focus\:text-left:focus) {
.dark\:focus\:text-left:focus:where(.dark, .dark *) {
text-align: left !important;
}
`)

View File

@ -5,7 +5,7 @@ crosscheck(() => {
let config = {
important: false,
prefix: 'tw-',
darkMode: 'class',
darkMode: 'selector',
content: [
{
raw: html`<!-- The string "!*" can cause problems if we don't handle it, let's include it -->

View File

@ -4,7 +4,7 @@ crosscheck(() => {
test('important modifier', () => {
let config = {
important: false,
darkMode: 'class',
darkMode: 'selector',
content: [
{
raw: html`

View File

@ -4,7 +4,7 @@ crosscheck(({ stable, oxide }) => {
test('important selector', () => {
let config = {
important: '#app',
darkMode: 'class',
darkMode: 'selector',
content: [
{
raw: html`
@ -146,19 +146,22 @@ crosscheck(({ stable, oxide }) => {
text-align: right;
}
}
#app :is(:is(:where([dir='rtl']) .rtl\:active\:text-center:active)) {
#app :is(.rtl\:active\:text-center:active:where([dir='rtl'], [dir='rtl'] *)) {
text-align: center;
}
#app :is(:where(.dark) .dark\:before\:underline):before {
#app :is(.dark\:before\:underline:where(.dark, .dark *)):before {
content: var(--tw-content);
text-decoration-line: underline;
}
#app :is(:is(:where(.dark) .dark\:focus\:text-left:focus)) {
#app :is(.dark\:focus\:text-left:focus:where(.dark, .dark *)) {
text-align: left;
}
#app
:is(
:where([dir='rtl']) :is(:where(.dark) .hover\:\[\&\:\:file-selector-button\]\:rtl\:dark\:bg-black\/100)
.hover\:\[\&\:\:file-selector-button\]\:rtl\:dark\:bg-black\/100:where(
.dark,
.dark *
):where([dir='rtl'], [dir='rtl'] *)
)::file-selector-button:hover {
background-color: #000;
}
@ -169,7 +172,7 @@ crosscheck(({ stable, oxide }) => {
test('pseudo-elements are appended after the `:is()`', () => {
let config = {
important: '#app',
darkMode: 'class',
darkMode: 'selector',
content: [
{
raw: html` <div class="dark:before:bg-black"></div> `,
@ -187,7 +190,7 @@ crosscheck(({ stable, oxide }) => {
return run(input, config).then((result) => {
stable.expect(result.css).toMatchFormattedCss(css`
${defaults}
#app :is(:where(.dark) .dark\:before\:bg-black)::before {
#app .dark\:before\:bg-black:where(.dark, .dark *)::before {
content: var(--tw-content);
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
@ -195,7 +198,7 @@ crosscheck(({ stable, oxide }) => {
`)
oxide.expect(result.css).toMatchFormattedCss(css`
${defaults}
#app :is(:where(.dark) .dark\:before\:bg-black)::before {
#app .dark\:before\:bg-black:where(.dark, .dark *)::before {
content: var(--tw-content);
background-color: #000;
}

View File

@ -3,7 +3,7 @@ import { crosscheck, run, html, css, defaults } from './util/run'
crosscheck(({ stable, oxide }) => {
test('it works', () => {
let config = {
darkMode: 'class',
darkMode: 'selector',
content: [
{
raw: html`
@ -304,8 +304,10 @@ crosscheck(({ stable, oxide }) => {
margin-right: auto;
}
.drop-empty-rules:hover,
.group:hover .apply-group,
:is(:where(.dark) .apply-dark-mode) {
.group:hover .apply-group {
font-weight: 700;
}
.apply-dark-mode:where(.dark, .dark *) {
font-weight: 700;
}
.apply-with-existing:hover {
@ -340,7 +342,7 @@ crosscheck(({ stable, oxide }) => {
.apply-order-b {
margin: 1.5rem 1.25rem 1.25rem;
}
:is(:where(.dark) .group:hover .apply-dark-group-example-a) {
.group:hover .apply-dark-group-example-a:where(.dark, .dark *) {
--tw-bg-opacity: 1;
background-color: rgb(34 197 94 / var(--tw-bg-opacity));
}
@ -802,12 +804,12 @@ crosscheck(({ stable, oxide }) => {
text-align: left;
}
}
:is(:where(.dark) .dark\:custom-util) {
.dark\:custom-util:where(.dark, .dark *) {
background: #abcdef;
}
@media (min-width: 768px) {
@media (prefers-reduced-motion: no-preference) {
:is(:where(.dark) .md\:dark\:motion-safe\:foo\:active\:custom-util:active) {
.md\:dark\:motion-safe\:foo\:active\:custom-util:active:where(.dark, .dark *) {
background: #abcdef !important;
}
}
@ -877,8 +879,10 @@ crosscheck(({ stable, oxide }) => {
margin-right: auto;
}
.drop-empty-rules:hover,
.group:hover .apply-group,
:is(:where(.dark) .apply-dark-mode) {
.group:hover .apply-group {
font-weight: 700;
}
.apply-dark-mode:where(.dark, .dark *) {
font-weight: 700;
}
.apply-with-existing:hover {
@ -912,7 +916,7 @@ crosscheck(({ stable, oxide }) => {
.apply-order-b {
margin: 1.5rem 1.25rem 1.25rem;
}
:is(:where(.dark) .group:hover .apply-dark-group-example-a) {
.group:hover .apply-dark-group-example-a:where(.dark, .dark *) {
background-color: #22c55e;
}
@media (min-width: 640px) {
@ -1364,12 +1368,12 @@ crosscheck(({ stable, oxide }) => {
text-align: left;
}
}
:is(:where(.dark) .dark\:custom-util) {
.dark\:custom-util:where(.dark, .dark *) {
background: #abcdef;
}
@media (min-width: 768px) {
@media (prefers-reduced-motion: no-preference) {
:is(:where(.dark) .md\:dark\:motion-safe\:foo\:active\:custom-util:active) {
.md\:dark\:motion-safe\:foo\:active\:custom-util:active:where(.dark, .dark *) {
background: #abcdef !important;
}
}

View File

@ -5,7 +5,7 @@ import { crosscheck, run, html, css } from './util/run'
crosscheck(() => {
test('modify selectors', () => {
let config = {
darkMode: 'class',
darkMode: 'selector',
content: [
{
raw: html`

View File

@ -3,7 +3,7 @@ import { crosscheck, run, html, css } from './util/run'
crosscheck(({ stable, oxide }) => {
test('opacity', () => {
let config = {
darkMode: 'class',
darkMode: 'selector',
content: [
{
raw: html`
@ -43,7 +43,7 @@ crosscheck(({ stable, oxide }) => {
test('colors defined as functions work when opacity plugins are disabled', () => {
let config = {
darkMode: 'class',
darkMode: 'selector',
content: [
{
raw: html`

View File

@ -5,7 +5,7 @@ crosscheck(({ stable, oxide }) => {
stable.test('prefix', () => {
let config = {
prefix: 'tw-',
darkMode: 'class',
darkMode: 'selector',
content: [
{
raw: html`
@ -128,7 +128,7 @@ crosscheck(({ stable, oxide }) => {
.custom-component {
font-weight: 700;
}
:is(:where(.tw-dark) .tw-group:hover .custom-component) {
.tw-group:hover .custom-component:where(.tw-dark, .tw-dark *) {
font-weight: 400;
}
.tw--ml-4 {
@ -171,14 +171,14 @@ crosscheck(({ stable, oxide }) => {
text-align: right;
}
}
:is(:where([dir='rtl']) .rtl\:active\:tw-text-center:active) {
.rtl\:active\:tw-text-center:active:where([dir='rtl'], [dir='rtl'] *) {
text-align: center;
}
:is(:where(.tw-dark) .dark\:tw-bg-\[rgb\(255\,0\,0\)\]) {
.dark\:tw-bg-\[rgb\(255\,0\,0\)\]:where(.tw-dark, .tw-dark *) {
--tw-bg-opacity: 1;
background-color: rgb(255 0 0 / var(--tw-bg-opacity));
}
:is(:where(.tw-dark) .dark\:focus\:tw-text-left:focus) {
.dark\:focus\:tw-text-left:focus:where(.tw-dark, .tw-dark *) {
text-align: left;
}
`)

View File

@ -3,25 +3,25 @@ import { applyImportantSelector } from '../../src/util/applyImportantSelector'
crosscheck(() => {
it.each`
before | after
${'.foo'} | ${'#app :is(.foo)'}
${'.foo .bar'} | ${'#app :is(.foo .bar)'}
${'.foo:hover'} | ${'#app :is(.foo:hover)'}
${'.foo .bar:hover'} | ${'#app :is(.foo .bar:hover)'}
${'.foo::before'} | ${'#app :is(.foo)::before'}
${'.foo::before'} | ${'#app :is(.foo)::before'}
${'.foo::file-selector-button'} | ${'#app :is(.foo)::file-selector-button'}
${'.foo::-webkit-progress-bar'} | ${'#app :is(.foo)::-webkit-progress-bar'}
${'.foo:hover::before'} | ${'#app :is(.foo:hover)::before'}
before | after
${'.foo'} | ${'#app :is(.foo)'}
${'.foo .bar'} | ${'#app :is(.foo .bar)'}
${'.foo:hover'} | ${'#app :is(.foo:hover)'}
${'.foo .bar:hover'} | ${'#app :is(.foo .bar:hover)'}
${'.foo::before'} | ${'#app :is(.foo)::before'}
${'.foo::before'} | ${'#app :is(.foo)::before'}
${'.foo::file-selector-button'} | ${'#app :is(.foo)::file-selector-button'}
${'.foo::-webkit-progress-bar'} | ${'#app :is(.foo)::-webkit-progress-bar'}
${'.foo:hover::before'} | ${'#app :is(.foo:hover)::before'}
${':is(:where(.dark) :is(:where([dir="rtl"]) .foo::before))'} | ${'#app :is(:where(.dark) :is(:where([dir="rtl"]) .foo))::before'}
${':is(:where(.dark) .foo) .bar'} | ${'#app :is(:is(:where(.dark) .foo) .bar)'}
${':is(.foo) :is(.bar)'} | ${'#app :is(:is(.foo) :is(.bar))'}
${':is(.foo)::before'} | ${'#app :is(.foo)::before'}
${'.foo:before'} | ${'#app :is(.foo):before'}
${'.foo::some-uknown-pseudo'} | ${'#app :is(.foo)::some-uknown-pseudo'}
${'.foo::some-uknown-pseudo:hover'} | ${'#app :is(.foo)::some-uknown-pseudo:hover'}
${'.foo:focus::some-uknown-pseudo:hover'} | ${'#app :is(.foo:focus)::some-uknown-pseudo:hover'}
${'.foo:hover::some-uknown-pseudo:focus'} | ${'#app :is(.foo:hover)::some-uknown-pseudo:focus'}
${':is(:where(.dark) .foo) .bar'} | ${'#app :is(:is(:where(.dark) .foo) .bar)'}
${':is(.foo) :is(.bar)'} | ${'#app :is(:is(.foo) :is(.bar))'}
${':is(.foo)::before'} | ${'#app :is(.foo)::before'}
${'.foo:before'} | ${'#app :is(.foo):before'}
${'.foo::some-uknown-pseudo'} | ${'#app :is(.foo)::some-uknown-pseudo'}
${'.foo::some-uknown-pseudo:hover'} | ${'#app :is(.foo)::some-uknown-pseudo:hover'}
${'.foo:focus::some-uknown-pseudo:hover'} | ${'#app :is(.foo:focus)::some-uknown-pseudo:hover'}
${'.foo:hover::some-uknown-pseudo:focus'} | ${'#app :is(.foo:hover)::some-uknown-pseudo:focus'}
`('should generate "$after" from "$before"', ({ before, after }) => {
expect(applyImportantSelector(before, '#app')).toEqual(after)
})

View File

@ -319,11 +319,6 @@
background-color: #fde047;
}
}
@media print {
.print\:bg-yellow-300 {
background-color: #fde047;
}
}
@media (min-width: 640px) {
.sm\:shadow-md,
.sm\:active\:shadow-md:active {
@ -389,26 +384,38 @@
background-color: #fde047;
}
}
:is(:where([dir="ltr"]) .ltr\:shadow-md),
:is(:where([dir="rtl"]) .rtl\:shadow-md),
:is(:where(.dark) .dark\:shadow-md),
:is(
:where(.dark)
.group:disabled:focus:hover
.dark\:group-disabled\:group-focus\:group-hover\:shadow-md
),
:is(
:where(.dark)
.peer:disabled:focus:hover
~ .dark\:peer-disabled\:peer-focus\:peer-hover\:shadow-md
) {
.ltr\:shadow-md:where([dir="ltr"], [dir="ltr"] *) {
--tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.rtl\:shadow-md:where([dir="rtl"], [dir="rtl"] *) {
--tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.dark\:shadow-md:where(.dark, .dark *) {
--tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.group:disabled:focus:hover .dark\:group-disabled\:group-focus\:group-hover\:shadow-md:where(.dark, .dark *) {
--tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.peer:disabled:focus:hover ~ .dark\:peer-disabled\:peer-focus\:peer-hover\:shadow-md:where(.dark, .dark *) {
--tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
@media (min-width: 1024px) {
:is(:where(.dark) .lg\:dark\:shadow-md) {
.lg\:dark\:shadow-md:where(.dark, .dark *) {
--tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color),
0 2px 4px -2px var(--tw-shadow-color);
@ -417,7 +424,7 @@
}
}
@media (min-width: 1280px) {
:is(:where(.dark) .xl\:dark\:disabled\:shadow-md:disabled) {
.xl\:dark\:disabled\:shadow-md:disabled:where(.dark, .dark *) {
--tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color),
0 2px 4px -2px var(--tw-shadow-color);
@ -427,7 +434,7 @@
}
@media (min-width: 1536px) {
@media (prefers-reduced-motion: no-preference) {
:is(:where(.dark) .\32 xl\:dark\:motion-safe\:focus-within\:shadow-md:focus-within) {
.\32 xl\:dark\:motion-safe\:focus-within\:shadow-md:focus-within:where(.dark, .dark *) {
--tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color),
0 2px 4px -2px var(--tw-shadow-color);
@ -441,3 +448,8 @@
display: flex;
}
}
@media print {
.print\:bg-yellow-300 {
background-color: #fde047;
}
}

View File

@ -337,12 +337,6 @@
background-color: rgb(253 224 71 / var(--tw-bg-opacity));
}
}
@media print {
.print\:bg-yellow-300 {
--tw-bg-opacity: 1;
background-color: rgb(253 224 71 / var(--tw-bg-opacity));
}
}
@media (min-width: 640px) {
.sm\:shadow-md,
.sm\:active\:shadow-md:active {
@ -410,26 +404,38 @@
background-color: rgb(253 224 71 / var(--tw-bg-opacity));
}
}
:is(:where([dir="ltr"]) .ltr\:shadow-md),
:is(:where([dir="rtl"]) .rtl\:shadow-md),
:is(:where(.dark) .dark\:shadow-md),
:is(
:where(.dark)
.group:disabled:focus:hover
.dark\:group-disabled\:group-focus\:group-hover\:shadow-md
),
:is(
:where(.dark)
.peer:disabled:focus:hover
~ .dark\:peer-disabled\:peer-focus\:peer-hover\:shadow-md
) {
.ltr\:shadow-md:where([dir="ltr"], [dir="ltr"] *) {
--tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.rtl\:shadow-md:where([dir="rtl"], [dir="rtl"] *) {
--tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.dark\:shadow-md:where(.dark, .dark *) {
--tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.group:disabled:focus:hover .dark\:group-disabled\:group-focus\:group-hover\:shadow-md:where(.dark, .dark *) {
--tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.peer:disabled:focus:hover ~ .dark\:peer-disabled\:peer-focus\:peer-hover\:shadow-md:where(.dark, .dark *) {
--tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
@media (min-width: 1024px) {
:is(:where(.dark) .lg\:dark\:shadow-md) {
.lg\:dark\:shadow-md:where(.dark, .dark *) {
--tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color),
0 2px 4px -2px var(--tw-shadow-color);
@ -438,7 +444,7 @@
}
}
@media (min-width: 1280px) {
:is(:where(.dark) .xl\:dark\:disabled\:shadow-md:disabled) {
.xl\:dark\:disabled\:shadow-md:disabled:where(.dark, .dark *) {
--tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color),
0 2px 4px -2px var(--tw-shadow-color);
@ -448,7 +454,7 @@
}
@media (min-width: 1536px) {
@media (prefers-reduced-motion: no-preference) {
:is(:where(.dark) .\32 xl\:dark\:motion-safe\:focus-within\:shadow-md:focus-within) {
.\32 xl\:dark\:motion-safe\:focus-within\:shadow-md:focus-within:where(.dark, .dark *) {
--tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color),
0 2px 4px -2px var(--tw-shadow-color);
@ -461,4 +467,10 @@
.forced-colors\:flex {
display: flex;
}
}
}
@media print {
.print\:bg-yellow-300 {
--tw-bg-opacity: 1;
background-color: rgb(253 224 71 / var(--tw-bg-opacity));
}
}

View File

@ -6,7 +6,7 @@ import { crosscheck, run, html, css, defaults } from './util/run'
crosscheck(({ stable, oxide }) => {
test('variants', () => {
let config = {
darkMode: 'class',
darkMode: 'selector',
content: [path.resolve(__dirname, './variants.test.html')],
corePlugins: { preflight: false },
}
@ -1156,7 +1156,7 @@ crosscheck(({ stable, oxide }) => {
test('stacking dark and rtl variants', async () => {
let config = {
darkMode: 'class',
darkMode: 'selector',
content: [
{
raw: html`<div class="dark:rtl:italic" />`,
@ -1172,7 +1172,7 @@ crosscheck(({ stable, oxide }) => {
let result = await run(input, config)
expect(result.css).toMatchFormattedCss(css`
:is(:where(.dark) :is(:where([dir='rtl']) .dark\:rtl\:italic)) {
.dark\:rtl\:italic:where([dir='rtl'], [dir='rtl'] *):where(.dark, .dark *) {
font-style: italic;
}
`)
@ -1180,7 +1180,7 @@ crosscheck(({ stable, oxide }) => {
test('stacking dark and rtl variants with pseudo elements', async () => {
let config = {
darkMode: 'class',
darkMode: 'selector',
content: [
{
raw: html`<div class="dark:rtl:placeholder:italic" />`,
@ -1196,7 +1196,10 @@ crosscheck(({ stable, oxide }) => {
let result = await run(input, config)
expect(result.css).toMatchFormattedCss(css`
:is(:where(.dark) :is(:where([dir='rtl']) .dark\:rtl\:placeholder\:italic))::placeholder {
.dark\:rtl\:placeholder\:italic:where([dir='rtl'], [dir='rtl'] *):where(
.dark,
.dark *
)::placeholder {
font-style: italic;
}
`)

7
types/config.d.ts vendored
View File

@ -74,6 +74,13 @@ type DarkModeConfig =
| 'class'
// Use the `class` strategy with a custom class instead of `.dark`.
| ['class', string]
// Use the `selector` strategy — same as `class` but uses `:where()` for more predicable behavior
| 'selector'
// Use the `selector` strategy with a custom selector instead of `.dark`.
| ['selector', string]
// Use the `variant` strategy, which allows you to completely customize the selector
// It takes a string or an array of strings, which are passed directly to `addVariant()`
| ['variant', string | string[]]
type Screen = { raw: string } | { min: string } | { max: string } | { min: string; max: string }
type ScreensConfig = string[] | KeyValuePair<string, string | Screen | Screen[]>