Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/dashboard/src/theme-context.tsx
2498 views
1
/**
2
* Copyright (c) 2021 Gitpod GmbH. All rights reserved.
3
* Licensed under the GNU Affero General Public License (AGPL).
4
* See License.AGPL.txt in the project root for license information.
5
*/
6
7
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
8
9
export const ThemeContext = createContext<{
10
isDark?: boolean;
11
setIsDark: React.Dispatch<boolean>;
12
}>({
13
setIsDark: () => null,
14
});
15
16
export const ThemeContextProvider: React.FC = ({ children }) => {
17
const [isDark, setIsDark] = useState<boolean>(document.documentElement.classList.contains("dark"));
18
19
const actuallySetIsDark = useCallback((dark: boolean) => {
20
document.documentElement.classList.toggle("dark", dark);
21
setIsDark(dark);
22
}, []);
23
24
useEffect(() => {
25
const observer = new MutationObserver(() => {
26
if (document.documentElement.classList.contains("dark") !== isDark) {
27
setIsDark(!isDark);
28
}
29
});
30
observer.observe(document.documentElement, { attributes: true });
31
return function cleanUp() {
32
observer.disconnect();
33
};
34
// eslint-disable-next-line react-hooks/exhaustive-deps
35
}, []);
36
37
// Sets theme, tracks it in local storage and listens for changes in localstorage
38
useEffect(() => {
39
const updateTheme = () => {
40
const isDark =
41
localStorage.theme === "dark" ||
42
(localStorage.theme !== "light" && window.matchMedia("(prefers-color-scheme: dark)").matches);
43
actuallySetIsDark(isDark);
44
};
45
updateTheme();
46
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
47
if (mediaQuery instanceof EventTarget) {
48
mediaQuery.addEventListener("change", updateTheme);
49
} else {
50
// backward compatibility for Safari < 14
51
(mediaQuery as MediaQueryList).addListener(updateTheme);
52
}
53
window.addEventListener("storage", updateTheme);
54
return function cleanup() {
55
if (mediaQuery instanceof EventTarget) {
56
mediaQuery.removeEventListener("change", updateTheme);
57
} else {
58
// backward compatibility for Safari < 14
59
(mediaQuery as MediaQueryList).removeListener(updateTheme);
60
}
61
window.removeEventListener("storage", updateTheme);
62
};
63
}, [actuallySetIsDark]);
64
65
const ctx = useMemo(() => ({ isDark, setIsDark: actuallySetIsDark }), [actuallySetIsDark, isDark]);
66
67
return <ThemeContext.Provider value={ctx}>{children}</ThemeContext.Provider>;
68
};
69
70
export const useTheme = () => {
71
return useContext(ThemeContext);
72
};
73
74
/**
75
* Helper for making components themable and invertable, i.e. supports being used on an inverted backround.
76
*
77
* @param lightClass css class for light theme
78
* @param darkClass corresponding css class for dark theme
79
* @param inverted if the classes should be inverted, i.e. the component is on an inverted background
80
* @returns Array containing the two class strings w/ the proper one prefixed with `dark:`
81
*/
82
export const invertable = (lightClass: string, darkClass: string, inverted = false) => {
83
return [!inverted ? lightClass : darkClass, !inverted ? `dark:${darkClass}` : `dark:${lightClass}`];
84
};
85
86