From 5494cff825325e79e9828963c3b9d4c8ed7b29a5 Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Fri, 29 Nov 2024 16:51:12 +0100 Subject: [PATCH] refactor(tooling): Split Theme into `Theme` and `ThemePreference` (#4239) * refactor(tooling): Split Theme into `Theme` and `ThemePreference` * fmt * fixes * fix: Avoid setting the preference to system on every load * typo * fmt * fix explorer * chore: Add `iota-` prefixes to theme localstorage key in dashboard and wallet * fix: Proper loading reactivity --------- Co-authored-by: evavirseda --- .../components/providers/ThemeProvider.tsx | 86 +++++++++++++------ apps/core/src/contexts/ThemeContext.tsx | 8 +- apps/core/src/enums/theme.enums.ts | 5 ++ .../src/components/header/ThemeSwitcher.tsx | 39 ++------- .../app/(protected)/layout.tsx | 11 ++- apps/wallet-dashboard/app/page.tsx | 6 +- .../staking-overview/StartStaking.tsx | 1 + .../providers/AppProviders.tsx | 2 +- .../components/menu/content/ThemeSettings.tsx | 10 +-- .../menu/content/WalletSettingsMenuList.tsx | 4 +- apps/wallet/src/ui/index.tsx | 2 +- 11 files changed, 96 insertions(+), 78 deletions(-) diff --git a/apps/core/src/components/providers/ThemeProvider.tsx b/apps/core/src/components/providers/ThemeProvider.tsx index 43a65cfa558..c69bbf78e13 100644 --- a/apps/core/src/components/providers/ThemeProvider.tsx +++ b/apps/core/src/components/providers/ThemeProvider.tsx @@ -1,8 +1,8 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { PropsWithChildren, useState, useEffect, useCallback } from 'react'; -import { Theme } from '../../enums'; +import { PropsWithChildren, useState, useEffect } from 'react'; +import { Theme, ThemePreference } from '../../enums'; import { ThemeContext } from '../../contexts'; interface ThemeProviderProps { @@ -12,40 +12,72 @@ interface ThemeProviderProps { export function ThemeProvider({ children, appId }: PropsWithChildren) { const storageKey = `theme_${appId}`; - const getSystemTheme = () => - window.matchMedia('(prefers-color-scheme: dark)').matches ? Theme.Dark : Theme.Light; + const getSystemTheme = () => { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? Theme.Dark : Theme.Light; + }; - const getInitialTheme = () => { - if (typeof window === 'undefined') { - return Theme.System; - } else { - const storedTheme = localStorage?.getItem(storageKey); - return storedTheme ? (storedTheme as Theme) : Theme.System; - } + const getThemePreference = () => { + const storedTheme = localStorage?.getItem(storageKey) as ThemePreference | null; + return storedTheme ? storedTheme : ThemePreference.System; }; - const [theme, setTheme] = useState(getInitialTheme); + const [systemTheme, setSystemTheme] = useState(Theme.Light); + const [themePreference, setThemePreference] = useState(ThemePreference.System); + const [isLoadingPreference, setIsLoadingPreference] = useState(true); - const applyTheme = useCallback((currentTheme: Theme) => { - const selectedTheme = currentTheme === Theme.System ? getSystemTheme() : currentTheme; - const documentElement = document.documentElement.classList; - documentElement.toggle(Theme.Dark, selectedTheme === Theme.Dark); - documentElement.toggle(Theme.Light, selectedTheme === Theme.Light); + // Load the theme values on client + useEffect(() => { + if (typeof window === 'undefined') return; + + setSystemTheme(getSystemTheme()); + setThemePreference(getThemePreference()); + + // Make the theme preference listener wait + // until the preference is loaded in the next render + setIsLoadingPreference(false); }, []); + // When the theme preference changes.. useEffect(() => { - if (typeof window === 'undefined') return; + if (typeof window === 'undefined' || isLoadingPreference) return; + + // Update localStorage with the new preference + localStorage.setItem(storageKey, themePreference); - localStorage.setItem(storageKey, theme); - applyTheme(theme); + // In case of SystemPreference, listen for system theme changes + if (themePreference === ThemePreference.System) { + const handleSystemThemeChange = () => { + const systemTheme = getSystemTheme(); + setSystemTheme(systemTheme); + }; + const systemThemeMatcher = window.matchMedia('(prefers-color-scheme: dark)'); + systemThemeMatcher.addEventListener('change', handleSystemThemeChange); + return () => systemThemeMatcher.removeEventListener('change', handleSystemThemeChange); + } + }, [themePreference, storageKey, isLoadingPreference]); - if (theme === Theme.System) { - const systemTheme = window.matchMedia('(prefers-color-scheme: dark)'); - const handleSystemThemeChange = () => applyTheme(Theme.System); - systemTheme.addEventListener('change', handleSystemThemeChange); - return () => systemTheme.removeEventListener('change', handleSystemThemeChange); + // Derive the active theme from the preference + const theme = (() => { + switch (themePreference) { + case ThemePreference.Dark: + return Theme.Dark; + case ThemePreference.Light: + return Theme.Light; + case ThemePreference.System: + return systemTheme; } - }, [theme, applyTheme, storageKey]); + })(); + + // When the theme (preference or derived) changes update the CSS class + useEffect(() => { + const documentElement = document.documentElement.classList; + documentElement.toggle(Theme.Dark, theme === Theme.Dark); + documentElement.toggle(Theme.Light, theme === Theme.Light); + }, [theme]); - return {children}; + return ( + + {children} + + ); } diff --git a/apps/core/src/contexts/ThemeContext.tsx b/apps/core/src/contexts/ThemeContext.tsx index 3406e50d5c1..b24c0de824d 100644 --- a/apps/core/src/contexts/ThemeContext.tsx +++ b/apps/core/src/contexts/ThemeContext.tsx @@ -2,14 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 import { createContext } from 'react'; -import { Theme } from '../enums'; +import { Theme, ThemePreference } from '../enums'; export interface ThemeContextType { theme: Theme; - setTheme: (theme: Theme) => void; + themePreference: ThemePreference; + setThemePreference: (theme: ThemePreference) => void; } export const ThemeContext = createContext({ theme: Theme.Light, - setTheme: () => {}, + themePreference: ThemePreference.System, + setThemePreference: () => {}, }); diff --git a/apps/core/src/enums/theme.enums.ts b/apps/core/src/enums/theme.enums.ts index 2df40a61af4..1225b1eb403 100644 --- a/apps/core/src/enums/theme.enums.ts +++ b/apps/core/src/enums/theme.enums.ts @@ -4,5 +4,10 @@ export enum Theme { Light = 'light', Dark = 'dark', +} + +export enum ThemePreference { + Light = 'light', + Dark = 'dark', System = 'system', } diff --git a/apps/explorer/src/components/header/ThemeSwitcher.tsx b/apps/explorer/src/components/header/ThemeSwitcher.tsx index d8dc4c2fb24..b947cafe76c 100644 --- a/apps/explorer/src/components/header/ThemeSwitcher.tsx +++ b/apps/explorer/src/components/header/ThemeSwitcher.tsx @@ -3,46 +3,21 @@ import { Button, ButtonType } from '@iota/apps-ui-kit'; import { DarkMode, LightMode } from '@iota/ui-icons'; -import { useEffect, useLayoutEffect } from 'react'; -import { useTheme, Theme } from '@iota/core'; +import { useTheme, Theme, ThemePreference } from '@iota/core'; export function ThemeSwitcher(): React.JSX.Element { - const { theme, setTheme } = useTheme(); + const { theme, themePreference, setThemePreference } = useTheme(); const ThemeIcon = theme === Theme.Dark ? DarkMode : LightMode; function handleOnClick(): void { - const newTheme = theme === Theme.Light ? Theme.Dark : Theme.Light; - setTheme(newTheme); - saveThemeToLocalStorage(newTheme); + const newTheme = + themePreference === ThemePreference.Light + ? ThemePreference.Dark + : ThemePreference.Light; + setThemePreference(newTheme); } - function saveThemeToLocalStorage(newTheme: Theme): void { - localStorage.setItem('theme', newTheme); - } - - function updateDocumentClass(theme: Theme): void { - document.documentElement.classList.toggle('dark', theme === Theme.Dark); - } - - useLayoutEffect(() => { - const storedTheme = localStorage.getItem('theme') as Theme | null; - if (storedTheme) { - setTheme(storedTheme); - updateDocumentClass(storedTheme); - } else { - const prefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches; - const preferredTheme = prefersDarkTheme ? Theme.Dark : Theme.Light; - - setTheme(preferredTheme); - updateDocumentClass(preferredTheme); - } - }, []); - - useEffect(() => { - updateDocumentClass(theme); - }, [theme]); - return (