Skip to content

Commit 3fb57e5

Browse files
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 1852504. * 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 <[email protected]>
1 parent 78fedd5 commit 3fb57e5

20 files changed

+446
-134
lines changed

CHANGELOG.md

+9
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Don't remove keyframe stops when using important utilities ([#12639](https://github.com/tailwindlabs/tailwindcss/pull/12639))
1313
- Don't add spaces to gradients and grid track names when followed by `calc()` ([#12704](https://github.com/tailwindlabs/tailwindcss/pull/12704))
14+
- Restore old behavior for `class` dark mode strategy ([#12717](https://github.com/tailwindlabs/tailwindcss/pull/12717))
15+
16+
### Added
17+
18+
- Add new `selector` and `variant` strategies for dark mode ([#12717](https://github.com/tailwindlabs/tailwindcss/pull/12717))
19+
20+
### Changed
21+
22+
- Support `rtl` and `ltr` variants on same element as `dir` attribute ([#12717](https://github.com/tailwindlabs/tailwindcss/pull/12717))
1423

1524
## [3.4.0] - 2023-12-19
1625

src/corePlugins.js

+44-5
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,8 @@ export let variantPlugins = {
207207
},
208208

209209
directionVariants: ({ addVariant }) => {
210-
addVariant('ltr', ':is(:where([dir="ltr"]) &)')
211-
addVariant('rtl', ':is(:where([dir="rtl"]) &)')
210+
addVariant('ltr', '&:where([dir="ltr"], [dir="ltr"] *)')
211+
addVariant('rtl', '&:where([dir="rtl"], [dir="rtl"] *)')
212212
},
213213

214214
reducedMotionVariants: ({ addVariant }) => {
@@ -217,7 +217,7 @@ export let variantPlugins = {
217217
},
218218

219219
darkVariants: ({ config, addVariant }) => {
220-
let [mode, className = '.dark'] = [].concat(config('darkMode', 'media'))
220+
let [mode, selector = '.dark'] = [].concat(config('darkMode', 'media'))
221221

222222
if (mode === false) {
223223
mode = 'media'
@@ -228,10 +228,49 @@ export let variantPlugins = {
228228
])
229229
}
230230

231-
if (mode === 'class') {
232-
addVariant('dark', `:is(:where(${className}) &)`)
231+
if (mode === 'variant') {
232+
let formats
233+
if (Array.isArray(selector)) {
234+
formats = selector
235+
} else if (typeof selector === 'function') {
236+
formats = selector
237+
} else if (typeof selector === 'string') {
238+
formats = [selector]
239+
}
240+
241+
// TODO: We could also add these warnings if the user passes a function that returns string | string[]
242+
// But this is an advanced enough use case that it's probably not necessary
243+
if (Array.isArray(formats)) {
244+
for (let format of formats) {
245+
if (format === '.dark') {
246+
mode = false
247+
log.warn('darkmode-variant-without-selector', [
248+
'When using `variant` for `darkMode`, you must provide a selector.',
249+
'Example: `darkMode: ["variant", ".your-selector &"]`',
250+
])
251+
} else if (!format.includes('&')) {
252+
mode = false
253+
log.warn('darkmode-variant-without-ampersand', [
254+
'When using `variant` for `darkMode`, your selector must contain `&`.',
255+
'Example `darkMode: ["variant", ".your-selector &"]`',
256+
])
257+
}
258+
}
259+
}
260+
261+
selector = formats
262+
}
263+
264+
if (mode === 'selector') {
265+
// New preferred behavior
266+
addVariant('dark', `&:where(${selector}, ${selector} *)`)
233267
} else if (mode === 'media') {
234268
addVariant('dark', '@media (prefers-color-scheme: dark)')
269+
} else if (mode === 'variant') {
270+
addVariant('dark', selector)
271+
} else if (mode === 'class') {
272+
// Old behavior
273+
addVariant('dark', `:is(${selector} &)`)
235274
}
236275
},
237276

src/lib/setupContextUtils.js

+22-1
Original file line numberDiff line numberDiff line change
@@ -767,14 +767,35 @@ function resolvePlugins(context, root) {
767767
variantPlugins['supportsVariants'],
768768
variantPlugins['reducedMotionVariants'],
769769
variantPlugins['prefersContrastVariants'],
770-
variantPlugins['printVariant'],
771770
variantPlugins['screenVariants'],
772771
variantPlugins['orientationVariants'],
773772
variantPlugins['directionVariants'],
774773
variantPlugins['darkVariants'],
775774
variantPlugins['forcedColorsVariants'],
775+
variantPlugins['printVariant'],
776776
]
777777

778+
// This is a compatibility fix for the pre 3.4 dark mode behavior
779+
// `class` retains the old behavior, but `selector` keeps the new behavior
780+
let isLegacyDarkMode =
781+
context.tailwindConfig.darkMode === 'class' ||
782+
(Array.isArray(context.tailwindConfig.darkMode) &&
783+
context.tailwindConfig.darkMode[0] === 'class')
784+
785+
if (isLegacyDarkMode) {
786+
afterVariants = [
787+
variantPlugins['supportsVariants'],
788+
variantPlugins['reducedMotionVariants'],
789+
variantPlugins['prefersContrastVariants'],
790+
variantPlugins['darkVariants'],
791+
variantPlugins['screenVariants'],
792+
variantPlugins['orientationVariants'],
793+
variantPlugins['directionVariants'],
794+
variantPlugins['forcedColorsVariants'],
795+
variantPlugins['printVariant'],
796+
]
797+
}
798+
778799
return [...corePluginList, ...beforeVariants, ...userPlugins, ...afterVariants, ...layerPlugins]
779800
}
780801

src/util/pseudoElements.js

+4
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ let elementProperties = {
6060
':first-letter': ['terminal', 'jumpable'],
6161
':first-line': ['terminal', 'jumpable'],
6262

63+
':where': [],
64+
':is': [],
65+
':has': [],
66+
6367
// The default value is used when the pseudo-element is not recognized
6468
// Because it's not recognized, we don't know if it's terminal or not
6569
// So we assume it can be moved AND can have user-action pseudo classes attached to it

tests/apply.test.js

+22-22
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ crosscheck(({ stable, oxide }) => {
3535

3636
test('@apply', () => {
3737
let config = {
38-
darkMode: 'class',
38+
darkMode: 'selector',
3939
content: [{ raw: sharedHtml }],
4040
}
4141

@@ -216,14 +216,14 @@ crosscheck(({ stable, oxide }) => {
216216
text-align: left;
217217
}
218218
}
219-
:is(:where(.dark) .apply-dark-variant) {
219+
.apply-dark-variant:where(.dark, .dark *) {
220220
text-align: center;
221221
}
222-
:is(:where(.dark) .apply-dark-variant:hover) {
222+
.apply-dark-variant:hover:where(.dark, .dark *) {
223223
text-align: right;
224224
}
225225
@media (min-width: 1024px) {
226-
:is(:where(.dark) .apply-dark-variant) {
226+
.apply-dark-variant:where(.dark, .dark *) {
227227
text-align: left;
228228
}
229229
}
@@ -513,14 +513,14 @@ crosscheck(({ stable, oxide }) => {
513513
text-align: left;
514514
}
515515
}
516-
:is(:where(.dark) .apply-dark-variant) {
516+
.apply-dark-variant:where(.dark, .dark *) {
517517
text-align: center;
518518
}
519-
:is(:where(.dark) .apply-dark-variant:hover) {
519+
.apply-dark-variant:hover:where(.dark, .dark *) {
520520
text-align: right;
521521
}
522522
@media (min-width: 1024px) {
523-
:is(:where(.dark) .apply-dark-variant) {
523+
.apply-dark-variant:where(.dark, .dark *) {
524524
text-align: left;
525525
}
526526
}
@@ -755,7 +755,7 @@ crosscheck(({ stable, oxide }) => {
755755

756756
test('@apply error with unknown utility', async () => {
757757
let config = {
758-
darkMode: 'class',
758+
darkMode: 'selector',
759759
content: [{ raw: sharedHtml }],
760760
}
761761

@@ -775,7 +775,7 @@ crosscheck(({ stable, oxide }) => {
775775

776776
test('@apply error with nested @screen', async () => {
777777
let config = {
778-
darkMode: 'class',
778+
darkMode: 'selector',
779779
content: [{ raw: sharedHtml }],
780780
}
781781

@@ -799,7 +799,7 @@ crosscheck(({ stable, oxide }) => {
799799

800800
test('@apply error with nested @anyatrulehere', async () => {
801801
let config = {
802-
darkMode: 'class',
802+
darkMode: 'selector',
803803
content: [{ raw: sharedHtml }],
804804
}
805805

@@ -823,7 +823,7 @@ crosscheck(({ stable, oxide }) => {
823823

824824
test('@apply error when using .group utility', async () => {
825825
let config = {
826-
darkMode: 'class',
826+
darkMode: 'selector',
827827
content: [{ raw: '<div class="foo"></div>' }],
828828
}
829829

@@ -846,7 +846,7 @@ crosscheck(({ stable, oxide }) => {
846846
test('@apply error when using a prefixed .group utility', async () => {
847847
let config = {
848848
prefix: 'tw-',
849-
darkMode: 'class',
849+
darkMode: 'selector',
850850
content: [{ raw: html`<div class="foo"></div>` }],
851851
}
852852

@@ -868,7 +868,7 @@ crosscheck(({ stable, oxide }) => {
868868

869869
test('@apply error when using .peer utility', async () => {
870870
let config = {
871-
darkMode: 'class',
871+
darkMode: 'selector',
872872
content: [{ raw: '<div class="foo"></div>' }],
873873
}
874874

@@ -891,7 +891,7 @@ crosscheck(({ stable, oxide }) => {
891891
test('@apply error when using a prefixed .peer utility', async () => {
892892
let config = {
893893
prefix: 'tw-',
894-
darkMode: 'class',
894+
darkMode: 'selector',
895895
content: [{ raw: html`<div class="foo"></div>` }],
896896
}
897897

@@ -2360,7 +2360,7 @@ crosscheck(({ stable, oxide }) => {
23602360

23612361
it('pseudo elements inside apply are moved outside of :is() or :has()', () => {
23622362
let config = {
2363-
darkMode: 'class',
2363+
darkMode: 'selector',
23642364
content: [
23652365
{
23662366
raw: html` <div class="foo bar baz qux steve bob"></div> `,
@@ -2404,18 +2404,18 @@ crosscheck(({ stable, oxide }) => {
24042404

24052405
return run(input, config).then((result) => {
24062406
expect(result.css).toMatchFormattedCss(css`
2407-
:is(:where(.dark) .foo)::before,
2408-
:is(:where([dir='rtl']) :is(:where(.dark) .bar))::before,
2409-
:is(:where([dir='rtl']) :is(:where(.dark) .baz:hover))::before {
2407+
.foo:where(.dark, .dark *)::before,
2408+
.bar:where(.dark, .dark *):where([dir='rtl'], [dir='rtl'] *)::before,
2409+
.baz:hover:where(.dark, .dark *):where([dir='rtl'], [dir='rtl'] *)::before {
24102410
background-color: #000;
24112411
}
2412-
:is(:where([dir='rtl']) :is(:where(.dark) .qux))::file-selector-button:hover {
2412+
.qux:where(.dark, .dark *):where([dir='rtl'], [dir='rtl'] *)::file-selector-button:hover {
24132413
background-color: #000;
24142414
}
2415-
:is(:where([dir='rtl']) :is(:where(.dark) .steve):hover):before {
2415+
.steve:where(.dark, .dark *):hover:where([dir='rtl'], [dir='rtl'] *):before {
24162416
background-color: #000;
24172417
}
2418-
:is(:where([dir='rtl']) :is(:where(.dark) .bob))::file-selector-button:hover {
2418+
.bob:where(.dark, .dark *):hover:where([dir='rtl'], [dir='rtl'] *)::file-selector-button {
24192419
background-color: #000;
24202420
}
24212421
:has([dir='rtl'] .foo:hover):before {
@@ -2430,7 +2430,7 @@ crosscheck(({ stable, oxide }) => {
24302430

24312431
stable.test('::ng-deep, ::deep, ::v-deep pseudo elements are left alone', () => {
24322432
let config = {
2433-
darkMode: 'class',
2433+
darkMode: 'selector',
24342434
content: [
24352435
{
24362436
raw: html` <div class="foo bar"></div> `,

tests/custom-separator.test.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { crosscheck, run, html, css } from './util/run'
33
crosscheck(() => {
44
test('custom separator', () => {
55
let config = {
6-
darkMode: 'class',
6+
darkMode: 'selector',
77
content: [
88
{
99
raw: html`
@@ -33,10 +33,10 @@ crosscheck(() => {
3333
text-align: right;
3434
}
3535
}
36-
:is(:where([dir='rtl']) .rtl_active_text-center:active) {
36+
.rtl_active_text-center:active:where([dir='rtl'], [dir='rtl'] *) {
3737
text-align: center;
3838
}
39-
:is(:where(.dark) .dark_focus_text-left:focus) {
39+
.dark_focus_text-left:focus:where(.dark, .dark *) {
4040
text-align: left;
4141
}
4242
`)
@@ -45,7 +45,7 @@ crosscheck(() => {
4545

4646
test('dash is not supported', () => {
4747
let config = {
48-
darkMode: 'class',
48+
darkMode: 'selector',
4949
content: [{ raw: 'lg-hover-font-bold' }],
5050
separator: '-',
5151
}

0 commit comments

Comments
 (0)