diff --git a/src/corePlugins.js b/src/corePlugins.js index 3d3c599e7..9b31afe0e 100644 --- a/src/corePlugins.js +++ b/src/corePlugins.js @@ -242,6 +242,9 @@ export let variantPlugins = { if (mode === 'class') { addVariant('dark', `:is(:where(${className}) &)`) + } else if (mode === 'legacy') { + // Exists for pre v3.4 compatibility + addVariant('dark', `:is(${className} &)`) } else if (mode === 'media') { addVariant('dark', '@media (prefers-color-scheme: dark)') } else if (mode === 'variant') { diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index e677399b8..1095145eb 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -765,6 +765,27 @@ function resolvePlugins(context, root) { variantPlugins['forcedColorsVariants'], ] + // This is a compatibility fix for the pre 3.4 dark mode behavior + // You can enable the old behavior by setting `darkMode: ['legacy', selector]` + let isLegacyDarkMode = context.tailwindConfig.darkMode === 'legacy' + || (Array.isArray(context.tailwindConfig.darkMode) && context.tailwindConfig.darkMode[0] === 'legacy') + + if (isLegacyDarkMode) { + afterVariants = [ + variantPlugins['supportsVariants'], + variantPlugins['directionVariants'], + variantPlugins['reducedMotionVariants'], + variantPlugins['prefersContrastVariants'], + variantPlugins['printVariant'], + variantPlugins['darkVariants'], + variantPlugins['screenVariants'], + variantPlugins['orientationVariants'], + + // Forced colors didn't exist before 3.4 so it can keep its position at the end + variantPlugins['forcedColorsVariants'], + ] + } + return [...corePluginList, ...beforeVariants, ...userPlugins, ...afterVariants, ...layerPlugins] } diff --git a/tests/dark-mode.test.js b/tests/dark-mode.test.js index 924a9f335..966bd52ad 100644 --- a/tests/dark-mode.test.js +++ b/tests/dark-mode.test.js @@ -120,6 +120,108 @@ it('should default to the `media` mode when mode is set to `false`', () => { }) }) +it('should support legacy dark mode behavior', () => { + let config = { + darkMode: 'legacy', + 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 legacy dark mode', () => { + let config = { + darkMode: ['legacy', '.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: legacy', () => { + let config = { + darkMode: 'legacy', + content: [{ raw: html`` }], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + 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)); + } + } + `) + }) +}) + +it('should use modern sorting otherwise', () => { + 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` + .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)); + } + } + :is(:where(.dark) .dark\:text-green-100) { + --tw-text-opacity: 1; + color: rgb(220 252 231 / var(--tw-text-opacity)); + } + `) + }) +}) + it('should allow customization of the dark mode variant', () => { let config = { darkMode: ['variant', '&:not(.light *)'], @@ -128,17 +230,37 @@ it('should allow customization of the dark mode variant', () => { } let input = css` - @tailwind base; - @tailwind components; @tailwind utilities; ` return run(input, config).then((result) => { expect(result.css).toMatchFormattedCss(css` - ${defaults} .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 *) { + font-weight: 700; + } + .dark\:font-bold:not(.extralight *) { + font-weight: 700; + } + `) + }) +}) diff --git a/types/config.d.ts b/types/config.d.ts index 69ea448df..87a7fdfd2 100644 --- a/types/config.d.ts +++ b/types/config.d.ts @@ -75,8 +75,13 @@ type DarkModeConfig = | 'class' // Use the `class` strategy with a custom class instead of `.dark`. | ['class', string] + // Use the `legacy` strategy — same as `class` but restores pre-v3.4 behavior + | 'legacy' + // Use the `legacy` strategy with a custom class instead of `.dark` + | ['legacy', string] // Use the `variant` strategy, which allows you to completely customize the selector - | ['variant', string] + // 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