Skip to content

Commit

Permalink
[material-ui] Merge CssVarsProvider into ThemeProvider (#43115)
Browse files Browse the repository at this point in the history
  • Loading branch information
siriwatknp authored Aug 6, 2024
1 parent 562d662 commit 5d4b7b6
Show file tree
Hide file tree
Showing 32 changed files with 656 additions and 166 deletions.
10 changes: 4 additions & 6 deletions docs/public/static/error-codes.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,8 @@
"15": "MUI: withStyles is no longer exported from @mui/material/styles.\nYou have to import it from @mui/styles.\nSee https://mui.com/r/migration-v4/#mui-material-styles for more details.",
"16": "MUI: withTheme is no longer exported from @mui/material/styles.\nYou have to import it from @mui/styles.\nSee https://mui.com/r/migration-v4/#mui-material-styles for more details.",
"17": "MUI: Expected valid input target. Did you use a custom `slots.input` and forget to forward refs? See https://mui.com/r/input-component-ref-interface for more info.",
"18": "MUI: `vars` is a private field used for CSS variables support.\nPlease use another name.",
"19": "MUI: `useColorScheme` must be called under <CssVarsProvider />",
"20": "MUI: The `experimental_sx` has been moved to `theme.unstable_sx`.For more details, see https://github.com/mui/material-ui/pull/35150.",
"21": "MUI: The provided shorthand %s is invalid. The format should be `@<breakpoint | number>` or `@<breakpoint | number>/<container>`.\nFor example, `@sm` or `@600` or `@40rem/sidebar`.",
"22": "MUI: Missing or invalid value of `colorSchemes.%s` from the `extendTheme` function.",
"23": "MUI: The provided `colorSchemes.%s` to the `extendTheme` function is either missing or invalid."
"18": "MUI: The provided shorthand %s is invalid. The format should be `@<breakpoint | number>` or `@<breakpoint | number>/<container>`.\nFor example, `@sm` or `@600` or `@40rem/sidebar`.",
"19": "MUI: The `experimental_sx` has been moved to `theme.unstable_sx`.For more details, see https://github.com/mui/material-ui/pull/35150.",
"20": "MUI: `vars` is a private field used for CSS variables support.\nPlease use another name.",
"21": "MUI: The `colorSchemes.%s` option is either missing or invalid."
}
6 changes: 3 additions & 3 deletions packages/mui-material/src/Chip/Chip.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from '@mui/internal-test-utils';
import Avatar from '@mui/material/Avatar';
import Chip, { chipClasses as classes } from '@mui/material/Chip';
import { ThemeProvider, createTheme, hexToRgb, extendTheme } from '@mui/material/styles';
import { ThemeProvider, createTheme, hexToRgb } from '@mui/material/styles';
import CheckBox from '../internal/svg-icons/CheckBox';
import defaultTheme from '../styles/defaultTheme';
import describeConformance from '../../test/describeConformance';
Expand Down Expand Up @@ -710,15 +710,15 @@ describe('<Chip />', () => {

describe('CSS vars', () => {
it('should not throw when there is theme value is CSS variable', () => {
const theme = extendTheme();
const theme = createTheme({ cssVariables: true });
theme.palette = theme.colorSchemes.light.palette;
theme.palette.text = {
...theme.palette.text,
primary: 'var(--mui-palette-grey-900)',
};
expect(() =>
render(
<ThemeProvider theme={theme}>
<ThemeProvider disableStyleSheetGeneration theme={theme}>
<Chip label="Test Chip" />
</ThemeProvider>,
),
Expand Down
6 changes: 5 additions & 1 deletion packages/mui-material/src/CssBaseline/CssBaseline.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ export const body = (theme) => ({

export const styles = (theme, enableColorScheme = false) => {
const colorSchemeStyles = {};
if (enableColorScheme && theme.colorSchemes) {
if (
enableColorScheme &&
theme.colorSchemes &&
typeof theme.getColorSchemeSelector === 'function'
) {
Object.entries(theme.colorSchemes).forEach(([key, scheme]) => {
const selector = theme.getColorSchemeSelector(key);
if (selector.startsWith('@')) {
Expand Down
17 changes: 0 additions & 17 deletions packages/mui-material/src/styles/ThemeProvider.d.ts

This file was deleted.

27 changes: 0 additions & 27 deletions packages/mui-material/src/styles/ThemeProvider.js

This file was deleted.

80 changes: 79 additions & 1 deletion packages/mui-material/src/styles/ThemeProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,36 @@
import * as React from 'react';
import { expect } from 'chai';
import { createRenderer } from '@mui/internal-test-utils';
import { ThemeProvider } from '@mui/material/styles';
import { ThemeProvider, createTheme, useColorScheme } from '@mui/material/styles';

describe('ThemeProvider', () => {
const { render } = createRenderer();
let originalMatchmedia: typeof window.matchMedia;
let storage: Record<string, string> = {};

beforeEach(() => {
originalMatchmedia = window.matchMedia;
// Create mocks of localStorage getItem and setItem functions
storage = {};
Object.defineProperty(global, 'localStorage', {
value: {
getItem: (key: string) => storage[key],
setItem: (key: string, value: string) => {
storage[key] = value;
},
},
configurable: true,
});
window.matchMedia = () =>
({
addListener: () => {},
removeListener: () => {},
}) as unknown as MediaQueryList;
});

afterEach(() => {
window.matchMedia = originalMatchmedia;
});

it('When theme is a function, it should not show warning', () => {
expect(() =>
Expand All @@ -15,4 +41,56 @@ describe('ThemeProvider', () => {
),
).not.toWarnDev();
});

describe('light & dark', () => {
function ModeSwitcher() {
const { mode, setMode } = useColorScheme();
if (!mode) {
return null;
}
return (
<select
data-testid="mode-switcher"
value={mode}
onChange={(event) => {
setMode(event.target.value as 'light' | 'dark' | 'system');
}}
>
<option value="system">System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
);
}

it('should be able to use `useColorScheme`', () => {
const theme = createTheme({
colorSchemes: { dark: true },
});
expect(() =>
render(
<ThemeProvider theme={theme}>
<ModeSwitcher />
</ThemeProvider>,
),
).not.toErrorDev();
});

it('should be able to switch between modes', async () => {
const theme = createTheme({
colorSchemes: { dark: true },
});
const { getByTestId, user } = render(
<ThemeProvider theme={theme}>
<ModeSwitcher />
</ThemeProvider>,
);

expect(getByTestId('mode-switcher')).to.have.property('value', 'system');

await user.selectOptions(getByTestId('mode-switcher'), 'dark');

expect(getByTestId('mode-switcher')).to.have.property('value', 'dark');
});
});
});
73 changes: 73 additions & 0 deletions packages/mui-material/src/styles/ThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
'use client';
import * as React from 'react';
import { DefaultTheme } from '@mui/system';
import ThemeProviderNoVars from './ThemeProviderNoVars';
import { CssThemeVariables } from './createThemeNoVars';
import { CssVarsProvider } from './ThemeProviderWithVars';
import { CssVarsTheme } from './createThemeWithVars';
import THEME_ID from './identifier';

type ThemeProviderCssVariablesProps = CssThemeVariables extends { enabled: true }
? {
/**
* The node for attaching the `theme.colorSchemeSelector`.
* @default document
*/
colorSchemeNode?: Element | null;
/**
* If `true`, the provider creates its own context and generate stylesheet as if it is a root `ThemeProvider`.
*/
disableNestedContext?: boolean;
/**
* If `true`, the style sheet for CSS theme variables won't be generated.
*
* This is useful for controlling nested ThemeProvider behavior.
* @default false
*/
disableStyleSheetGeneration?: boolean;
}
: {};

export interface ThemeProviderProps<Theme = DefaultTheme> extends ThemeProviderCssVariablesProps {
children?: React.ReactNode;
theme: Partial<Theme> | ((outerTheme: Theme) => Theme);
/**
* The document used to perform `disableTransitionOnChange` feature
* @default document
*/
documentNode?: Document | null;
/**
* The window that attaches the 'storage' event listener
* @default window
*/
storageWindow?: Window | null;
/**
* localStorage key used to store application `mode`
* @default 'mui-mode'
*/
modeStorageKey?: string;
/**
* localStorage key used to store `colorScheme`
* @default 'mui-color-scheme'
*/
colorSchemeStorageKey?: string;
/**
* Disable CSS transitions when switching between modes or color schemes
* @default false
*/
disableTransitionOnChange?: boolean;
}

export default function ThemeProvider<Theme = DefaultTheme>({
theme,
...props
}: ThemeProviderProps<Theme>) {
if (typeof theme === 'function') {
return <ThemeProviderNoVars theme={theme} {...props} />;
}
const muiTheme = (THEME_ID in theme ? theme[THEME_ID] : theme) as ThemeProviderProps['theme'];
if (!('colorSchemes' in muiTheme)) {
return <ThemeProviderNoVars theme={theme} {...props} />;
}
return <CssVarsProvider theme={theme as unknown as CssVarsTheme} {...props} />;
}
23 changes: 23 additions & 0 deletions packages/mui-material/src/styles/ThemeProviderNoVars.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client';
import * as React from 'react';
import { ThemeProvider as SystemThemeProvider, DefaultTheme } from '@mui/system';
import THEME_ID from './identifier';

export interface ThemeProviderNoVarsProps<Theme = DefaultTheme> {
children?: React.ReactNode;
theme: Partial<Theme> | ((outerTheme: Theme) => Theme);
}

export default function ThemeProviderNoVars<Theme = DefaultTheme>({
theme: themeInput,
...props
}: ThemeProviderNoVarsProps<Theme>): React.ReactElement<ThemeProviderNoVarsProps<Theme>> {
const scopedTheme = THEME_ID in themeInput ? themeInput[THEME_ID] : undefined;
return (
<SystemThemeProvider
{...props}
themeId={scopedTheme ? THEME_ID : undefined}
theme={scopedTheme || themeInput}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createRenderer, screen } from '@mui/internal-test-utils';
import Box from '@mui/material/Box';
import { CssVarsProvider, extendTheme, useTheme } from '@mui/material/styles';

describe('[Material UI] CssVarsProvider', () => {
describe('[Material UI] ThemeProviderWithVars', () => {
let originalMatchmedia;
const { render } = createRenderer();
const storage = {};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
'use client';
import * as React from 'react';
import { unstable_createCssVarsProvider as createCssVarsProvider, SxProps } from '@mui/system';
import styleFunctionSx from '@mui/system/styleFunctionSx';
import extendTheme, { SupportedColorScheme, CssVarsTheme } from './extendTheme';
import { unstable_createCssVarsProvider as createCssVarsProvider, SxProps } from '@mui/system';
import { SupportedColorScheme, CssVarsTheme } from './createThemeWithVars';
import createTheme from './createTheme';
import createTypography from './createTypography';
import THEME_ID from './identifier';
import { defaultConfig } from '../InitColorSchemeScript/InitColorSchemeScript';

const defaultTheme = extendTheme();

const {
CssVarsProvider,
useColorScheme,
getInitColorSchemeScript: deprecatedGetInitColorSchemeScript,
} = createCssVarsProvider<SupportedColorScheme, typeof THEME_ID>({
themeId: THEME_ID,
theme: defaultTheme,
// @ts-ignore ignore module augmentation tests
theme: () => createTheme({ cssVariables: true }),
colorSchemeStorageKey: defaultConfig.colorSchemeStorageKey,
modeStorageKey: defaultConfig.modeStorageKey,
defaultColorScheme: {
Expand Down
39 changes: 39 additions & 0 deletions packages/mui-material/src/styles/createColorScheme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { ColorSystemOptions } from './createThemeWithVars';
import createPalette, { PaletteOptions } from './createPalette';
import getOverlayAlpha from './getOverlayAlpha';

const defaultDarkOverlays = [...Array(25)].map((_, index) => {
if (index === 0) {
return undefined;
}
const overlay = getOverlayAlpha(index);
return `linear-gradient(rgba(255 255 255 / ${overlay}), rgba(255 255 255 / ${overlay}))`;
});

export function getOpacity(mode: 'light' | 'dark') {
return {
inputPlaceholder: mode === 'dark' ? 0.5 : 0.42,
inputUnderline: mode === 'dark' ? 0.7 : 0.42,
switchTrackDisabled: mode === 'dark' ? 0.2 : 0.12,
switchTrack: mode === 'dark' ? 0.3 : 0.38,
};
}
export function getOverlays(mode: 'light' | 'dark') {
return mode === 'dark' ? defaultDarkOverlays : [];
}

export default function createColorScheme(options: ColorSystemOptions) {
const {
palette: paletteInput = { mode: 'light' } as PaletteOptions, // need to cast to avoid module augmentation test
opacity,
overlays,
...rest
} = options;
const palette = createPalette(paletteInput);
return {
palette,
opacity: { ...getOpacity(palette.mode), ...opacity },
overlays: overlays || getOverlays(palette.mode),
...rest,
} as unknown as ColorSystemOptions;
}
Loading

0 comments on commit 5d4b7b6

Please sign in to comment.