1
1
// Copyright (c) 2024 IOTA Stiftung
2
2
// SPDX-License-Identifier: Apache-2.0
3
3
4
- import { PropsWithChildren , useState , useEffect , useCallback } from 'react' ;
5
- import { Theme } from '../../enums' ;
4
+ import { PropsWithChildren , useState , useEffect } from 'react' ;
5
+ import { Theme , ThemePreference } from '../../enums' ;
6
6
import { ThemeContext } from '../../contexts' ;
7
7
8
8
interface ThemeProviderProps {
@@ -12,40 +12,72 @@ interface ThemeProviderProps {
12
12
export function ThemeProvider ( { children, appId } : PropsWithChildren < ThemeProviderProps > ) {
13
13
const storageKey = `theme_${ appId } ` ;
14
14
15
- const getSystemTheme = ( ) =>
16
- window . matchMedia ( '(prefers-color-scheme: dark)' ) . matches ? Theme . Dark : Theme . Light ;
15
+ const getSystemTheme = ( ) => {
16
+ return window . matchMedia ( '(prefers-color-scheme: dark)' ) . matches ? Theme . Dark : Theme . Light ;
17
+ } ;
17
18
18
- const getInitialTheme = ( ) => {
19
- if ( typeof window === 'undefined' ) {
20
- return Theme . System ;
21
- } else {
22
- const storedTheme = localStorage ?. getItem ( storageKey ) ;
23
- return storedTheme ? ( storedTheme as Theme ) : Theme . System ;
24
- }
19
+ const getThemePreference = ( ) => {
20
+ const storedTheme = localStorage ?. getItem ( storageKey ) as ThemePreference | null ;
21
+ return storedTheme ? storedTheme : ThemePreference . System ;
25
22
} ;
26
23
27
- const [ theme , setTheme ] = useState < Theme > ( getInitialTheme ) ;
24
+ const [ systemTheme , setSystemTheme ] = useState < Theme > ( Theme . Light ) ;
25
+ const [ themePreference , setThemePreference ] = useState < ThemePreference > ( ThemePreference . System ) ;
26
+ const [ isLoadingPreference , setIsLoadingPreference ] = useState ( true ) ;
28
27
29
- const applyTheme = useCallback ( ( currentTheme : Theme ) => {
30
- const selectedTheme = currentTheme === Theme . System ? getSystemTheme ( ) : currentTheme ;
31
- const documentElement = document . documentElement . classList ;
32
- documentElement . toggle ( Theme . Dark , selectedTheme === Theme . Dark ) ;
33
- documentElement . toggle ( Theme . Light , selectedTheme === Theme . Light ) ;
28
+ // Load the theme values on client
29
+ useEffect ( ( ) => {
30
+ if ( typeof window === 'undefined' ) return ;
31
+
32
+ setSystemTheme ( getSystemTheme ( ) ) ;
33
+ setThemePreference ( getThemePreference ( ) ) ;
34
+
35
+ // Make the theme preference listener wait
36
+ // until the preference is loaded in the next render
37
+ setIsLoadingPreference ( false ) ;
34
38
} , [ ] ) ;
35
39
40
+ // When the theme preference changes..
36
41
useEffect ( ( ) => {
37
- if ( typeof window === 'undefined' ) return ;
42
+ if ( typeof window === 'undefined' || isLoadingPreference ) return ;
43
+
44
+ // Update localStorage with the new preference
45
+ localStorage . setItem ( storageKey , themePreference ) ;
38
46
39
- localStorage . setItem ( storageKey , theme ) ;
40
- applyTheme ( theme ) ;
47
+ // In case of SystemPreference, listen for system theme changes
48
+ if ( themePreference === ThemePreference . System ) {
49
+ const handleSystemThemeChange = ( ) => {
50
+ const systemTheme = getSystemTheme ( ) ;
51
+ setSystemTheme ( systemTheme ) ;
52
+ } ;
53
+ const systemThemeMatcher = window . matchMedia ( '(prefers-color-scheme: dark)' ) ;
54
+ systemThemeMatcher . addEventListener ( 'change' , handleSystemThemeChange ) ;
55
+ return ( ) => systemThemeMatcher . removeEventListener ( 'change' , handleSystemThemeChange ) ;
56
+ }
57
+ } , [ themePreference , storageKey , isLoadingPreference ] ) ;
41
58
42
- if ( theme === Theme . System ) {
43
- const systemTheme = window . matchMedia ( '(prefers-color-scheme: dark)' ) ;
44
- const handleSystemThemeChange = ( ) => applyTheme ( Theme . System ) ;
45
- systemTheme . addEventListener ( 'change' , handleSystemThemeChange ) ;
46
- return ( ) => systemTheme . removeEventListener ( 'change' , handleSystemThemeChange ) ;
59
+ // Derive the active theme from the preference
60
+ const theme = ( ( ) => {
61
+ switch ( themePreference ) {
62
+ case ThemePreference . Dark :
63
+ return Theme . Dark ;
64
+ case ThemePreference . Light :
65
+ return Theme . Light ;
66
+ case ThemePreference . System :
67
+ return systemTheme ;
47
68
}
48
- } , [ theme , applyTheme , storageKey ] ) ;
69
+ } ) ( ) ;
70
+
71
+ // When the theme (preference or derived) changes update the CSS class
72
+ useEffect ( ( ) => {
73
+ const documentElement = document . documentElement . classList ;
74
+ documentElement . toggle ( Theme . Dark , theme === Theme . Dark ) ;
75
+ documentElement . toggle ( Theme . Light , theme === Theme . Light ) ;
76
+ } , [ theme ] ) ;
49
77
50
- return < ThemeContext . Provider value = { { theme, setTheme } } > { children } </ ThemeContext . Provider > ;
78
+ return (
79
+ < ThemeContext . Provider value = { { theme, setThemePreference, themePreference } } >
80
+ { children }
81
+ </ ThemeContext . Provider >
82
+ ) ;
51
83
}
0 commit comments