From 17180e8232b605e896c5ef2d7d02bfb35f77bcb4 Mon Sep 17 00:00:00 2001 From: pharmpy-dev-123 <101794402+pharmpy-dev-123@users.noreply.github.com> Date: Mon, 17 Oct 2022 19:20:59 +0200 Subject: [PATCH] Get rid of flash of erroneously styled content Using MUI's experimental `useColorScheme`. Fixes #1. --- gatsby-ssr.js | 5 ++ src/layouts/default.tsx | 13 +++-- src/ui/Header.tsx | 70 ++++++++++++++++++++------ src/ui/lib/component/useForceUpdate.ts | 11 ++++ src/ui/lib/component/useIsMounted.ts | 25 +++++++++ src/ui/lib/text/HighlightGrammar.tsx | 7 ++- src/ui/theme/ModeContext.ts | 19 ------- src/ui/theme/ModeProvider.tsx | 27 ---------- src/ui/theme/useMode.ts | 7 ++- src/ui/theme/useNavigatorMode.ts | 7 --- src/ui/useTheme.ts | 13 +---- 11 files changed, 113 insertions(+), 91 deletions(-) create mode 100644 gatsby-ssr.js create mode 100644 src/ui/lib/component/useForceUpdate.ts create mode 100644 src/ui/lib/component/useIsMounted.ts delete mode 100644 src/ui/theme/ModeContext.ts delete mode 100644 src/ui/theme/ModeProvider.tsx delete mode 100644 src/ui/theme/useNavigatorMode.ts diff --git a/gatsby-ssr.js b/gatsby-ssr.js new file mode 100644 index 0000000..27e0c09 --- /dev/null +++ b/gatsby-ssr.js @@ -0,0 +1,5 @@ +import {getInitColorSchemeScript} from '@mui/material/styles'; + +export function onRenderBody({setPreBodyComponents}) { + setPreBodyComponents([getInitColorSchemeScript()]); +} diff --git a/src/layouts/default.tsx b/src/layouts/default.tsx index 15184e9..fb1481b 100644 --- a/src/layouts/default.tsx +++ b/src/layouts/default.tsx @@ -7,11 +7,14 @@ import '@fontsource/roboto/700.css'; import type {PropsOf} from '@emotion/react/types/helper'; -import {ThemeProvider} from '@mui/material/styles'; +import { + ThemeProvider, + Experimental_CssVarsProvider as CssVarsProvider, +} from '@mui/material/styles'; +// eslint-disable-next-line import/no-unassigned-import +import type {} from '@mui/material/themeCssVarsAugmentation'; import CssBaseline from '@mui/material/CssBaseline'; -import ModeProvider from '../ui/theme/ModeProvider'; - import useTheme from '../ui/useTheme'; import Header from '../ui/Header'; import Main from '../ui/Main'; @@ -33,9 +36,9 @@ function Layout({path, ...rest}: LayoutProps) { function App(props: LayoutProps) { return ( - + - + ); } diff --git a/src/ui/Header.tsx b/src/ui/Header.tsx index 7f9ec2e..991b5fc 100644 --- a/src/ui/Header.tsx +++ b/src/ui/Header.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import {useTheme} from '@mui/material/styles'; +import {useTheme, useColorScheme} from '@mui/material/styles'; import AppBar from '@mui/material/AppBar'; import Toolbar from '@mui/material/Toolbar'; @@ -9,9 +9,12 @@ import IconButton from '@mui/material/IconButton'; import Box from '@mui/material/Box'; import EditIcon from '@mui/icons-material/Edit'; +import CircleIcon from '@mui/icons-material/Circle'; import LightModeIcon from '@mui/icons-material/LightMode'; import DarkModeIcon from '@mui/icons-material/DarkMode'; +import SystemModeIcon from '@mui/icons-material/Brightness4'; +import useIsMounted from './lib/component/useIsMounted'; import Breadcrumbs from './navigation/BreadCrumbs'; import useMode from './theme/useMode'; @@ -19,27 +22,64 @@ type HeaderProps = { path: string; }; +type Mode = 'system' | 'light' | 'dark'; + +type ModeMapOptions = { + system: T; + light: T; + dark: T; +}; + +function modeMap(mode: Mode, options: ModeMapOptions): T { + return options[mode]; +} + +function ModeSwitch() { + const isMounted = useIsMounted(); + const {mode, setMode} = useColorScheme(); + + let onClick; + let ariaLabel; + let Icon = CircleIcon; + if (mode !== undefined && isMounted()) { + const nextMode = modeMap(mode, { + system: 'light', + light: 'dark', + dark: 'system', + }); + onClick = () => { + setMode(nextMode); + }; + + ariaLabel = `switch to ${nextMode} mode`; + Icon = modeMap(mode, { + system: SystemModeIcon, + light: LightModeIcon, + dark: DarkModeIcon, + }); + } + + return ( + + + + ); +} + function Header({path}: HeaderProps) { - const { - palette: {mode}, - } = useTheme(); - const [, setMode] = useMode(); return ( - { - setMode(mode === 'dark' ? 'light' : 'dark'); - }} - > - {mode === 'dark' ? : } - + { + // eslint-disable-next-line react/hook-use-state + const [, updateState] = useState>(); + return useCallback(() => { + updateState({}); + }, []); +}; + +export default useForceUpdate; diff --git a/src/ui/lib/component/useIsMounted.ts b/src/ui/lib/component/useIsMounted.ts new file mode 100644 index 0000000..2253077 --- /dev/null +++ b/src/ui/lib/component/useIsMounted.ts @@ -0,0 +1,25 @@ +import {useRef, useEffect, useState} from 'react'; +import useForceUpdate from './useForceUpdate'; + +/** + * See https://gist.github.com/jaydenseric/a67cfb1b809b1b789daa17dfe6f83daa + * + * Do not use to avoid warning when calling setState on an unmounted component. + * See https://github.com/facebook/react/pull/22114 + */ +const useIsMounted = () => { + const componentIsMounted = useRef(false); + const forceUpdate = useForceUpdate(); + + useEffect(() => { + componentIsMounted.current = true; + forceUpdate(); + return () => { + componentIsMounted.current = false; + }; + }, [forceUpdate]); + + return () => componentIsMounted.current; +}; + +export default useIsMounted; diff --git a/src/ui/lib/text/HighlightGrammar.tsx b/src/ui/lib/text/HighlightGrammar.tsx index 2b9b5e4..e50bf2a 100644 --- a/src/ui/lib/text/HighlightGrammar.tsx +++ b/src/ui/lib/text/HighlightGrammar.tsx @@ -13,7 +13,8 @@ import r from 'react-syntax-highlighter/dist/esm/languages/prism/r'; import bash from 'react-syntax-highlighter/dist/esm/languages/prism/bash'; import dark from 'react-syntax-highlighter/dist/esm/styles/prism/material-dark'; import light from 'react-syntax-highlighter/dist/esm/styles/prism/material-light'; -import {useTheme} from '@mui/material/styles'; + +import useMode from '../../theme/useMode'; import saveTextToClipboard from '../output/saveTextToClipboard'; SyntaxHighlighter.registerLanguage('python', python); @@ -60,9 +61,7 @@ const customStyle = {margin: 0, display: 'flex', flex: '1'}; type Style = typeof dark | typeof light; function HighlightGrammar({language, word}: Props) { - const { - palette: {mode}, - } = useTheme(); + const mode = useMode(); const style: Style = mode === 'dark' ? dark : light; const [tooltipText, setTooltipText] = useState(init); const [open, setOpen] = useState(false); diff --git a/src/ui/theme/ModeContext.ts b/src/ui/theme/ModeContext.ts deleted file mode 100644 index a29ffbd..0000000 --- a/src/ui/theme/ModeContext.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type {SetStateAction} from 'react'; -import type React from 'react'; -import {createContext} from 'react'; - -import type {PaletteMode} from '@mui/material'; - -export type State = PaletteMode | undefined; - -export type Dispatch = React.Dispatch>; - -// eslint-disable-next-line @typescript-eslint/naming-convention -const ModeContext = createContext<[State, Dispatch]>([ - undefined, - () => { - // NOTE no-op by default - }, -]); - -export default ModeContext; diff --git a/src/ui/theme/ModeProvider.tsx b/src/ui/theme/ModeProvider.tsx deleted file mode 100644 index 6f717a2..0000000 --- a/src/ui/theme/ModeProvider.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React, {useEffect, useMemo, useState} from 'react'; -import type {PropsOf} from '@emotion/react/types/helper'; -import type {PaletteMode} from '@mui/material'; -import type {Dispatch, State} from './ModeContext'; -import ModeContext from './ModeContext'; -import useNavigatorMode from './useNavigatorMode'; - -type ModeProviderProps = Record & - Omit, 'value'>; - -function ModeProvider(props: ModeProviderProps) { - const init = useNavigatorMode(); - const [mode, setMode] = useState(init); - - useEffect(() => { - setMode(init); - }, [init]); - - const value = useMemo<[State, Dispatch]>( - () => [mode, setMode], - [mode, setMode], - ); - - return ; -} - -export default ModeProvider; diff --git a/src/ui/theme/useMode.ts b/src/ui/theme/useMode.ts index 55a72d0..fb6ec85 100644 --- a/src/ui/theme/useMode.ts +++ b/src/ui/theme/useMode.ts @@ -1,6 +1,9 @@ import {useContext} from 'react'; -import ModeContext from './ModeContext'; +import {useColorScheme} from '@mui/material/styles'; -const useMode = () => useContext(ModeContext); +const useMode = () => { + const {mode: colorSchemeMode, systemMode} = useColorScheme(); + return colorSchemeMode === 'system' ? systemMode : colorSchemeMode; +}; export default useMode; diff --git a/src/ui/theme/useNavigatorMode.ts b/src/ui/theme/useNavigatorMode.ts deleted file mode 100644 index f99b157..0000000 --- a/src/ui/theme/useNavigatorMode.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type {PaletteMode} from '@mui/material'; -import {useMediaQuery} from '@mui/material'; - -const useNavigatorMode = (): PaletteMode => - useMediaQuery('(prefers-color-scheme: dark)') ? 'dark' : 'light'; - -export default useNavigatorMode; diff --git a/src/ui/useTheme.ts b/src/ui/useTheme.ts index b7c31e2..2041cbe 100644 --- a/src/ui/useTheme.ts +++ b/src/ui/useTheme.ts @@ -1,19 +1,8 @@ import {useMemo} from 'react'; import {createTheme} from '@mui/material/styles'; -import useMode from './theme/useMode'; - const useTheme = () => { - const [mode] = useMode(); - return useMemo( - () => - createTheme({ - palette: { - mode, - }, - }), - [mode], - ); + return useMemo(() => createTheme(), []); }; export default useTheme;