import noop from 'lodash/noop'; import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; import { LocalStorageThemeMode, StorageOnlyThemeMode, ThemeMode } from 'src/types'; import { createGlobalStyle, ThemeProvider } from 'styled-components'; import { themeNames } from './theme'; export const THEME_SELECT_VERSION = '0.1.0'; // validate type (necessary if older theme still stored in local storage) export function isOfTypeThemeName(keyInput: LocalStorageThemeMode) { return keyInput === ThemeMode.LIGHT || keyInput === ThemeMode.DARK; } type ThemeContextType = { // displayed theme theme: ThemeMode; // theme in local storage storedTheme: LocalStorageThemeMode; // update theme in local storage setStoredTheme: (name: LocalStorageThemeMode) => void; }; export const ThemeContext = createContext({ theme: ThemeMode.LIGHT, storedTheme: StorageOnlyThemeMode.SYSTEM, setStoredTheme: noop }); export const useTheme = () => useContext(ThemeContext); export const THEME_LOCAL_STORAGE_KEY = 'THEME_MODE'; const GlobalStyles = createGlobalStyle` body { font-family: 'Skiff Sans Text', sans-serif; -webkit-font-smoothing: antialiased; font-smoothing: antialiased; } `; export const AppThemeProvider: React.FC = ({ children }) => { const [themeName, setThemeName] = useState(ThemeMode.DARK); const [storedThemeState, setStoredThemeState] = useState(StorageOnlyThemeMode.SYSTEM); let darkSystemThemeMediaQuery: MediaQueryList | undefined; if (typeof window !== 'undefined') { darkSystemThemeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); } const updateRootData = (theme: ThemeMode) => { const themeColor = themeNames[theme]; Object.keys(themeColor).forEach((key) => { document.body.style.setProperty(key, themeColor[key]); }); }; const updateThemeName = useCallback( (name: LocalStorageThemeMode, darkSystemThemeMediaQuery?: MediaQueryList) => { setStoredThemeState(name); // if name is light or dark, set it to theme if (isOfTypeThemeName(name)) { setThemeName(name as ThemeMode); updateRootData(name as ThemeMode); } else if (darkSystemThemeMediaQuery !== undefined) { // otherwise parse the system theme const themeMode = darkSystemThemeMediaQuery.matches ? ThemeMode.DARK : ThemeMode.LIGHT; setThemeName(themeMode); updateRootData(themeMode); } }, [darkSystemThemeMediaQuery] ); const setStoredTheme = (name: LocalStorageThemeMode) => { // update local storage localStorage.setItem(THEME_LOCAL_STORAGE_KEY, name); updateThemeName(name, darkSystemThemeMediaQuery); }; // Get theme from localStorage when app loads useEffect(() => { // if nothing stored default to system const storedThemeMode = (localStorage.getItem(THEME_LOCAL_STORAGE_KEY) as ThemeMode) || StorageOnlyThemeMode.SYSTEM; updateThemeName(storedThemeMode, darkSystemThemeMediaQuery); }, []); // Listen to live system changes useEffect(() => { if (storedThemeState !== StorageOnlyThemeMode.SYSTEM || darkSystemThemeMediaQuery === undefined) return; const updateSystemTheme = () => updateThemeName(StorageOnlyThemeMode.SYSTEM, darkSystemThemeMediaQuery); darkSystemThemeMediaQuery.addEventListener('change', updateSystemTheme); return () => { if (darkSystemThemeMediaQuery === undefined) return; darkSystemThemeMediaQuery.removeEventListener('change', updateSystemTheme); }; }, [storedThemeState, darkSystemThemeMediaQuery, updateThemeName]); // updated `theme` object will be defined by design system and used here return ( {children} ); };