All files / src/components ThemeProvider.tsx

100% Statements 37/37
92.85% Branches 13/14
100% Functions 2/2
100% Lines 37/37

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 931x 1x 1x 1x                                                     1x 1x 1x 1x 1x   1x                                         1x 1x 30x   30x 18x 18x 18x 30x   30x   30x 1x 1x 1x 1x 1x   30x 30x   30x 30x 30x 30x 30x 30x   30x 30x 30x     30x  
import { useAppContext } from '@/context';
import { ThemeProvider as MuiThemeProvider } from '@mui/material/styles';
import { FC, ReactNode, useEffect } from 'react';
import {
  darkPalette,
  darkTheme,
  lightPalette,
  lightTheme,
  ThemeContext,
  ThemeContextType,
  ThemeMode,
} from '../themes';
 
/**
 * Props for {@link ThemeProvider}.
 * @example
 * ```tsx
 * <ThemeProvider><App /></ThemeProvider>
 * ```
 * @source
 */
interface ThemeProviderProps {
  /** The app subtree to render under the theme. */
  children: ReactNode;
}
 
// `DisplaySettings.fontSize` is optional — narrow to the non-undefined union so
// Record can key off valid size names only. The fallback path in the effect
// below handles the undefined case at runtime.
type FontSize = NonNullable<DisplaySettings['fontSize']>;
const FONT_SIZE_PX: Record<FontSize, string> = {
  small: '14px',
  medium: '16px',
  large: '18px',
};
 
/**
 * Wraps the app in MUI's `ThemeProvider` plus a local `ThemeContext`, and
 * keeps the root `<html>` font-size in sync with `userSettings.fontSize`
 * (small = 14px, medium = 16px, large = 18px). Because every styled
 * component in the project uses `rem`, changing the root size rescales the
 * whole UI proportionally.
 * @param props - Component props.
 * - `children` - App tree to render under the theme.
 * @returns The children wrapped in `ThemeContext.Provider` and MUI's theme.
 * @example
 * ```tsx
 * <AppContext.Provider value={appValue}>
 *   <ThemeProvider>
 *     <App />
 *   </ThemeProvider>
 * </AppContext.Provider>
 * // Mounting with userSettings.fontSize === "large" sets
 * // <html style="font-size: 18px"> and picks the light/dark theme based on
 * // userSettings.theme.
 * ```
 * @source
 */
export const ThemeProvider: FC<ThemeProviderProps> = ({ children }) => {
  const { userSettings, setUserSettings } = useAppContext();
 
  useEffect(() => {
    const size = userSettings.display?.fontSize;
    document.documentElement.style.fontSize =
      (size !== undefined && FONT_SIZE_PX[size]) || FONT_SIZE_PX.medium;
  }, [userSettings.display?.fontSize]);
 
  const mode: ThemeMode = userSettings.display?.theme === 'dark' ? 'dark' : 'light';
 
  const toggleTheme = () => {
    setUserSettings({
      ...userSettings,
      display: { ...userSettings.display, theme: mode === 'light' ? 'dark' : 'light' },
    });
  };
 
  const currentTheme = mode === 'light' ? lightTheme : darkTheme;
  const currentPalette = mode === 'light' ? lightPalette : darkPalette;
 
  const themeContextValue: ThemeContextType = {
    mode,
    toggleTheme,
    currentTheme,
    currentPalette,
  };
 
  return (
    <ThemeContext.Provider value={themeContextValue}>
      <MuiThemeProvider theme={currentTheme}>{children}</MuiThemeProvider>
    </ThemeContext.Provider>
  );
};