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

Add InitColorSchemeScript for Next.js App Router #42247

Merged
merged 25 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
82d7240
add InitColorSchemeScript
siriwatknp May 15, 2024
38de969
Revert "add InitColorSchemeScript"
siriwatknp May 17, 2024
c2ce63d
Revert "Revert "add InitColorSchemeScript""
siriwatknp May 17, 2024
f876485
Merge branch 'next' of https://github.com/mui/material-ui into feat/i…
siriwatknp May 17, 2024
8006268
Merge branch 'next' of https://github.com/mui/material-ui into feat/i…
siriwatknp Jun 10, 2024
e18a6d0
reexport InitColorSchemeScript as component
siriwatknp Jun 10, 2024
10683e6
add test
siriwatknp Jun 10, 2024
394b59e
update docs
siriwatknp Jun 10, 2024
1ef5f65
spec
siriwatknp Jun 10, 2024
bef0a77
add nonce type
siriwatknp Jun 10, 2024
9582d24
ignore CssVarsProvider from rsc
siriwatknp Jun 10, 2024
285b3cc
Merge branch 'next' of https://github.com/mui/material-ui into feat/i…
siriwatknp Jun 13, 2024
8fa07a0
split InitColorSchemeScript
siriwatknp Jun 13, 2024
34eab69
fix path
siriwatknp Jun 13, 2024
d480e9c
fix ignore path
siriwatknp Jun 13, 2024
00129fe
fix lint
siriwatknp Jun 13, 2024
334d09e
update docs
siriwatknp Jun 13, 2024
d3859a0
add InitColorSchemeScript to joy
siriwatknp Jun 13, 2024
92493b2
use InitColorSchemeScript in docs
siriwatknp Jun 13, 2024
3256350
update docs
siriwatknp Jun 13, 2024
2471c88
fix joy rsc ignore
siriwatknp Jun 13, 2024
52dddfd
trigger build
siriwatknp Jun 13, 2024
1b3cd47
Merge branch 'next' of https://github.com/mui/material-ui into feat/i…
siriwatknp Jun 14, 2024
c11fb6b
skip demo for InitColorSchemeScript
siriwatknp Jun 14, 2024
0e38328
pnpm install
siriwatknp Jun 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -286,12 +286,10 @@ function App() {
}
```

For a server-side application, provide the same value to [`getInitColorSchemeScript()`](/material-ui/customization/css-theme-variables/usage/#server-side-rendering):
For a server-side application, provide the same value to [`InitColorSchemeScript`](/material-ui/customization/css-theme-variables/usage/#server-side-rendering):

```js
getInitColorSchemeScript({
defaultMode: 'dark',
});
<InitColorSchemeScript defaultMode="dark" />
```

:::warning
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,23 +115,42 @@ The structure of this object is nearly identical to the theme structure, the onl

## Server-side rendering

Place `getInitColorSchemeScript()` before the `<Main />` tag to prevent the dark-mode SSR flickering during the hydration phase.
Place `<InitColorSchemeScript />` before the `<Main />` tag to prevent the dark-mode SSR flickering during the hydration phase.

### Next.js App Router

Add the following code to the [root layout](https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts#root-layout-required) file:

```jsx title="app/layout.js"
import InitColorSchemeScript from '@mui/material/InitColorSchemeScript';

export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<InitColorSchemeScript /> {/* must come before the <main> element */}
<main>{children}</main>
</body>
</html>
);
}
```

### Next.js Pages Router

Add the following code to the custom [`pages/_document.js`](https://nextjs.org/docs/pages/building-your-application/routing/custom-document) file:

```jsx
```jsx title="pages/_document.js"
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { getInitColorSchemeScript } from '@mui/material/styles';
import InitColorSchemeScript from '@mui/material/InitColorSchemeScript';

export default class MyDocument extends Document {
render() {
return (
<Html data-color-scheme="light">
<Head>...</Head>
<body>
{getInitColorSchemeScript()}
<InitColorSchemeScript /> {/* must come before the <Main> element */}
<Main />
<NextScript />
</body>
Expand All @@ -158,7 +177,7 @@ const StyledComponent = styled('button')(({ theme }) => ({

## API

### `<CssVarsProvider>` props
### `<CssVarsProvider>` &nbsp;props

- `defaultMode?: 'light' | 'dark' | 'system'` - Application's default mode (`light` by default)
- `disableTransitionOnChange : boolean` - Disable CSS transitions when switching between modes
Expand All @@ -171,10 +190,9 @@ const StyledComponent = styled('button')(({ theme }) => ({
- `mode: string` - The user's selected mode
- `setMode: mode => {…}` - Function for setting the `mode`. The `mode` is saved to internal state and local storage; if `mode` is null, it will be reset to the default mode

### `getInitColorSchemeScript: (options) => React.ReactElement`

**options**
### `<InitColorSchemeScript>` &nbsp;props

- `defaultMode?: 'light' | 'dark' | 'system'`: - Application's default mode before React renders the tree (`light` by default)
- `modeStorageKey?: string`: - localStorage key used to store application `mode`
- `attribute?: string` - DOM attribute for applying color scheme
- `nonce?: string` - Optional nonce passed to the injected script tag, used to allow-list the next-themes script in your CSP
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import * as React from 'react';
import InitColorSchemeScript from '@mui/material/InitColorSchemeScript';

<InitColorSchemeScript nonce="foo-bar" />;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as React from 'react';
import { expect } from 'chai';
import { createRenderer } from '@mui/internal-test-utils';
import InitColorSchemeScript from '@mui/material/InitColorSchemeScript';

describe('InitColorSchemeScript', () => {
const { render } = createRenderer();

it('should render as expected', () => {
const { container } = render(<InitColorSchemeScript />);
expect(container.firstChild).to.have.tagName('script');
});
});
1 change: 1 addition & 0 deletions packages/mui-material/src/InitColorSchemeScript/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { InitColorSchemeScript as default } from '../styles/CssVarsProvider';
59 changes: 31 additions & 28 deletions packages/mui-material/src/styles/CssVarsProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
'use client';
import * as React from 'react';
import { unstable_createCssVarsProvider as createCssVarsProvider, SxProps } from '@mui/system';
import styleFunctionSx from '@mui/system/styleFunctionSx';
Expand All @@ -8,32 +7,30 @@ import THEME_ID from './identifier';

const defaultTheme = extendTheme();

const { CssVarsProvider, useColorScheme, getInitColorSchemeScript } = createCssVarsProvider<
SupportedColorScheme,
typeof THEME_ID
>({
themeId: THEME_ID,
theme: defaultTheme,
attribute: 'data-mui-color-scheme',
modeStorageKey: 'mui-mode',
colorSchemeStorageKey: 'mui-color-scheme',
defaultColorScheme: {
light: 'light',
dark: 'dark',
},
resolveTheme: (theme) => {
const newTheme = {
...theme,
typography: createTypography(theme.palette, theme.typography),
};

newTheme.unstable_sx = function sx(props: SxProps<CssVarsTheme>) {
return styleFunctionSx({ sx: props, theme: this });
};

return newTheme;
},
});
const { CssVarsProvider, useColorScheme, getInitColorSchemeScript, InitColorSchemeScript } =
createCssVarsProvider<SupportedColorScheme, typeof THEME_ID>({
themeId: THEME_ID,
theme: defaultTheme,
attribute: 'data-mui-color-scheme',
modeStorageKey: 'mui-mode',
colorSchemeStorageKey: 'mui-color-scheme',
defaultColorScheme: {
light: 'light',
dark: 'dark',
},
resolveTheme: (theme) => {
const newTheme = {
...theme,
typography: createTypography(theme.palette, theme.typography),
};

newTheme.unstable_sx = function sx(props: SxProps<CssVarsTheme>) {
return styleFunctionSx({ sx: props, theme: this });
};

return newTheme;
},
});

let warnedOnce = false;

Expand All @@ -54,4 +51,10 @@ function Experimental_CssVarsProvider(props: any) {
return <CssVarsProvider {...props} />;
}

export { useColorScheme, getInitColorSchemeScript, CssVarsProvider, Experimental_CssVarsProvider };
export {
useColorScheme,
getInitColorSchemeScript,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we deprecate getInitColorSchemeScript? Or do we expect users to still use it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I think it can be removed but I'd do it in a separate PR and add note to the migration doc in the same PR.

CssVarsProvider,
Experimental_CssVarsProvider,
InitColorSchemeScript,
};
3 changes: 3 additions & 0 deletions packages/mui-system/src/cssVars/createCssVarsProvider.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ export interface CreateCssVarsProviderResult<
) => React.ReactElement<any>;
useColorScheme: () => ColorSchemeContextValue<ColorScheme>;
getInitColorSchemeScript: typeof getInitColorSchemeScript;
InitColorSchemeScript: (
props: Parameters<typeof getInitColorSchemeScript>[0],
) => React.ReactElement;
}

export default function createCssVarsProvider<
Expand Down
6 changes: 5 additions & 1 deletion packages/mui-system/src/cssVars/createCssVarsProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -368,5 +368,9 @@ export default function createCssVarsProvider(options) {
...params,
});

return { CssVarsProvider, useColorScheme, getInitColorSchemeScript };
const InitColorSchemeScript = React.memo(function InitColorSchemeScript(props) {
return getInitColorSchemeScript(props);
});

return { CssVarsProvider, useColorScheme, getInitColorSchemeScript, InitColorSchemeScript };
}
5 changes: 5 additions & 0 deletions packages/mui-system/src/cssVars/getInitColorSchemeScript.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export interface GetInitColorSchemeScriptOptions {
* @default 'data-color-scheme'
*/
attribute?: string;
/** Nonce string to pass to the inline script for CSP headers */
nonce?: string | undefined;
}

export default function getInitColorSchemeScript(options?: GetInitColorSchemeScriptOptions) {
Expand All @@ -51,10 +53,13 @@ export default function getInitColorSchemeScript(options?: GetInitColorSchemeScr
colorSchemeStorageKey = DEFAULT_COLOR_SCHEME_STORAGE_KEY,
attribute = DEFAULT_ATTRIBUTE,
colorSchemeNode = 'document.documentElement',
nonce,
} = options || {};
return (
<script
key="mui-color-scheme-init"
suppressHydrationWarning
nonce={typeof window === 'undefined' ? nonce : ''}
Comment on lines +66 to +67
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: `(function() {
Expand Down
8 changes: 7 additions & 1 deletion packages/rsc-builder/buildRsc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type Project = {
rootPath: string;
additionalPaths?: string[];
additionalFiles?: string[];
ignorePaths?: string[];
};

const PROJECTS: Project[] = [
Expand All @@ -21,6 +22,9 @@ const PROJECTS: Project[] = [
{
name: 'material',
rootPath: path.join(process.cwd(), 'packages/mui-material'),
ignorePaths: [
'packages/mui-material/src/styles/CssVarsProvider.tsx', // no need 'use client' because of `styles/index` export
],
},
{
name: 'joy',
Expand Down Expand Up @@ -113,7 +117,9 @@ async function run(argv: yargs.ArgumentsCamelCase<CommandOptions>) {

components.forEach(async (component) => {
try {
processFile(component.filename);
if (!project.ignorePaths?.some((p) => component.filename.includes(p))) {
processFile(component.filename);
}
} catch (error: any) {
error.message = `${path.relative(process.cwd(), component.filename)}: ${error.message}`;
throw error;
Expand Down