diff --git a/next-themes.tsx b/next-themes.tsx index b7ba0de..196b574 100644 --- a/next-themes.tsx +++ b/next-themes.tsx @@ -1,327 +1,335 @@ -import React, { createContext, useCallback, useContext, useEffect, useState, useRef, memo } from "react"; -import NextHead from "next/head"; +import React, { + createContext, + useCallback, + useContext, + useEffect, + useState, + useRef, + memo +} from 'react'; +import NextHead from 'next/head'; export interface UseThemeProps { - /** List of all available theme names */ - themes: string[]; - /** Forced theme name for the current page */ - forcedTheme?: string; - /** Update the theme */ - setTheme: (theme: string) => void; - /** Active theme name */ - 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; - /** If enableSystem is true, returns the System theme preference ("dark" or "light"), regardless what the active theme is */ - systemTheme?: "dark" | "light"; + /** List of all available theme names */ + themes: string[]; + /** Forced theme name for the current page */ + forcedTheme?: string; + /** Update the theme */ + setTheme: (theme: string) => void; + /** Active theme name */ + 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; + /** If enableSystem is true, returns the System theme preference ("dark" or "light"), regardless what the active theme is */ + systemTheme?: 'dark' | 'light'; } export interface ThemeProviderProps { - /** List of all available theme names */ - themes?: string[]; - /** Forced theme name for the current page */ - forcedTheme?: string; - /** Whether to switch between dark and light themes based on prefers-color-scheme */ - enableSystem?: boolean; - /** Disable all CSS transitions when switching themes */ - 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; - /** Key used to store theme setting in localStorage */ - 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; - /** HTML attribute modified based on the active theme. Accepts `class` and `data-*` (meaning any data attribute, `data-mode`, `data-color`, etc.) */ - attribute?: string | "class"; - /** Mapping of theme name to HTML attribute value. Object where key is the theme name and value is the attribute value */ - value?: ValueObject; + /** List of all available theme names */ + themes?: string[]; + /** Forced theme name for the current page */ + forcedTheme?: string; + /** Whether to switch between dark and light themes based on prefers-color-scheme */ + enableSystem?: boolean; + /** Disable all CSS transitions when switching themes */ + 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; + /** Key used to store theme setting in localStorage */ + 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; + /** HTML attribute modified based on the active theme. Accepts `class` and `data-*` (meaning any data attribute, `data-mode`, `data-color`, etc.) */ + attribute?: string | 'class'; + /** Mapping of theme name to HTML attribute value. Object where key is the theme name and value is the attribute value */ + value?: ValueObject; } const ThemeContext = createContext({ - setTheme: (_) => {}, - themes: [] + setTheme: (_) => {}, + themes: [] }); export const useTheme = () => useContext(ThemeContext); -const colorSchemes = ["light", "dark"]; -const MEDIA = "(prefers-color-scheme: dark)"; +const colorSchemes = ['light', 'dark']; +const MEDIA = '(prefers-color-scheme: dark)'; interface ValueObject { - [themeName: string]: string; + [themeName: string]: string; } export const ThemeProvider: React.FC = ({ - forcedTheme, - disableTransitionOnChange = false, - enableSystem = true, - enableColorScheme = true, - storageKey = "theme", - themes = ["light", "dark"], - defaultTheme = enableSystem ? "system" : "light", - attribute = "data-theme", - value, - children + forcedTheme, + disableTransitionOnChange = false, + enableSystem = true, + enableColorScheme = true, + storageKey = 'theme', + themes = ['light', 'dark'], + defaultTheme = enableSystem ? 'system' : 'light', + attribute = 'data-theme', + value, + children }) => { - const [theme, setThemeState] = useState(() => getTheme(storageKey, defaultTheme)); - const [resolvedTheme, setResolvedTheme] = useState(() => getTheme(storageKey)); - console.log({ getThemeBeforeMount: getTheme(storageKey) }); - const attrs = !value ? themes : Object.values(value); - - const handleMediaQuery = useCallback( - (e?) => { - const systemTheme = getSystemTheme(e); - setResolvedTheme(systemTheme); - if (theme === "system" && !forcedTheme) changeTheme(systemTheme, false); - }, - [theme, forcedTheme] - ); - - // Ref hack to avoid adding handleMediaQuery as a dep - const mediaListener = useRef(handleMediaQuery); - mediaListener.current = handleMediaQuery; - - const changeTheme = useCallback((theme, updateStorage = true, updateDOM = true) => { - console.log("changeTheme"); - let name = value?.[theme] || theme; - - const enable = disableTransitionOnChange && updateDOM ? disableAnimation() : null; - - if (updateStorage) { - try { - localStorage.setItem(storageKey, theme); - } catch (e) { - // Unsupported - } - } - - if (theme === "system" && enableSystem) { - const resolved = getSystemTheme(); - name = value?.[resolved] || resolved; - } - - if (updateDOM) { - const d = document.documentElement; - - if (attribute === "class") { - d.classList.remove(...attrs); - d.classList.add(name); - } else { - d.setAttribute(attribute, name); - } - enable?.(); - } - }, []); - - useEffect(() => { - const handler = (...args: any) => mediaListener.current(...args); - - // Always listen to System preference - const media = window.matchMedia(MEDIA); - - // Intentionally use deprecated listener methods to support iOS & old browsers - media.addListener(handler); - handler(media); - - return () => media.removeListener(handler); - }, []); - - const setTheme = useCallback( - (newTheme) => { - if (forcedTheme) { - changeTheme(newTheme, true, false); - - changeTheme(newTheme); - } - setThemeState(newTheme); - }, - [forcedTheme] - ); - - // localStorage event handling - useEffect(() => { - const handleStorage = (e: StorageEvent) => { - if (e.key !== storageKey) { - return; - } - // If default theme set, use it if localstorage === null (happens on local storage manual deletion) - const theme = e.newValue || defaultTheme; - setTheme(theme); - }; - console.log("onWindow"); - - window.addEventListener("storage", handleStorage); - return () => window.removeEventListener("storage", handleStorage); - }, [setTheme]); - - // color-scheme handling - useEffect(() => { - if (!enableColorScheme) return; - console.log("enableColorScheme"); - - let colorScheme = - // If theme is forced to light or dark, use that - forcedTheme && colorSchemes.includes(forcedTheme) - ? forcedTheme - : // If regular theme is light or dark - theme && colorSchemes.includes(theme) - ? theme - : // If theme is system, use the resolved version - theme === "system" - ? resolvedTheme || null - : null; - - // color-scheme tells browser how to render built-in elements like forms, scrollbars, etc. - // if color-scheme is null, this will remove the property - document.documentElement.style.setProperty("color-scheme", colorScheme); - }, [enableColorScheme, theme, resolvedTheme, forcedTheme]); - - return ( - - - {children} - - ); + const [theme, setThemeState] = useState(() => getTheme(storageKey, defaultTheme)); + const [resolvedTheme, setResolvedTheme] = useState(() => getTheme(storageKey)); + console.log({ getThemeBeforeMount: getTheme(storageKey) }); + const attrs = !value ? themes : Object.values(value); + + const handleMediaQuery = useCallback( + (e?) => { + const systemTheme = getSystemTheme(e); + setResolvedTheme(systemTheme); + if (theme === 'system' && !forcedTheme) changeTheme(systemTheme, false); + }, + [theme, forcedTheme] + ); + + // Ref hack to avoid adding handleMediaQuery as a dep + const mediaListener = useRef(handleMediaQuery); + mediaListener.current = handleMediaQuery; + + const changeTheme = useCallback((theme, updateStorage = true, updateDOM = true) => { + console.log('changeTheme'); + let name = value?.[theme] || theme; + + const enable = disableTransitionOnChange && updateDOM ? disableAnimation() : null; + + if (updateStorage) { + try { + localStorage.setItem(storageKey, theme); + } catch (e) { + // Unsupported + } + } + + if (theme === 'system' && enableSystem) { + const resolved = getSystemTheme(); + name = value?.[resolved] || resolved; + } + + if (updateDOM) { + const d = document.documentElement; + + if (attribute === 'class') { + d.classList.remove(...attrs); + d.classList.add(name); + } else { + d.setAttribute(attribute, name); + } + enable?.(); + } + }, []); + + useEffect(() => { + const handler = (...args: any) => mediaListener.current(...args); + + // Always listen to System preference + const media = window.matchMedia(MEDIA); + + // Intentionally use deprecated listener methods to support iOS & old browsers + media.addListener(handler); + handler(media); + + return () => media.removeListener(handler); + }, []); + + const setTheme = useCallback( + (newTheme) => { + if (forcedTheme) { + changeTheme(newTheme, true, false); + + changeTheme(newTheme); + } + setThemeState(newTheme); + }, + [forcedTheme] + ); + + // localStorage event handling + useEffect(() => { + const handleStorage = (e: StorageEvent) => { + if (e.key !== storageKey) { + return; + } + // If default theme set, use it if localstorage === null (happens on local storage manual deletion) + const theme = e.newValue || defaultTheme; + setTheme(theme); + }; + console.log('onWindow'); + + window.addEventListener('storage', handleStorage); + return () => window.removeEventListener('storage', handleStorage); + }, [setTheme]); + + // color-scheme handling + useEffect(() => { + if (!enableColorScheme) return; + console.log('enableColorScheme'); + + let colorScheme = + // If theme is forced to light or dark, use that + forcedTheme && colorSchemes.includes(forcedTheme) + ? forcedTheme + : // If regular theme is light or dark + theme && colorSchemes.includes(theme) + ? theme + : // If theme is system, use the resolved version + theme === 'system' + ? resolvedTheme || null + : null; + + // color-scheme tells browser how to render built-in elements like forms, scrollbars, etc. + // if color-scheme is null, this will remove the property + document.documentElement.style.setProperty('color-scheme', colorScheme); + }, [enableColorScheme, theme, resolvedTheme, forcedTheme]); + + return ( + + + {children} + + ); }; const ThemeScript = memo( - ({ - forcedTheme, - storageKey, - attribute, - enableSystem, - defaultTheme, - value, - attrs - }: { - forcedTheme?: string; - storageKey: string; - attribute?: string; - enableSystem?: boolean; - defaultTheme: string; - value?: ValueObject; - attrs: any; - }) => { - // Code-golfing the amount of characters in the script - const optimization = (() => { - if (attribute === "class") { - const removeClasses = `d.remove(${attrs.map((t: string) => `'${t}'`).join(",")})`; - - return `var d=document.documentElement.classList;${removeClasses};`; - } else { - return `var d=document.documentElement;`; - } - })(); - - const updateDOM = (name: string, literal?: boolean) => { - name = value?.[name] || name; - const val = literal ? name : `'${name}'`; - - if (attribute === "class") { - return `d.add(${val})`; - } - - return `d.setAttribute('${attribute}', ${val})`; - }; - - const defaultSystem = defaultTheme === "system"; - // return null; - console.log("themescript"); - return ( - - {forcedTheme ? ( -