From 4735d90a436a136b5dcdf0309ecb83ccb8293953 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Tue, 21 Mar 2023 18:36:34 +0100 Subject: [PATCH 01/26] feat: Render theme CSS variables in new SSR style slot Adds a new `serverStyle` slot to the `FluentProvider` that is only rendered when the provider is a child of an `SSRProvider`. Also updates `useFluentProviderThemeTag` to return the CSS rule that contains all CSS variables. This PR makes use of dangerous HTML, since react will output certain characters in unicode which can result in invalid CSS, [this approach is also taken by Emotion](https://github.com/emotion-js/emotion/blob/89b6dbb3c13d49ef1fa3d88888672d810853f05a/packages/react/src/emotion-element.js#L72-L78) --- apps/ssr-tests-v9/src/build.ts | 3 +- .../FluentProvider-node.test.tsx | 59 +++++++++++++++++++ .../FluentProvider/FluentProvider.test.tsx | 45 +++++++++++++- .../FluentProvider/FluentProvider.types.ts | 4 ++ .../FluentProvider/renderFluentProvider.tsx | 5 +- .../FluentProvider/useFluentProvider.ts | 14 ++++- .../FluentProvider/useFluentProviderStyles.ts | 5 ++ .../useFluentProviderThemeStyleTag.test.tsx | 14 ++--- .../useFluentProviderThemeStyleTag.ts | 26 ++++---- .../react-utilities/src/index.ts | 2 +- .../src/ssr/SSRContext-node.test.tsx | 14 ++++- .../src/ssr/SSRContext.test.tsx | 14 ++++- .../react-utilities/src/ssr/SSRContext.tsx | 7 +++ 13 files changed, 186 insertions(+), 26 deletions(-) create mode 100644 packages/react-components/react-provider/src/components/FluentProvider/FluentProvider-node.test.tsx diff --git a/apps/ssr-tests-v9/src/build.ts b/apps/ssr-tests-v9/src/build.ts index d160793a2045a..ad7d352b48572 100644 --- a/apps/ssr-tests-v9/src/build.ts +++ b/apps/ssr-tests-v9/src/build.ts @@ -29,7 +29,7 @@ async function build() { // https://github.com/facebook/react/issues/13097 const skippedPaths = ['react-portal']; - const rawStoriesGlobs = getPackageStoriesGlob({ + let rawStoriesGlobs = getPackageStoriesGlob({ packageName: '@fluentui/react-components', callerPath: __dirname, }).filter( @@ -44,6 +44,7 @@ async function build() { ) as string[]; rawStoriesGlobs.push(path.resolve(path.join(__dirname, './stories/**/index.stories.tsx'))); + rawStoriesGlobs = rawStoriesGlobs.filter(x => x.includes('react-button')); const storiesGlobs = rawStoriesGlobs // TODO: Find a better way for this. Pass the path via params? 👇 .map(pattern => path.resolve(__dirname, pattern)); 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..a47af85d69814 --- /dev/null +++ b/packages/react-components/react-provider/src/components/FluentProvider/FluentProvider-node.test.tsx @@ -0,0 +1,59 @@ +/* + * @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 { SSRProvider } from '@fluentui/react-utilities'; +import { webLightTheme } from '@fluentui/react-theme'; +import { FluentProvider } from './FluentProvider'; +import * as prettier from 'prettier'; + +jest.mock('@fluentui/react-theme', () => ({ + ...jest.requireActual('@fluentui/react-theme'), + webLightTheme: { + colorNeutralForeground1: 'black', + colorNeutralBackground1: 'white', + }, +})); + +const parseHTMLString = (html: string) => { + return prettier.format(html, { parser: 'html' }); +}; + +describe('FluentProvider (node)', () => { + it('should render CSS variables as inline style when wrapped with SSRPRovider', () => { + const html = ReactDOM.renderToStaticMarkup( + + + , + ); + + expect(parseHTMLString(html)).toMatchInlineSnapshot(` + "
+ +
" + `); + }); + + it('should not render CSS variables as inline style when not wrapped with SSRPRovider', () => { + 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..593da4200ad06 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 { resetIdsForTests, SSRProvider } from '@fluentui/react-utilities'; import { render } from '@testing-library/react'; import * as React from 'react'; import * as renderer 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 @@ -17,6 +18,9 @@ describe('FluentProvider', () => { isConformant({ disabledTests: ['component-handles-classname'], Component: FluentProvider, + renderOptions: { + wrapper: SSRProvider, + }, displayName: 'FluentProvider', }); @@ -35,6 +39,45 @@ describe('FluentProvider', () => { expect(tree).toMatchSnapshot(); }); + it('renders style element with css variables if wrapped with a SSRProvider', () => { + const { container } = render( + + + foo + + , + ); + + expect(container).toMatchInlineSnapshot(` +
+
+ + foo +
+
+ `); + }); + + it('does not render style element with css variables if not wrapped with a SSRProvider', () => { + const { container } = render(); + expect(container).toMatchInlineSnapshot(` +
+
+
+ `); + }); + 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..84dd0fd098469 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 @@ -11,6 +11,10 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utili export type FluentProviderSlots = { root: Slot<'div'>; + /** + * HTMLStyleElement rendered during SSR that contains theme CSS variables + */ + serverStyle?: Slot<'style'>; }; // exported for callers to avoid referencing react-shared-context 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..addade655ad5a 100644 --- a/packages/react-components/react-provider/src/components/FluentProvider/renderFluentProvider.tsx +++ b/packages/react-components/react-provider/src/components/FluentProvider/renderFluentProvider.tsx @@ -35,7 +35,10 @@ export const renderFluentProvider_unstable = ( - {state.root.children} + + {slots.serverStyle && } + {slotProps.root.children} + diff --git a/packages/react-components/react-provider/src/components/FluentProvider/useFluentProvider.ts b/packages/react-components/react-provider/src/components/FluentProvider/useFluentProvider.ts index faf476bb6b2b2..00a9f6e3a30af 100644 --- a/packages/react-components/react-provider/src/components/FluentProvider/useFluentProvider.ts +++ b/packages/react-components/react-provider/src/components/FluentProvider/useFluentProvider.ts @@ -10,7 +10,7 @@ import type { ThemeContextValue_unstable as ThemeContextValue, } from '@fluentui/react-shared-contexts'; -import { getNativeElementProps, useMergedRefs } from '@fluentui/react-utilities'; +import { getNativeElementProps, resolveShorthand, useIsInSSRContext, useMergedRefs } from '@fluentui/react-utilities'; import * as React from 'react'; import { useFluentProviderThemeStyleTag } from './useFluentProviderThemeStyleTag'; import type { FluentProviderProps, FluentProviderState } from './FluentProvider.types'; @@ -69,6 +69,7 @@ export const useFluentProvider_unstable = ( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const { styleTagId, rule } = useFluentProviderThemeStyleTag({ theme: mergedTheme, targetDocument }); return { applyStylesToPortals, // eslint-disable-next-line @typescript-eslint/naming-convention @@ -78,10 +79,11 @@ export const useFluentProvider_unstable = ( theme: mergedTheme, // eslint-disable-next-line @typescript-eslint/naming-convention overrides_unstable: mergedOverrides, - themeClassName: useFluentProviderThemeStyleTag({ theme: mergedTheme, targetDocument }), + themeClassName: styleTagId, components: { root: 'div', + serverStyle: 'style', }, root: getNativeElementProps('div', { @@ -89,6 +91,14 @@ export const useFluentProvider_unstable = ( dir, ref: useMergedRefs(ref, useFocusVisible({ targetDocument })), }), + + serverStyle: resolveShorthand(props.serverStyle, { + required: useIsInSSRContext(), + defaultProps: { + id: styleTagId, + dangerouslySetInnerHTML: { __html: rule }, + }, + }), }; }; diff --git a/packages/react-components/react-provider/src/components/FluentProvider/useFluentProviderStyles.ts b/packages/react-components/react-provider/src/components/FluentProvider/useFluentProviderStyles.ts index accc4f7d21087..e25aab7d266b5 100644 --- a/packages/react-components/react-provider/src/components/FluentProvider/useFluentProviderStyles.ts +++ b/packages/react-components/react-provider/src/components/FluentProvider/useFluentProviderStyles.ts @@ -6,6 +6,7 @@ import { SlotClassNames } from '@fluentui/react-utilities'; export const fluentProviderClassNames: SlotClassNames = { root: 'fui-FluentProvider', + serverStyle: 'fui-FluentProvider__serverStyle', }; const useStyles = makeStyles({ @@ -29,5 +30,9 @@ export const useFluentProviderStyles_unstable = (state: FluentProviderState) => state.root.className, ); + if (state.serverStyle) { + state.serverStyle.className = mergeClasses(fluentProviderClassNames.serverStyle, state.serverStyle.className); + } + return state; }; diff --git a/packages/react-components/react-provider/src/components/FluentProvider/useFluentProviderThemeStyleTag.test.tsx b/packages/react-components/react-provider/src/components/FluentProvider/useFluentProviderThemeStyleTag.test.tsx index 44e6b104d0b6c..fa15988a78d15 100644 --- a/packages/react-components/react-provider/src/components/FluentProvider/useFluentProviderThemeStyleTag.test.tsx +++ b/packages/react-components/react-provider/src/components/FluentProvider/useFluentProviderThemeStyleTag.test.tsx @@ -25,7 +25,7 @@ describe('useFluentProviderThemeStyleTag', () => { ); // Assert - expect(document.getElementById(result.current)).not.toBeNull(); + expect(document.getElementById(result.current.styleTagId)).not.toBeNull(); }); it('should remove style tag on unmount', () => { @@ -38,7 +38,7 @@ describe('useFluentProviderThemeStyleTag', () => { unmount(); // Assert - expect(document.getElementById(result.current)).toBeNull(); + expect(document.getElementById(result.current.styleTagId)).toBeNull(); }); it('should render css variables in theme', () => { @@ -48,11 +48,11 @@ describe('useFluentProviderThemeStyleTag', () => { ); // Assert - const tag = document.getElementById(result.current) as HTMLStyleElement; + const tag = document.getElementById(result.current.styleTagId) as HTMLStyleElement; const sheet = tag.sheet as CSSStyleSheet; const rule = sheet.cssRules[0] as CSSStyleRule; - expect(rule.selectorText).toEqual(`.${result.current}`); + expect(rule.selectorText).toEqual(`.${result.current.styleTagId}`); expect(rule.cssText).toMatchInlineSnapshot(`".fui-FluentProvider1 {--css-variable-1: 1; --css-variable-2: 2;}"`); }); @@ -66,10 +66,10 @@ describe('useFluentProviderThemeStyleTag', () => { rerender(); // Assert - const tag = document.getElementById(result.current) as HTMLStyleElement; + const tag = document.getElementById(result.current.styleTagId) as HTMLStyleElement; const sheet = tag.sheet as CSSStyleSheet; const rule = sheet.cssRules[0] as CSSStyleRule; - expect(rule.selectorText).toEqual(`.${result.current}`); + expect(rule.selectorText).toEqual(`.${result.current.styleTagId}`); expect(rule.cssText).toMatchInlineSnapshot(`".fui-FluentProvider1 {--css-variable-update: xxx;}"`); }); @@ -82,7 +82,7 @@ describe('useFluentProviderThemeStyleTag', () => { () => useFluentProviderThemeStyleTag({ theme: defaultTheme, targetDocument: document }), { wrapper: props => {props.children} }, ); - const tag = document.getElementById(result.current) as HTMLStyleElement; + const tag = document.getElementById(result.current.styleTagId) as HTMLStyleElement; expect(tag.getAttribute('id')).toBe('fui-FluentProvider1'); expect(tag.getAttribute('nonce')).toBe('random'); diff --git a/packages/react-components/react-provider/src/components/FluentProvider/useFluentProviderThemeStyleTag.ts b/packages/react-components/react-provider/src/components/FluentProvider/useFluentProviderThemeStyleTag.ts index afe33b6d5bef8..3abf3deaa37b8 100644 --- a/packages/react-components/react-provider/src/components/FluentProvider/useFluentProviderThemeStyleTag.ts +++ b/packages/react-components/react-provider/src/components/FluentProvider/useFluentProviderThemeStyleTag.ts @@ -1,4 +1,4 @@ -import { useId, useIsomorphicLayoutEffect } from '@fluentui/react-utilities'; +import { useId, useIsInSSRContext, useIsomorphicLayoutEffect } from '@fluentui/react-utilities'; import { useRenderer_unstable } from '@griffel/react'; import * as React from 'react'; @@ -46,6 +46,7 @@ const insertSheet = (tag: HTMLStyleElement, rule: string) => { */ export const useFluentProviderThemeStyleTag = (options: Pick) => { const { targetDocument, theme } = options; + const isInSSRContext = useIsInSSRContext(); const renderer = useRenderer_unstable(); const styleTag = React.useRef(); @@ -64,17 +65,20 @@ export const useFluentProviderThemeStyleTag = (options: Pick { - styleTag.current = createStyleTag(targetDocument, { ...styleElementAttributes, id: styleTagId }); + if (!isInSSRContext) { + // eslint-disable-next-line react-hooks/rules-of-hooks + useInsertionEffect(() => { + styleTag.current = createStyleTag(targetDocument, { ...styleElementAttributes, id: styleTagId }); - if (styleTag.current) { - insertSheet(styleTag.current, rule); + if (styleTag.current) { + insertSheet(styleTag.current, rule); - return () => { - styleTag.current?.remove(); - }; - } - }, [styleTagId, targetDocument, rule, styleElementAttributes]); + return () => { + styleTag.current?.remove(); + }; + } + }, [styleTagId, targetDocument, rule, styleElementAttributes]); + } - return styleTagId; + return { styleTagId, rule }; }; diff --git a/packages/react-components/react-utilities/src/index.ts b/packages/react-components/react-utilities/src/index.ts index 743ee85e64d6d..bf2a57deac75c 100644 --- a/packages/react-components/react-utilities/src/index.ts +++ b/packages/react-components/react-utilities/src/index.ts @@ -32,7 +32,7 @@ export { } from './hooks/index'; export type { RefObjectFunction, UseControllableStateOptions, UseOnClickOrScrollOutsideOptions } from './hooks/index'; -export { canUseDOM, useIsSSR, SSRProvider } from './ssr/index'; +export { canUseDOM, useIsSSR, useIsInSSRContext, SSRProvider } from './ssr/index'; export { clamp, diff --git a/packages/react-components/react-utilities/src/ssr/SSRContext-node.test.tsx b/packages/react-components/react-utilities/src/ssr/SSRContext-node.test.tsx index aa67c1bb3579b..636698c0a8e12 100644 --- a/packages/react-components/react-utilities/src/ssr/SSRContext-node.test.tsx +++ b/packages/react-components/react-utilities/src/ssr/SSRContext-node.test.tsx @@ -5,7 +5,7 @@ // 👆 this is intentionally to test in SSR like environment import { renderHook } from '@testing-library/react-hooks'; -import { SSRProvider, useIsSSR } from './SSRContext'; +import { SSRProvider, useIsSSR, useIsInSSRContext } from './SSRContext'; describe('useIsSSR (node)', () => { afterEach(() => { @@ -27,3 +27,15 @@ describe('useIsSSR (node)', () => { expect(result.current).toBe(true); }); }); + +describe('useIsInSSRContext (node)', () => { + it('returns true if wrapped by an SSRProvider', () => { + const { result } = renderHook(() => useIsInSSRContext(), { wrapper: SSRProvider }); + expect(result.current).toBe(true); + }); + + it('returns false if not wrapped by an SSRProvider', () => { + const { result } = renderHook(() => useIsInSSRContext()); + expect(result.current).toBe(false); + }); +}); diff --git a/packages/react-components/react-utilities/src/ssr/SSRContext.test.tsx b/packages/react-components/react-utilities/src/ssr/SSRContext.test.tsx index fcc7a445cadf9..04f535a50f326 100644 --- a/packages/react-components/react-utilities/src/ssr/SSRContext.test.tsx +++ b/packages/react-components/react-utilities/src/ssr/SSRContext.test.tsx @@ -1,5 +1,5 @@ import { renderHook } from '@testing-library/react-hooks'; -import { SSRProvider, useIsSSR } from './SSRContext'; +import { SSRProvider, useIsSSR, useIsInSSRContext } from './SSRContext'; describe('useIsSSR', () => { it('returns "false" outside of SSRProvider', () => { @@ -14,3 +14,15 @@ describe('useIsSSR', () => { expect(result.current).toBe(false); }); }); + +describe('useIsInSSRContext', () => { + it('returns true if wrapped by an SSRProvider', () => { + const { result } = renderHook(() => useIsInSSRContext(), { wrapper: SSRProvider }); + expect(result.current).toBe(true); + }); + + it('returns false if not wrapped by an SSRProvider', () => { + const { result } = renderHook(() => useIsInSSRContext()); + expect(result.current).toBe(false); + }); +}); diff --git a/packages/react-components/react-utilities/src/ssr/SSRContext.tsx b/packages/react-components/react-utilities/src/ssr/SSRContext.tsx index 7cbce13cce005..a96d91fce8d67 100644 --- a/packages/react-components/react-utilities/src/ssr/SSRContext.tsx +++ b/packages/react-components/react-utilities/src/ssr/SSRContext.tsx @@ -41,6 +41,13 @@ export const SSRProvider: React.FC<{ children: React.ReactNode }> = props => { return {props.children}; }; +/** + * @returns Whether the current component is wrapped by an SSRProvider. + */ +export function useIsInSSRContext(): boolean { + return useSSRContext() !== defaultSSRContextValue; +} + /** * Returns whether the component is currently being server side rendered or hydrated on the client. Can be used to delay * browser-specific rendering until after hydration. May cause re-renders on a client when is used within SSRProvider. From 008b7bb8d4ca0592a4a47737bc1f202f4913c481 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Tue, 21 Mar 2023 18:44:35 +0100 Subject: [PATCH 02/26] changefile --- ...eact-provider-51de7ed7-e932-4b2f-aa6b-c871f1b8c016.json | 7 +++++++ ...act-utilities-3fde228b-0da2-4f87-8814-85c1dc4b6515.json | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 change/@fluentui-react-provider-51de7ed7-e932-4b2f-aa6b-c871f1b8c016.json create mode 100644 change/@fluentui-react-utilities-3fde228b-0da2-4f87-8814-85c1dc4b6515.json 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..55b1ebb5819ae --- /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 new SSR style slot", + "packageName": "@fluentui/react-provider", + "email": "lingfangao@hotmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-utilities-3fde228b-0da2-4f87-8814-85c1dc4b6515.json b/change/@fluentui-react-utilities-3fde228b-0da2-4f87-8814-85c1dc4b6515.json new file mode 100644 index 0000000000000..fc26a4dd568e8 --- /dev/null +++ b/change/@fluentui-react-utilities-3fde228b-0da2-4f87-8814-85c1dc4b6515.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: implement useInSSRContext", + "packageName": "@fluentui/react-utilities", + "email": "lingfangao@hotmail.com", + "dependentChangeType": "patch" +} From 564366c3cb139415a46a13576b79323f094db935 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Tue, 21 Mar 2023 18:45:02 +0100 Subject: [PATCH 03/26] revert --- apps/ssr-tests-v9/src/build.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/ssr-tests-v9/src/build.ts b/apps/ssr-tests-v9/src/build.ts index ad7d352b48572..d160793a2045a 100644 --- a/apps/ssr-tests-v9/src/build.ts +++ b/apps/ssr-tests-v9/src/build.ts @@ -29,7 +29,7 @@ async function build() { // https://github.com/facebook/react/issues/13097 const skippedPaths = ['react-portal']; - let rawStoriesGlobs = getPackageStoriesGlob({ + const rawStoriesGlobs = getPackageStoriesGlob({ packageName: '@fluentui/react-components', callerPath: __dirname, }).filter( @@ -44,7 +44,6 @@ async function build() { ) as string[]; rawStoriesGlobs.push(path.resolve(path.join(__dirname, './stories/**/index.stories.tsx'))); - rawStoriesGlobs = rawStoriesGlobs.filter(x => x.includes('react-button')); const storiesGlobs = rawStoriesGlobs // TODO: Find a better way for this. Pass the path via params? 👇 .map(pattern => path.resolve(__dirname, pattern)); From 1f9ca81cc5cd3699e75c98dab05416bab3b435c9 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Tue, 21 Mar 2023 18:48:24 +0100 Subject: [PATCH 04/26] remove mock --- .../FluentProvider/FluentProvider-node.test.tsx | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) 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 index a47af85d69814..8808eb1f349bc 100644 --- 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 @@ -7,27 +7,22 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom/server'; import { SSRProvider } from '@fluentui/react-utilities'; -import { webLightTheme } from '@fluentui/react-theme'; import { FluentProvider } from './FluentProvider'; import * as prettier from 'prettier'; -jest.mock('@fluentui/react-theme', () => ({ - ...jest.requireActual('@fluentui/react-theme'), - webLightTheme: { - colorNeutralForeground1: 'black', - colorNeutralBackground1: 'white', - }, -})); - const parseHTMLString = (html: string) => { return prettier.format(html, { parser: 'html' }); }; describe('FluentProvider (node)', () => { + const testTheme = { + colorNeutralForeground1: 'black', + colorNeutralBackground1: 'white', + }; it('should render CSS variables as inline style when wrapped with SSRPRovider', () => { const html = ReactDOM.renderToStaticMarkup( - + , ); @@ -47,7 +42,7 @@ describe('FluentProvider (node)', () => { }); it('should not render CSS variables as inline style when not wrapped with SSRPRovider', () => { - const html = ReactDOM.renderToStaticMarkup(); + const html = ReactDOM.renderToStaticMarkup(); expect(parseHTMLString(html)).toMatchInlineSnapshot(` "
Date: Tue, 21 Mar 2023 18:54:31 +0100 Subject: [PATCH 05/26] add explainer --- .../components/FluentProvider/useFluentProviderThemeStyleTag.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react-components/react-provider/src/components/FluentProvider/useFluentProviderThemeStyleTag.ts b/packages/react-components/react-provider/src/components/FluentProvider/useFluentProviderThemeStyleTag.ts index 3abf3deaa37b8..a04c9e12b27d7 100644 --- a/packages/react-components/react-provider/src/components/FluentProvider/useFluentProviderThemeStyleTag.ts +++ b/packages/react-components/react-provider/src/components/FluentProvider/useFluentProviderThemeStyleTag.ts @@ -66,6 +66,8 @@ export const useFluentProviderThemeStyleTag = (options: Pick { styleTag.current = createStyleTag(targetDocument, { ...styleElementAttributes, id: styleTagId }); From 12696d3db46d3f95fb4f9aa336dc146b3ed0d294 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Tue, 21 Mar 2023 19:04:01 +0100 Subject: [PATCH 06/26] update md --- .../react-provider/etc/react-provider.api.md | 6 +++++- .../react-utilities/etc/react-utilities.api.md | 3 +++ .../react-components/react-utilities/src/ssr/SSRContext.tsx | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) 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..4c58e002e4e81 100644 --- a/packages/react-components/react-provider/etc/react-provider.api.md +++ b/packages/react-components/react-provider/etc/react-provider.api.md @@ -55,6 +55,7 @@ export type FluentProviderProps = Omit, 'dir // @public (undocumented) export type FluentProviderSlots = { root: Slot<'div'>; + serverStyle?: Slot<'style'>; }; // @public (undocumented) @@ -76,7 +77,10 @@ export function useFluentProviderContextValues_unstable(state: FluentProviderSta export const useFluentProviderStyles_unstable: (state: FluentProviderState) => FluentProviderState; // @public -export const useFluentProviderThemeStyleTag: (options: Pick) => string; +export const useFluentProviderThemeStyleTag: (options: Pick) => { + styleTagId: string; + rule: string; +}; // (No @packageDocumentation comment for this package) diff --git a/packages/react-components/react-utilities/etc/react-utilities.api.md b/packages/react-components/react-utilities/etc/react-utilities.api.md index b60ec22357e97..c8cd4d9737b73 100644 --- a/packages/react-components/react-utilities/etc/react-utilities.api.md +++ b/packages/react-components/react-utilities/etc/react-utilities.api.md @@ -197,6 +197,9 @@ export function useForceUpdate(): DispatchWithoutAction; // @public export function useId(prefix?: string, providedId?: string): string; +// @internal (undocumented) +export function useIsInSSRContext(): boolean; + // @public export const useIsomorphicLayoutEffect: typeof React_2.useEffect; diff --git a/packages/react-components/react-utilities/src/ssr/SSRContext.tsx b/packages/react-components/react-utilities/src/ssr/SSRContext.tsx index a96d91fce8d67..4291fbfc216f0 100644 --- a/packages/react-components/react-utilities/src/ssr/SSRContext.tsx +++ b/packages/react-components/react-utilities/src/ssr/SSRContext.tsx @@ -43,6 +43,7 @@ export const SSRProvider: React.FC<{ children: React.ReactNode }> = props => { /** * @returns Whether the current component is wrapped by an SSRProvider. + * @internal */ export function useIsInSSRContext(): boolean { return useSSRContext() !== defaultSSRContextValue; From cc23599cea653cdd3168856b79274f6af1bf2c40 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Tue, 21 Mar 2023 20:48:56 +0100 Subject: [PATCH 07/26] add nonce --- .../FluentProvider-node.test.tsx | 35 +++++++++++++++++++ .../FluentProvider/FluentProvider.test.tsx | 24 +++++++++++-- .../FluentProvider/useFluentProvider.ts | 3 ++ 3 files changed, 60 insertions(+), 2 deletions(-) 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 index 8808eb1f349bc..42281f06d8927 100644 --- 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 @@ -9,6 +9,8 @@ import * as ReactDOM from 'react-dom/server'; import { SSRProvider } from '@fluentui/react-utilities'; import { FluentProvider } from './FluentProvider'; import * as prettier from 'prettier'; +import { createDOMRenderer } from '@griffel/core'; +import { RendererProvider } from '@griffel/react'; const parseHTMLString = (html: string) => { return prettier.format(html, { parser: 'html' }); @@ -51,4 +53,37 @@ describe('FluentProvider (node)', () => { >
" `); }); + + 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 593da4200ad06..37cace8bdfbf7 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,11 +1,14 @@ import { resetIdsForTests, SSRProvider } 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'; +import { createDOMRenderer } from '@griffel/core'; +import { RendererProvider } from '@griffel/react'; +import { fluentProviderClassNames } from './useFluentProviderStyles'; describe('FluentProvider', () => { // eslint-disable-next-line @typescript-eslint/no-empty-function @@ -32,7 +35,7 @@ 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(); @@ -78,6 +81,23 @@ describe('FluentProvider', () => { `); }); + it('renders nonce with SSR style element', () => { + const nonce = 'random'; + const renderer = createDOMRenderer(document, { + styleElementAttributes: { nonce }, + }); + + const { container } = render( + + + + + , + ); + + expect(container.querySelector(`.${fluentProviderClassNames.serverStyle}`)?.getAttribute('nonce')).toEqual(nonce); + }); + describe('applies "dir" attribute', () => { it('ltr', () => { const { getByText } = render(Test); diff --git a/packages/react-components/react-provider/src/components/FluentProvider/useFluentProvider.ts b/packages/react-components/react-provider/src/components/FluentProvider/useFluentProvider.ts index 00a9f6e3a30af..a6e3a1f3f7575 100644 --- a/packages/react-components/react-provider/src/components/FluentProvider/useFluentProvider.ts +++ b/packages/react-components/react-provider/src/components/FluentProvider/useFluentProvider.ts @@ -14,6 +14,7 @@ import { getNativeElementProps, resolveShorthand, useIsInSSRContext, useMergedRe import * as React from 'react'; import { useFluentProviderThemeStyleTag } from './useFluentProviderThemeStyleTag'; import type { FluentProviderProps, FluentProviderState } from './FluentProvider.types'; +import { useRenderer_unstable } from '@griffel/react'; /** * Create the state required to render FluentProvider. @@ -69,6 +70,7 @@ export const useFluentProvider_unstable = ( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const renderer = useRenderer_unstable(); const { styleTagId, rule } = useFluentProviderThemeStyleTag({ theme: mergedTheme, targetDocument }); return { applyStylesToPortals, @@ -97,6 +99,7 @@ export const useFluentProvider_unstable = ( defaultProps: { id: styleTagId, dangerouslySetInnerHTML: { __html: rule }, + nonce: renderer.styleElementAttributes?.nonce, }, }), }; From 5a5d447aed65c08ad35f4a61506637794eb91254 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Wed, 22 Mar 2023 09:44:29 +0100 Subject: [PATCH 08/26] make slot private --- .../react-provider/etc/react-provider.api.md | 3 +-- .../FluentProvider/FluentProvider-node.test.tsx | 8 ++------ .../FluentProvider/FluentProvider.test.tsx | 4 +--- .../FluentProvider/FluentProvider.types.ts | 6 +++++- .../FluentProvider/renderFluentProvider.tsx | 9 +++++++-- .../components/FluentProvider/useFluentProvider.ts | 13 ++++++++++--- .../FluentProvider/useFluentProviderStyles.ts | 5 ----- 7 files changed, 26 insertions(+), 22 deletions(-) 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 4c58e002e4e81..ae3055209ba46 100644 --- a/packages/react-components/react-provider/etc/react-provider.api.md +++ b/packages/react-components/react-provider/etc/react-provider.api.md @@ -55,11 +55,10 @@ export type FluentProviderProps = Omit, 'dir // @public (undocumented) export type FluentProviderSlots = { root: Slot<'div'>; - serverStyle?: Slot<'style'>; }; // @public (undocumented) -export type FluentProviderState = ComponentState & Pick & Required> & { +export type FluentProviderState = ComponentState & Pick & Required> & { theme: ThemeContextValue_unstable; themeClassName: string; }; 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 index 42281f06d8927..2e71761e81378 100644 --- 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 @@ -33,7 +33,7 @@ describe('FluentProvider (node)', () => { dir="ltr" class="fui-FluentProvider fui-FluentProvider1 " > - +
" `); }); @@ -73,7 +80,7 @@ describe('FluentProvider (node)', () => { dir="ltr" class="fui-FluentProvider fui-FluentProvider1 " > - - foo - - - `); - }); - - it('does not render style element with css variables if not wrapped with a SSRProvider', () => { + it('does not render style element when not in SSR', () => { const { container } = render(); - expect(container).toMatchInlineSnapshot(` -
-
-
- `); - }); - - it('renders nonce with SSR style element', () => { - const nonce = 'random'; - const renderer = createDOMRenderer(document, { - styleElementAttributes: { nonce }, - }); - - const { container } = render( - - - - - , - ); - - expect(container.querySelector('style')?.getAttribute('nonce')).toEqual(nonce); + expect(container.querySelector('style')).toBeNull(); }); describe('applies "dir" attribute', () => { diff --git a/packages/react-components/react-provider/src/components/FluentProvider/useFluentProvider.ts b/packages/react-components/react-provider/src/components/FluentProvider/useFluentProvider.ts index 91916f70b7c45..f9f217fec09d4 100644 --- a/packages/react-components/react-provider/src/components/FluentProvider/useFluentProvider.ts +++ b/packages/react-components/react-provider/src/components/FluentProvider/useFluentProvider.ts @@ -10,17 +10,12 @@ import type { ThemeContextValue_unstable as ThemeContextValue, } from '@fluentui/react-shared-contexts'; -import { - getNativeElementProps, - resolveShorthand, - Slot, - useIsInSSRContext, - useMergedRefs, -} from '@fluentui/react-utilities'; +import { canUseDOM, getNativeElementProps, resolveShorthand, Slot, useMergedRefs } from '@fluentui/react-utilities'; import * as React from 'react'; import { useFluentProviderThemeStyleTag } from './useFluentProviderThemeStyleTag'; import type { FluentProviderProps, FluentProviderState } from './FluentProvider.types'; import { useRenderer_unstable } from '@griffel/react'; +import { FUI_THEME_STYLE_ATTR } from '../../constants'; /** * Create the state required to render FluentProvider. @@ -100,15 +95,18 @@ export const useFluentProvider_unstable = ( ref: useMergedRefs(ref, useFocusVisible({ targetDocument })), }), - // eslint-disable-next-line @typescript-eslint/naming-convention - serverStyle: resolveShorthand(undefined as undefined | Slot<'style'>, { - required: useIsInSSRContext(), - defaultProps: { - id: styleTagId, - dangerouslySetInnerHTML: { __html: rule }, - ...renderer.styleElementAttributes, + serverStyle: resolveShorthand & { 'data-fui-theme'?: '' }>( + undefined as undefined | Slot<'style'>, + { + required: !canUseDOM(), + defaultProps: { + [FUI_THEME_STYLE_ATTR]: '', + id: styleTagId, + dangerouslySetInnerHTML: { __html: rule }, + ...renderer.styleElementAttributes, + }, }, - }), + ), }; }; diff --git a/packages/react-components/react-provider/src/components/FluentProvider/useFluentProviderThemeStyleTag.test.tsx b/packages/react-components/react-provider/src/components/FluentProvider/useFluentProviderThemeStyleTag.test.tsx index fa15988a78d15..a65ca6182c383 100644 --- a/packages/react-components/react-provider/src/components/FluentProvider/useFluentProviderThemeStyleTag.test.tsx +++ b/packages/react-components/react-provider/src/components/FluentProvider/useFluentProviderThemeStyleTag.test.tsx @@ -5,8 +5,17 @@ import { renderHook } from '@testing-library/react-hooks'; import * as React from 'react'; import { useFluentProviderThemeStyleTag } from './useFluentProviderThemeStyleTag'; +import { FUI_THEME_STYLE_ATTR } from '../../constants'; jest.mock('@fluentui/react-theme'); +const createDocumentMock = (): Document => { + const externalDocument = document.implementation.createDocument('http://www.w3.org/1999/xhtml', 'html', null); + const body = document.createElement('body'); + const head = document.createElement('head'); + externalDocument.documentElement.appendChild(head); + externalDocument.documentElement.appendChild(body); + return externalDocument; +}; describe('useFluentProviderThemeStyleTag', () => { const defaultTheme = { @@ -87,4 +96,21 @@ describe('useFluentProviderThemeStyleTag', () => { expect(tag.getAttribute('id')).toBe('fui-FluentProvider1'); expect(tag.getAttribute('nonce')).toBe('random'); }); + + it('should move style tags in body to head on first render', () => { + const targetDocument = createDocumentMock(); + const ssrStyleElement = targetDocument.createElement('style'); + ssrStyleElement.setAttribute(FUI_THEME_STYLE_ATTR, ''); + // Kinda hacky - assume the useId call returns as expected (ids are reset after each test) + ssrStyleElement.setAttribute('id', 'fui-FluentProvider1'); + targetDocument.body.append(ssrStyleElement); + + jest.spyOn(targetDocument, 'createElement'); + renderHook(() => useFluentProviderThemeStyleTag({ theme: defaultTheme, targetDocument })); + + expect(targetDocument.body.querySelector('style')).toBeNull(); + expect(targetDocument.head.querySelectorAll('style').length).toBe(1); + // eslint-disable-next-line deprecation/deprecation + expect(targetDocument.createElement).toHaveBeenCalledTimes(0); + }); }); diff --git a/packages/react-components/react-provider/src/components/FluentProvider/useFluentProviderThemeStyleTag.ts b/packages/react-components/react-provider/src/components/FluentProvider/useFluentProviderThemeStyleTag.ts index a04c9e12b27d7..267b8e43a2724 100644 --- a/packages/react-components/react-provider/src/components/FluentProvider/useFluentProviderThemeStyleTag.ts +++ b/packages/react-components/react-provider/src/components/FluentProvider/useFluentProviderThemeStyleTag.ts @@ -1,6 +1,7 @@ -import { useId, useIsInSSRContext, useIsomorphicLayoutEffect } from '@fluentui/react-utilities'; +import { useId, useIsomorphicLayoutEffect } from '@fluentui/react-utilities'; import { useRenderer_unstable } from '@griffel/react'; import * as React from 'react'; +import { FUI_THEME_STYLE_ATTR } from '../../constants'; import type { FluentProviderState } from './FluentProvider.types'; import { fluentProviderClassNames } from './useFluentProviderStyles'; @@ -17,6 +18,7 @@ const createStyleTag = (target: Document | undefined, elementAttributes: Record< const tag = target.createElement('style'); + elementAttributes[FUI_THEME_STYLE_ATTR] = ''; Object.keys(elementAttributes).forEach(attrName => { tag.setAttribute(attrName, elementAttributes[attrName]); }); @@ -46,10 +48,10 @@ const insertSheet = (tag: HTMLStyleElement, rule: string) => { */ export const useFluentProviderThemeStyleTag = (options: Pick) => { const { targetDocument, theme } = options; - const isInSSRContext = useIsInSSRContext(); + useHandleSSRStyleElements(targetDocument); const renderer = useRenderer_unstable(); - const styleTag = React.useRef(); + const styleTag = React.useRef(); const styleTagId = useId(fluentProviderClassNames.root); const styleElementAttributes = renderer.styleElementAttributes; @@ -65,22 +67,41 @@ export const useFluentProviderThemeStyleTag = (options: Pick { - styleTag.current = createStyleTag(targetDocument, { ...styleElementAttributes, id: styleTagId }); - - if (styleTag.current) { - insertSheet(styleTag.current, rule); - - return () => { - styleTag.current?.remove(); - }; - } - }, [styleTagId, targetDocument, rule, styleElementAttributes]); - } + useInsertionEffect(() => { + // The style element could already have been created during SSR - no need to recreate it + const ssrStyleElement = targetDocument?.getElementById(styleTagId) as HTMLStyleElement; + if (ssrStyleElement) { + styleTag.current = ssrStyleElement; + return () => styleTag.current?.remove(); + } + + styleTag.current = createStyleTag(targetDocument, { ...styleElementAttributes, id: styleTagId }); + + if (styleTag.current) { + insertSheet(styleTag.current, rule); + + return () => { + styleTag.current?.remove(); + }; + } + }, [styleTagId, targetDocument, rule, styleElementAttributes]); return { styleTagId, rule }; }; + +function useHandleSSRStyleElements(targetDocument: Document | undefined | null) { + // Using a state factory so that this logic only runs once per render + // Each FluentProvider can create its own style element during SSR as a slot + // Moves all theme style elements to document head during render to avoid hydration errors. + // Should be strict mode safe since the logic is idempotent. + React.useState(() => { + if (!targetDocument) { + return; + } + + const themeStyleElements = targetDocument.body.querySelectorAll(`[${FUI_THEME_STYLE_ATTR}]`); + themeStyleElements.forEach(styleElement => { + targetDocument.head.append(styleElement); + }); + }); +} diff --git a/packages/react-components/react-provider/src/constants.ts b/packages/react-components/react-provider/src/constants.ts new file mode 100644 index 0000000000000..940b75082bcf0 --- /dev/null +++ b/packages/react-components/react-provider/src/constants.ts @@ -0,0 +1 @@ +export const FUI_THEME_STYLE_ATTR = 'data-fui-theme'; diff --git a/packages/react-components/react-utilities/etc/react-utilities.api.md b/packages/react-components/react-utilities/etc/react-utilities.api.md index c8cd4d9737b73..b60ec22357e97 100644 --- a/packages/react-components/react-utilities/etc/react-utilities.api.md +++ b/packages/react-components/react-utilities/etc/react-utilities.api.md @@ -197,9 +197,6 @@ export function useForceUpdate(): DispatchWithoutAction; // @public export function useId(prefix?: string, providedId?: string): string; -// @internal (undocumented) -export function useIsInSSRContext(): boolean; - // @public export const useIsomorphicLayoutEffect: typeof React_2.useEffect; diff --git a/packages/react-components/react-utilities/src/index.ts b/packages/react-components/react-utilities/src/index.ts index bf2a57deac75c..743ee85e64d6d 100644 --- a/packages/react-components/react-utilities/src/index.ts +++ b/packages/react-components/react-utilities/src/index.ts @@ -32,7 +32,7 @@ export { } from './hooks/index'; export type { RefObjectFunction, UseControllableStateOptions, UseOnClickOrScrollOutsideOptions } from './hooks/index'; -export { canUseDOM, useIsSSR, useIsInSSRContext, SSRProvider } from './ssr/index'; +export { canUseDOM, useIsSSR, SSRProvider } from './ssr/index'; export { clamp, diff --git a/packages/react-components/react-utilities/src/ssr/SSRContext-node.test.tsx b/packages/react-components/react-utilities/src/ssr/SSRContext-node.test.tsx index 636698c0a8e12..aa67c1bb3579b 100644 --- a/packages/react-components/react-utilities/src/ssr/SSRContext-node.test.tsx +++ b/packages/react-components/react-utilities/src/ssr/SSRContext-node.test.tsx @@ -5,7 +5,7 @@ // 👆 this is intentionally to test in SSR like environment import { renderHook } from '@testing-library/react-hooks'; -import { SSRProvider, useIsSSR, useIsInSSRContext } from './SSRContext'; +import { SSRProvider, useIsSSR } from './SSRContext'; describe('useIsSSR (node)', () => { afterEach(() => { @@ -27,15 +27,3 @@ describe('useIsSSR (node)', () => { expect(result.current).toBe(true); }); }); - -describe('useIsInSSRContext (node)', () => { - it('returns true if wrapped by an SSRProvider', () => { - const { result } = renderHook(() => useIsInSSRContext(), { wrapper: SSRProvider }); - expect(result.current).toBe(true); - }); - - it('returns false if not wrapped by an SSRProvider', () => { - const { result } = renderHook(() => useIsInSSRContext()); - expect(result.current).toBe(false); - }); -}); diff --git a/packages/react-components/react-utilities/src/ssr/SSRContext.test.tsx b/packages/react-components/react-utilities/src/ssr/SSRContext.test.tsx index 04f535a50f326..fcc7a445cadf9 100644 --- a/packages/react-components/react-utilities/src/ssr/SSRContext.test.tsx +++ b/packages/react-components/react-utilities/src/ssr/SSRContext.test.tsx @@ -1,5 +1,5 @@ import { renderHook } from '@testing-library/react-hooks'; -import { SSRProvider, useIsSSR, useIsInSSRContext } from './SSRContext'; +import { SSRProvider, useIsSSR } from './SSRContext'; describe('useIsSSR', () => { it('returns "false" outside of SSRProvider', () => { @@ -14,15 +14,3 @@ describe('useIsSSR', () => { expect(result.current).toBe(false); }); }); - -describe('useIsInSSRContext', () => { - it('returns true if wrapped by an SSRProvider', () => { - const { result } = renderHook(() => useIsInSSRContext(), { wrapper: SSRProvider }); - expect(result.current).toBe(true); - }); - - it('returns false if not wrapped by an SSRProvider', () => { - const { result } = renderHook(() => useIsInSSRContext()); - expect(result.current).toBe(false); - }); -}); diff --git a/packages/react-components/react-utilities/src/ssr/SSRContext.tsx b/packages/react-components/react-utilities/src/ssr/SSRContext.tsx index 4291fbfc216f0..7cbce13cce005 100644 --- a/packages/react-components/react-utilities/src/ssr/SSRContext.tsx +++ b/packages/react-components/react-utilities/src/ssr/SSRContext.tsx @@ -41,14 +41,6 @@ export const SSRProvider: React.FC<{ children: React.ReactNode }> = props => { return {props.children}; }; -/** - * @returns Whether the current component is wrapped by an SSRProvider. - * @internal - */ -export function useIsInSSRContext(): boolean { - return useSSRContext() !== defaultSSRContextValue; -} - /** * Returns whether the component is currently being server side rendered or hydrated on the client. Can be used to delay * browser-specific rendering until after hydration. May cause re-renders on a client when is used within SSRProvider. From 2bea36312db6c7dd3ec46f330892def561c2e36c Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Wed, 22 Mar 2023 13:45:02 +0100 Subject: [PATCH 10/26] remove changefile --- ...act-utilities-3fde228b-0da2-4f87-8814-85c1dc4b6515.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 change/@fluentui-react-utilities-3fde228b-0da2-4f87-8814-85c1dc4b6515.json diff --git a/change/@fluentui-react-utilities-3fde228b-0da2-4f87-8814-85c1dc4b6515.json b/change/@fluentui-react-utilities-3fde228b-0da2-4f87-8814-85c1dc4b6515.json deleted file mode 100644 index fc26a4dd568e8..0000000000000 --- a/change/@fluentui-react-utilities-3fde228b-0da2-4f87-8814-85c1dc4b6515.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "minor", - "comment": "feat: implement useInSSRContext", - "packageName": "@fluentui/react-utilities", - "email": "lingfangao@hotmail.com", - "dependentChangeType": "patch" -} From f795b0858f347f2f9df0b88cd76f8567c67474b7 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Wed, 22 Mar 2023 14:21:13 +0100 Subject: [PATCH 11/26] pr feedback --- .../react-provider/etc/react-provider.api.md | 8 +++++-- .../FluentProvider-node.test.tsx | 2 +- .../FluentProvider/FluentProvider.types.ts | 23 +++++++++++-------- .../FluentProvider/renderFluentProvider.tsx | 21 +++++++++-------- .../FluentProvider/useFluentProvider.ts | 21 +++++++---------- .../useFluentProviderThemeStyleTag.ts | 2 +- 6 files changed, 42 insertions(+), 35 deletions(-) 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 ae3055209ba46..b3d9e7cc5eba5 100644 --- a/packages/react-components/react-provider/etc/react-provider.api.md +++ b/packages/react-components/react-provider/etc/react-provider.api.md @@ -58,9 +58,13 @@ export type FluentProviderSlots = { }; // @public (undocumented) -export type FluentProviderState = ComponentState & Pick & Required> & { +export type FluentProviderState = ComponentState & Pick & Required> & { theme: ThemeContextValue_unstable; themeClassName: string; + serverStyleProps: { + cssRule: string; + rendererAttributes: Record; + }; }; // @public @@ -75,7 +79,7 @@ export function useFluentProviderContextValues_unstable(state: FluentProviderSta // @public export const useFluentProviderStyles_unstable: (state: FluentProviderState) => FluentProviderState; -// @public +// @internal export const useFluentProviderThemeStyleTag: (options: Pick) => { styleTagId: string; rule: string; 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 index d90ad655765aa..4dbafa7c333ae 100644 --- 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 @@ -80,7 +80,7 @@ describe('FluentProvider (node)', () => { dir="ltr" class="fui-FluentProvider fui-FluentProvider1 " > - -
" - `); + afterEach(() => { + resetIdsForTests(); }); - it('should not render CSS variables as inline style when not wrapped with SSRPRovider', () => { + it('should render CSS variables as inline style', () => { const html = ReactDOM.renderToStaticMarkup(); expect(parseHTMLString(html)).toMatchInlineSnapshot(` From 8ee281aeab66648d3ff7908f9dec5073025d2e58 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Thu, 23 Mar 2023 12:16:51 +0100 Subject: [PATCH 20/26] remove cruft --- .../src/components/FluentProvider/FluentProvider.test.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 ac6144b2474a6..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,4 +1,4 @@ -import { resetIdsForTests, SSRProvider } from '@fluentui/react-utilities'; +import { resetIdsForTests } from '@fluentui/react-utilities'; import { render } from '@testing-library/react'; import * as React from 'react'; import * as reactTestRenderer from 'react-test-renderer'; @@ -18,9 +18,6 @@ describe('FluentProvider', () => { isConformant({ disabledTests: ['component-handles-classname'], Component: FluentProvider, - renderOptions: { - wrapper: SSRProvider, - }, displayName: 'FluentProvider', }); From cc056f7ec4c0fcd3c535fc6cf1f431e5e60ca4b4 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Thu, 23 Mar 2023 12:17:32 +0100 Subject: [PATCH 21/26] use ternary render --- .../src/components/FluentProvider/renderFluentProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 dd61f081d1592..9276aab3c6f29 100644 --- a/packages/react-components/react-provider/src/components/FluentProvider/renderFluentProvider.tsx +++ b/packages/react-components/react-provider/src/components/FluentProvider/renderFluentProvider.tsx @@ -36,7 +36,7 @@ export const renderFluentProvider_unstable = ( - {!canUseDOM() && ( + {canUseDOM() ? null : (