Skip to content

Commit

Permalink
feat(config): respect selectors and mediaQuery when making variants
Browse files Browse the repository at this point in the history
re #63 re #47
  • Loading branch information
RyanClementsHax committed May 9, 2023
1 parent b9cb9e4 commit a238c55
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 48 deletions.
222 changes: 184 additions & 38 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@ import { mock } from 'jest-mock-extended'
import { PluginAPI } from 'tailwindcss/types/config'
import multiThemePlugin from '.'
import { Theme } from './config'
import { MultiThemePluginOptions, defaultThemeName } from './utils/optionsUtils'
import { MultiThemePluginOptions } from './utils/optionsUtils'

describe('multiThemePlugin', () => {
describe('handler', () => {
let config: MultiThemePluginOptions, api: PluginAPI
let api: PluginAPI

beforeEach(() => {
config = {
beforeEach(() => {
api = mock<PluginAPI>({
e: jest.fn(x => `escaped-${x}`),
theme: jest.fn(x => x) as PluginAPI['theme']
})
})

describe('variants', () => {
it('adds variants for each theme using name as a class if no selectors or mediaQuery provided', () => {
const config: MultiThemePluginOptions = {
defaultTheme: {
extend: {
colors: {
Expand All @@ -19,53 +26,192 @@ describe('multiThemePlugin', () => {
},
themes: [
{
name: 'dark',
name: 'darkTheme',
extend: {
colors: {
primary: 'another',
secondary: 'something'
},
spacing: {
'0.5': '10px'
primary: 'dark'
}
}
},
{
name: 'light',
selectors: ['[data-theme="light"]'],
extend: {
colors: {
primary: 'light'
}
}
},
{
name: 'neon',
mediaQuery: '@media (prefers-color-scheme: dark)',
extend: {
colors: {
primary: 'neon'
}
}
},
{
name: 'soft',
selectors: ['[data-theme="light"]'],
mediaQuery: '@media (prefers-color-scheme: dark)',
extend: {
colors: {
primary: 'soft'
}
}
}
]
}

api = mock<PluginAPI>({
e: jest.fn(x => `escaped-${x}`),
theme: jest.fn(x => x) as PluginAPI['theme']
})
multiThemePlugin(config).handler(api)

expect(api.addVariant).toHaveBeenCalledWith('defaultTheme', [
'.escaped-defaultTheme &'
])
expect(api.addVariant).toHaveBeenCalledWith('darkTheme', [
'.escaped-darkTheme &'
])
expect(api.addVariant).not.toHaveBeenCalledWith('light', [
'.escaped-light &'
])
expect(api.addVariant).not.toHaveBeenCalledWith('neon', [
'.escaped-neon &'
])
expect(api.addVariant).not.toHaveBeenCalledWith('soft', [
'.escaped-soft &'
])
})

it('adds variants for each theme', () => {
it('adds variants for each theme using selectors if present', () => {
const config: MultiThemePluginOptions = {
defaultTheme: {
extend: {
colors: {
primary: 'thing'
}
}
},
themes: [
{
name: 'darkTheme',
selectors: ['.dark-mode', '[data-theme="dark"]'],
extend: {
colors: {
primary: 'first'
}
}
},
{
name: 'neon',
selectors: ['.high-contrast', '[data-theme="high-contrast"]'],
extend: {
colors: {
primary: 'second'
}
}
},
{
name: 'soft',
extend: {
colors: {
primary: 'third'
}
}
}
]
}

multiThemePlugin(config).handler(api)

for (const themeName of [
defaultThemeName,
...(config?.themes?.map(x => x.name) ?? [])
]) {
expect(api.addVariant).toHaveBeenCalledWith(
themeName === defaultThemeName ? 'defaultTheme' : themeName,
`.escaped-${
themeName === defaultThemeName ? 'defaultTheme' : themeName
} &`
)
expect(api.addVariant).toHaveBeenCalledWith('darkTheme', [
'.dark-mode &',
'[data-theme="dark"] &'
])
expect(api.addVariant).toHaveBeenCalledWith('neon', [
'.high-contrast &',
'[data-theme="high-contrast"] &'
])
expect(api.addVariant).toHaveBeenCalledWith('soft', ['.escaped-soft &'])
})

it('doesnt add any selector based variants when none provided', () => {
const config: MultiThemePluginOptions = {
defaultTheme: {
extend: {
colors: {
primary: 'thing'
}
}
},
themes: [
{
name: 'darkTheme',
selectors: ['.dark-mode', '[data-theme="dark"]'],
extend: {
colors: {
primary: 'first'
}
}
},
{
name: 'neon',
selectors: ['.high-contrast', '[data-theme="high-contrast"]'],
extend: {
colors: {
primary: 'second'
}
}
},
{
name: 'soft',
selectors: [],
extend: {
colors: {
primary: 'third'
}
}
}
]
}

multiThemePlugin(config).handler(api)

expect(api.addVariant).not.toHaveBeenCalledWith('soft', expect.anything())
})
})

describe('styles', () => {
let api: PluginAPI
it('adds a media based variant when one provided', () => {
const config: MultiThemePluginOptions = {
defaultTheme: {
extend: {
colors: {
primary: 'thing'
}
}
},
themes: [
{
name: 'darkTheme',
mediaQuery: '@media (prefers-color-scheme: dark)',
extend: {
colors: {
primary: 'first'
}
}
}
]
}

beforeEach(() => {
api = mock<PluginAPI>({
e: jest.fn(x => `escaped-${x}`),
theme: jest.fn(x => x) as PluginAPI['theme']
})
multiThemePlugin(config).handler(api)

expect(api.addVariant).toHaveBeenCalledWith(
'darkTheme',
'@media (prefers-color-scheme: dark)'
)
})
})

describe('styles', () => {
it('adds the custom vars for each theme', () => {
const config: MultiThemePluginOptions = {
defaultTheme: {
Expand Down Expand Up @@ -131,7 +277,7 @@ describe('multiThemePlugin', () => {
},
themes: [
{
name: 'dark',
name: 'darkTheme',
selectors: ['.dark-mode', '[data-theme="dark"]'],
extend: {
colors: {
Expand Down Expand Up @@ -176,7 +322,7 @@ describe('multiThemePlugin', () => {
},
themes: [
{
name: 'dark',
name: 'darkTheme',
selectors: [],
extend: {
colors: {
Expand Down Expand Up @@ -208,7 +354,7 @@ describe('multiThemePlugin', () => {
},
themes: [
{
name: 'dark',
name: 'darkTheme',
selectors: ['[data-theme="dark"]'],
mediaQuery: '@media (prefers-color-scheme: dark)',
extend: {
Expand Down Expand Up @@ -242,7 +388,7 @@ describe('multiThemePlugin', () => {
},
themes: [
{
name: 'dark',
name: 'darkTheme',
selectors: ['[data-theme="dark"]'],
extend: {
colors: {
Expand Down
33 changes: 23 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,35 @@ const defaultOptions: MultiThemePluginOptions = {
const addThemeVariants = (
themes: ThemeConfig[],
{ addVariant, e }: PluginAPI
): void =>
void themes.map(({ name }) =>
addVariant(
name === defaultThemeName ? 'defaultTheme' : name,
`.${e(name === defaultThemeName ? 'defaultTheme' : name)} &`
)
)
) => {
for (const { name, selectors: _selectors, mediaQuery } of themes) {
const variantName = name === defaultThemeName ? 'defaultTheme' : name
const shouldAddNameBasedVariant = !_selectors && !mediaQuery
const selectors =
_selectors ?? (shouldAddNameBasedVariant ? [`.${e(variantName)}`] : [])

if (selectors.length > 0) {
addVariant(
variantName,
selectors.map(x => `${x} &`)
)
}

if (mediaQuery) {
addVariant(variantName, mediaQuery)
}
}
}

/**
* @param themes the themes to add as variants
* @param api the tailwind plugin helpers
*/
const addThemeStyles = (themes: ThemeConfig[], api: PluginAPI): void => {
const { addBase, e } = api
themes.forEach(({ name, selectors, extend, mediaQuery }) => {
selectors ??= name === defaultThemeName ? [':root'] : [`.${e(name)}`]
for (const { name, selectors: _selectors, extend, mediaQuery } of themes) {
const selectors =
_selectors ?? (name === defaultThemeName ? [':root'] : [`.${e(name)}`])
if (selectors.length > 0) {
addBase({
[selectors.join(', ')]: resolveThemeExtensionAsCustomProps(extend, api)
Expand All @@ -54,7 +67,7 @@ const addThemeStyles = (themes: ThemeConfig[], api: PluginAPI): void => {
}
})
}
})
}
}

const multiThemePlugin = plugin.withOptions<MultiThemePluginOptions>(
Expand Down
18 changes: 18 additions & 0 deletions src/utils/optionsUtils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,23 @@ describe('themeUtils', () => {
})
).toThrow()
})

it('throws an error if a theme with name "dark" is configured with selectors', () => {
expect(() =>
validateOptions({
defaultTheme: { extend: {} },
themes: [{ name: 'dark', selectors: [], extend: {} }]
})
).toThrow()
})

it('throws an error if a theme with name "dark" is configured with a mediaQuery', () => {
expect(() =>
validateOptions({
defaultTheme: { extend: {} },
themes: [{ name: 'dark', mediaQuery: '', extend: {} }]
})
).toThrow()
})
})
})
11 changes: 11 additions & 0 deletions src/utils/optionsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ export const validateOptions = ({
if ((defaultTheme as ThemeConfig)?.selectors) {
throw new Error('The default theme cannot have any selectors')
}
const darkTheme = themes.find(x => x.name === 'dark')
if (darkTheme?.selectors) {
throw new Error(
'Tailwind configures "dark" theme automatically which prevents any attempt to use custom selectors. This is a limitation of tailwind, not tailwindcss-themer. Please rename your "dark" theme to something else or remove the "selectors" field from your "dark" theme.'
)
}
if (darkTheme?.mediaQuery || darkTheme?.mediaQuery?.length === 0) {
throw new Error(
'Tailwind configures "dark" theme automatically which prevents any attempt to use a custom media query. This is a limitation of tailwind, not tailwindcss-themer. Please rename your "dark" theme to something else or remove the "mediaQuery" field from your "dark" theme.'
)
}
}

/**
Expand Down

0 comments on commit a238c55

Please sign in to comment.