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

feat(react-provider): add applyStylesTo prop for theme token styling control #33632

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
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 @@
{
dmytrokirpa marked this conversation as resolved.
Show resolved Hide resolved
"type": "patch",
"comment": "feat: allow to apply FluentProvider's tokens to body element",
"packageName": "@fluentui/react-provider",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,50 @@ describe('FluentProvider', () => {
expect(element).toHaveStyle({ textAlign: 'right' });
});
});

describe('applies "applyStylesTo" attribute', () => {
const styleTagId = `${fluentProviderClassNames.root}1`;

beforeEach(() => {
document.body.className = '';
});

it('theme tokens are scoped to provider element by default', () => {
const { getByTestId } = render(<FluentProvider data-testid="provider">Test</FluentProvider>);

// ThemeProvider has tokens classname applied
expect(getByTestId('provider')).toHaveClass(styleTagId);

// It should not be applied to the body
expect(document.body).not.toHaveClass(styleTagId);
});

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

// ThemeProvider has tokens classname applied
expect(getByTestId('provider')).toHaveClass(styleTagId);

// It should not be applied to the body
expect(document.body).not.toHaveClass(styleTagId);
});

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

// ThemeProvider doesn't have them tokens classname applied
expect(getByTestId('provider')).not.toHaveClass(styleTagId);

// It should be applied to the body
expect(document.body).toHaveClass(styleTagId);
});
});
});
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 @@ -24,9 +25,21 @@ export const useFluentProviderStyles_unstable = (state: FluentProviderState) =>
const renderer = useRenderer_unstable();
const styles = useStyles({ dir: state.dir, renderer });

// Apply theme class name to body element when `applyStylesTo` is 'body'.
React.useEffect(() => {
if (state.applyStylesTo !== 'body') {
return;
}

const classes = state.themeClassName.split(' ');
state.targetDocument?.body.classList.add(...classes);

return () => state.targetDocument?.body.classList.remove(...classes);
}, []);

state.root.className = mergeClasses(
fluentProviderClassNames.root,
state.themeClassName,
state.applyStylesTo === 'provider' && state.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 usGlobalStyles = makeStaticStyles({
body: {
backgroundColor: tokens.colorNeutralBackground2,
color: tokens.colorNeutralForeground1,
fontSize: tokens.fontSizeBase500,
},
h1: {
fontSize: tokens.fontSizeBase500,
},
});

const usLocalStyles = 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 = usLocalStyles();

usGlobalStyles();

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