Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor #300

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions next-themes/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const DARK = 'dark'
export const LIGHT = 'light'
166 changes: 81 additions & 85 deletions next-themes/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,85 @@
import * as React from 'react'
import { script } from './script'
import type { Attribute, ThemeProviderProps, UseThemeProps } from './types'
import { DARK, LIGHT } from './constants'

const colorSchemes = ['light', 'dark']
const colorSchemes = [LIGHT, DARK]
const MEDIA = '(prefers-color-scheme: dark)'
const isServer = typeof window === 'undefined'
const ThemeContext = React.createContext<UseThemeProps | undefined>(undefined)
const defaultContext: UseThemeProps = { setTheme: _ => {}, themes: [] }

export const useTheme = () => React.useContext(ThemeContext) ?? defaultContext
const defaultThemes = [LIGHT, DARK]

export const ThemeProvider = (props: ThemeProviderProps): React.ReactNode => {
const context = React.useContext(ThemeContext)
// Helpers
const getTheme = (key: string, fallback?: string) => {
if (isServer) return undefined
let theme
try {
theme = localStorage.getItem(key) || undefined
} catch (e) {
// Unsupported
}
return theme || fallback
}

// Ignore nested context providers, just passthrough children
if (context) return props.children
return <Theme {...props} />
const disableAnimation = () => {
const css = document.createElement('style')
css.appendChild(document.createTextNode('*,*::before,*::after{transition:none!important}'))
document.head.appendChild(css)

return () => {
// Force restyle
;(() => window.getComputedStyle(document.body))()

// Wait for next tick before removing
setTimeout(() => {
document.head.removeChild(css)
}, 1)
}
}

const getSystemTheme = (e?: MediaQueryList | MediaQueryListEvent) => {
if (!e) e = window.matchMedia(MEDIA)
const isDark = e.matches
const systemTheme = isDark ? DARK : LIGHT
return systemTheme
}

const defaultThemes = ['light', 'dark']
export const useTheme = () => React.useContext(ThemeContext) ?? defaultContext

const ThemeScript = React.memo(
({
forcedTheme,
storageKey,
attribute,
enableSystem,
enableColorScheme,
defaultTheme,
value,
themes,
nonce
}: Omit<ThemeProviderProps, 'children'> & { defaultTheme: string }) => {
const scriptArgs = JSON.stringify([
attribute,
storageKey,
defaultTheme,
forcedTheme,
themes,
value,
enableSystem,
enableColorScheme
]).slice(1, -1)

return (
<script
suppressHydrationWarning
nonce={typeof window === 'undefined' ? nonce : ''}
dangerouslySetInnerHTML={{ __html: `(${script.toString()})(${scriptArgs})` }}
/>
)
}
)

const Theme = ({
forcedTheme,
Expand All @@ -29,7 +90,7 @@ const Theme = ({
enableColorScheme = true,
storageKey = 'theme',
themes = defaultThemes,
defaultTheme = enableSystem ? 'system' : 'light',
defaultTheme = enableSystem ? 'system' : LIGHT,
attribute = 'data-theme',
value,
children,
Expand All @@ -50,17 +111,17 @@ const Theme = ({

const name = value ? value[resolved] : resolved
const enable = disableTransitionOnChange ? disableAnimation() : null
const d = document.documentElement
const docEl = document.documentElement

const handleAttribute = (attr: Attribute) => {
if (attr === 'class') {
d.classList.remove(...attrs)
if (name) d.classList.add(name)
docEl.classList.remove(...attrs)
if (name) docEl.classList.add(name)
} else if (attr.startsWith('data-')) {
if (name) {
d.setAttribute(attr, name)
docEl.setAttribute(attr, name)
} else {
d.removeAttribute(attr)
docEl.removeAttribute(attr)
}
}
}
Expand All @@ -71,8 +132,7 @@ const Theme = ({
if (enableColorScheme) {
const fallback = colorSchemes.includes(defaultTheme) ? defaultTheme : null
const colorScheme = colorSchemes.includes(resolved) ? resolved : fallback
// @ts-ignore
d.style.colorScheme = colorScheme
docEl.style.colorScheme = colorScheme
}

enable?.()
Expand Down Expand Up @@ -170,74 +230,10 @@ const Theme = ({
)
}

const ThemeScript = React.memo(
({
forcedTheme,
storageKey,
attribute,
enableSystem,
enableColorScheme,
defaultTheme,
value,
themes,
nonce
}: Omit<ThemeProviderProps, 'children'> & { defaultTheme: string }) => {
const scriptArgs = JSON.stringify([
attribute,
storageKey,
defaultTheme,
forcedTheme,
themes,
value,
enableSystem,
enableColorScheme
]).slice(1, -1)

return (
<script
suppressHydrationWarning
nonce={typeof window === 'undefined' ? nonce : ''}
dangerouslySetInnerHTML={{ __html: `(${script.toString()})(${scriptArgs})` }}
/>
)
}
)

// Helpers
const getTheme = (key: string, fallback?: string) => {
if (isServer) return undefined
let theme
try {
theme = localStorage.getItem(key) || undefined
} catch (e) {
// Unsupported
}
return theme || fallback
}

const disableAnimation = () => {
const css = document.createElement('style')
css.appendChild(
document.createTextNode(
`*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}`
)
)
document.head.appendChild(css)

return () => {
// Force restyle
;(() => window.getComputedStyle(document.body))()

// Wait for next tick before removing
setTimeout(() => {
document.head.removeChild(css)
}, 1)
}
}
export const ThemeProvider = (props: ThemeProviderProps): React.ReactNode => {
const context = React.useContext(ThemeContext)

const getSystemTheme = (e?: MediaQueryList | MediaQueryListEvent) => {
if (!e) e = window.matchMedia(MEDIA)
const isDark = e.matches
const systemTheme = isDark ? 'dark' : 'light'
return systemTheme
// Ignore nested context providers, just passthrough children
if (context) return props.children
return <Theme {...props} />
}
5 changes: 3 additions & 2 deletions next-themes/src/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ export const script = (
enableSystem,
enableColorScheme
) => {
const [DARK, LIGHT] = ['dark', 'light']
const el = document.documentElement
const systemThemes = ['light', 'dark']
const systemThemes = [LIGHT, DARK]
const isClass = attribute === 'class'
const classes = isClass && value ? themes.map(t => value[t] || t) : themes

Expand All @@ -31,7 +32,7 @@ export const script = (
}

function getSystemTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
return window.matchMedia('(prefers-color-scheme: dark)').matches ? DARK : LIGHT
}

if (forcedTheme) {
Expand Down
28 changes: 14 additions & 14 deletions next-themes/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,38 +8,38 @@ export interface UseThemeProps {
/** List of all available theme names */
themes: string[]
/** Forced theme name for the current page */
forcedTheme?: string | undefined
forcedTheme?: string
/** Update the theme */
setTheme: React.Dispatch<React.SetStateAction<string>>
/** Active theme name */
theme?: string | undefined
theme?: string
/** If `enableSystem` is true and the active theme is "system", this returns whether the system preference resolved to "dark" or "light". Otherwise, identical to `theme` */
resolvedTheme?: string | undefined
resolvedTheme?: string
/** If enableSystem is true, returns the System theme preference ("dark" or "light"), regardless what the active theme is */
systemTheme?: 'dark' | 'light' | undefined
systemTheme?: 'dark' | 'light'
}

export type Attribute = `data-${string}` | 'class'

export interface ThemeProviderProps extends React.PropsWithChildren {
/** List of all available theme names */
themes?: string[] | undefined
themes?: string[]
/** Forced theme name for the current page */
forcedTheme?: string | undefined
forcedTheme?: string
/** Whether to switch between dark and light themes based on prefers-color-scheme */
enableSystem?: boolean | undefined
enableSystem?: boolean
/** Disable all CSS transitions when switching themes */
disableTransitionOnChange?: boolean | undefined
disableTransitionOnChange?: boolean
/** Whether to indicate to browsers which color scheme is used (dark or light) for built-in UI like inputs and buttons */
enableColorScheme?: boolean | undefined
enableColorScheme?: boolean
/** Key used to store theme setting in localStorage */
storageKey?: string | undefined
storageKey?: string
/** Default theme name (for v0.0.12 and lower the default was light). If `enableSystem` is false, the default theme is light */
defaultTheme?: string | undefined
defaultTheme?: string
/** HTML attribute modified based on the active theme. Accepts `class`, `data-*` (meaning any data attribute, `data-mode`, `data-color`, etc.), or an array which could include both */
attribute?: Attribute | Attribute[] | undefined
attribute?: Attribute | Attribute[]
/** Mapping of theme name to HTML attribute value. Object where key is the theme name and value is the attribute value */
value?: ValueObject | undefined
value?: ValueObject
/** Nonce string to pass to the inline script for CSP headers */
nonce?: string | undefined
nonce?: string
}
Loading