Merge pull request #419 from SagnikPradhan/feat/system-theme-preference

feat: dark/light mode
This commit is contained in:
Jean-Philippe Sirois
2021-10-19 15:00:42 -04:00
committed by GitHub
4 changed files with 89 additions and 12 deletions
+35 -9
View File
@@ -2,11 +2,14 @@ import React, { createContext, FC, useMemo } from "react"
import { ThemeProvider as BaseThemeProvider } from "styled-components"
import { darkTheme, lightTheme } from "./design/themes"
import { useIsBrowser } from "./hooks/useIsBrowser"
import { useLocalStorage } from "./hooks/useLocalStorage"
import { useMedia } from "./hooks/useMedia"
type ThemeType = "dark" | "light"
export interface IThemeContext {
theme: "dark" | "light"
setTheme: () => void
theme: ThemeType
toggleTheme: () => void
}
@@ -16,28 +19,51 @@ interface IScopedDownChildren {
export const ThemeContext = createContext<IThemeContext | null>(null)
const ThemeProvider: FC<IScopedDownChildren> = ({ children }) => {
const [theme, setTheme] = useLocalStorage("theme", "dark")
function useTheme() {
const preferredTheme = useMedia<ThemeType>(
["(prefers-color-scheme: light)", "(prefers-color-scheme: dark)"],
["light", "dark"],
"dark"
)
const [userSelectedTheme, setUserSelectedTheme] = useLocalStorage<
ThemeType | "unset"
>("user-selected-theme", "unset")
const theme = useMemo(
() => (userSelectedTheme !== "unset" ? userSelectedTheme : preferredTheme),
[preferredTheme, userSelectedTheme]
)
const themeObject = useMemo(
() => (theme === "dark" ? darkTheme : lightTheme),
[theme]
)
return {
theme,
themeObject,
setTheme: setUserSelectedTheme,
}
}
const ThemeProvider: FC<IScopedDownChildren> = ({ children }) => {
const isBrowser = useIsBrowser()
const { theme, themeObject, setTheme } = useTheme()
const contextValue = useMemo(
() => ({
theme,
setTheme,
toggleTheme: () => {
setTheme(theme === "light" ? "dark" : "light")
},
toggleTheme: () => setTheme(theme === "light" ? "dark" : "light"),
}),
[theme, setTheme]
)
return (
<ThemeContext.Provider value={contextValue}>
<BaseThemeProvider theme={themeObject}>{children}</BaseThemeProvider>
<BaseThemeProvider theme={themeObject}>
{isBrowser ? children : undefined}
</BaseThemeProvider>
</ThemeContext.Provider>
)
}
+11
View File
@@ -0,0 +1,11 @@
import { useEffect, useState } from "react"
export function useIsBrowser() {
const isBrowserCheck = () => typeof window !== "undefined"
const [isBrowser, setIsBrowser] = useState(isBrowserCheck)
useEffect(() => setIsBrowser(isBrowserCheck), [])
return isBrowser
}
+7 -3
View File
@@ -1,9 +1,13 @@
/* globals window */
import { useEffect, useState } from "react"
export const useLocalStorage = (name: string, initialValue: string) => {
export const useLocalStorage = <Value extends unknown = unknown>(
name: string,
initialValue: Value
) => {
const windowGlobal = typeof window !== "undefined" && window
const [value, setValue] = useState(() => {
const [value, setValue] = useState<Value>(() => {
if (windowGlobal) {
const currentValue = windowGlobal.localStorage.getItem(name)
return currentValue ? JSON.parse(currentValue) : initialValue
@@ -17,5 +21,5 @@ export const useLocalStorage = (name: string, initialValue: string) => {
}
}, [name, value, windowGlobal])
return [value, setValue]
return [value, setValue] as const
}
+36
View File
@@ -0,0 +1,36 @@
/* globals window */
import { useEffect, useState } from "react"
export function useMedia<Value extends unknown>(
queries: string[],
values: Value[],
defaultValue: Value
) {
const [currentValue, setCurrentValue] = useState(defaultValue)
useEffect(() => {
const windowGlobal = typeof window !== "undefined" && window
if (!windowGlobal) return
const mediaQueries = queries.map((query) => windowGlobal.matchMedia(query))
const getCurrentValue = () => {
const index = mediaQueries.findIndex((value) => value.matches)
return values[index] || defaultValue
}
setCurrentValue(getCurrentValue)
const handler = () => setCurrentValue(getCurrentValue)
mediaQueries.forEach((query) => query.addEventListener("change", handler))
return () =>
mediaQueries.forEach((query) =>
query.removeEventListener("change", handler)
)
}, [queries, values, defaultValue])
return currentValue
}