diff --git a/change/@fluentui-react-theme-provider-2020-09-15-19-50-09-xgao-applyThemeToBody.json b/change/@fluentui-react-theme-provider-2020-09-15-19-50-09-xgao-applyThemeToBody.json new file mode 100644 index 0000000000000..784f45fbad758 --- /dev/null +++ b/change/@fluentui-react-theme-provider-2020-09-15-19-50-09-xgao-applyThemeToBody.json @@ -0,0 +1,8 @@ +{ + "type": "minor", + "comment": "Support applyTo prop and align styles with Fabric component.", + "packageName": "@fluentui/react-theme-provider", + "email": "xgao@microsoft.com", + "dependentChangeType": "patch", + "date": "2020-09-16T02:50:09.294Z" +} diff --git a/packages/react-theme-provider/etc/react-theme-provider.api.md b/packages/react-theme-provider/etc/react-theme-provider.api.md index 729c8a7784b6d..d2ea9c664dfab 100644 --- a/packages/react-theme-provider/etc/react-theme-provider.api.md +++ b/packages/react-theme-provider/etc/react-theme-provider.api.md @@ -107,9 +107,9 @@ export const ThemeProvider: React.ForwardRefExoticComponent { + applyTo?: 'element' | 'body' | 'none'; ref?: React.Ref; renderer?: StyleRenderer; - targetWindow?: Window | null; theme?: PartialTheme | Theme; } @@ -140,7 +140,7 @@ export const useThemeProvider: (props: ThemeProviderProps, ref: React.Ref void; +export function useThemeProviderClasses(state: ThemeProviderState): void; // @public (undocumented) export const useThemeProviderState: (draftState: ThemeProviderState) => void; diff --git a/packages/react-theme-provider/src/ThemeProvider.stories.tsx b/packages/react-theme-provider/src/ThemeProvider.stories.tsx index aee6a9306fa73..b134e819b99b2 100644 --- a/packages/react-theme-provider/src/ThemeProvider.stories.tsx +++ b/packages/react-theme-provider/src/ThemeProvider.stories.tsx @@ -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', + }, }, }, }; @@ -40,7 +44,7 @@ export const NestedTheming = () => { const [isLight, setIsLight] = React.useState(true); return ( - +
I am {isLight ? 'light theme' : 'dark theme'}
diff --git a/packages/react-theme-provider/src/ThemeProvider.test.tsx b/packages/react-theme-provider/src/ThemeProvider.test.tsx index 47a00e42974c3..a60389fd9c765 100644 --- a/packages/react-theme-provider/src/ThemeProvider.test.tsx +++ b/packages/react-theme-provider/src/ThemeProvider.test.tsx @@ -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: [], @@ -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(Hello); const tree = component.toJSON(); @@ -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( + + app + , + ); + 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 = ( + + app + + ); + + 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(); + }); }); diff --git a/packages/react-theme-provider/src/ThemeProvider.tsx b/packages/react-theme-provider/src/ThemeProvider.tsx index c6f95f7639733..a6a1abdb3b951 100644 --- a/packages/react-theme-provider/src/ThemeProvider.tsx +++ b/packages/react-theme-provider/src/ThemeProvider.tsx @@ -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. @@ -14,13 +15,16 @@ export const ThemeProvider = React.forwardRef + app + +`; + +exports[`ThemeProvider can apply body theme to none 1`] = ` +
+ app +
+`; + exports[`ThemeProvider can handle a partial theme 1`] = `
{ 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', @@ -129,6 +139,7 @@ export const defaultTokens: Tokens = { larger: '48px', largest: '64px', }, + ...defaultFonts, paddingLeft: '20px', paddingRight: '20px', paddingTop: '0', @@ -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', diff --git a/packages/react-theme-provider/src/getTokens.ts b/packages/react-theme-provider/src/getTokens.ts index f9ce7f330c7e1..3ff934aacdbfd 100644 --- a/packages/react-theme-provider/src/getTokens.ts +++ b/packages/react-theme-provider/src/getTokens.ts @@ -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. diff --git a/packages/react-theme-provider/src/useThemeProviderClasses.tsx b/packages/react-theme-provider/src/useThemeProviderClasses.tsx index 161eea14a8ac1..243707fcbc57a 100644 --- a/packages/react-theme-provider/src/useThemeProviderClasses.tsx +++ b/packages/react-theme-provider/src/useThemeProviderClasses.tsx @@ -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); +}