diff --git a/change/@fluentui-react-provider-5a31a189-02e4-434d-969d-974f758f5b17.json b/change/@fluentui-react-provider-5a31a189-02e4-434d-969d-974f758f5b17.json new file mode 100644 index 0000000000000..bff0eb0936cc5 --- /dev/null +++ b/change/@fluentui-react-provider-5a31a189-02e4-434d-969d-974f758f5b17.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "feat: allow to apply FluentProvider's tokens to body element", + "packageName": "@fluentui/react-provider", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-provider/library/etc/react-provider.api.md b/packages/react-components/react-provider/library/etc/react-provider.api.md index e7e94b84cad4f..2e0c52433a131 100644 --- a/packages/react-components/react-provider/library/etc/react-provider.api.md +++ b/packages/react-components/react-provider/library/etc/react-provider.api.md @@ -24,6 +24,7 @@ export function createCSSRuleFromTheme(selector: string, theme: PartialTheme | u // @public (undocumented) export const FluentProvider: React_2.ForwardRefExoticComponent, "dir"> & { + applyStylesTo?: "body" | "provider" | undefined; applyStylesToPortals?: boolean | undefined; customStyleHooks_unstable?: Partial<{ useAccordionHeaderStyles_unstable: (state: unknown) => void; @@ -195,6 +196,7 @@ export type FluentProviderCustomStyleHooks = CustomStyleHooksContextValue_unstab // @public (undocumented) export type FluentProviderProps = Omit, 'dir'> & { + applyStylesTo?: 'body' | 'provider'; applyStylesToPortals?: boolean; customStyleHooks_unstable?: FluentProviderCustomStyleHooks; dir?: 'ltr' | 'rtl'; @@ -209,7 +211,7 @@ export type FluentProviderSlots = { }; // @public (undocumented) -export type FluentProviderState = ComponentState & Pick & Required> & { +export type FluentProviderState = ComponentState & Pick & Required> & { theme: ThemeContextValue_unstable; themeClassName: string; serverStyleProps: { diff --git a/packages/react-components/react-provider/library/src/components/FluentProvider/FluentProvider.test.tsx b/packages/react-components/react-provider/library/src/components/FluentProvider/FluentProvider.test.tsx index e062340a26a2e..ae0424fd2b57f 100644 --- a/packages/react-components/react-provider/library/src/components/FluentProvider/FluentProvider.test.tsx +++ b/packages/react-components/react-provider/library/src/components/FluentProvider/FluentProvider.test.tsx @@ -83,4 +83,41 @@ describe('FluentProvider', () => { expect(element).toHaveStyle({ textAlign: 'right' }); }); }); + + describe('applies "applyStylesTo" attribute', () => { + const themeClassName = `${fluentProviderClassNames.root}1`; + + beforeEach(() => { + document.body.className = ''; + }); + + it('applies theme tokens to provider element by default', () => { + const { getByTestId } = render(Test); + + expect(getByTestId('provider')).toHaveClass(themeClassName); + expect(document.body).not.toHaveClass(themeClassName); + }); + + it('applies theme tokens to provider element when applyStylesTo="provider"', () => { + const { getByTestId } = render( + + Test + , + ); + + expect(getByTestId('provider')).toHaveClass(themeClassName); + expect(document.body).not.toHaveClass(themeClassName); + }); + + it('applies theme tokens to the body when applyStylesTo="body"', () => { + const { getByTestId } = render( + + Test + , + ); + + expect(getByTestId('provider')).not.toHaveClass(themeClassName); + expect(document.body).toHaveClass(themeClassName); + }); + }); }); diff --git a/packages/react-components/react-provider/library/src/components/FluentProvider/FluentProvider.types.ts b/packages/react-components/react-provider/library/src/components/FluentProvider/FluentProvider.types.ts index 4820905d6c014..3d4f8336f394e 100644 --- a/packages/react-components/react-provider/library/src/components/FluentProvider/FluentProvider.types.ts +++ b/packages/react-components/react-provider/library/src/components/FluentProvider/FluentProvider.types.ts @@ -24,6 +24,15 @@ export type FluentProviderSlots = { export type FluentProviderCustomStyleHooks = CustomStyleHooksContextValue; export type FluentProviderProps = Omit, 'dir'> & { + /** + * Determines the element to which theme token styles are applied. + * - 'body' applies styles to the document body element, which can be useful for global styles. + * - 'provider' applies styles to the FluentProvider element (default). + * + * @default 'provider' + */ + applyStylesTo?: 'body' | 'provider'; + /** * Passes styles applied to a component down to portals if enabled. * @default true @@ -50,7 +59,10 @@ export type FluentProviderProps = Omit, 'dir export type FluentProviderState = ComponentState & Pick & Required< - Pick + Pick< + FluentProviderProps, + 'applyStylesTo' | 'applyStylesToPortals' | 'customStyleHooks_unstable' | 'dir' | 'overrides_unstable' + > > & { theme: ThemeContextValue; themeClassName: string; diff --git a/packages/react-components/react-provider/library/src/components/FluentProvider/useFluentProvider.ts b/packages/react-components/react-provider/library/src/components/FluentProvider/useFluentProvider.ts index b13cc39defd1b..7e47bf502c270 100644 --- a/packages/react-components/react-provider/library/src/components/FluentProvider/useFluentProvider.ts +++ b/packages/react-components/react-provider/library/src/components/FluentProvider/useFluentProvider.ts @@ -47,6 +47,7 @@ export const useFluentProvider_unstable = ( * see https://github.com/microsoft/fluentui/blob/0dc74a19f3aa5a058224c20505016fbdb84db172/packages/fluentui/react-northstar/src/utils/mergeProviderContexts.ts#L89-L93 */ const { + applyStylesTo = 'provider', applyStylesToPortals = true, // eslint-disable-next-line @typescript-eslint/naming-convention customStyleHooks_unstable, @@ -88,6 +89,7 @@ export const useFluentProvider_unstable = ( } return { + applyStylesTo, applyStylesToPortals, // eslint-disable-next-line @typescript-eslint/naming-convention customStyleHooks_unstable: mergedCustomStyleHooks, diff --git a/packages/react-components/react-provider/library/src/components/FluentProvider/useFluentProviderStyles.styles.ts b/packages/react-components/react-provider/library/src/components/FluentProvider/useFluentProviderStyles.styles.ts index 28edd49510cb9..e9778db21c063 100644 --- a/packages/react-components/react-provider/library/src/components/FluentProvider/useFluentProviderStyles.styles.ts +++ b/packages/react-components/react-provider/library/src/components/FluentProvider/useFluentProviderStyles.styles.ts @@ -1,3 +1,4 @@ +import * as React from 'react'; import { makeStyles, mergeClasses } from '@griffel/core'; import { useRenderer_unstable } from '@griffel/react'; import { tokens, typographyStyles } from '@fluentui/react-theme'; @@ -21,12 +22,26 @@ const useStyles = makeStyles({ export const useFluentProviderStyles_unstable = (state: FluentProviderState) => { 'use no memo'; + const { applyStylesTo, dir, targetDocument, themeClassName } = state; + const renderer = useRenderer_unstable(); - const styles = useStyles({ dir: state.dir, renderer }); + const styles = useStyles({ dir, renderer }); + + // Apply theme class name to body element when `applyStylesTo` is 'body'. + React.useEffect(() => { + if (applyStylesTo !== 'body') { + return; + } + + const classes = themeClassName.split(' '); + targetDocument?.body.classList.add(...classes); + + return () => targetDocument?.body.classList.remove(...classes); + }, [applyStylesTo, themeClassName, targetDocument]); state.root.className = mergeClasses( fluentProviderClassNames.root, - state.themeClassName, + applyStylesTo === 'provider' && themeClassName, styles.root, state.root.className, ); diff --git a/packages/react-components/react-provider/stories/src/Provider/FluentProviderApplyStylesToBody.stories.tsx b/packages/react-components/react-provider/stories/src/Provider/FluentProviderApplyStylesToBody.stories.tsx new file mode 100644 index 0000000000000..5ff3b73b6d47e --- /dev/null +++ b/packages/react-components/react-provider/stories/src/Provider/FluentProviderApplyStylesToBody.stories.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { tokens, FluentProvider, webDarkTheme, makeStyles, makeStaticStyles } from '@fluentui/react-components'; + +const useGlobalStyles = makeStaticStyles({ + body: { + backgroundColor: tokens.colorNeutralBackground2, + color: tokens.colorNeutralForeground1, + fontSize: tokens.fontSizeBase500, + }, + h1: { + fontSize: tokens.fontSizeBase500, + }, +}); + +const useLocalStyles = makeStyles({ + provider: { + backgroundColor: 'none', + }, +}); + +/** + * The `applyStylesTo` controls whether theme tokens should be applied to FluentProvider or document body, + * which can be useful for global styles. + */ +export const ApplyStylesToBody = () => { + const styles = useLocalStyles(); + + useGlobalStyles(); + + return ( + +

Document body and this element styled with global styles and theme tokens

+
+ ); +}; diff --git a/packages/react-components/react-provider/stories/src/Provider/index.stories.tsx b/packages/react-components/react-provider/stories/src/Provider/index.stories.tsx index 1c399b7073cbc..d97683c5cb242 100644 --- a/packages/react-components/react-provider/stories/src/Provider/index.stories.tsx +++ b/packages/react-components/react-provider/stories/src/Provider/index.stories.tsx @@ -1,4 +1,4 @@ -import { Meta } from '@storybook/react'; +import type { Meta } from '@storybook/react'; import { FluentProvider } from '@fluentui/react-components'; import descriptionMd from './FluentProviderDescription.md'; @@ -6,6 +6,7 @@ import bestPracticesMd from './FluentProviderBestPractices.md'; export { Default } from './FluentProviderDefault.stories'; export { Dir } from './FluentProviderDir.stories'; +export { ApplyStylesToBody } from './FluentProviderApplyStylesToBody.stories'; export { ApplyStylesToPortals } from './FluentProviderApplyStylesToPortals.stories'; export { Nested } from './FluentProviderNested.stories'; export { Frame } from './FluentProviderFrame.stories'; @@ -19,5 +20,8 @@ export default { component: [descriptionMd, bestPracticesMd].join('\n'), }, }, + reactStorybookAddon: { + disabledDecorators: ['FluentProvider'], + }, }, } as Meta;