From 3fb57e55abdba6fa68da6cb605fcaad5ce94764a Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 5 Jan 2024 14:39:34 -0500 Subject: [PATCH] Restore old behavior for `class` dark mode, add new `selector` and `variant` options for dark mode (#12717) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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> --- CHANGELOG.md | 9 + src/corePlugins.js | 49 ++++- src/lib/setupContextUtils.js | 23 ++- src/util/pseudoElements.js | 4 + tests/apply.test.js | 44 ++--- tests/custom-separator.test.js | 8 +- tests/dark-mode.test.js | 204 +++++++++++++++++++- tests/important-boolean.test.js | 6 +- tests/important-modifier-prefix.test.js | 2 +- tests/important-modifier.test.js | 2 +- tests/important-selector.test.js | 19 +- tests/kitchen-sink.test.js | 26 +-- tests/modify-selectors.test.js | 2 +- tests/opacity.test.js | 4 +- tests/prefix.test.js | 10 +- tests/util/apply-important-selector.test.js | 36 ++-- tests/variants.oxide.test.css | 54 ++++-- tests/variants.test.css | 58 +++--- tests/variants.test.js | 13 +- types/config.d.ts | 7 + 20 files changed, 446 insertions(+), 134 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 993e3594fc2b..837728b7df24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/corePlugins.js b/src/corePlugins.js index a04dce82e3bd..01afcec269d3 100644 --- a/src/corePlugins.js +++ b/src/corePlugins.js @@ -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} &)`) } }, diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index 91d5f66bfc8e..72aa8f56aa68 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -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] } diff --git a/src/util/pseudoElements.js b/src/util/pseudoElements.js index 5795cdd42045..e518801f42ba 100644 --- a/src/util/pseudoElements.js +++ b/src/util/pseudoElements.js @@ -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 diff --git a/tests/apply.test.js b/tests/apply.test.js index fe2b572496ba..ca5416e9261a 100644 --- a/tests/apply.test.js +++ b/tests/apply.test.js @@ -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: '
' }], } @@ -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`
` }], } @@ -868,7 +868,7 @@ crosscheck(({ stable, oxide }) => { test('@apply error when using .peer utility', async () => { let config = { - darkMode: 'class', + darkMode: 'selector', content: [{ raw: '
' }], } @@ -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`
` }], } @@ -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`
`, @@ -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`
`, diff --git a/tests/custom-separator.test.js b/tests/custom-separator.test.js index 6546f4ed2c63..aee13e67e2f3 100644 --- a/tests/custom-separator.test.js +++ b/tests/custom-separator.test.js @@ -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: '-', } diff --git a/tests/dark-mode.test.js b/tests/dark-mode.test.js index 9845e0fdfe61..6fbea28d915b 100644 --- a/tests/dark-mode.test.js +++ b/tests/dark-mode.test.js @@ -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`
` }], + 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`
` }], + 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`
`, + }, + ], + 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`
`, + }, + ], + 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`
` }], + 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`
` }], + 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`
` }], + 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; + } + `) + }) + }) }) diff --git a/tests/important-boolean.test.js b/tests/important-boolean.test.js index 6b028736c441..4853f2a1f1db 100644 --- a/tests/important-boolean.test.js +++ b/tests/important-boolean.test.js @@ -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; } `) diff --git a/tests/important-modifier-prefix.test.js b/tests/important-modifier-prefix.test.js index 782ec809e417..1e9f2bd22d68 100644 --- a/tests/important-modifier-prefix.test.js +++ b/tests/important-modifier-prefix.test.js @@ -5,7 +5,7 @@ crosscheck(() => { let config = { important: false, prefix: 'tw-', - darkMode: 'class', + darkMode: 'selector', content: [ { raw: html` diff --git a/tests/important-modifier.test.js b/tests/important-modifier.test.js index 6f6f1a8de54a..a4da55dd4ca5 100644 --- a/tests/important-modifier.test.js +++ b/tests/important-modifier.test.js @@ -4,7 +4,7 @@ crosscheck(() => { test('important modifier', () => { let config = { important: false, - darkMode: 'class', + darkMode: 'selector', content: [ { raw: html` diff --git a/tests/important-selector.test.js b/tests/important-selector.test.js index 24533f02f34d..840c572864c6 100644 --- a/tests/important-selector.test.js +++ b/tests/important-selector.test.js @@ -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`
`, @@ -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; } diff --git a/tests/kitchen-sink.test.js b/tests/kitchen-sink.test.js index 44665ffab4a6..27dedb46a8ef 100644 --- a/tests/kitchen-sink.test.js +++ b/tests/kitchen-sink.test.js @@ -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; } } diff --git a/tests/modify-selectors.test.js b/tests/modify-selectors.test.js index 3176b17f3da5..2efbe206a11f 100644 --- a/tests/modify-selectors.test.js +++ b/tests/modify-selectors.test.js @@ -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` diff --git a/tests/opacity.test.js b/tests/opacity.test.js index 88b88bd1539a..b5a1a53cfee7 100644 --- a/tests/opacity.test.js +++ b/tests/opacity.test.js @@ -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` diff --git a/tests/prefix.test.js b/tests/prefix.test.js index a1fb6733ebc8..a93e570ebbf7 100644 --- a/tests/prefix.test.js +++ b/tests/prefix.test.js @@ -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; } `) diff --git a/tests/util/apply-important-selector.test.js b/tests/util/apply-important-selector.test.js index cc2cacf524fd..ddbb86a1c08e 100644 --- a/tests/util/apply-important-selector.test.js +++ b/tests/util/apply-important-selector.test.js @@ -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) }) diff --git a/tests/variants.oxide.test.css b/tests/variants.oxide.test.css index 2aaab5e70730..c04721895c07 100644 --- a/tests/variants.oxide.test.css +++ b/tests/variants.oxide.test.css @@ -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; + } +} diff --git a/tests/variants.test.css b/tests/variants.test.css index c2fadabcbab7..95404e1cbe39 100644 --- a/tests/variants.test.css +++ b/tests/variants.test.css @@ -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; } -} \ No newline at end of file +} +@media print { + .print\:bg-yellow-300 { + --tw-bg-opacity: 1; + background-color: rgb(253 224 71 / var(--tw-bg-opacity)); + } +} diff --git a/tests/variants.test.js b/tests/variants.test.js index 14535dfd0ce1..6cf246476acf 100644 --- a/tests/variants.test.js +++ b/tests/variants.test.js @@ -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`
`, @@ -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`
`, @@ -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; } `) diff --git a/types/config.d.ts b/types/config.d.ts index b5d9ddc802b8..80b58d07028a 100644 --- a/types/config.d.ts +++ b/types/config.d.ts @@ -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