From edc3d333078b241e302ee9b4a86e36061775cd44 Mon Sep 17 00:00:00 2001 From: Jan Potoms <2109932+Janpot@users.noreply.github.com> Date: Wed, 21 Feb 2024 10:13:09 +0100 Subject: [PATCH 01/26] [utils] Port `useLocalStorageState` hook from Toolpad (#41096) Signed-off-by: Jan Potoms <2109932+Janpot@users.noreply.github.com> --- .../src/components/header/ThemeModeToggle.tsx | 24 +-- .../modules/components/AppSettingsDrawer.js | 35 +--- .../components/HighlightedCodeWithTabs.tsx | 25 +-- .../src/useLocalStorageState/index.ts | 1 + .../useLocalStorageState.ts | 155 ++++++++++++++++++ 5 files changed, 173 insertions(+), 67 deletions(-) create mode 100644 packages/mui-utils/src/useLocalStorageState/index.ts create mode 100644 packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts diff --git a/docs/src/components/header/ThemeModeToggle.tsx b/docs/src/components/header/ThemeModeToggle.tsx index ff22e076ae3797..6a7667f796d8df 100644 --- a/docs/src/components/header/ThemeModeToggle.tsx +++ b/docs/src/components/header/ThemeModeToggle.tsx @@ -6,6 +6,7 @@ import DarkModeOutlined from '@mui/icons-material/DarkModeOutlined'; import LightModeOutlined from '@mui/icons-material/LightModeOutlined'; import useMediaQuery from '@mui/material/useMediaQuery'; import { useChangeTheme } from 'docs/src/modules/components/ThemeContext'; +import useLocalStorageState from '@mui/utils/useLocalStorageState'; function CssVarsModeToggle(props: { onChange: (checked: boolean) => void }) { const [mounted, setMounted] = React.useState(false); @@ -39,30 +40,19 @@ function CssVarsModeToggle(props: { onChange: (checked: boolean) => void }) { export default function ThemeModeToggle() { const theme = useTheme(); const changeTheme = useChangeTheme(); - const [mode, setMode] = React.useState(null); + const [mode, setMode] = useLocalStorageState('mui-mode', 'system'); const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); - - React.useEffect(() => { - let initialMode = 'system'; - try { - initialMode = localStorage.getItem('mui-mode') || initialMode; - } catch (error) { - // do nothing - } - setMode(initialMode); - }, []); + const preferredMode = prefersDarkMode ? 'dark' : 'light'; const handleChangeThemeMode = (checked: boolean) => { const paletteMode = checked ? 'dark' : 'light'; setMode(paletteMode); + }; - try { - localStorage.setItem('mui-mode', paletteMode); // syncing with homepage, can be removed once all pages are migrated to CSS variables - } catch (error) { - // do nothing - } + React.useEffect(() => { + const paletteMode = mode === 'system' ? preferredMode : mode; changeTheme({ paletteMode }); - }; + }, [changeTheme, mode, preferredMode]); if (mode === null) { return ; diff --git a/docs/src/modules/components/AppSettingsDrawer.js b/docs/src/modules/components/AppSettingsDrawer.js index 3ddad7cb06a71b..58d2a830cd787e 100644 --- a/docs/src/modules/components/AppSettingsDrawer.js +++ b/docs/src/modules/components/AppSettingsDrawer.js @@ -18,6 +18,7 @@ import FormatTextdirectionLToRIcon from '@mui/icons-material/FormatTextdirection import FormatTextdirectionRToLIcon from '@mui/icons-material/FormatTextdirectionRToL'; import { useChangeTheme } from 'docs/src/modules/components/ThemeContext'; import { useTranslate } from '@mui/docs/i18n'; +import useLocalStorageState from '@mui/utils/useLocalStorageState'; const Heading = styled(Typography)(({ theme }) => ({ margin: '20px 0 10px', @@ -42,45 +43,23 @@ function AppSettingsDrawer(props) { const t = useTranslate(); const upperTheme = useTheme(); const changeTheme = useChangeTheme(); - const [mode, setMode] = React.useState(null); + const [mode, setMode] = useLocalStorageState('mui-mode', 'system'); const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); const preferredMode = prefersDarkMode ? 'dark' : 'light'; - React.useEffect(() => { - // syncing with homepage, can be removed once all pages are migrated to CSS variables - let initialMode = 'system'; - try { - initialMode = localStorage.getItem('mui-mode') || initialMode; - } catch (error) { - // do nothing - } - setMode(initialMode); - }, [preferredMode]); - const handleChangeThemeMode = (event, paletteMode) => { if (paletteMode === null) { return; } setMode(paletteMode); - - if (paletteMode === 'system') { - try { - localStorage.setItem('mui-mode', 'system'); // syncing with homepage, can be removed once all pages are migrated to CSS variables - } catch (error) { - // thrown when cookies are disabled. - } - changeTheme({ paletteMode: preferredMode }); - } else { - try { - localStorage.setItem('mui-mode', paletteMode); // syncing with homepage, can be removed once all pages are migrated to CSS variables - } catch (error) { - // thrown when cookies are disabled. - } - changeTheme({ paletteMode }); - } }; + React.useEffect(() => { + const paletteMode = mode === 'system' ? preferredMode : mode; + changeTheme({ paletteMode }); + }, [changeTheme, mode, preferredMode]); + const handleChangeDirection = (event, direction) => { if (direction === null) { direction = upperTheme.direction; diff --git a/docs/src/modules/components/HighlightedCodeWithTabs.tsx b/docs/src/modules/components/HighlightedCodeWithTabs.tsx index f31a70348e42df..bedd57ba5882f6 100644 --- a/docs/src/modules/components/HighlightedCodeWithTabs.tsx +++ b/docs/src/modules/components/HighlightedCodeWithTabs.tsx @@ -4,6 +4,7 @@ import { Tabs, TabsOwnProps } from '@mui/base/Tabs'; import { TabsList as TabsListBase } from '@mui/base/TabsList'; import { TabPanel as TabPanelBase } from '@mui/base/TabPanel'; import { Tab as TabBase } from '@mui/base/Tab'; +import useLocalStorageState from '@mui/utils/useLocalStorageState'; import HighlightedCode from './HighlightedCode'; const TabList = styled(TabsListBase)(({ theme }) => ({ @@ -85,36 +86,16 @@ export default function HighlightedCodeWithTabs({ storageKey?: string; } & Record) { const availableTabs = React.useMemo(() => tabs.map(({ tab }) => tab), [tabs]); - const [activeTab, setActiveTab] = React.useState(availableTabs[0]); + const [activeTab, setActiveTab] = useLocalStorageState(storageKey ?? null, availableTabs[0]); const [mounted, setMounted] = React.useState(false); React.useEffect(() => { - try { - setActiveTab((prev) => { - if (storageKey === undefined) { - return prev; - } - const storedValues = localStorage.getItem(storageKey); - - return storedValues && availableTabs.includes(storedValues) ? storedValues : prev; - }); - } catch (error) { - // ignore error - } setMounted(true); - }, [availableTabs, storageKey]); + }, []); const handleChange: TabsOwnProps['onChange'] = (event, newValue) => { setActiveTab(newValue as string); - if (storageKey === undefined) { - return; - } - try { - localStorage.setItem(storageKey, newValue as string); - } catch (error) { - // ignore error - } }; const ownerState = { mounted }; diff --git a/packages/mui-utils/src/useLocalStorageState/index.ts b/packages/mui-utils/src/useLocalStorageState/index.ts new file mode 100644 index 00000000000000..33ff661f99ed20 --- /dev/null +++ b/packages/mui-utils/src/useLocalStorageState/index.ts @@ -0,0 +1 @@ +export { default } from './useLocalStorageState'; diff --git a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts new file mode 100644 index 00000000000000..d8ce3c4b836071 --- /dev/null +++ b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts @@ -0,0 +1,155 @@ +'use client'; + +import * as React from 'react'; + +const NOOP = () => {}; + +// storage events only work across tabs, we'll use an event emitter to announce within the current tab +const currentTabChangeListeners = new Map void>>(); + +function onCurrentTabStorageChange(key: string, handler: () => void) { + let listeners = currentTabChangeListeners.get(key); + + if (!listeners) { + listeners = new Set(); + currentTabChangeListeners.set(key, listeners); + } + + listeners.add(handler); +} + +function offCurrentTabStorageChange(key: string, handler: () => void) { + const listeners = currentTabChangeListeners.get(key); + if (!listeners) { + return; + } + + listeners.delete(handler); + + if (listeners.size === 0) { + currentTabChangeListeners.delete(key); + } +} + +function emitCurrentTabStorageChange(key: string) { + const listeners = currentTabChangeListeners.get(key); + if (listeners) { + listeners.forEach((listener) => listener()); + } +} + +function subscribe(area: Storage, key: string, cb: () => void): () => void { + const storageHandler = (event: StorageEvent) => { + if (event.storageArea === area && event.key === key) { + cb(); + } + }; + window.addEventListener('storage', storageHandler); + onCurrentTabStorageChange(key, cb); + return () => { + window.removeEventListener('storage', storageHandler); + offCurrentTabStorageChange(key, cb); + }; +} + +function getSnapshot(area: Storage, key: string): string | null { + return area.getItem(key); +} + +function setValue(area: Storage, key: string, value: string | null) { + if (typeof window !== 'undefined') { + if (value === null) { + area.removeItem(key); + } else { + area.setItem(key, String(value)); + } + emitCurrentTabStorageChange(key); + } +} + +type Initializer = () => T; + +type UseStorageStateHookResult = [T, React.Dispatch>]; + +function useLocalStorageStateServer( + key: string | null, + initializer: string | Initializer, +): UseStorageStateHookResult; +function useLocalStorageStateServer( + key: string | null, + initializer?: string | null | Initializer, +): UseStorageStateHookResult; +function useLocalStorageStateServer( + key: string | null, + initializer: string | null | Initializer = null, +): UseStorageStateHookResult | UseStorageStateHookResult { + const [initialValue] = React.useState(initializer); + return [initialValue, () => {}]; +} + +/** + * Sync state to local storage so that it persists through a page refresh. Usage is + * similar to useState except we pass in a storage key so that we can default + * to that value on page load instead of the specified initial value. + * + * Since the storage API isn't available in server-rendering environments, we + * return initialValue during SSR and hydration. + * + * Things this hook does different from existing solutions: + * - SSR-capable: it shows initial value during SSR and hydration, but immediately + * initializes when clientside mounted. + * - Sync state across tabs: When another tab changes the value in the storage area, the + * current tab follows suit. + */ +function useLocalStorageStateBrowser( + key: string | null, + initializer: string | Initializer, +): UseStorageStateHookResult; +function useLocalStorageStateBrowser( + key: string | null, + initializer?: string | null | Initializer, +): UseStorageStateHookResult; +function useLocalStorageStateBrowser( + key: string | null, + initializer: string | null | Initializer = null, +): UseStorageStateHookResult | UseStorageStateHookResult { + const [initialValue] = React.useState(initializer); + const area = window.localStorage; + const subscribeKey = React.useCallback( + (cb: () => void) => (key ? subscribe(area, key, cb) : NOOP), + [area, key], + ); + const getKeySnapshot = React.useCallback( + () => (key && getSnapshot(area, key)) ?? initialValue, + [area, initialValue, key], + ); + const getKeyServerSnapshot = React.useCallback(() => initialValue, [initialValue]); + + const storedValue = React.useSyncExternalStore( + subscribeKey, + getKeySnapshot, + getKeyServerSnapshot, + ); + + const setStoredValue = React.useCallback( + (value: React.SetStateAction) => { + if (key) { + const valueToStore = value instanceof Function ? value(storedValue) : value; + setValue(area, key, valueToStore); + } + }, + [area, key, storedValue], + ); + + const [nonStoredValue, setNonStoredValue] = React.useState(initialValue); + + if (!key) { + return [nonStoredValue, setNonStoredValue]; + } + + return [storedValue, setStoredValue]; +} + +export default typeof window === 'undefined' + ? useLocalStorageStateServer + : useLocalStorageStateBrowser; From babbcf8e5cc8cdec5dfd3bc8a18b3ecbf030d9ee Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Wed, 21 Feb 2024 15:17:20 +0100 Subject: [PATCH 02/26] Fix useStorageState regressions --- docs/src/components/header/ThemeModeToggle.tsx | 2 +- .../mui-utils/src/useLocalStorageState/useLocalStorageState.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/components/header/ThemeModeToggle.tsx b/docs/src/components/header/ThemeModeToggle.tsx index 6a7667f796d8df..8576ebde7fbf4f 100644 --- a/docs/src/components/header/ThemeModeToggle.tsx +++ b/docs/src/components/header/ThemeModeToggle.tsx @@ -20,7 +20,7 @@ function CssVarsModeToggle(props: { onChange: (checked: boolean) => void }) { { props.onChange(calculatedMode === 'light'); setMode(calculatedMode === 'dark' ? 'light' : 'dark'); diff --git a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts index d8ce3c4b836071..69e3cdb4f6b3bf 100644 --- a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts +++ b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts @@ -150,6 +150,6 @@ function useLocalStorageStateBrowser( return [storedValue, setStoredValue]; } -export default typeof window === 'undefined' +export default typeof window === 'undefined' || !window.localStorage ? useLocalStorageStateServer : useLocalStorageStateBrowser; From 3111b221041cda94d75e35e735763e49a184162e Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Wed, 21 Feb 2024 15:21:03 +0100 Subject: [PATCH 03/26] Update useLocalStorageState.ts --- .../mui-utils/src/useLocalStorageState/useLocalStorageState.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts index 69e3cdb4f6b3bf..773d804478bb03 100644 --- a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts +++ b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts @@ -83,8 +83,7 @@ function useLocalStorageStateServer( key: string | null, initializer: string | null | Initializer = null, ): UseStorageStateHookResult | UseStorageStateHookResult { - const [initialValue] = React.useState(initializer); - return [initialValue, () => {}]; + return React.useState(initializer); } /** From d5c5b009ad605ec6ab6554e724bf188d1d87ce0c Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Wed, 21 Feb 2024 15:30:38 +0100 Subject: [PATCH 04/26] Update ThemeModeToggle.tsx --- docs/src/components/header/ThemeModeToggle.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/src/components/header/ThemeModeToggle.tsx b/docs/src/components/header/ThemeModeToggle.tsx index 8576ebde7fbf4f..37f61fb6d6160c 100644 --- a/docs/src/components/header/ThemeModeToggle.tsx +++ b/docs/src/components/header/ThemeModeToggle.tsx @@ -15,8 +15,9 @@ function CssVarsModeToggle(props: { onChange: (checked: boolean) => void }) { setMounted(true); }, []); const calculatedMode = mode === 'system' ? systemMode : mode; + const title = calculatedMode === 'dark' ? 'Turn on the light' : 'Turn off the light'; return ( - + Date: Wed, 21 Feb 2024 15:33:38 +0100 Subject: [PATCH 05/26] Update ThemeModeToggle.tsx --- docs/src/components/header/ThemeModeToggle.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/src/components/header/ThemeModeToggle.tsx b/docs/src/components/header/ThemeModeToggle.tsx index 37f61fb6d6160c..53d60a4b98d7a7 100644 --- a/docs/src/components/header/ThemeModeToggle.tsx +++ b/docs/src/components/header/ThemeModeToggle.tsx @@ -15,19 +15,23 @@ function CssVarsModeToggle(props: { onChange: (checked: boolean) => void }) { setMounted(true); }, []); const calculatedMode = mode === 'system' ? systemMode : mode; - const title = calculatedMode === 'dark' ? 'Turn on the light' : 'Turn off the light'; + + if (!mounted) { + return ; + } + return ( - + { props.onChange(calculatedMode === 'light'); setMode(calculatedMode === 'dark' ? 'light' : 'dark'); }} > - {!calculatedMode || !mounted + {!calculatedMode ? null : { light: , From 977f7edde28c5d2ff6861c1adc745c3c1e8bf7ba Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Wed, 21 Feb 2024 16:00:23 +0100 Subject: [PATCH 06/26] Update useLocalStorageState.ts --- .../useLocalStorageState.ts | 41 ++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts index 773d804478bb03..8dcec5be480307 100644 --- a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts +++ b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts @@ -38,7 +38,10 @@ function emitCurrentTabStorageChange(key: string) { } } -function subscribe(area: Storage, key: string, cb: () => void): () => void { +function subscribe(area: Storage, key: string | null, cb: () => void): () => void { + if (!key) { + return () => {}; + } const storageHandler = (event: StorageEvent) => { if (event.storageArea === area && event.key === key) { cb(); @@ -52,18 +55,33 @@ function subscribe(area: Storage, key: string, cb: () => void): () => void { }; } -function getSnapshot(area: Storage, key: string): string | null { - return area.getItem(key); +function getSnapshot(area: Storage, key: string | null): string | null { + if (!key) { + return null; + } + try { + return area.getItem(key); + } catch { + // ignore + // See https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API#feature-detecting_localstorage + return null; + } } -function setValue(area: Storage, key: string, value: string | null) { - if (typeof window !== 'undefined') { +function setValue(area: Storage, key: string | null, value: string | null) { + if (!key) { + return; + } + try { if (value === null) { area.removeItem(key); } else { area.setItem(key, String(value)); } emitCurrentTabStorageChange(key); + } catch { + // ignore + // See https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API#feature-detecting_localstorage } } @@ -114,12 +132,9 @@ function useLocalStorageStateBrowser( ): UseStorageStateHookResult | UseStorageStateHookResult { const [initialValue] = React.useState(initializer); const area = window.localStorage; - const subscribeKey = React.useCallback( - (cb: () => void) => (key ? subscribe(area, key, cb) : NOOP), - [area, key], - ); + const subscribeKey = React.useCallback((cb: () => void) => subscribe(area, key, cb), [area, key]); const getKeySnapshot = React.useCallback( - () => (key && getSnapshot(area, key)) ?? initialValue, + () => getSnapshot(area, key) ?? initialValue, [area, initialValue, key], ); const getKeyServerSnapshot = React.useCallback(() => initialValue, [initialValue]); @@ -132,10 +147,8 @@ function useLocalStorageStateBrowser( const setStoredValue = React.useCallback( (value: React.SetStateAction) => { - if (key) { - const valueToStore = value instanceof Function ? value(storedValue) : value; - setValue(area, key, valueToStore); - } + const valueToStore = value instanceof Function ? value(storedValue) : value; + setValue(area, key, valueToStore); }, [area, key, storedValue], ); From ac7599c22f5f1bd56f813d0e5beb21ca43a4768c Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Wed, 21 Feb 2024 16:03:42 +0100 Subject: [PATCH 07/26] Update useLocalStorageState.ts --- .../mui-utils/src/useLocalStorageState/useLocalStorageState.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts index 8dcec5be480307..6d065f039a8837 100644 --- a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts +++ b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts @@ -162,6 +162,6 @@ function useLocalStorageStateBrowser( return [storedValue, setStoredValue]; } -export default typeof window === 'undefined' || !window.localStorage +export default typeof window === 'undefined' ? useLocalStorageStateServer : useLocalStorageStateBrowser; From 4038038670e8966914eb43526c773bbd35784082 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Wed, 21 Feb 2024 16:15:56 +0100 Subject: [PATCH 08/26] Update useLocalStorageState.ts --- .../mui-utils/src/useLocalStorageState/useLocalStorageState.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts index 6d065f039a8837..ae0c920dd68ec9 100644 --- a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts +++ b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts @@ -2,8 +2,6 @@ import * as React from 'react'; -const NOOP = () => {}; - // storage events only work across tabs, we'll use an event emitter to announce within the current tab const currentTabChangeListeners = new Map void>>(); From 0f37e1f1d405f3a20c09d3fac030a0f82de2c235 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Wed, 21 Feb 2024 16:30:57 +0100 Subject: [PATCH 09/26] Update useLocalStorageState.ts --- .../mui-utils/src/useLocalStorageState/useLocalStorageState.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts index ae0c920dd68ec9..918da18c94d988 100644 --- a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts +++ b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts @@ -76,11 +76,12 @@ function setValue(area: Storage, key: string | null, value: string | null) { } else { area.setItem(key, String(value)); } - emitCurrentTabStorageChange(key); } catch { // ignore // See https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API#feature-detecting_localstorage + return; } + emitCurrentTabStorageChange(key); } type Initializer = () => T; From ebdf2bcae2306647624c434d0d64fe2a3eab1ae8 Mon Sep 17 00:00:00 2001 From: Olivier Tassinari Date: Sat, 24 Feb 2024 21:01:13 +0100 Subject: [PATCH 10/26] convention; no blank space here --- packages/mui-material-next/src/index.ts | 1 - .../mui-utils/src/useLocalStorageState/useLocalStorageState.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/mui-material-next/src/index.ts b/packages/mui-material-next/src/index.ts index c8726df41789c2..06124e3d20d038 100644 --- a/packages/mui-material-next/src/index.ts +++ b/packages/mui-material-next/src/index.ts @@ -1,5 +1,4 @@ 'use client'; - export { default as Badge } from './Badge'; export * from './Badge'; diff --git a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts index 918da18c94d988..5aea48d9416146 100644 --- a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts +++ b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts @@ -1,5 +1,4 @@ 'use client'; - import * as React from 'react'; // storage events only work across tabs, we'll use an event emitter to announce within the current tab From 39ef9baccade07044aba1fe6b596d50614f247e8 Mon Sep 17 00:00:00 2001 From: Olivier Tassinari Date: Sat, 24 Feb 2024 21:04:42 +0100 Subject: [PATCH 11/26] convention --- docs/src/components/header/ThemeModeToggle.tsx | 1 + docs/src/modules/components/AppSettingsDrawer.js | 1 + .../components/HighlightedCodeWithTabs.tsx | 15 ++++++++------- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/src/components/header/ThemeModeToggle.tsx b/docs/src/components/header/ThemeModeToggle.tsx index 53d60a4b98d7a7..b6f1d1889a7e3c 100644 --- a/docs/src/components/header/ThemeModeToggle.tsx +++ b/docs/src/components/header/ThemeModeToggle.tsx @@ -46,6 +46,7 @@ export default function ThemeModeToggle() { const theme = useTheme(); const changeTheme = useChangeTheme(); const [mode, setMode] = useLocalStorageState('mui-mode', 'system'); + console.log('mode', mode); const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); const preferredMode = prefersDarkMode ? 'dark' : 'light'; diff --git a/docs/src/modules/components/AppSettingsDrawer.js b/docs/src/modules/components/AppSettingsDrawer.js index 58d2a830cd787e..f124b6cd74235c 100644 --- a/docs/src/modules/components/AppSettingsDrawer.js +++ b/docs/src/modules/components/AppSettingsDrawer.js @@ -44,6 +44,7 @@ function AppSettingsDrawer(props) { const upperTheme = useTheme(); const changeTheme = useChangeTheme(); const [mode, setMode] = useLocalStorageState('mui-mode', 'system'); + console.log('mode', mode); const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); const preferredMode = prefersDarkMode ? 'dark' : 'light'; diff --git a/docs/src/modules/components/HighlightedCodeWithTabs.tsx b/docs/src/modules/components/HighlightedCodeWithTabs.tsx index bedd57ba5882f6..7372011ff47f06 100644 --- a/docs/src/modules/components/HighlightedCodeWithTabs.tsx +++ b/docs/src/modules/components/HighlightedCodeWithTabs.tsx @@ -78,13 +78,14 @@ type TabsConfig = { language: string; tab: string; }; -export default function HighlightedCodeWithTabs({ - tabs, - storageKey, -}: { - tabs: Array; - storageKey?: string; -} & Record) { + +export default function HighlightedCodeWithTabs( + props: { + tabs: Array; + storageKey?: string; + } & Record, +) { + const { tabs, storageKey } = props; const availableTabs = React.useMemo(() => tabs.map(({ tab }) => tab), [tabs]); const [activeTab, setActiveTab] = useLocalStorageState(storageKey ?? null, availableTabs[0]); From 4bfc8f657edc9b9c998c47590cf6bc0307ee55c4 Mon Sep 17 00:00:00 2001 From: Olivier Tassinari Date: Sat, 24 Feb 2024 21:16:41 +0100 Subject: [PATCH 12/26] need to return null for the first render as we migh need special handling during hydration --- .../src/components/header/ThemeModeToggle.tsx | 7 ++--- .../modules/components/AppSettingsDrawer.js | 4 +-- .../useLocalStorageState.ts | 28 +++++++++++-------- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/docs/src/components/header/ThemeModeToggle.tsx b/docs/src/components/header/ThemeModeToggle.tsx index b6f1d1889a7e3c..0e58564f01b436 100644 --- a/docs/src/components/header/ThemeModeToggle.tsx +++ b/docs/src/components/header/ThemeModeToggle.tsx @@ -16,10 +16,6 @@ function CssVarsModeToggle(props: { onChange: (checked: boolean) => void }) { }, []); const calculatedMode = mode === 'system' ? systemMode : mode; - if (!mounted) { - return ; - } - return ( void }) { setMode(calculatedMode === 'dark' ? 'light' : 'dark'); }} > - {!calculatedMode + {!calculatedMode || !mounted ? null : { light: , @@ -60,6 +56,7 @@ export default function ThemeModeToggle() { changeTheme({ paletteMode }); }, [changeTheme, mode, preferredMode]); + // Server-side hydration if (mode === null) { return ; } diff --git a/docs/src/modules/components/AppSettingsDrawer.js b/docs/src/modules/components/AppSettingsDrawer.js index f124b6cd74235c..9e9a29ec628686 100644 --- a/docs/src/modules/components/AppSettingsDrawer.js +++ b/docs/src/modules/components/AppSettingsDrawer.js @@ -38,7 +38,7 @@ const IconToggleButton = styled(ToggleButton)({ }, }); -function AppSettingsDrawer(props) { +export default function AppSettingsDrawer(props) { const { onClose, open = false, ...other } = props; const t = useTranslate(); const upperTheme = useTheme(); @@ -180,5 +180,3 @@ AppSettingsDrawer.propTypes = { onClose: PropTypes.func.isRequired, open: PropTypes.bool, }; - -export default AppSettingsDrawer; diff --git a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts index 5aea48d9416146..0d4aadd3ba25f7 100644 --- a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts +++ b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts @@ -35,20 +35,20 @@ function emitCurrentTabStorageChange(key: string) { } } -function subscribe(area: Storage, key: string | null, cb: () => void): () => void { +function subscribe(area: Storage, key: string | null, callbark: () => void): () => void { if (!key) { return () => {}; } const storageHandler = (event: StorageEvent) => { if (event.storageArea === area && event.key === key) { - cb(); + callbark(); } }; window.addEventListener('storage', storageHandler); - onCurrentTabStorageChange(key, cb); + onCurrentTabStorageChange(key, callbark); return () => { window.removeEventListener('storage', storageHandler); - offCurrentTabStorageChange(key, cb); + offCurrentTabStorageChange(key, callbark); }; } @@ -89,17 +89,16 @@ type UseStorageStateHookResult = [T, React.Dispatch>] function useLocalStorageStateServer( key: string | null, - initializer: string | Initializer, + initializer: any, ): UseStorageStateHookResult; function useLocalStorageStateServer( key: string | null, initializer?: string | null | Initializer, ): UseStorageStateHookResult; -function useLocalStorageStateServer( - key: string | null, - initializer: string | null | Initializer = null, -): UseStorageStateHookResult | UseStorageStateHookResult { - return React.useState(initializer); +function useLocalStorageStateServer(): + | UseStorageStateHookResult + | UseStorageStateHookResult { + return React.useState(null); } /** @@ -130,12 +129,17 @@ function useLocalStorageStateBrowser( ): UseStorageStateHookResult | UseStorageStateHookResult { const [initialValue] = React.useState(initializer); const area = window.localStorage; - const subscribeKey = React.useCallback((cb: () => void) => subscribe(area, key, cb), [area, key]); + const subscribeKey = React.useCallback( + (callbark: () => void) => subscribe(area, key, callbark), + [area, key], + ); const getKeySnapshot = React.useCallback( () => getSnapshot(area, key) ?? initialValue, [area, initialValue, key], ); - const getKeyServerSnapshot = React.useCallback(() => initialValue, [initialValue]); + + // Start with null for the hydration, and then switch to the actual value. + const getKeyServerSnapshot = () => null; const storedValue = React.useSyncExternalStore( subscribeKey, From 6041a938180ef43bca53cd07b414e27ec6f2ab0c Mon Sep 17 00:00:00 2001 From: Olivier Tassinari Date: Sat, 24 Feb 2024 21:37:57 +0100 Subject: [PATCH 13/26] simplify the logic --- .../src/components/header/ThemeModeToggle.tsx | 39 ++++++++----------- .../useLocalStorageState.ts | 1 + 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/docs/src/components/header/ThemeModeToggle.tsx b/docs/src/components/header/ThemeModeToggle.tsx index 0e58564f01b436..ca7b286023a442 100644 --- a/docs/src/components/header/ThemeModeToggle.tsx +++ b/docs/src/components/header/ThemeModeToggle.tsx @@ -8,12 +8,8 @@ import useMediaQuery from '@mui/material/useMediaQuery'; import { useChangeTheme } from 'docs/src/modules/components/ThemeContext'; import useLocalStorageState from '@mui/utils/useLocalStorageState'; -function CssVarsModeToggle(props: { onChange: (checked: boolean) => void }) { - const [mounted, setMounted] = React.useState(false); +function CssVarsModeToggle(props: { onChange: (newMode: string) => void }) { const { mode, systemMode, setMode } = useColorScheme(); - React.useEffect(() => { - setMounted(true); - }, []); const calculatedMode = mode === 'system' ? systemMode : mode; return ( @@ -23,11 +19,12 @@ function CssVarsModeToggle(props: { onChange: (checked: boolean) => void }) { disableTouchRipple disabled={!calculatedMode} onClick={() => { - props.onChange(calculatedMode === 'light'); - setMode(calculatedMode === 'dark' ? 'light' : 'dark'); + const newMode = calculatedMode === 'dark' ? 'light' : 'dark'; + props.onChange(newMode); + setMode(newMode); }} > - {!calculatedMode || !mounted + {!calculatedMode ? null : { light: , @@ -44,17 +41,13 @@ export default function ThemeModeToggle() { const [mode, setMode] = useLocalStorageState('mui-mode', 'system'); console.log('mode', mode); const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); - const preferredMode = prefersDarkMode ? 'dark' : 'light'; - - const handleChangeThemeMode = (checked: boolean) => { - const paletteMode = checked ? 'dark' : 'light'; - setMode(paletteMode); - }; + const systemMode = prefersDarkMode ? 'dark' : 'light'; + const calculatedMode = mode === 'system' ? systemMode : mode; React.useEffect(() => { - const paletteMode = mode === 'system' ? preferredMode : mode; + const paletteMode = mode === 'system' ? systemMode : mode; changeTheme({ paletteMode }); - }, [changeTheme, mode, preferredMode]); + }, [changeTheme, mode, systemMode]); // Server-side hydration if (mode === null) { @@ -63,21 +56,23 @@ export default function ThemeModeToggle() { if (theme.vars) { // Temporarily renders conditionally because `useColorScheme` could not be used in the pages that haven't migrated to CSS theme variables. - return ; + return ; } - const checked = mode === 'system' ? prefersDarkMode : mode === 'dark'; - return ( - + { - handleChangeThemeMode(!checked); + setMode(calculatedMode === 'dark' ? 'light' : 'dark'); }} > - {checked ? : } + {calculatedMode === 'dark' ? ( + + ) : ( + + )} ); diff --git a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts index 0d4aadd3ba25f7..01f119408102bc 100644 --- a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts +++ b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts @@ -41,6 +41,7 @@ function subscribe(area: Storage, key: string | null, callbark: () => void): () } const storageHandler = (event: StorageEvent) => { if (event.storageArea === area && event.key === key) { + console.log('value change'); callbark(); } }; From c22dc5cc6cc0a67422529ac003030a2d31731301 Mon Sep 17 00:00:00 2001 From: Olivier Tassinari Date: Sat, 24 Feb 2024 21:47:32 +0100 Subject: [PATCH 14/26] add no-ssr --- docs/src/components/header/ThemeModeToggle.tsx | 13 ++++++++----- docs/src/modules/components/AppSettingsDrawer.js | 10 +++++----- docs/src/modules/components/ThemeContext.js | 10 +++++----- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/docs/src/components/header/ThemeModeToggle.tsx b/docs/src/components/header/ThemeModeToggle.tsx index ca7b286023a442..278e3640ebcd58 100644 --- a/docs/src/components/header/ThemeModeToggle.tsx +++ b/docs/src/components/header/ThemeModeToggle.tsx @@ -39,15 +39,18 @@ export default function ThemeModeToggle() { const theme = useTheme(); const changeTheme = useChangeTheme(); const [mode, setMode] = useLocalStorageState('mui-mode', 'system'); - console.log('mode', mode); - const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', { noSsr: true }); const systemMode = prefersDarkMode ? 'dark' : 'light'; const calculatedMode = mode === 'system' ? systemMode : mode; + console.log('render', { + mode, + prefersDarkMode, + calculatedMode, + }); React.useEffect(() => { - const paletteMode = mode === 'system' ? systemMode : mode; - changeTheme({ paletteMode }); - }, [changeTheme, mode, systemMode]); + changeTheme({ paletteMode: calculatedMode }); + }, [changeTheme, calculatedMode]); // Server-side hydration if (mode === null) { diff --git a/docs/src/modules/components/AppSettingsDrawer.js b/docs/src/modules/components/AppSettingsDrawer.js index 9e9a29ec628686..5eea38a89953a6 100644 --- a/docs/src/modules/components/AppSettingsDrawer.js +++ b/docs/src/modules/components/AppSettingsDrawer.js @@ -45,8 +45,9 @@ export default function AppSettingsDrawer(props) { const changeTheme = useChangeTheme(); const [mode, setMode] = useLocalStorageState('mui-mode', 'system'); console.log('mode', mode); - const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); - const preferredMode = prefersDarkMode ? 'dark' : 'light'; + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', { noSsr: true }); + const systemMode = prefersDarkMode ? 'dark' : 'light'; + const calculatedMode = mode === 'system' ? systemMode : mode; const handleChangeThemeMode = (event, paletteMode) => { if (paletteMode === null) { @@ -57,9 +58,8 @@ export default function AppSettingsDrawer(props) { }; React.useEffect(() => { - const paletteMode = mode === 'system' ? preferredMode : mode; - changeTheme({ paletteMode }); - }, [changeTheme, mode, preferredMode]); + changeTheme({ paletteMode: calculatedMode }); + }, [changeTheme, calculatedMode]); const handleChangeDirection = (event, direction) => { if (direction === null) { diff --git a/docs/src/modules/components/ThemeContext.js b/docs/src/modules/components/ThemeContext.js index 50fe41c5a7adf5..b50f3c6a28b4e3 100644 --- a/docs/src/modules/components/ThemeContext.js +++ b/docs/src/modules/components/ThemeContext.js @@ -112,7 +112,7 @@ if (process.env.NODE_ENV !== 'production') { export function ThemeProvider(props) { const { children } = props; const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', { noSsr: true }); - const preferredMode = prefersDarkMode ? 'dark' : 'light'; + const systemMode = prefersDarkMode ? 'dark' : 'light'; const [themeOptions, dispatch] = React.useReducer( (state, action) => { @@ -171,22 +171,22 @@ export function ThemeProvider(props) { useEnhancedEffect(() => { const nextPaletteColors = JSON.parse(getCookie('paletteColors') || 'null'); - let nextPaletteMode = preferredMode; // syncing with homepage, can be removed once all pages are migrated to CSS variables + let nextPaletteMode = systemMode; // syncing with homepage, can be removed once all pages are migrated to CSS variables try { - nextPaletteMode = localStorage.getItem('mui-mode') ?? preferredMode; + nextPaletteMode = localStorage.getItem('mui-mode') ?? systemMode; } catch (error) { // mainly thrown when cookies are disabled. } if (nextPaletteMode === 'system') { - nextPaletteMode = preferredMode; + nextPaletteMode = systemMode; } dispatch({ type: 'CHANGE', payload: { paletteColors: nextPaletteColors, paletteMode: nextPaletteMode }, }); - }, [preferredMode]); + }, [systemMode]); useEnhancedEffect(() => { document.body.dir = direction; From f23494505422c3586f69746e61b39d5bc36d3561 Mon Sep 17 00:00:00 2001 From: Olivier Tassinari Date: Sat, 24 Feb 2024 22:26:58 +0100 Subject: [PATCH 15/26] early exit for duplicated work --- .../src/components/header/ThemeModeToggle.tsx | 1 + docs/src/layouts/AppHeader.tsx | 1 - .../modules/components/AppSettingsDrawer.js | 1 + docs/src/modules/components/ThemeContext.js | 24 +++++++++++++++---- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/docs/src/components/header/ThemeModeToggle.tsx b/docs/src/components/header/ThemeModeToggle.tsx index 278e3640ebcd58..faf88ea8c17549 100644 --- a/docs/src/components/header/ThemeModeToggle.tsx +++ b/docs/src/components/header/ThemeModeToggle.tsx @@ -49,6 +49,7 @@ export default function ThemeModeToggle() { }); React.useEffect(() => { + console.log('changeTheme 1'); changeTheme({ paletteMode: calculatedMode }); }, [changeTheme, calculatedMode]); diff --git a/docs/src/layouts/AppHeader.tsx b/docs/src/layouts/AppHeader.tsx index 6e467d01819e7d..7c8d9de1f13614 100644 --- a/docs/src/layouts/AppHeader.tsx +++ b/docs/src/layouts/AppHeader.tsx @@ -39,7 +39,6 @@ interface AppHeaderProps { export default function AppHeader(props: AppHeaderProps) { const { gitHubRepository = 'https://github.com/mui' } = props; - const t = useTranslate(); return ( diff --git a/docs/src/modules/components/AppSettingsDrawer.js b/docs/src/modules/components/AppSettingsDrawer.js index 5eea38a89953a6..dd09252690d0f6 100644 --- a/docs/src/modules/components/AppSettingsDrawer.js +++ b/docs/src/modules/components/AppSettingsDrawer.js @@ -58,6 +58,7 @@ export default function AppSettingsDrawer(props) { }; React.useEffect(() => { + console.log('changeTheme 2'); changeTheme({ paletteMode: calculatedMode }); }, [changeTheme, calculatedMode]); diff --git a/docs/src/modules/components/ThemeContext.js b/docs/src/modules/components/ThemeContext.js index b50f3c6a28b4e3..fd3604ee196552 100644 --- a/docs/src/modules/components/ThemeContext.js +++ b/docs/src/modules/components/ThemeContext.js @@ -151,6 +151,15 @@ export function ThemeProvider(props) { paletteColors: themeInitialOptions.paletteColors, }; case 'CHANGE': + // No value changed + if ( + (!action.payload.paletteMode || action.payload.paletteMode === state.paletteMode) && + (!action.payload.direction || action.payload.direction === state.direction) && + (!action.payload.paletteColors || action.payload.paletteColors === state.paletteColors) + ) { + return state; + } + return { ...state, paletteMode: action.payload.paletteMode || state.paletteMode, @@ -161,7 +170,7 @@ export function ThemeProvider(props) { throw new Error(`Unrecognized type ${action.type}`); } }, - { ...themeInitialOptions, paletteMode: 'light' }, + { ...themeInitialOptions }, ); const userLanguage = useUserLanguage(); @@ -170,21 +179,28 @@ export function ThemeProvider(props) { useLazyCSS('/static/styles/prism-okaidia.css', '#prismjs'); useEnhancedEffect(() => { - const nextPaletteColors = JSON.parse(getCookie('paletteColors') || 'null'); + let nextPaletteColors = JSON.parse(getCookie('paletteColors') || 'null'); + // Set default value if no value is found in cookie + if (nextPaletteColors === null) { + nextPaletteColors = themeInitialOptions.paletteColors; + } + let nextPaletteMode = systemMode; // syncing with homepage, can be removed once all pages are migrated to CSS variables try { nextPaletteMode = localStorage.getItem('mui-mode') ?? systemMode; } catch (error) { // mainly thrown when cookies are disabled. } - if (nextPaletteMode === 'system') { nextPaletteMode = systemMode; } dispatch({ type: 'CHANGE', - payload: { paletteColors: nextPaletteColors, paletteMode: nextPaletteMode }, + payload: { + paletteColors: nextPaletteColors, + paletteMode: nextPaletteMode + }, }); }, [systemMode]); From 6e58a21cd028b43c3f22478fbbe7998443902b6e Mon Sep 17 00:00:00 2001 From: Olivier Tassinari Date: Sat, 24 Feb 2024 22:59:41 +0100 Subject: [PATCH 16/26] fix lousy code hygiene --- .../src/components/header/ThemeModeToggle.tsx | 4 ++ docs/src/modules/components/ThemeContext.js | 2 +- .../src/cssVars/createCssVarsProvider.js | 45 +++++++++--------- .../src/cssVars/useCurrentColorScheme.ts | 47 ++++++++++--------- 4 files changed, 54 insertions(+), 44 deletions(-) diff --git a/docs/src/components/header/ThemeModeToggle.tsx b/docs/src/components/header/ThemeModeToggle.tsx index faf88ea8c17549..c33d50f149a825 100644 --- a/docs/src/components/header/ThemeModeToggle.tsx +++ b/docs/src/components/header/ThemeModeToggle.tsx @@ -48,6 +48,10 @@ export default function ThemeModeToggle() { calculatedMode, }); + React.useEffect(() => { + console.log('theme2', theme.cssVarPrefix); + }, [theme]); + React.useEffect(() => { console.log('changeTheme 1'); changeTheme({ paletteMode: calculatedMode }); diff --git a/docs/src/modules/components/ThemeContext.js b/docs/src/modules/components/ThemeContext.js index fd3604ee196552..aa211c1216a00f 100644 --- a/docs/src/modules/components/ThemeContext.js +++ b/docs/src/modules/components/ThemeContext.js @@ -199,7 +199,7 @@ export function ThemeProvider(props) { type: 'CHANGE', payload: { paletteColors: nextPaletteColors, - paletteMode: nextPaletteMode + paletteMode: nextPaletteMode, }, }); }, [systemMode]); diff --git a/packages/mui-system/src/cssVars/createCssVarsProvider.js b/packages/mui-system/src/cssVars/createCssVarsProvider.js index dc6882ab204cb3..e4969c4ad5b44e 100644 --- a/packages/mui-system/src/cssVars/createCssVarsProvider.js +++ b/packages/mui-system/src/cssVars/createCssVarsProvider.js @@ -60,22 +60,23 @@ export default function createCssVarsProvider(options) { return value; }; - function CssVarsProvider({ - children, - theme: themeProp = defaultTheme, - modeStorageKey = defaultModeStorageKey, - colorSchemeStorageKey = defaultColorSchemeStorageKey, - attribute = defaultAttribute, - defaultMode = designSystemMode, - defaultColorScheme = designSystemColorScheme, - disableTransitionOnChange = designSystemTransitionOnChange, - storageWindow = typeof window === 'undefined' ? undefined : window, - documentNode = typeof document === 'undefined' ? undefined : document, - colorSchemeNode = typeof document === 'undefined' ? undefined : document.documentElement, - colorSchemeSelector = ':root', - disableNestedContext = false, - disableStyleSheetGeneration = false, - }) { + function CssVarsProvider(props) { + const { + children, + theme: themeProp = defaultTheme, + modeStorageKey = defaultModeStorageKey, + colorSchemeStorageKey = defaultColorSchemeStorageKey, + attribute = defaultAttribute, + defaultMode = designSystemMode, + defaultColorScheme = designSystemColorScheme, + disableTransitionOnChange = designSystemTransitionOnChange, + storageWindow = typeof window === 'undefined' ? undefined : window, + documentNode = typeof document === 'undefined' ? undefined : document, + colorSchemeNode = typeof document === 'undefined' ? undefined : document.documentElement, + colorSchemeSelector = ':root', + disableNestedContext = false, + disableStyleSheetGeneration = false, + } = props; const hasMounted = React.useRef(false); const upperTheme = muiUseTheme(); const ctx = React.useContext(ColorSchemeContext); @@ -248,14 +249,14 @@ export default function createCssVarsProvider(options) { const contextValue = React.useMemo( () => ({ - mode, - systemMode, - setMode, - lightColorScheme, - darkColorScheme, + allColorSchemes, colorScheme, + darkColorScheme, + lightColorScheme, + mode, setColorScheme, - allColorSchemes, + setMode, + systemMode, }), [ allColorSchemes, diff --git a/packages/mui-system/src/cssVars/useCurrentColorScheme.ts b/packages/mui-system/src/cssVars/useCurrentColorScheme.ts index a44c4790419d77..f1676f09c3a4df 100644 --- a/packages/mui-system/src/cssVars/useCurrentColorScheme.ts +++ b/packages/mui-system/src/cssVars/useCurrentColorScheme.ts @@ -254,11 +254,11 @@ export default function useCurrentColorScheme { + (event?: MediaQueryListEvent) => { if (state.mode === 'system') { setState((currentState) => ({ ...currentState, - systemMode: e?.matches ? 'dark' : 'light', + systemMode: event?.matches ? 'dark' : 'light', })); } }, @@ -284,29 +284,34 @@ export default function useCurrentColorScheme { - const handleStorage = (event: StorageEvent) => { - const value = event.newValue; - if ( - typeof event.key === 'string' && - event.key.startsWith(colorSchemeStorageKey) && - (!value || joinedColorSchemes.match(value)) - ) { - // If the key is deleted, value will be null then reset color scheme to the default one. - if (event.key.endsWith('light')) { - setColorScheme({ light: value as SupportedColorScheme | null }); + if (storageWindow) { + const handleStorage = (event: StorageEvent) => { + const value = event.newValue; + if ( + typeof event.key === 'string' && + event.key.startsWith(colorSchemeStorageKey) && + (!value || joinedColorSchemes.match(value)) + ) { + // If the key is deleted, value will be null then reset color scheme to the default one. + if (event.key.endsWith('light')) { + setColorScheme({ light: value as SupportedColorScheme | null }); + } + if (event.key.endsWith('dark')) { + setColorScheme({ dark: value as SupportedColorScheme | null }); + } } - if (event.key.endsWith('dark')) { - setColorScheme({ dark: value as SupportedColorScheme | null }); + if ( + event.key === modeStorageKey && + (!value || ['light', 'dark', 'system'].includes(value)) + ) { + setMode((value as Mode) || defaultMode); } - } - if (event.key === modeStorageKey && (!value || ['light', 'dark', 'system'].includes(value))) { - setMode((value as Mode) || defaultMode); - } - }; - if (storageWindow) { + }; // For syncing color-scheme changes between iframes storageWindow.addEventListener('storage', handleStorage); - return () => storageWindow.removeEventListener('storage', handleStorage); + return () => { + storageWindow.removeEventListener('storage', handleStorage); + }; } return undefined; }, [ From 9f9ddc3c7be28d03b4a6ab688edb6bffe3280e35 Mon Sep 17 00:00:00 2001 From: Olivier Tassinari Date: Sat, 24 Feb 2024 23:09:56 +0100 Subject: [PATCH 17/26] omg really --- .../src/cssVars/useCurrentColorScheme.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/mui-system/src/cssVars/useCurrentColorScheme.ts b/packages/mui-system/src/cssVars/useCurrentColorScheme.ts index f1676f09c3a4df..d2522351211752 100644 --- a/packages/mui-system/src/cssVars/useCurrentColorScheme.ts +++ b/packages/mui-system/src/cssVars/useCurrentColorScheme.ts @@ -256,10 +256,15 @@ export default function useCurrentColorScheme { if (state.mode === 'system') { - setState((currentState) => ({ - ...currentState, - systemMode: event?.matches ? 'dark' : 'light', - })); + setState((currentState) => { + const systemMode = event?.matches ? 'dark' : 'light'; + + // Early exit, nothing changed. + if (currentState.systemMode === systemMode) { + return currentState; + } + return { ...currentState, systemMode }; + }); } }, [state.mode], @@ -278,8 +283,9 @@ export default function useCurrentColorScheme media.removeListener(handler); + return () => { + media.removeListener(handler); + }; }, []); // Handle when localStorage has changed From d49bc1d9dc94d3c0947259b965fcaca194d52272 Mon Sep 17 00:00:00 2001 From: Olivier Tassinari Date: Sat, 24 Feb 2024 23:13:19 +0100 Subject: [PATCH 18/26] remove console logs --- docs/src/components/header/ThemeModeToggle.tsx | 10 ---------- docs/src/modules/components/AppSettingsDrawer.js | 2 -- 2 files changed, 12 deletions(-) diff --git a/docs/src/components/header/ThemeModeToggle.tsx b/docs/src/components/header/ThemeModeToggle.tsx index c33d50f149a825..338fc40f2bc90f 100644 --- a/docs/src/components/header/ThemeModeToggle.tsx +++ b/docs/src/components/header/ThemeModeToggle.tsx @@ -42,18 +42,8 @@ export default function ThemeModeToggle() { const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', { noSsr: true }); const systemMode = prefersDarkMode ? 'dark' : 'light'; const calculatedMode = mode === 'system' ? systemMode : mode; - console.log('render', { - mode, - prefersDarkMode, - calculatedMode, - }); React.useEffect(() => { - console.log('theme2', theme.cssVarPrefix); - }, [theme]); - - React.useEffect(() => { - console.log('changeTheme 1'); changeTheme({ paletteMode: calculatedMode }); }, [changeTheme, calculatedMode]); diff --git a/docs/src/modules/components/AppSettingsDrawer.js b/docs/src/modules/components/AppSettingsDrawer.js index dd09252690d0f6..9c456ae614656a 100644 --- a/docs/src/modules/components/AppSettingsDrawer.js +++ b/docs/src/modules/components/AppSettingsDrawer.js @@ -44,7 +44,6 @@ export default function AppSettingsDrawer(props) { const upperTheme = useTheme(); const changeTheme = useChangeTheme(); const [mode, setMode] = useLocalStorageState('mui-mode', 'system'); - console.log('mode', mode); const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', { noSsr: true }); const systemMode = prefersDarkMode ? 'dark' : 'light'; const calculatedMode = mode === 'system' ? systemMode : mode; @@ -58,7 +57,6 @@ export default function AppSettingsDrawer(props) { }; React.useEffect(() => { - console.log('changeTheme 2'); changeTheme({ paletteMode: calculatedMode }); }, [changeTheme, calculatedMode]); From 3f94ebfcfa76e43668c3a3eb7cb9d0153e75d7ec Mon Sep 17 00:00:00 2001 From: Olivier Tassinari Date: Sat, 24 Feb 2024 23:27:54 +0100 Subject: [PATCH 19/26] group related logic --- .../src/components/header/ThemeModeToggle.tsx | 40 +++++++++---------- docs/src/modules/components/ThemeContext.js | 5 ++- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/docs/src/components/header/ThemeModeToggle.tsx b/docs/src/components/header/ThemeModeToggle.tsx index 338fc40f2bc90f..032c604ba542ea 100644 --- a/docs/src/components/header/ThemeModeToggle.tsx +++ b/docs/src/components/header/ThemeModeToggle.tsx @@ -52,26 +52,26 @@ export default function ThemeModeToggle() { return ; } - if (theme.vars) { - // Temporarily renders conditionally because `useColorScheme` could not be used in the pages that haven't migrated to CSS theme variables. - return ; + // TODO remove this code branch, all pages should be migrated to use CssVarsProvider + if (!theme.vars) { + return ( + + { + setMode(calculatedMode === 'dark' ? 'light' : 'dark'); + }} + > + {calculatedMode === 'dark' ? ( + + ) : ( + + )} + + + ); } - return ( - - { - setMode(calculatedMode === 'dark' ? 'light' : 'dark'); - }} - > - {calculatedMode === 'dark' ? ( - - ) : ( - - )} - - - ); + return ; } diff --git a/docs/src/modules/components/ThemeContext.js b/docs/src/modules/components/ThemeContext.js index aa211c1216a00f..45cd47425a2c5d 100644 --- a/docs/src/modules/components/ThemeContext.js +++ b/docs/src/modules/components/ThemeContext.js @@ -111,8 +111,6 @@ if (process.env.NODE_ENV !== 'production') { export function ThemeProvider(props) { const { children } = props; - const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', { noSsr: true }); - const systemMode = prefersDarkMode ? 'dark' : 'light'; const [themeOptions, dispatch] = React.useReducer( (state, action) => { @@ -178,6 +176,9 @@ export function ThemeProvider(props) { useLazyCSS('/static/styles/prism-okaidia.css', '#prismjs'); + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', { noSsr: true }); + const systemMode = prefersDarkMode ? 'dark' : 'light'; + useEnhancedEffect(() => { let nextPaletteColors = JSON.parse(getCookie('paletteColors') || 'null'); // Set default value if no value is found in cookie From b6c1a2848672ac74191c7d70097fc0ad5d5d87c2 Mon Sep 17 00:00:00 2001 From: Olivier Tassinari Date: Sat, 24 Feb 2024 23:37:01 +0100 Subject: [PATCH 20/26] leave comments to make sure future iteration will take steps toward the right direction --- docs/src/components/header/ThemeModeToggle.tsx | 7 ++++++- .../modules/components/AppSettingsDrawer.js | 15 +++++++++------ docs/src/modules/components/ThemeContext.js | 18 +++--------------- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/docs/src/components/header/ThemeModeToggle.tsx b/docs/src/components/header/ThemeModeToggle.tsx index 032c604ba542ea..4821f9842d189d 100644 --- a/docs/src/components/header/ThemeModeToggle.tsx +++ b/docs/src/components/header/ThemeModeToggle.tsx @@ -36,17 +36,22 @@ function CssVarsModeToggle(props: { onChange: (newMode: string) => void }) { } export default function ThemeModeToggle() { - const theme = useTheme(); + // TODO implement the dark mode toggle with useColorScheme(). + // This already take cares of locale storage and media query. const changeTheme = useChangeTheme(); const [mode, setMode] = useLocalStorageState('mui-mode', 'system'); const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', { noSsr: true }); const systemMode = prefersDarkMode ? 'dark' : 'light'; const calculatedMode = mode === 'system' ? systemMode : mode; + // TODO remove, where changeTheme() is imported from should use useColorScheme(). + // Delegating this to the UI component is wrong. React.useEffect(() => { changeTheme({ paletteMode: calculatedMode }); }, [changeTheme, calculatedMode]); + const theme = useTheme(); + // Server-side hydration if (mode === null) { return ; diff --git a/docs/src/modules/components/AppSettingsDrawer.js b/docs/src/modules/components/AppSettingsDrawer.js index 9c456ae614656a..266da80df00678 100644 --- a/docs/src/modules/components/AppSettingsDrawer.js +++ b/docs/src/modules/components/AppSettingsDrawer.js @@ -43,28 +43,31 @@ export default function AppSettingsDrawer(props) { const t = useTranslate(); const upperTheme = useTheme(); const changeTheme = useChangeTheme(); + + // TODO implement the dark mode toggle with useColorScheme(). + // This already take cares of locale storage and media query. const [mode, setMode] = useLocalStorageState('mui-mode', 'system'); const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', { noSsr: true }); const systemMode = prefersDarkMode ? 'dark' : 'light'; const calculatedMode = mode === 'system' ? systemMode : mode; + // TODO remove, where changeTheme() is imported from should use useColorScheme(). + // Delegating this to the UI component is wrong. + React.useEffect(() => { + changeTheme({ paletteMode: calculatedMode }); + }, [changeTheme, calculatedMode]); + const handleChangeThemeMode = (event, paletteMode) => { if (paletteMode === null) { return; } - setMode(paletteMode); }; - React.useEffect(() => { - changeTheme({ paletteMode: calculatedMode }); - }, [changeTheme, calculatedMode]); - const handleChangeDirection = (event, direction) => { if (direction === null) { direction = upperTheme.direction; } - changeTheme({ direction }); }; diff --git a/docs/src/modules/components/ThemeContext.js b/docs/src/modules/components/ThemeContext.js index 45cd47425a2c5d..a15a3466af47cf 100644 --- a/docs/src/modules/components/ThemeContext.js +++ b/docs/src/modules/components/ThemeContext.js @@ -176,9 +176,6 @@ export function ThemeProvider(props) { useLazyCSS('/static/styles/prism-okaidia.css', '#prismjs'); - const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', { noSsr: true }); - const systemMode = prefersDarkMode ? 'dark' : 'light'; - useEnhancedEffect(() => { let nextPaletteColors = JSON.parse(getCookie('paletteColors') || 'null'); // Set default value if no value is found in cookie @@ -186,24 +183,15 @@ export function ThemeProvider(props) { nextPaletteColors = themeInitialOptions.paletteColors; } - let nextPaletteMode = systemMode; // syncing with homepage, can be removed once all pages are migrated to CSS variables - try { - nextPaletteMode = localStorage.getItem('mui-mode') ?? systemMode; - } catch (error) { - // mainly thrown when cookies are disabled. - } - if (nextPaletteMode === 'system') { - nextPaletteMode = systemMode; - } - dispatch({ type: 'CHANGE', payload: { paletteColors: nextPaletteColors, - paletteMode: nextPaletteMode, + // TODO: have the value come from useColorScheme(); + // paletteMode: nextPaletteMode, }, }); - }, [systemMode]); + }, []); useEnhancedEffect(() => { document.body.dir = direction; From c209835927ceeebd10863f5e1a08331e8812a751 Mon Sep 17 00:00:00 2001 From: Olivier Tassinari Date: Sun, 25 Feb 2024 00:24:57 +0100 Subject: [PATCH 21/26] good to go --- docs/src/components/header/ThemeModeToggle.tsx | 2 +- .../modules/components/MaterialYouUsageDemo.tsx | 15 +-------------- docs/src/modules/components/ThemeContext.js | 2 +- .../mui-material/src/styles/CssVarsProvider.tsx | 5 +---- .../src/cssVars/useCurrentColorScheme.ts | 2 +- .../useLocalStorageState/useLocalStorageState.ts | 1 - 6 files changed, 5 insertions(+), 22 deletions(-) diff --git a/docs/src/components/header/ThemeModeToggle.tsx b/docs/src/components/header/ThemeModeToggle.tsx index 4821f9842d189d..774f92ce1026e5 100644 --- a/docs/src/components/header/ThemeModeToggle.tsx +++ b/docs/src/components/header/ThemeModeToggle.tsx @@ -44,7 +44,7 @@ export default function ThemeModeToggle() { const systemMode = prefersDarkMode ? 'dark' : 'light'; const calculatedMode = mode === 'system' ? systemMode : mode; - // TODO remove, where changeTheme() is imported from should use useColorScheme(). + // TODO remove, the module where "changeTheme() is imported from" should use useColorScheme() to set the mode directly. // Delegating this to the UI component is wrong. React.useEffect(() => { changeTheme({ paletteMode: calculatedMode }); diff --git a/docs/src/modules/components/MaterialYouUsageDemo.tsx b/docs/src/modules/components/MaterialYouUsageDemo.tsx index 6f42e405dfcc02..69cb6fba05bdf3 100644 --- a/docs/src/modules/components/MaterialYouUsageDemo.tsx +++ b/docs/src/modules/components/MaterialYouUsageDemo.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useTheme as md2UseTheme, alpha } from '@mui/material/styles'; +import { alpha } from '@mui/material/styles'; import ReplayRoundedIcon from '@mui/icons-material/ReplayRounded'; import Box from '@mui/material/Box'; import Divider from '@mui/material/Divider'; @@ -13,13 +13,10 @@ import Typography from '@mui/material/Typography'; import { extendTheme, CssVarsProvider as MaterialYouCssVarsProvider, - useColorScheme, } from '@mui/material-next/styles'; import BrandingProvider from 'docs/src/BrandingProvider'; import HighlightedCode from 'docs/src/modules/components/HighlightedCode'; -type Mode = 'light' | 'dark' | 'system'; - const materialYouTheme = extendTheme(); const shallowEqual = (item1: { [k: string]: any }, item2: { [k: string]: any }) => { let equal = true; @@ -93,14 +90,6 @@ export const prependLinesSpace = (code: string, size: number = 2) => { return newCode.join('\n'); }; -function ModeSwitcher({ md2Mode }: { md2Mode: Mode }) { - const { setMode } = useColorScheme(); - React.useEffect(() => { - setMode(md2Mode); - }, [md2Mode, setMode]); - return null; -} - interface MaterialYouUsageDemoProps { /** * Name of the component to show in the code block. @@ -195,7 +184,6 @@ export default function MaterialYouUsageDemo - {renderDemo(demoProps)} diff --git a/docs/src/modules/components/ThemeContext.js b/docs/src/modules/components/ThemeContext.js index a15a3466af47cf..ba834b7309cd8c 100644 --- a/docs/src/modules/components/ThemeContext.js +++ b/docs/src/modules/components/ThemeContext.js @@ -168,7 +168,7 @@ export function ThemeProvider(props) { throw new Error(`Unrecognized type ${action.type}`); } }, - { ...themeInitialOptions }, + themeInitialOptions, ); const userLanguage = useUserLanguage(); diff --git a/packages/mui-material/src/styles/CssVarsProvider.tsx b/packages/mui-material/src/styles/CssVarsProvider.tsx index 90cd0733dd72a7..75f5c6e6f18154 100644 --- a/packages/mui-material/src/styles/CssVarsProvider.tsx +++ b/packages/mui-material/src/styles/CssVarsProvider.tsx @@ -35,10 +35,7 @@ const { CssVarsProvider, useColorScheme, getInitColorSchemeScript } = createCssV }; newTheme.unstable_sx = function sx(props: SxProps) { - return styleFunctionSx({ - sx: props, - theme: this, - }); + return styleFunctionSx({ sx: props, theme: this }); }; return newTheme; diff --git a/packages/mui-system/src/cssVars/useCurrentColorScheme.ts b/packages/mui-system/src/cssVars/useCurrentColorScheme.ts index d2522351211752..f9f46bbfdc9dbf 100644 --- a/packages/mui-system/src/cssVars/useCurrentColorScheme.ts +++ b/packages/mui-system/src/cssVars/useCurrentColorScheme.ts @@ -160,7 +160,7 @@ export default function useCurrentColorScheme void): () } const storageHandler = (event: StorageEvent) => { if (event.storageArea === area && event.key === key) { - console.log('value change'); callbark(); } }; From 6c9b1e9784b10b8a876777c5e653548b5efeaee1 Mon Sep 17 00:00:00 2001 From: Olivier Tassinari Date: Sun, 25 Feb 2024 00:32:11 +0100 Subject: [PATCH 22/26] fix ci --- docs/src/modules/components/ThemeContext.js | 110 ++++++++++---------- 1 file changed, 53 insertions(+), 57 deletions(-) diff --git a/docs/src/modules/components/ThemeContext.js b/docs/src/modules/components/ThemeContext.js index ba834b7309cd8c..0b531c369b9cf7 100644 --- a/docs/src/modules/components/ThemeContext.js +++ b/docs/src/modules/components/ThemeContext.js @@ -5,7 +5,6 @@ import { createTheme as createMdTheme, } from '@mui/material/styles'; import { deepmerge } from '@mui/utils'; -import useMediaQuery from '@mui/material/useMediaQuery'; import { enUS, zhCN, ptBR } from '@mui/material/locale'; import { unstable_useEnhancedEffect as useEnhancedEffect } from '@mui/material/utils'; import { getCookie } from 'docs/src/modules/utils/helpers'; @@ -112,64 +111,61 @@ if (process.env.NODE_ENV !== 'production') { export function ThemeProvider(props) { const { children } = props; - const [themeOptions, dispatch] = React.useReducer( - (state, action) => { - switch (action.type) { - case 'SET_SPACING': - return { - ...state, - spacing: action.payload, - }; - case 'INCREASE_SPACING': { - return { - ...state, - spacing: state.spacing + 1, - }; - } - case 'DECREASE_SPACING': { - return { - ...state, - spacing: state.spacing - 1, - }; + const [themeOptions, dispatch] = React.useReducer((state, action) => { + switch (action.type) { + case 'SET_SPACING': + return { + ...state, + spacing: action.payload, + }; + case 'INCREASE_SPACING': { + return { + ...state, + spacing: state.spacing + 1, + }; + } + case 'DECREASE_SPACING': { + return { + ...state, + spacing: state.spacing - 1, + }; + } + case 'SET_DENSE': + return { + ...state, + dense: action.payload, + }; + case 'RESET_DENSITY': + return { + ...state, + dense: themeInitialOptions.dense, + spacing: themeInitialOptions.spacing, + }; + case 'RESET_COLORS': + return { + ...state, + paletteColors: themeInitialOptions.paletteColors, + }; + case 'CHANGE': + // No value changed + if ( + (!action.payload.paletteMode || action.payload.paletteMode === state.paletteMode) && + (!action.payload.direction || action.payload.direction === state.direction) && + (!action.payload.paletteColors || action.payload.paletteColors === state.paletteColors) + ) { + return state; } - case 'SET_DENSE': - return { - ...state, - dense: action.payload, - }; - case 'RESET_DENSITY': - return { - ...state, - dense: themeInitialOptions.dense, - spacing: themeInitialOptions.spacing, - }; - case 'RESET_COLORS': - return { - ...state, - paletteColors: themeInitialOptions.paletteColors, - }; - case 'CHANGE': - // No value changed - if ( - (!action.payload.paletteMode || action.payload.paletteMode === state.paletteMode) && - (!action.payload.direction || action.payload.direction === state.direction) && - (!action.payload.paletteColors || action.payload.paletteColors === state.paletteColors) - ) { - return state; - } - return { - ...state, - paletteMode: action.payload.paletteMode || state.paletteMode, - direction: action.payload.direction || state.direction, - paletteColors: action.payload.paletteColors || state.paletteColors, - }; - default: - throw new Error(`Unrecognized type ${action.type}`); - } - }, - themeInitialOptions, - ); + return { + ...state, + paletteMode: action.payload.paletteMode || state.paletteMode, + direction: action.payload.direction || state.direction, + paletteColors: action.payload.paletteColors || state.paletteColors, + }; + default: + throw new Error(`Unrecognized type ${action.type}`); + } + }, themeInitialOptions); const userLanguage = useUserLanguage(); const { dense, direction, paletteColors, paletteMode, spacing } = themeOptions; From dd935db74dc9ff962ea5ccd60be4eec56e78da26 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Tue, 12 Mar 2024 10:57:33 +0100 Subject: [PATCH 23/26] Fix types --- .../useLocalStorageState.ts | 31 +++++-------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts index 0d4aadd3ba25f7..70c6bf57ca4de7 100644 --- a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts +++ b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts @@ -83,21 +83,14 @@ function setValue(area: Storage, key: string | null, value: string | null) { emitCurrentTabStorageChange(key); } -type Initializer = () => T; +type Initializer = () => string | null; -type UseStorageStateHookResult = [T, React.Dispatch>]; +type UseStorageStateHookResult = [ + string | null, + React.Dispatch>, +]; -function useLocalStorageStateServer( - key: string | null, - initializer: any, -): UseStorageStateHookResult; -function useLocalStorageStateServer( - key: string | null, - initializer?: string | null | Initializer, -): UseStorageStateHookResult; -function useLocalStorageStateServer(): - | UseStorageStateHookResult - | UseStorageStateHookResult { +function useLocalStorageStateServer(): UseStorageStateHookResult { return React.useState(null); } @@ -117,16 +110,8 @@ function useLocalStorageStateServer(): */ function useLocalStorageStateBrowser( key: string | null, - initializer: string | Initializer, -): UseStorageStateHookResult; -function useLocalStorageStateBrowser( - key: string | null, - initializer?: string | null | Initializer, -): UseStorageStateHookResult; -function useLocalStorageStateBrowser( - key: string | null, - initializer: string | null | Initializer = null, -): UseStorageStateHookResult | UseStorageStateHookResult { + initializer: string | null | Initializer = null, +): UseStorageStateHookResult { const [initialValue] = React.useState(initializer); const area = window.localStorage; const subscribeKey = React.useCallback( From cd4e2747fda2d16d92c3cfc9a8afe6e6f81ea9e0 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Tue, 12 Mar 2024 11:15:30 +0100 Subject: [PATCH 24/26] update --- .../src/components/header/ThemeModeToggle.tsx | 18 +++----------- .../modules/components/AppSettingsDrawer.js | 18 +++----------- docs/src/modules/components/ThemeContext.js | 24 ++++++++++++++++--- 3 files changed, 27 insertions(+), 33 deletions(-) diff --git a/docs/src/components/header/ThemeModeToggle.tsx b/docs/src/components/header/ThemeModeToggle.tsx index 774f92ce1026e5..fda2d61676d99d 100644 --- a/docs/src/components/header/ThemeModeToggle.tsx +++ b/docs/src/components/header/ThemeModeToggle.tsx @@ -4,9 +4,7 @@ import IconButton from '@mui/material/IconButton'; import Tooltip from '@mui/material/Tooltip'; import DarkModeOutlined from '@mui/icons-material/DarkModeOutlined'; import LightModeOutlined from '@mui/icons-material/LightModeOutlined'; -import useMediaQuery from '@mui/material/useMediaQuery'; -import { useChangeTheme } from 'docs/src/modules/components/ThemeContext'; -import useLocalStorageState from '@mui/utils/useLocalStorageState'; +import { useColorSchemeShim } from 'docs/src/modules/components/ThemeContext'; function CssVarsModeToggle(props: { onChange: (newMode: string) => void }) { const { mode, systemMode, setMode } = useColorScheme(); @@ -36,20 +34,10 @@ function CssVarsModeToggle(props: { onChange: (newMode: string) => void }) { } export default function ThemeModeToggle() { - // TODO implement the dark mode toggle with useColorScheme(). - // This already take cares of locale storage and media query. - const changeTheme = useChangeTheme(); - const [mode, setMode] = useLocalStorageState('mui-mode', 'system'); - const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', { noSsr: true }); - const systemMode = prefersDarkMode ? 'dark' : 'light'; + // TODO replace with useColorScheme once all pages support css vars + const { mode, systemMode, setMode } = useColorSchemeShim(); const calculatedMode = mode === 'system' ? systemMode : mode; - // TODO remove, the module where "changeTheme() is imported from" should use useColorScheme() to set the mode directly. - // Delegating this to the UI component is wrong. - React.useEffect(() => { - changeTheme({ paletteMode: calculatedMode }); - }, [changeTheme, calculatedMode]); - const theme = useTheme(); // Server-side hydration diff --git a/docs/src/modules/components/AppSettingsDrawer.js b/docs/src/modules/components/AppSettingsDrawer.js index 16b5cf86071c66..8b04f7d36e3dd6 100644 --- a/docs/src/modules/components/AppSettingsDrawer.js +++ b/docs/src/modules/components/AppSettingsDrawer.js @@ -1,7 +1,6 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { styled, useTheme } from '@mui/material/styles'; -import useMediaQuery from '@mui/material/useMediaQuery'; import Drawer from '@mui/material/Drawer'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; @@ -16,9 +15,8 @@ import DarkModeOutlinedIcon from '@mui/icons-material/DarkModeOutlined'; import SettingsBrightnessIcon from '@mui/icons-material/SettingsBrightness'; import FormatTextdirectionLToRIcon from '@mui/icons-material/FormatTextdirectionLToR'; import FormatTextdirectionRToLIcon from '@mui/icons-material/FormatTextdirectionRToL'; -import { useChangeTheme } from 'docs/src/modules/components/ThemeContext'; +import { useColorSchemeShim, useChangeTheme } from 'docs/src/modules/components/ThemeContext'; import { useTranslate } from '@mui/docs/i18n'; -import useLocalStorageState from '@mui/utils/useLocalStorageState'; const Heading = styled(Typography)(({ theme }) => ({ margin: '16px 0 8px', @@ -44,18 +42,8 @@ export default function AppSettingsDrawer(props) { const upperTheme = useTheme(); const changeTheme = useChangeTheme(); - // TODO implement the dark mode toggle with useColorScheme(). - // This already take cares of locale storage and media query. - const [mode, setMode] = useLocalStorageState('mui-mode', 'system'); - const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', { noSsr: true }); - const systemMode = prefersDarkMode ? 'dark' : 'light'; - const calculatedMode = mode === 'system' ? systemMode : mode; - - // TODO remove, where changeTheme() is imported from should use useColorScheme(). - // Delegating this to the UI component is wrong. - React.useEffect(() => { - changeTheme({ paletteMode: calculatedMode }); - }, [changeTheme, calculatedMode]); + // TODO replace with useColorScheme once all pages support css vars + const { mode, setMode } = useColorSchemeShim(); const handleChangeThemeMode = (event, paletteMode) => { if (paletteMode === null) { diff --git a/docs/src/modules/components/ThemeContext.js b/docs/src/modules/components/ThemeContext.js index 0b531c369b9cf7..46713fcf715e8b 100644 --- a/docs/src/modules/components/ThemeContext.js +++ b/docs/src/modules/components/ThemeContext.js @@ -15,6 +15,8 @@ import { getThemedComponents, getMetaThemeColor, } from 'docs/src/modules/brandingTheme'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import useLocalStorageState from '@mui/utils/useLocalStorageState'; const languageMap = { en: enUS, @@ -172,6 +174,10 @@ export function ThemeProvider(props) { useLazyCSS('/static/styles/prism-okaidia.css', '#prismjs'); + // TODO replace with useColorScheme once all pages support css vars + const { mode, systemMode } = useColorSchemeShim(); + const calculatedMode = mode === 'system' ? systemMode : mode; + useEnhancedEffect(() => { let nextPaletteColors = JSON.parse(getCookie('paletteColors') || 'null'); // Set default value if no value is found in cookie @@ -183,11 +189,10 @@ export function ThemeProvider(props) { type: 'CHANGE', payload: { paletteColors: nextPaletteColors, - // TODO: have the value come from useColorScheme(); - // paletteMode: nextPaletteMode, + paletteMode: calculatedMode, }, }); - }, []); + }, [calculatedMode]); useEnhancedEffect(() => { document.body.dir = direction; @@ -272,3 +277,16 @@ export function useChangeTheme() { const dispatch = React.useContext(DispatchContext); return React.useCallback((options) => dispatch({ type: 'CHANGE', payload: options }), [dispatch]); } + +// TODO: remove once all pages support css vars and replace call sites with useColorScheme() +export function useColorSchemeShim() { + const [mode, setMode] = useLocalStorageState('mui-mode', 'system'); + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', { noSsr: true }); + const systemMode = prefersDarkMode ? 'dark' : 'light'; + + return { + mode, + systemMode, + setMode, + }; +} From 371978ba227c76d736a51cf9f43040c81aa5e325 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Tue, 12 Mar 2024 11:44:42 +0100 Subject: [PATCH 25/26] Update comment --- .../src/useLocalStorageState/useLocalStorageState.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts index 70c6bf57ca4de7..b4dfb7bdbf967a 100644 --- a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts +++ b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts @@ -100,13 +100,7 @@ function useLocalStorageStateServer(): UseStorageStateHookResult { * to that value on page load instead of the specified initial value. * * Since the storage API isn't available in server-rendering environments, we - * return initialValue during SSR and hydration. - * - * Things this hook does different from existing solutions: - * - SSR-capable: it shows initial value during SSR and hydration, but immediately - * initializes when clientside mounted. - * - Sync state across tabs: When another tab changes the value in the storage area, the - * current tab follows suit. + * return null during SSR and hydration. */ function useLocalStorageStateBrowser( key: string | null, From e217b3da45bea6c9036864f28fb84d65bdc26c41 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Tue, 12 Mar 2024 11:46:36 +0100 Subject: [PATCH 26/26] Update useLocalStorageState.ts --- .../src/useLocalStorageState/useLocalStorageState.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts index b4dfb7bdbf967a..2d785fc5512f05 100644 --- a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts +++ b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts @@ -90,8 +90,10 @@ type UseStorageStateHookResult = [ React.Dispatch>, ]; +const serverValue: UseStorageStateHookResult = [null, () => {}]; + function useLocalStorageStateServer(): UseStorageStateHookResult { - return React.useState(null); + return serverValue; } /**