Path: blob/main/components/dashboard/src/theme-context.tsx
2498 views
/**1* Copyright (c) 2021 Gitpod GmbH. All rights reserved.2* Licensed under the GNU Affero General Public License (AGPL).3* See License.AGPL.txt in the project root for license information.4*/56import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";78export const ThemeContext = createContext<{9isDark?: boolean;10setIsDark: React.Dispatch<boolean>;11}>({12setIsDark: () => null,13});1415export const ThemeContextProvider: React.FC = ({ children }) => {16const [isDark, setIsDark] = useState<boolean>(document.documentElement.classList.contains("dark"));1718const actuallySetIsDark = useCallback((dark: boolean) => {19document.documentElement.classList.toggle("dark", dark);20setIsDark(dark);21}, []);2223useEffect(() => {24const observer = new MutationObserver(() => {25if (document.documentElement.classList.contains("dark") !== isDark) {26setIsDark(!isDark);27}28});29observer.observe(document.documentElement, { attributes: true });30return function cleanUp() {31observer.disconnect();32};33// eslint-disable-next-line react-hooks/exhaustive-deps34}, []);3536// Sets theme, tracks it in local storage and listens for changes in localstorage37useEffect(() => {38const updateTheme = () => {39const isDark =40localStorage.theme === "dark" ||41(localStorage.theme !== "light" && window.matchMedia("(prefers-color-scheme: dark)").matches);42actuallySetIsDark(isDark);43};44updateTheme();45const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");46if (mediaQuery instanceof EventTarget) {47mediaQuery.addEventListener("change", updateTheme);48} else {49// backward compatibility for Safari < 1450(mediaQuery as MediaQueryList).addListener(updateTheme);51}52window.addEventListener("storage", updateTheme);53return function cleanup() {54if (mediaQuery instanceof EventTarget) {55mediaQuery.removeEventListener("change", updateTheme);56} else {57// backward compatibility for Safari < 1458(mediaQuery as MediaQueryList).removeListener(updateTheme);59}60window.removeEventListener("storage", updateTheme);61};62}, [actuallySetIsDark]);6364const ctx = useMemo(() => ({ isDark, setIsDark: actuallySetIsDark }), [actuallySetIsDark, isDark]);6566return <ThemeContext.Provider value={ctx}>{children}</ThemeContext.Provider>;67};6869export const useTheme = () => {70return useContext(ThemeContext);71};7273/**74* Helper for making components themable and invertable, i.e. supports being used on an inverted backround.75*76* @param lightClass css class for light theme77* @param darkClass corresponding css class for dark theme78* @param inverted if the classes should be inverted, i.e. the component is on an inverted background79* @returns Array containing the two class strings w/ the proper one prefixed with `dark:`80*/81export const invertable = (lightClass: string, darkClass: string, inverted = false) => {82return [!inverted ? lightClass : darkClass, !inverted ? `dark:${darkClass}` : `dark:${lightClass}`];83};848586