diff --git a/change/@fluentui-react-provider-51de7ed7-e932-4b2f-aa6b-c871f1b8c016.json b/change/@fluentui-react-provider-51de7ed7-e932-4b2f-aa6b-c871f1b8c016.json new file mode 100644 index 0000000000000..f09e6a7a33ed5 --- /dev/null +++ b/change/@fluentui-react-provider-51de7ed7-e932-4b2f-aa6b-c871f1b8c016.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: Render theme CSS variables in SSR style element", + "packageName": "@fluentui/react-provider", + "email": "lingfangao@hotmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-provider/etc/react-provider.api.md b/packages/react-components/react-provider/etc/react-provider.api.md index d0255a7bceaa6..af4ee0d9cbc89 100644 --- a/packages/react-components/react-provider/etc/react-provider.api.md +++ b/packages/react-components/react-provider/etc/react-provider.api.md @@ -61,6 +61,10 @@ export type FluentProviderSlots = { export type FluentProviderState = ComponentState & Pick & Required> & { theme: ThemeContextValue_unstable; themeClassName: string; + serverStyleProps: { + cssRule: string; + attributes: Record; + }; }; // @public @@ -75,8 +79,13 @@ export function useFluentProviderContextValues_unstable(state: FluentProviderSta // @public export const useFluentProviderStyles_unstable: (state: FluentProviderState) => FluentProviderState; -// @public -export const useFluentProviderThemeStyleTag: (options: Pick) => string; +// @internal +export const useFluentProviderThemeStyleTag: (options: Pick & { + rendererAttributes: Record; +}) => { + styleTagId: string; + rule: string; +}; // (No @packageDocumentation comment for this package) diff --git a/packages/react-components/react-provider/src/components/FluentProvider/FluentProvider-node.test.tsx b/packages/react-components/react-provider/src/components/FluentProvider/FluentProvider-node.test.tsx new file mode 100644 index 0000000000000..178716721acbd --- /dev/null +++ b/packages/react-components/react-provider/src/components/FluentProvider/FluentProvider-node.test.tsx @@ -0,0 +1,74 @@ +/* + * @jest-environment node + */ + +// 👆 this is intentionally to test in SSR like environment + +import * as React from 'react'; +import * as ReactDOM from 'react-dom/server'; +import { resetIdsForTests } from '@fluentui/react-utilities'; +import { FluentProvider } from './FluentProvider'; +import * as prettier from 'prettier'; +import { createDOMRenderer } from '@griffel/core'; +import { RendererProvider } from '@griffel/react'; +import { PartialTheme } from '@fluentui/react-theme'; + +const parseHTMLString = (html: string) => { + return prettier.format(html, { parser: 'html' }); +}; + +describe('FluentProvider (node)', () => { + const testTheme: PartialTheme = { + colorNeutralForeground1: 'black', + colorNeutralBackground1: 'white', + }; + + afterEach(() => { + resetIdsForTests(); + }); + + it('should render CSS variables as inline style', () => { + const html = ReactDOM.renderToStaticMarkup(); + + expect(parseHTMLString(html)).toMatchInlineSnapshot(` + "
+ +
" + `); + }); + + it('renders nonce with SSR style element', () => { + const nonce = 'random'; + const renderer = createDOMRenderer(undefined, { + styleElementAttributes: { nonce }, + }); + + const html = ReactDOM.renderToStaticMarkup( + + + , + ); + + expect(parseHTMLString(html)).toMatchInlineSnapshot(` + "
+ +
" + `); + }); +}); diff --git a/packages/react-components/react-provider/src/components/FluentProvider/FluentProvider.test.tsx b/packages/react-components/react-provider/src/components/FluentProvider/FluentProvider.test.tsx index 7d0e5992c44da..edf895890f0d7 100644 --- a/packages/react-components/react-provider/src/components/FluentProvider/FluentProvider.test.tsx +++ b/packages/react-components/react-provider/src/components/FluentProvider/FluentProvider.test.tsx @@ -1,10 +1,11 @@ import { resetIdsForTests } from '@fluentui/react-utilities'; import { render } from '@testing-library/react'; import * as React from 'react'; -import * as renderer from 'react-test-renderer'; +import * as reactTestRenderer from 'react-test-renderer'; import { FluentProvider } from './FluentProvider'; import { isConformant } from '../../testing/isConformant'; +import { teamsLightTheme } from '@fluentui/react-theme'; describe('FluentProvider', () => { // eslint-disable-next-line @typescript-eslint/no-empty-function @@ -28,13 +29,18 @@ describe('FluentProvider', () => { * Note: see more visual regression tests for FluentProvider in /apps/vr-tests. */ it('renders a default state', () => { - const component = renderer.create( + const component = reactTestRenderer.create( Default FluentProvider, ); const tree = component.toJSON(); expect(tree).toMatchSnapshot(); }); + it('does not render style element when not in SSR', () => { + const { container } = render(); + expect(container.querySelector('style')).toBeNull(); + }); + describe('applies "dir" attribute', () => { it('ltr', () => { const { getByText } = render(Test); diff --git a/packages/react-components/react-provider/src/components/FluentProvider/FluentProvider.types.ts b/packages/react-components/react-provider/src/components/FluentProvider/FluentProvider.types.ts index 3d6f2230e564f..462d4293f76a8 100644 --- a/packages/react-components/react-provider/src/components/FluentProvider/FluentProvider.types.ts +++ b/packages/react-components/react-provider/src/components/FluentProvider/FluentProvider.types.ts @@ -48,6 +48,19 @@ export type FluentProviderState = ComponentState & > & { theme: ThemeContextValue; themeClassName: string; + /** + * Props used to render SSR theme variables style element + */ + serverStyleProps: { + /** + * CSS rule containing CSS variables + */ + cssRule: string; + /** + * Additional attributes applied to the style element + */ + attributes: Record; + }; }; export type FluentProviderContextValues = Pick< diff --git a/packages/react-components/react-provider/src/components/FluentProvider/renderFluentProvider.tsx b/packages/react-components/react-provider/src/components/FluentProvider/renderFluentProvider.tsx index d7af3e313d314..9276aab3c6f29 100644 --- a/packages/react-components/react-provider/src/components/FluentProvider/renderFluentProvider.tsx +++ b/packages/react-components/react-provider/src/components/FluentProvider/renderFluentProvider.tsx @@ -9,8 +9,8 @@ import { CustomStyleHooksProvider_unstable as CustomStyleHooksProvider, CustomStyleHooksContextValue_unstable as CustomStyleHooksContextValue, } from '@fluentui/react-shared-contexts'; -import { getSlots } from '@fluentui/react-utilities'; -import type { FluentProviderSlots, FluentProviderContextValues, FluentProviderState } from './FluentProvider.types'; +import { canUseDOM, getSlots } from '@fluentui/react-utilities'; +import type { FluentProviderContextValues, FluentProviderState, FluentProviderSlots } from './FluentProvider.types'; /** * Render the final JSX of FluentProvider @@ -35,7 +35,18 @@ export const renderFluentProvider_unstable = ( - {state.root.children} + + {canUseDOM() ? null : ( +