Add dark mode variant (#2279)
* Initial dark mode prototype * Isolate dark mode logic to config extension * Update fixtures * Fix lint warnings
This commit is contained in:
parent
455b56164b
commit
5701d7847b
6 changed files with 59394 additions and 1 deletions
240
__tests__/darkMode.test.js
Normal file
240
__tests__/darkMode.test.js
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
import postcss from 'postcss'
|
||||
import tailwind from '../src/index'
|
||||
|
||||
function run(input, config = {}) {
|
||||
return postcss([tailwind({ experimental: { darkModeVariant: true }, ...config })]).process(
|
||||
input,
|
||||
{ from: undefined }
|
||||
)
|
||||
}
|
||||
|
||||
test('dark mode variants cannot be generated without enabling the dark mode experiment', () => {
|
||||
const input = `
|
||||
@variants dark {
|
||||
.text-red {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(1)
|
||||
return expect(run(input, { experimental: {} })).rejects.toThrow()
|
||||
})
|
||||
|
||||
test('generating dark mode variants uses the media strategy by default', () => {
|
||||
const input = `
|
||||
@variants dark {
|
||||
.text-red {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.text-red {
|
||||
color: red;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\\:text-red {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('dark mode variants can be generated using the class strategy', () => {
|
||||
const input = `
|
||||
@variants dark {
|
||||
.text-red {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.text-red {
|
||||
color: red;
|
||||
}
|
||||
.dark .dark\\:text-red {
|
||||
color: red;
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input, { dark: 'class' }).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('dark mode variants stack with other variants', () => {
|
||||
const input = `
|
||||
@variants responsive, dark, hover, focus {
|
||||
.text-red {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.text-red {
|
||||
color: red;
|
||||
}
|
||||
.hover\\:text-red:hover {
|
||||
color: red;
|
||||
}
|
||||
.focus\\:text-red:focus {
|
||||
color: red;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\\:text-red {
|
||||
color: red;
|
||||
}
|
||||
.dark\\:hover\\:text-red:hover {
|
||||
color: red;
|
||||
}
|
||||
.dark\\:focus\\:text-red:focus {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
@media (min-width: 500px) {
|
||||
.sm\\:text-red {
|
||||
color: red;
|
||||
}
|
||||
.sm\\:hover\\:text-red:hover {
|
||||
color: red;
|
||||
}
|
||||
.sm\\:focus\\:text-red:focus {
|
||||
color: red;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.sm\\:dark\\:text-red {
|
||||
color: red;
|
||||
}
|
||||
.sm\\:dark\\:hover\\:text-red:hover {
|
||||
color: red;
|
||||
}
|
||||
.sm\\:dark\\:focus\\:text-red:focus {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (min-width: 800px) {
|
||||
.lg\\:text-red {
|
||||
color: red;
|
||||
}
|
||||
.lg\\:hover\\:text-red:hover {
|
||||
color: red;
|
||||
}
|
||||
.lg\\:focus\\:text-red:focus {
|
||||
color: red;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.lg\\:dark\\:text-red {
|
||||
color: red;
|
||||
}
|
||||
.lg\\:dark\\:hover\\:text-red:hover {
|
||||
color: red;
|
||||
}
|
||||
.lg\\:dark\\:focus\\:text-red:focus {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input, { theme: { screens: { sm: '500px', lg: '800px' } } }).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('dark mode variants stack with other variants when using the class strategy', () => {
|
||||
const input = `
|
||||
@variants responsive, dark, hover, focus {
|
||||
.text-red {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.text-red {
|
||||
color: red;
|
||||
}
|
||||
.hover\\:text-red:hover {
|
||||
color: red;
|
||||
}
|
||||
.focus\\:text-red:focus {
|
||||
color: red;
|
||||
}
|
||||
.dark .dark\\:text-red {
|
||||
color: red;
|
||||
}
|
||||
.dark .dark\\:hover\\:text-red:hover {
|
||||
color: red;
|
||||
}
|
||||
.dark .dark\\:focus\\:text-red:focus {
|
||||
color: red;
|
||||
}
|
||||
@media (min-width: 500px) {
|
||||
.sm\\:text-red {
|
||||
color: red;
|
||||
}
|
||||
.sm\\:hover\\:text-red:hover {
|
||||
color: red;
|
||||
}
|
||||
.sm\\:focus\\:text-red:focus {
|
||||
color: red;
|
||||
}
|
||||
.dark .sm\\:dark\\:text-red {
|
||||
color: red;
|
||||
}
|
||||
.dark .sm\\:dark\\:hover\\:text-red:hover {
|
||||
color: red;
|
||||
}
|
||||
.dark .sm\\:dark\\:focus\\:text-red:focus {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
@media (min-width: 800px) {
|
||||
.lg\\:text-red {
|
||||
color: red;
|
||||
}
|
||||
.lg\\:hover\\:text-red:hover {
|
||||
color: red;
|
||||
}
|
||||
.lg\\:focus\\:text-red:focus {
|
||||
color: red;
|
||||
}
|
||||
.dark .lg\\:dark\\:text-red {
|
||||
color: red;
|
||||
}
|
||||
.dark .lg\\:dark\\:hover\\:text-red:hover {
|
||||
color: red;
|
||||
}
|
||||
.dark .lg\\:dark\\:focus\\:text-red:focus {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input, { dark: 'class', theme: { screens: { sm: '500px', lg: '800px' } } }).then(
|
||||
result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
}
|
||||
)
|
||||
})
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -10,6 +10,7 @@ const featureFlags = {
|
|||
'defaultLineHeights',
|
||||
'extendedFontSizeScale',
|
||||
'applyComplexClasses',
|
||||
'darkModeVariant',
|
||||
],
|
||||
}
|
||||
|
||||
|
|
|
|||
47
src/flagged/darkModeVariant.js
Normal file
47
src/flagged/darkModeVariant.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import selectorParser from 'postcss-selector-parser'
|
||||
import buildSelectorVariant from '../util/buildSelectorVariant'
|
||||
import defaultConfig from '../../defaultConfig'
|
||||
|
||||
export default {
|
||||
dark: 'media',
|
||||
variants: {
|
||||
backgroundColor: [...defaultConfig.variants.backgroundColor, 'dark'],
|
||||
gradientColorStops: [...defaultConfig.variants.gradientColorStops, 'dark'],
|
||||
borderColor: [...defaultConfig.variants.borderColor, 'dark'],
|
||||
divideColor: [...defaultConfig.variants.divideColor, 'dark'],
|
||||
placeholderColor: [...defaultConfig.variants.placeholderColor, 'dark'],
|
||||
textColor: [...defaultConfig.variants.textColor, 'dark'],
|
||||
},
|
||||
plugins: [
|
||||
function({ addVariant, config, postcss, prefix }) {
|
||||
addVariant('dark', ({ container, separator, modifySelectors }) => {
|
||||
if (config('dark') === 'media') {
|
||||
const modified = modifySelectors(({ selector }) => {
|
||||
return buildSelectorVariant(selector, 'dark', separator, message => {
|
||||
throw container.error(message)
|
||||
})
|
||||
})
|
||||
const mediaQuery = postcss.atRule({
|
||||
name: 'media',
|
||||
params: '(prefers-color-scheme: dark)',
|
||||
})
|
||||
mediaQuery.append(modified)
|
||||
container.append(mediaQuery)
|
||||
return container
|
||||
}
|
||||
|
||||
if (config('dark') === 'class') {
|
||||
const parser = selectorParser(selectors => {
|
||||
selectors.walkClasses(sel => {
|
||||
sel.value = `dark${separator}${sel.value}`
|
||||
sel.parent.insertBefore(sel, selectorParser().astSync(prefix('.dark ')))
|
||||
})
|
||||
})
|
||||
return modifySelectors(({ selector }) => parser.processSync(selector))
|
||||
}
|
||||
|
||||
throw new Error("The `dark` config option must be either 'media' or 'class'.")
|
||||
})
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import uniformColorPalette from './flagged/uniformColorPalette.js'
|
|||
import extendedSpacingScale from './flagged/extendedSpacingScale.js'
|
||||
import defaultLineHeights from './flagged/defaultLineHeights.js'
|
||||
import extendedFontSizeScale from './flagged/extendedFontSizeScale.js'
|
||||
import darkModeVariant from './flagged/darkModeVariant'
|
||||
|
||||
function getDefaultConfigs(config) {
|
||||
const configs = [defaultConfig]
|
||||
|
|
@ -37,6 +38,10 @@ function getDefaultConfigs(config) {
|
|||
configs.unshift(extendedFontSizeScale)
|
||||
}
|
||||
|
||||
if (flagEnabled(config, 'darkModeVariant')) {
|
||||
configs.unshift(darkModeVariant)
|
||||
}
|
||||
|
||||
return configs
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ const defaultVariantGenerators = config => ({
|
|||
})
|
||||
|
||||
function prependStackableVariants(atRule, variants) {
|
||||
const stackableVariants = ['motion-safe', 'motion-reduce']
|
||||
const stackableVariants = ['dark', 'motion-safe', 'motion-reduce']
|
||||
|
||||
if (!_.some(variants, v => stackableVariants.includes(v))) {
|
||||
return variants
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue