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

ThemeProvider: support applyTo prop #14696

Merged
merged 10 commits into from
Sep 17, 2020
Merged
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,8 @@
{
"type": "minor",
"comment": "Support applyTo prop and align styles with Fabric component.",
"packageName": "@fluentui/react-theme-provider",
"email": "[email protected]",
"dependentChangeType": "patch",
"date": "2020-09-16T02:50:09.294Z"
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,9 @@ export const ThemeProvider: React.ForwardRefExoticComponent<Pick<ThemeProviderPr

// @public
export interface ThemeProviderProps extends ComponentProps, React.HTMLAttributes<HTMLDivElement> {
applyTo?: 'element' | 'body' | 'none';
ref?: React.Ref<HTMLElement>;
renderer?: StyleRenderer;
targetWindow?: Window | null;
theme?: PartialTheme | Theme;
}

Expand Down Expand Up @@ -140,7 +140,7 @@ export const useThemeProvider: (props: ThemeProviderProps, ref: React.Ref<HTMLEl
};

// @public (undocumented)
export const useThemeProviderClasses: (state: {}, theme?: import("@fluentui/theme").Theme | undefined, renderer?: import(".").StyleRenderer | undefined) => void;
export function useThemeProviderClasses(state: ThemeProviderState): void;

// @public (undocumented)
export const useThemeProviderState: (draftState: ThemeProviderState) => void;
Expand Down
20 changes: 12 additions & 8 deletions packages/react-theme-provider/src/ThemeProvider.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,23 @@ export default {

const lightTheme: PartialTheme = {
tokens: {
body: {
background: 'white',
contentColor: 'black',
fontFamily: 'Segoe UI',
color: {
body: {
background: 'white',
contentColor: 'black',
fontFamily: 'Segoe UI',
},
},
},
};

const darkTheme: PartialTheme = {
tokens: {
body: {
background: 'black',
contentColor: 'white',
color: {
body: {
background: 'black',
contentColor: 'white',
},
},
},
};
Expand All @@ -40,7 +44,7 @@ export const NestedTheming = () => {
const [isLight, setIsLight] = React.useState(true);

return (
<ThemeProvider theme={isLight ? lightTheme : darkTheme}>
<ThemeProvider className="root" applyTo="body" theme={isLight ? lightTheme : darkTheme}>
<button onClick={() => setIsLight(l => !l)}>Toggle theme</button>
<div>I am {isLight ? 'light theme' : 'dark theme'}</div>
<ThemeProvider theme={isLight ? darkTheme : lightTheme}>
Expand Down
47 changes: 47 additions & 0 deletions packages/react-theme-provider/src/ThemeProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useTheme } from './useTheme';
import { mount } from 'enzyme';
import { mergeThemes } from '@fluentui/theme';
import { createDefaultTheme } from './createDefaultTheme';
import { Stylesheet } from '@uifabric/merge-styles';

const lightTheme = mergeThemes({
stylesheets: [],
Expand All @@ -27,6 +28,12 @@ const darkTheme = mergeThemes({
});

describe('ThemeProvider', () => {
const stylesheet: Stylesheet = Stylesheet.getInstance();

beforeEach(() => {
stylesheet.reset();
});

it('renders a div', () => {
const component = renderer.create(<ThemeProvider>Hello</ThemeProvider>);
const tree = component.toJSON();
Expand Down Expand Up @@ -82,4 +89,44 @@ describe('ThemeProvider', () => {
const expectedTheme = mergeThemes(createDefaultTheme(), lightTheme);
expect(resolvedTheme).toEqual(expectedTheme);
});

it('can apply body theme to none', () => {
expect(document.body.className).toBe('');
const component = renderer.create(
<ThemeProvider className="foo" theme={darkTheme} applyTo="none">
app
</ThemeProvider>,
);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();

expect(document.body.className).toBe('');
});

it('can apply body theme to body', () => {
expect(document.body.className).toBe('');
const testClass = 'foo';
const TestComponent = (
<ThemeProvider className={testClass} theme={darkTheme} applyTo="body">
app
</ThemeProvider>
);

const wrapper = mount(TestComponent);
expect(document.body.className).not.toBe('');

const bodyStyles = document.body.className
.split(' ')
.map(bodyClass => stylesheet.insertedRulesFromClassName(bodyClass));

expect(bodyStyles).toMatchSnapshot();

wrapper.unmount();

expect(document.body.className).toBe('');

const component = renderer.create(TestComponent);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});
6 changes: 5 additions & 1 deletion packages/react-theme-provider/src/ThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useThemeProviderClasses } from './useThemeProviderClasses';
import { useThemeProvider } from './useThemeProvider';
import { mergeStylesRenderer } from './styleRenderers/mergeStylesRenderer';
import { useStylesheet } from '@fluentui/react-stylesheets';
import { useFocusRects } from '@uifabric/utilities';

/**
* ThemeProvider, used for providing css variables and registering stylesheets.
Expand All @@ -14,13 +15,16 @@ export const ThemeProvider = React.forwardRef<HTMLDivElement, ThemeProviderProps
// The renderer default value is required to be defined, so if you're recomposing
// this component, be sure to do so.
renderer: mergeStylesRenderer,
applyTo: 'element',
});

// Register stylesheets as needed.
useStylesheet(state.theme.stylesheets);

// Render styles.
useThemeProviderClasses(state, state.theme, state.renderer);
useThemeProviderClasses(state);

useFocusRects(state.ref);

// Return the rendered content.
return render(state);
Expand Down
16 changes: 10 additions & 6 deletions packages/react-theme-provider/src/ThemeProvider.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,23 @@ export interface ThemeProviderProps extends ComponentProps, React.HTMLAttributes
*/
theme?: PartialTheme | Theme;

/**
* Defines the target window to render into. Defaults to the global window. Providing `null`
* will opt out of style rendering, which is used for SSR.
*/
targetWindow?: Window | null;

/**
* Optional interface for registering dynamic styles. Defaults to using `merge-styles`. Use this
* to opt into a particular rendering implementation, such as `emotion`, `styled-components`, or `jss`.
* Note: performance will differ between all renders. Please measure your scenarios before using an alternative
* implementation.
*/
renderer?: StyleRenderer;

/**
* Defines where body-related theme is applied to.
* Setting to 'element' will apply body styles to the root element of ThemeProvider.
* Setting to 'body' will apply body styles to document body.
* Setting to 'none' will not apply body styles to either element or body.
*
* @default 'element';
*/
applyTo?: 'element' | 'body' | 'none';
}

/**
Expand Down

Large diffs are not rendered by default.

17 changes: 12 additions & 5 deletions packages/react-theme-provider/src/createDefaultTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,19 @@ export const createDefaultTheme = (): Theme => {
return defaultTheme;
};

// TODO: use default fonts from `theme` package.
const defaultFonts = {
// eslint-disable-next-line @fluentui/max-len
fontFamily: `'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif`,
fontSize: '14px',
fontWeight: 400,
mozOsxFontSmoothing: 'grayscale',
webkitFontSmoothing: 'antialiased',
};

export const defaultTokens: Tokens = {
color: {
body: { background: '#ffffff', contentColor: '#323130' },
body: { background: '#ffffff', contentColor: '#323130', ...defaultFonts },

brand: {
background: '#0078d4',
Expand Down Expand Up @@ -129,6 +139,7 @@ export const defaultTokens: Tokens = {
larger: '48px',
largest: '64px',
},
...defaultFonts,
paddingLeft: '20px',
paddingRight: '20px',
paddingTop: '0',
Expand All @@ -139,10 +150,6 @@ export const defaultTokens: Tokens = {
iconSize: '16px',
borderRadius: '2px',
borderWidth: '1px',
// eslint-disable-next-line @fluentui/max-len
fontFamily: `'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif`,
fontSize: '14px',
fontWeight: 400,
focusColor: '#605e5c',
focusInnerColor: '#ffffff',
focusWidth: '2px',
Expand Down
5 changes: 5 additions & 0 deletions packages/react-theme-provider/src/getTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export function getTokens(theme: Theme): Tokens {
body: {
background: semanticColors?.bodyBackground,
contentColor: semanticColors?.bodyText,
fontFamily: fonts?.medium.fontFamily,
fontWeight: fonts?.medium.fontWeight,
fontSize: fonts?.medium.fontSize,
mozOsxFontSmoothing: fonts?.medium.MozOsxFontSmoothing,
webkitFontSmoothing: fonts?.medium.WebkitFontSmoothing,
},

// accent is currently only mapped for primary button to use.
Expand Down
60 changes: 55 additions & 5 deletions packages/react-theme-provider/src/useThemeProviderClasses.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,69 @@
import { makeClasses } from './makeClasses';
import * as React from 'react';
import { css } from '@uifabric/utilities';
import { useDocument } from '@fluentui/react-window-provider';
import { IRawStyle } from '@uifabric/styling';
import { makeStyles } from './makeStyles';
import { ThemeProviderState } from './ThemeProvider.types';
import { tokensToStyleObject } from './tokensToStyleObject';

export const useThemeProviderClasses = makeClasses(theme => {
const useThemeProviderStyles = makeStyles(theme => {
const { tokens } = theme;
const tokenStyles = tokensToStyleObject(tokens) as IRawStyle;

return {
root: [
// eslint-disable-next-line @typescript-eslint/no-explicit-any
tokensToStyleObject(tokens) as any,
root: tokenStyles,
body: [
{
color: 'var(--color-body-contentColor)',
background: 'var(--color-body-background)',
fontFamily: 'var(--body-fontFamily)',
fontWeight: 'var(--body-fontWeight)',
fontSize: 'var(--body-fontSize)',
MozOsxFontSmoothing: 'var(--body-mozOsxFontSmoothing)',
WebkitFontSmoothing: 'var(--body-webkitFontSmoothing)',
},
],
};
});

/**
* Hook to add class to body element.
*/
function useApplyClassToBody(state: ThemeProviderState, classesToApply: string[]): void {
const { applyTo } = state;

const applyToBody = applyTo === 'body';
const body = useDocument()?.body;

React.useEffect(() => {
if (!applyToBody || !body) {
return;
}

for (const classToApply of classesToApply) {
if (classToApply) {
body.classList.add(classToApply);
}
}

return () => {
if (!applyToBody || !body) {
return;
}

for (const classToApply of classesToApply) {
if (classToApply) {
body.classList.remove(classToApply);
}
}
};
}, [applyToBody, body, classesToApply]);
}

export function useThemeProviderClasses(state: ThemeProviderState): void {
const classes = useThemeProviderStyles(state.theme, state.renderer);
useApplyClassToBody(state, [classes.root, classes.body]);

const { className, applyTo } = state;
state.className = css(className, classes.root, applyTo === 'element' && classes.body);
}