Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function createCSSRuleFromTheme(selector: string, theme: PartialTheme | u

// @public (undocumented)
export const FluentProvider: React_2.ForwardRefExoticComponent<Omit<ComponentProps<FluentProviderSlots>, "dir"> & {
applyStylesTo?: "body" | "provider" | undefined;
applyStylesToPortals?: boolean | undefined;
customStyleHooks_unstable?: Partial<{
useAccordionHeaderStyles_unstable: (state: unknown) => void;
Expand Down Expand Up @@ -195,6 +196,7 @@ export type FluentProviderCustomStyleHooks = CustomStyleHooksContextValue_unstab

// @public (undocumented)
export type FluentProviderProps = Omit<ComponentProps<FluentProviderSlots>, 'dir'> & {
applyStylesTo?: 'body' | 'provider';
applyStylesToPortals?: boolean;
customStyleHooks_unstable?: FluentProviderCustomStyleHooks;
dir?: 'ltr' | 'rtl';
Expand All @@ -209,7 +211,7 @@ export type FluentProviderSlots = {
};

// @public (undocumented)
export type FluentProviderState = ComponentState<FluentProviderSlots> & Pick<FluentProviderProps, 'targetDocument'> & Required<Pick<FluentProviderProps, 'applyStylesToPortals' | 'customStyleHooks_unstable' | 'dir' | 'overrides_unstable'>> & {
export type FluentProviderState = ComponentState<FluentProviderSlots> & Pick<FluentProviderProps, 'targetDocument'> & Required<Pick<FluentProviderProps, 'applyStylesTo' | 'applyStylesToPortals' | 'customStyleHooks_unstable' | 'dir' | 'overrides_unstable'>> & {
theme: ThemeContextValue_unstable;
themeClassName: string;
serverStyleProps: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<FluentProvider data-testid="provider">Test</FluentProvider>);

expect(getByTestId('provider')).toHaveClass(themeClassName);
expect(document.body).not.toHaveClass(themeClassName);
});

it('applies theme tokens to provider element when applyStylesTo="provider"', () => {
const { getByTestId } = render(
<FluentProvider applyStylesTo="provider" data-testid="provider">
Test
</FluentProvider>,
);

expect(getByTestId('provider')).toHaveClass(themeClassName);
expect(document.body).not.toHaveClass(themeClassName);
});

it('applies theme tokens to the body when applyStylesTo="body"', () => {
const { getByTestId } = render(
<FluentProvider applyStylesTo="body" data-testid="provider">
Test
</FluentProvider>,
);

expect(getByTestId('provider')).not.toHaveClass(themeClassName);
expect(document.body).toHaveClass(themeClassName);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ export type FluentProviderSlots = {
export type FluentProviderCustomStyleHooks = CustomStyleHooksContextValue;

export type FluentProviderProps = Omit<ComponentProps<FluentProviderSlots>, '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
Expand All @@ -50,7 +59,10 @@ export type FluentProviderProps = Omit<ComponentProps<FluentProviderSlots>, 'dir
export type FluentProviderState = ComponentState<FluentProviderSlots> &
Pick<FluentProviderProps, 'targetDocument'> &
Required<
Pick<FluentProviderProps, 'applyStylesToPortals' | 'customStyleHooks_unstable' | 'dir' | 'overrides_unstable'>
Pick<
FluentProviderProps,
'applyStylesTo' | 'applyStylesToPortals' | 'customStyleHooks_unstable' | 'dir' | 'overrides_unstable'
>
> & {
theme: ThemeContextValue;
themeClassName: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -88,6 +89,7 @@ export const useFluentProvider_unstable = (
}

return {
applyStylesTo,
applyStylesToPortals,
// eslint-disable-next-line @typescript-eslint/naming-convention
customStyleHooks_unstable: mergedCustomStyleHooks,
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<FluentProvider applyStylesTo="body" className={styles.provider} theme={webDarkTheme}>
<h1>Document body and this element styled with global styles and theme tokens</h1>
</FluentProvider>
);
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Meta } from '@storybook/react';
import type { Meta } from '@storybook/react';

import { FluentProvider } from '@fluentui/react-components';
import descriptionMd from './FluentProviderDescription.md';
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';
Expand All @@ -19,5 +20,8 @@ export default {
component: [descriptionMd, bestPracticesMd].join('\n'),
},
},
reactStorybookAddon: {
disabledDecorators: ['FluentProvider'],
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Disabled the FluentProvider decorator from @fluentui/react-storybook-addon, as it is unnecessary for stories that already include FluentProvider.

},
},
} as Meta;
Loading