diff --git a/docs/documentation/docs/assets/EnhancedThemeProviderSharePoint.gif b/docs/documentation/docs/assets/EnhancedThemeProviderSharePoint.gif new file mode 100644 index 000000000..ba7dd8fce Binary files /dev/null and b/docs/documentation/docs/assets/EnhancedThemeProviderSharePoint.gif differ diff --git a/docs/documentation/docs/assets/EnhancedThemeProviderTeams.gif b/docs/documentation/docs/assets/EnhancedThemeProviderTeams.gif new file mode 100644 index 000000000..db8cc15fd Binary files /dev/null and b/docs/documentation/docs/assets/EnhancedThemeProviderTeams.gif differ diff --git a/docs/documentation/docs/controls/EnhancedThemeProvider.md b/docs/documentation/docs/controls/EnhancedThemeProvider.md new file mode 100644 index 000000000..f89499ee7 --- /dev/null +++ b/docs/documentation/docs/controls/EnhancedThemeProvider.md @@ -0,0 +1,109 @@ +# Enhanced Theme Provider + +The reasons behind this control are many and concern the use of Fluent UI controls currently officially supported by SPFx, that is: +- `Problems with Teams theme support`, when hosting a Web Part like Tab or Personal App and specifically the lack of support by this version of Fluent UI React of the high contrast theme. +- `Lack of basic style`, such as fonts, for basic HTML elements when creating `Web Parts hosted in Teams as Tabs or personal App`. +- Lack of basic style, such as fonts, for basic HTML elements when creating `Web Parts in "isDomainIsolated" mode`, aka the Isolated Web Parts. + +Therefore, the control is to be considered as a sort of `wrapper for all react and non-react controls` that you want to add to the WebPart. + +The control `extends the functionality of the Fluent UI ThemeProvider control` (currently in version 7) by adding some logic thanks to the information contained in the 'context' property, that is: +- If the Web Part is hosted inside SharePoint, the theme passed through the 'Theme' property will be used or the default one of the current site will be taken. +- If the web part is hosted within Teams, the "Theme" property will be ignored and using the "Context" property checks which theme is currently applied and adds a handler to notify when the theme is changed. This allows you to manage the change of theme in Teams in real-time, without having to reload the Tab or the Personal App. + +Example of use in SharePoint in a `SharePointFullPage - Isolated web parts` (note that the titles H1, H2, H3 and the paragraph are normal html tags that automatically take the font and color style from the control): +![Enhanced Theme Provider - SharePointFullPage - Isolated web parts](../assets/EnhancedThemeProviderSharePoint.gif) + +As for Teams, given the inconsistency of the theme system of Fluent UI NorthStar (used in Teams) and Fluent UI React (used by SPFx), the themes are "emulated". + +The control contains the refining of Teams' `Default`, `Dark` and `Hight Contrast` themes. + +The `Default` and `Dark themes` were created simply using the Fluent UI Themes designer and the primary colors of their corresponding Teams themes. + +For the `Hight Contrast` theme, on the other hand, given the complexity of creating a completely new theme and above all in Hight Contrast mode (neither supported nor gives SharePoint nor gives Fluent UI v7), it was decided to create the theme by hand and support only "main controls". + +`This means that this theme is not perfect and above all not all controls will be displayed correctly.` + +This is not a big deal, as the same theme provided by SharePoint has the same problems, it does not support Hight Contrast rendering for all controls. + +For the `Hight Contrast` theme (in Teams), only these controls are supported by this control: `ChoiceGroup, Checkbox, ComboBox, DatePicker, SpinButton, TextField, Toggle, PrimaryButton, DefaultButton, CompoundButton, IconButton`, other fluent controls may have color rendering problems. + +Example of use in Teams as a `TeamsPersonalApp` (note that the titles H1, H2, H3 and the paragraph are normal html tags that automatically take the font and color style from the control): +![Enhanced Theme Provider - TeamsPersonalApp / TeamsTab](../assets/EnhancedThemeProviderTeams.gif) + +## How to use this control in your solutions + +- Check that you installed the `@pnp/spfx-controls-react` dependency. Check out the [getting started](../../#getting-started) page for more information about installing the dependency. +- In your component file, import the `EnhancedThemeProvider` control as follows: + +```TypeScript +import { EnhancedThemeProvider, getDefaultTheme, useTheme, ThemeContext } from "@pnp/spfx-controls-react/lib/EnhancedThemeProvider"; +``` + +- Example on use the `EnhancedThemeProvider` control with only required properties: + +```TypeScript + + {/* controls to apply the theme to */} + +``` + +- Example on use the `EnhancedThemeProvider` control with the most important properties: + +```TypeScript + + {/* controls to apply the theme to */} + +``` + +The control provides the passage and/or creation of the theme according to what has been said before. +In order to access the theme, from child controls, there are two modes, one for function-based controls, one for class-based controls. + +- Access the theme from the child control using a function component: +```TypeScript +export const ChildFunctionComponent = () => { + const theme = useTheme(); + + return ( + Example Child Control + ); +} +``` + +- Access the theme from the child control using a class component: +```TypeScript +export class ChildClassComponent extends React.Component { + public render() { + return ( + + {theme => + Example Child Control + } + + ) + } +}; +``` + +- Usage example using theme in child controls: +```TypeScript + + + + +``` + +## Implementation + +The `EnhancedThemeProvider` control can be configured with the following properties: + +| Property | Type | Required | Description | +| ---- | ---- | ---- | ---- | +| context | WebPartContext | yes | Set the context from the SPFx Web Part. | +| as | React.ElementType | no | A component that should be used as the root element of the ThemeProvider component. | +| ref | React.Ref | no | Optional ref to the root element. | +| theme | PartialTheme \| Theme | no | Defines the theme provided by the user. | +| renderer | StyleRenderer | no | 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. | +| applyTo | 'element' \| 'body' \| 'none' | no | 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.| + +![](https://telemetry.sharepointpnp.com/sp-dev-fx-controls-react/wiki/controls/EnhancedThemeProvider) diff --git a/docs/documentation/docs/index.md b/docs/documentation/docs/index.md index c8df50b35..c9d180e04 100644 --- a/docs/documentation/docs/index.md +++ b/docs/documentation/docs/index.md @@ -72,6 +72,7 @@ The following controls are currently available: - [DateTimePicker](./controls/DateTimePicker) (DateTime Picker) - [DragDropFiles](./controls/DragDropFiles) (Allow drag and drop of files in selected areas) - [DynamicForm](./controls/DynamicForm) (Dynamic Form component) +- [EnhancedThemeProvider](./controls/EnhancedThemeProvider) (Enhanced version of Fluent UI Theme Provider control used to improve support for themes and fonts when creating Tab or Personal App in SPFx for Teams or creating Isolated Web Parts) - [FieldCollectionData](./controls/FieldCollectionData) (control gives you the ability to insert a list / collection data which can be used in your web part / application customizer) - [FilePicker](./controls/FilePicker) (control that allows to browse and select a file from various places) - [FileTypeIcon](./controls/FileTypeIcon) (Control that shows the icon of a specified file path or application) diff --git a/docs/documentation/mkdocs.yml b/docs/documentation/mkdocs.yml index 3afd577cc..b62234428 100644 --- a/docs/documentation/mkdocs.yml +++ b/docs/documentation/mkdocs.yml @@ -29,6 +29,7 @@ nav: - DateTimePicker: 'controls/DateTimePicker.md' - DragDropFiles: 'controls/DragDropFiles.md' - DynamicForm: 'controls/DynamicForm.md' + - EnhancedThemeProvider: 'controls/EnhancedThemeProvider.md' - FieldCollectionData: 'controls/FieldCollectionData.md' - FilePicker: 'controls/FilePicker.md' - FileTypeIcon: 'controls/FileTypeIcon.md' diff --git a/src/EnhancedThemeProvider.ts b/src/EnhancedThemeProvider.ts new file mode 100644 index 000000000..245209638 --- /dev/null +++ b/src/EnhancedThemeProvider.ts @@ -0,0 +1 @@ +export * from './controls/EnhancedThemeProvider/index'; diff --git a/src/common/fluentUIThemes/FluentUIDefaultTheme.ts b/src/common/fluentUIThemes/FluentUIDefaultTheme.ts new file mode 100644 index 000000000..5ddcd35d8 --- /dev/null +++ b/src/common/fluentUIThemes/FluentUIDefaultTheme.ts @@ -0,0 +1,15 @@ +import { createTheme, getTheme, ITheme } from "office-ui-fabric-react/lib/Styling"; + +export const fluentUIDefaultTheme = (): ITheme => { + let currentTheme; + const themeColorsFromWindow: any = (window as any).__themeState__.theme; + if (themeColorsFromWindow) { + currentTheme = createTheme({ + palette: themeColorsFromWindow + }); + } + else + currentTheme = getTheme(); + + return currentTheme; +}; \ No newline at end of file diff --git a/src/common/fluentUIThemes/FluentUITeamsDarkTheme.ts b/src/common/fluentUIThemes/FluentUITeamsDarkTheme.ts new file mode 100644 index 000000000..0a3f017f9 --- /dev/null +++ b/src/common/fluentUIThemes/FluentUITeamsDarkTheme.ts @@ -0,0 +1,29 @@ +import { createTheme } from "office-ui-fabric-react/lib/Styling"; + +export const fluentUITeamsDarkTheme = createTheme({ + palette: { + themePrimary: "#7f85f5", + themeLighterAlt: "#05050a", + themeLighter: "#141527", + themeLight: "#262849", + themeTertiary: "#4c5093", + themeSecondary: "#7075d7", + themeDarkAlt: "#8c91f6", + themeDark: "#9da2f7", + themeDarker: "#b6baf9", + neutralLighterAlt: "#282828", + neutralLighter: "#313131", + neutralLight: "#3f3f3f", + neutralQuaternaryAlt: "#484848", + neutralQuaternary: "#4f4f4f", + neutralTertiaryAlt: "#6d6d6d", + neutralTertiary: "#c8c8c8", + neutralSecondary: "#d0d0d0", + neutralPrimaryAlt: "#dadada", + neutralPrimary: "#ffff", + neutralDark: "#f4f4f4", + black: "#ffffff", + white: "#1f1f1f" + }, + isInverted: true +}); \ No newline at end of file diff --git a/src/common/fluentUIThemes/FluentUITeamsDefaultTheme.ts b/src/common/fluentUIThemes/FluentUITeamsDefaultTheme.ts new file mode 100644 index 000000000..024602dd7 --- /dev/null +++ b/src/common/fluentUIThemes/FluentUITeamsDefaultTheme.ts @@ -0,0 +1,29 @@ +import { createTheme } from "office-ui-fabric-react/lib/Styling"; + +export const fluentUITeamsDefaultTheme = createTheme({ + palette: { + themePrimary: "#6264A7", + themeLighterAlt: "#f7f7fb", + themeLighter: "#e1e1f1", + themeLight: "#c8c9e4", + themeTertiary: "#989ac9", + themeSecondary: "#7173b0", + themeDarkAlt: "#585a95", + themeDark: "#4a4c7e", + themeDarker: "#37385d", + neutralLighterAlt: "#eeeeee", + neutralLighter: "#eaeaea", + neutralLight: "#e1e1e1", + neutralQuaternaryAlt: "#d1d1d1", + neutralQuaternary: "#c8c8c8", + neutralTertiaryAlt: "#c0c0c0", + neutralTertiary: "#acacac", + neutralSecondary: "#919191", + neutralPrimaryAlt: "#767676", + neutralPrimary: "#0b0b0b", + neutralDark: "#404040", + black: "#252525", + white: "#F5F5F5" + }, + isInverted: false +}); \ No newline at end of file diff --git a/src/common/fluentUIThemes/FluentUITeamsHighContrastTheme.ts b/src/common/fluentUIThemes/FluentUITeamsHighContrastTheme.ts new file mode 100644 index 000000000..df6f32f63 --- /dev/null +++ b/src/common/fluentUIThemes/FluentUITeamsHighContrastTheme.ts @@ -0,0 +1,37 @@ +import { createTheme } from "office-ui-fabric-react/lib/Styling"; + +export const fluentUITeamsHighContrastTheme = createTheme({ + palette: { + themePrimary: "#00ebff", + themeLighterAlt: "#0a0a00", + themeLighter: "#292900", + themeLight: "#4d4d00", + themeTertiary: "#999900", + themeSecondary: "#e0e000", + themeDarkAlt: "#ffff19", + themeDark: "#ffff3d", + themeDarker: "#ffff70", + neutralLighterAlt: "#0b0b0b", + neutralLighter: "#151515", + neutralLight: "#252525", + neutralQuaternaryAlt: "#2f2f2f", + neutralQuaternary: "#373737", + neutralTertiaryAlt: "#595959", + neutralTertiary: "#fafafa", + neutralSecondary: "#fbfbfb", + neutralPrimaryAlt: "#fcfcfc", + neutralPrimary: "#f8f8f8", + neutralDark: "#fdfdfd", + black: "#fefefe", + white: "#000000" + }, + isInverted: true, + semanticColors: { + buttonBackgroundDisabled: "#3ff23f", + buttonTextDisabled: "#000000", + primaryButtonBackgroundDisabled: "#3ff23f", + primaryButtonTextDisabled: "#000000", + link: "#ffff00", + linkHovered: "#ffff00" + } +}); \ No newline at end of file diff --git a/src/common/fluentUIThemes/index.ts b/src/common/fluentUIThemes/index.ts new file mode 100644 index 000000000..1baadde85 --- /dev/null +++ b/src/common/fluentUIThemes/index.ts @@ -0,0 +1,4 @@ +export * from './FluentUIDefaultTheme'; +export * from './FluentUITeamsDarkTheme'; +export * from './FluentUITeamsDefaultTheme'; +export * from './FluentUITeamsHighContrastTheme'; diff --git a/src/controls/EnhancedThemeProvider/EnhancedThemeProvider.tsx b/src/controls/EnhancedThemeProvider/EnhancedThemeProvider.tsx new file mode 100644 index 000000000..4d11385c3 --- /dev/null +++ b/src/controls/EnhancedThemeProvider/EnhancedThemeProvider.tsx @@ -0,0 +1,91 @@ +import { ThemeProvider } from '@fluentui/react-theme-provider'; +import { getVariant, VariantThemeType } from "@fluentui/scheme-utilities"; +import { initializeIcons } from 'office-ui-fabric-react/lib/Icons'; +import { createTheme, getTheme, ITheme } from "office-ui-fabric-react/lib/Styling"; +import * as React from "react"; +import { useCallback, useEffect, useState } from 'react'; +import { fluentUITeamsDarkTheme } from '../../common/fluentUIThemes/FluentUITeamsDarkTheme'; +import { fluentUITeamsDefaultTheme } from '../../common/fluentUIThemes/FluentUITeamsDefaultTheme'; +import { fluentUITeamsHighContrastTheme } from '../../common/fluentUIThemes/FluentUITeamsHighContrastTheme'; +import { IEnhancedThemeProviderProps } from './IEnhancedThemeProviderProps'; +import { ThemeContext, useTheme } from '@fluentui/react-theme-provider'; +import * as telemetry from '../../common/telemetry'; + +const getDefaultTheme = (): ITheme => { + let currentTheme; + const themeColorsFromWindow: any = (window as any)?.__themeState__?.theme; + if (themeColorsFromWindow) { + currentTheme = createTheme({ + palette: themeColorsFromWindow + }); + } + else { + currentTheme = getTheme(); + } + + return currentTheme; +}; + +const EnhancedThemeProvider = (props: IEnhancedThemeProviderProps) => { + + const [isInTeams, setIsInTeams] = useState(false); + const [teamsThemeName, setTeamsThemeName] = useState(null); + + // track the telemetry as 'ReactEnhancedThemeProvider' + useEffect(() => { + telemetry.track('ReactEnhancedThemeProvider'); + }, []); + // ***** + + useEffect(() => { + initializeIcons(); + }, []); + + useEffect(() => { + setIsInTeams((props.context.sdks.microsoftTeams) ? true : false); + }, [props.context]); + + useEffect(() => { + if (isInTeams) { + setTeamsThemeName(props.context.sdks.microsoftTeams?.context?.theme); + props.context.sdks?.microsoftTeams?.teamsJs?.registerOnThemeChangeHandler((theme: string) => { + setTeamsThemeName(theme); + }); + } + }, [props.context, isInTeams]); + + const themeToApply = useCallback( + () => { + let workingTheme: ITheme; + + if (isInTeams) { + switch (teamsThemeName) { + case "default": workingTheme = fluentUITeamsDefaultTheme; + break; + case "dark": workingTheme = fluentUITeamsDarkTheme; + break; + case "contrast": workingTheme = fluentUITeamsHighContrastTheme; + break; + default: workingTheme = fluentUITeamsDefaultTheme; + break; + } + } else if (props.theme) { + workingTheme = getVariant(props.theme, VariantThemeType.None); + } else { + workingTheme = getDefaultTheme(); + } + + return workingTheme; + }, + [props.theme, teamsThemeName]); + + return ( + + {props.children} + + ); +}; + +export { EnhancedThemeProvider, getDefaultTheme, useTheme, ThemeContext }; diff --git a/src/controls/EnhancedThemeProvider/IEnhancedThemeProviderProps.ts b/src/controls/EnhancedThemeProvider/IEnhancedThemeProviderProps.ts new file mode 100644 index 000000000..d38c1a9a6 --- /dev/null +++ b/src/controls/EnhancedThemeProvider/IEnhancedThemeProviderProps.ts @@ -0,0 +1,9 @@ +import { ThemeProviderProps } from '@fluentui/react-theme-provider'; +import { WebPartContext } from '@microsoft/sp-webpart-base'; + +export interface IEnhancedThemeProviderProps extends ThemeProviderProps { + /** + * Set the context from the SPFx component. + */ + context: WebPartContext; +} diff --git a/src/controls/EnhancedThemeProvider/index.ts b/src/controls/EnhancedThemeProvider/index.ts new file mode 100644 index 000000000..6e6bd071c --- /dev/null +++ b/src/controls/EnhancedThemeProvider/index.ts @@ -0,0 +1,2 @@ +export * from './EnhancedThemeProvider'; +export * from './IEnhancedThemeProviderProps'; diff --git a/src/controls/treeView/TreeItem.tsx b/src/controls/treeView/TreeItem.tsx index ce6229cd4..1e228bf07 100644 --- a/src/controls/treeView/TreeItem.tsx +++ b/src/controls/treeView/TreeItem.tsx @@ -5,7 +5,6 @@ import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox'; import { Icon } from 'office-ui-fabric-react/lib/Icon'; import { ITheme } from 'office-ui-fabric-react/lib/Styling'; import { css } from 'office-ui-fabric-react/lib/Utilities'; -import * as React from 'react'; import { ITreeItem } from './ITreeItem'; import { TreeItemActionsDisplayMode } from './ITreeItemActions'; import { TreeViewSelectionMode } from './ITreeViewProps'; diff --git a/src/webparts/controlsTest/components/ControlsTest.tsx b/src/webparts/controlsTest/components/ControlsTest.tsx index 69f2554d2..b24c1f855 100644 --- a/src/webparts/controlsTest/components/ControlsTest.tsx +++ b/src/webparts/controlsTest/components/ControlsTest.tsx @@ -183,6 +183,8 @@ import { ModernTaxonomyPicker } from "../../../controls/modernTaxonomyPicker/Mod import { AdaptiveCardHost, IAdaptiveCardHostActionResult, AdaptiveCardHostThemeType, CardObjectRegistry, CardElement, Action, HostCapabilities } from "../../../AdaptiveCardHost"; import { VariantThemeProvider, VariantType } from "../../../controls/variantThemeProvider"; import { Label } from "office-ui-fabric-react/lib/Label"; +import { EnhancedThemeProvider } from "../../../EnhancedThemeProvider"; +import { ControlsTestEnhancedThemeProvider, ControlsTestEnhancedThemeProviderFunctionComponent } from "./ControlsTestEnhancedThemeProvider"; @@ -2265,6 +2267,15 @@ export default class ControlsTest extends React.Component + +
+

Enhanced Theme Provider

+ + + + +
+ ); } diff --git a/src/webparts/controlsTest/components/ControlsTestEnhancedThemeProvider.tsx b/src/webparts/controlsTest/components/ControlsTestEnhancedThemeProvider.tsx new file mode 100644 index 000000000..2c6eb5698 --- /dev/null +++ b/src/webparts/controlsTest/components/ControlsTestEnhancedThemeProvider.tsx @@ -0,0 +1,97 @@ +import { ActionButton, Checkbox, ChoiceGroup, ComboBox, CompoundButton, DatePicker, DefaultButton, IconButton, Link, PrimaryButton, SelectableOptionMenuItemType, SpinButton, Stack, TextField, Toggle } from 'office-ui-fabric-react'; +import * as React from 'react'; +import { ThemeContext, useTheme } from '../../../EnhancedThemeProvider'; +import { Placeholder } from '../../../Placeholder'; + +export const ControlsTestEnhancedThemeProviderFunctionComponent = () => { + const theme = useTheme(); + + return ( + +

Title H1

+

Title H2

+

Title H3

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed mollis malesuada elit, in accumsan erat vehicula nec. Donec molestie eu quam vel pulvinar. Proin eu est a felis hendrerit sodales. Quisque non consequat sapien. Donec at neque libero. In vel ante nec ex sagittis consectetur. Ut euismod nunc sed ullamcorper tincidunt. Morbi justo dolor, rutrum vehicula urna quis, tempor pulvinar ligula. Sed quis gravida mi. In fermentum augue rhoncus odio lacinia pharetra. Aliquam elementum mollis nibh, rutrum iaculis tortor.

+ { alert("onConfigure"); }} /> + Fluent Link + Action Button + PrimaryButton + DefaultButton + CompoundButton + + + + +
+ ); +}; + +export class ControlsTestEnhancedThemeProvider extends React.Component { + public render() { + return ( + + {theme => + + + + + + + + + + + + + + + + + + + + + + + + } + + ); + } +}