Skip to content

Commit

Permalink
ThemeProvider: support applyTo prop (#14696)
Browse files Browse the repository at this point in the history
* remove un-used prop

* impl applyTo

* Change files

* fixes

* merge

* resolve comment

* update api

* call useFocusRects

* resolve comment

* udpate api
  • Loading branch information
xugao authored Sep 17, 2020
1 parent 7ea84b6 commit 730c615
Show file tree
Hide file tree
Showing 10 changed files with 989 additions and 27 deletions.
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"
}
4 changes: 2 additions & 2 deletions packages/react-theme-provider/etc/react-theme-provider.api.md
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);
}

0 comments on commit 730c615

Please sign in to comment.