Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[material-ui] Add storageManager prop to ThemeProvider #45136

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
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 @@

{{"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.

Check warning on line 194 in docs/data/material/customization/dark-mode/dark-mode.md

View workflow job for this annotation

GitHub Actions / runner / vale

[vale] reported by reviewdog 🐶 [Google.Will] Avoid using 'will'. Raw Output: {"message": "[Google.Will] Avoid using 'will'.", "location": {"path": "docs/data/material/customization/dark-mode/dark-mode.md", "range": {"start": {"line": 194, "column": 31}}}, "severity": "WARNING"}
:::

## 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 @@ -98,6 +98,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
Loading