Skip to content

Commit

Permalink
[material-ui] Add storageManager prop to ThemeProvider (@siriwatknp
Browse files Browse the repository at this point in the history
…) (#45437)
  • Loading branch information
github-actions[bot] authored Mar 5, 2025
1 parent 883b844 commit 50a760e
Show file tree
Hide file tree
Showing 12 changed files with 401 additions and 121 deletions.
72 changes: 72 additions & 0 deletions docs/data/material/customization/dark-mode/dark-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,78 @@ The `mode` is always `undefined` on first render, so make sure to handle this ca

{{"demo": "ToggleColorMode.js", "defaultCodeOpen": false}}

## Storage manager

By default, the [built-in support](#built-in-support) for color schemes uses the browser's `localStorage` API to store the user's mode and scheme preference.

To use a different storage manager, create a custom function with this signature:

```ts
type Unsubscribe = () => void;

function storageManager(params: { key: string }): {
get: (defaultValue: any) => any;
set: (value: any) => void;
subscribe: (handler: (value: any) => void) => Unsubscribe;
};
```

Then pass it to the `storageManager` prop of the `ThemeProvider` component:

```tsx
import { ThemeProvider, createTheme } from '@mui/material/styles';
import type { StorageManager } from '@mui/material/styles';

const theme = createTheme({
colorSchemes: {
dark: true,
},
});

function storageManager(params): StorageManager {
return {
get: (defaultValue) => {
// Your implementation
},
set: (value) => {
// Your implementation
},
subscribe: (handler) => {
// Your implementation
return () => {
// cleanup
};
},
};
}

function App() {
return (
<ThemeProvider theme={theme} storageManager={storageManager}>
...
</ThemeProvider>
);
}
```

:::warning
If you are using the `InitColorSchemeScript` component to [prevent SSR flickering](/material-ui/customization/css-theme-variables/configuration/#preventing-ssr-flickering), you have to include the `localStorage` implementation in your custom storage manager.
:::

### Disable storage

To disable the storage manager, pass `null` to the `storageManager` prop:

```tsx
<ThemeProvider theme={theme} storageManager={null}>
...
</ThemeProvider>
```

:::warning
Disabling the storage manager will cause the app to reset to its default mode whenever the user refreshes the page.
:::

## Disable transitions

To instantly switch between color schemes with no transition, apply the `disableTransitionOnChange` prop to the `ThemeProvider` component:
Expand Down
2 changes: 1 addition & 1 deletion packages/mui-material/src/styles/ThemeProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('ThemeProvider', () => {
originalMatchmedia = window.matchMedia;
// Create mocks of localStorage getItem and setItem functions
storage = {};
Object.defineProperty(global, 'localStorage', {
Object.defineProperty(window, 'localStorage', {
value: {
getItem: (key: string) => storage[key],
setItem: (key: string, value: string) => {
Expand Down
6 changes: 6 additions & 0 deletions packages/mui-material/src/styles/ThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';
import * as React from 'react';
import { DefaultTheme } from '@mui/system';
import { StorageManager } from '@mui/system/cssVars';
import ThemeProviderNoVars from './ThemeProviderNoVars';
import { CssThemeVariables } from './createThemeNoVars';
import { CssVarsProvider } from './ThemeProviderWithVars';
Expand Down Expand Up @@ -47,6 +48,11 @@ export interface ThemeProviderProps<Theme = DefaultTheme> extends ThemeProviderC
* @default window
*/
storageWindow?: Window | null;
/**
* The storage manager to be used for storing the mode and color scheme
* @default using `window.localStorage`
*/
storageManager?: StorageManager | null;
/**
* localStorage key used to store application `mode`
* @default 'mui-mode'
Expand Down
25 changes: 22 additions & 3 deletions packages/mui-material/src/styles/ThemeProviderWithVars.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import * as React from 'react';
import { extendTheme, CssVarsProvider, styled, useTheme, Overlays } from '@mui/material/styles';
import {
extendTheme,
ThemeProvider,
styled,
useTheme,
Overlays,
StorageManager,
} from '@mui/material/styles';
import type {} from '@mui/material/themeCssVarsAugmentation';

const customTheme = extendTheme({
Expand Down Expand Up @@ -53,7 +60,7 @@ function TestUseTheme() {
return <div style={{ background: theme.vars.palette.common.background }}>test</div>;
}

<CssVarsProvider theme={customTheme}>
<ThemeProvider theme={customTheme}>
<TestStyled
sx={(theme) => ({
// test that `theme` in sx has access to CSS vars
Expand All @@ -63,4 +70,16 @@ function TestUseTheme() {
},
})}
/>
</CssVarsProvider>;
</ThemeProvider>;

<ThemeProvider theme={customTheme} storageManager={null} />;

const storageManager: StorageManager = () => {
return {
get: () => 'light',
set: () => {},
subscribe: () => () => {},
};
};

<ThemeProvider theme={customTheme} storageManager={storageManager} />;
1 change: 1 addition & 0 deletions packages/mui-material/src/styles/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export { default as withStyles } from './withStyles';
export { default as withTheme } from './withTheme';

export * from './ThemeProviderWithVars';
export type { StorageManager } from '@mui/system/cssVars';

export { default as extendTheme } from './createThemeWithVars';

Expand Down
6 changes: 6 additions & 0 deletions packages/mui-system/src/cssVars/createCssVarsProvider.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import InitColorSchemeScript from '../InitColorSchemeScript';
import { Result } from './useCurrentColorScheme';
import type { StorageManager } from './localStorageManager';

export interface ColorSchemeContextValue<SupportedColorScheme extends string>
extends Result<SupportedColorScheme> {
Expand Down Expand Up @@ -70,6 +71,11 @@ export interface CreateCssVarsProviderResult<
* @default document
*/
colorSchemeNode?: Element | null;
/**
* The storage manager to be used for storing the mode and color scheme.
* @default using `window.localStorage`
*/
storageManager?: StorageManager | null;
/**
* The window that attaches the 'storage' event listener
* @default window
Expand Down
7 changes: 7 additions & 0 deletions packages/mui-system/src/cssVars/createCssVarsProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export default function createCssVarsProvider(options) {
modeStorageKey = defaultModeStorageKey,
colorSchemeStorageKey = defaultColorSchemeStorageKey,
disableTransitionOnChange = designSystemTransitionOnChange,
storageManager,
storageWindow = typeof window === 'undefined' ? undefined : window,
documentNode = typeof document === 'undefined' ? undefined : document,
colorSchemeNode = typeof document === 'undefined' ? undefined : document.documentElement,
Expand Down Expand Up @@ -119,6 +120,7 @@ export default function createCssVarsProvider(options) {
modeStorageKey,
colorSchemeStorageKey,
defaultMode,
storageManager,
storageWindow,
noSsr,
});
Expand Down Expand Up @@ -357,6 +359,11 @@ export default function createCssVarsProvider(options) {
* You should use this option in conjuction with `InitColorSchemeScript` component.
*/
noSsr: PropTypes.bool,
/**
* The storage manager to be used for storing the mode and color scheme
* @default using `window.localStorage`
*/
storageManager: PropTypes.func,
/**
* The window that attaches the 'storage' event listener.
* @default window
Expand Down
8 changes: 2 additions & 6 deletions packages/mui-system/src/cssVars/createCssVarsProvider.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('createCssVarsProvider', () => {
originalMatchmedia = window.matchMedia;

// Create mocks of localStorage getItem and setItem functions
Object.defineProperty(global, 'localStorage', {
Object.defineProperty(window, 'localStorage', {
value: {
getItem: spy((key) => storage[key]),
setItem: spy((key, value) => {
Expand Down Expand Up @@ -584,13 +584,9 @@ describe('createCssVarsProvider', () => {
</CssVarsProvider>,
);

expect(global.localStorage.setItem.calledWith(DEFAULT_MODE_STORAGE_KEY, 'system')).to.equal(
true,
);

fireEvent.click(screen.getByRole('button', { name: 'change to dark' }));

expect(global.localStorage.setItem.calledWith(DEFAULT_MODE_STORAGE_KEY, 'dark')).to.equal(
expect(window.localStorage.setItem.calledWith(DEFAULT_MODE_STORAGE_KEY, 'dark')).to.equal(
true,
);
});
Expand Down
1 change: 1 addition & 0 deletions packages/mui-system/src/cssVars/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export { default as prepareTypographyVars } from './prepareTypographyVars';
export type { ExtractTypographyTokens } from './prepareTypographyVars';
export { default as createCssVarsTheme } from './createCssVarsTheme';
export { createGetColorSchemeSelector } from './getColorSchemeSelector';
export type { StorageManager } from './localStorageManager';
80 changes: 80 additions & 0 deletions packages/mui-system/src/cssVars/localStorageManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
export interface StorageManager {
(options: { key: string; storageWindow?: Window | null }): {
/**
* Function to get the value from the storage
* @param defaultValue The default value to be returned if the key is not found
* @returns The value from the storage or the default value
*/
get(defaultValue: any): any;
/**
* Function to set the value in the storage
* @param value The value to be set
* @returns void
*/
set(value: any): void;
/**
* Function to subscribe to the value of the specified key triggered by external events
* @param handler The function to be called when the value changes
* @returns A function to unsubscribe the handler
* @example
* React.useEffect(() => {
* const unsubscribe = storageManager.subscribe((value) => {
* console.log(value);
* });
* return unsubscribe;
* }, []);
*/
subscribe(handler: (value: any) => void): () => void;
};
}

function noop() {}

const localStorageManager: StorageManager = ({ key, storageWindow }) => {
if (!storageWindow && typeof window !== 'undefined') {
storageWindow = window;
}
return {
get(defaultValue) {
if (typeof window === 'undefined') {
return undefined;
}
if (!storageWindow) {
return defaultValue;
}
let value;
try {
value = storageWindow.localStorage.getItem(key);
} catch {
// Unsupported
}
return value || defaultValue;
},
set: (value) => {
if (storageWindow) {
try {
storageWindow.localStorage.setItem(key, value);
} catch {
// Unsupported
}
}
},
subscribe: (handler) => {
if (!storageWindow) {
return noop;
}
const listener = (event: StorageEvent) => {
const value = event.newValue;
if (event.key === key) {
handler(value);
}
};
storageWindow.addEventListener('storage', listener);
return () => {
storageWindow.removeEventListener('storage', listener);
};
},
};
};

export default localStorageManager;
Loading

0 comments on commit 50a760e

Please sign in to comment.