Skip to content

Commit

Permalink
Merge pull request #1206 from fabiofranzini/enhanced-theme-provider
Browse files Browse the repository at this point in the history
Added 'EnhancedThemeProvider' control
  • Loading branch information
joelfmrodrigues committed May 8, 2022
2 parents bc73b28 + 912895e commit 81219ed
Show file tree
Hide file tree
Showing 17 changed files with 436 additions and 1 deletion.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
109 changes: 109 additions & 0 deletions docs/documentation/docs/controls/EnhancedThemeProvider.md
Original file line number Diff line number Diff line change
@@ -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
<EnhancedThemeProvider context={this.props.context}>
{/* controls to apply the theme to */}
</EnhancedThemeProvider>
```

- Example on use the `EnhancedThemeProvider` control with the most important properties:

```TypeScript
<EnhancedThemeProvider applyTo="element" context={this.props.context} theme={this.props.themeVariant}>
{/* controls to apply the theme to */}
</EnhancedThemeProvider>
```

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 (
<DefaultButton theme={theme}>Example Child Control</DefaultButton>
);
}
```

- Access the theme from the child control using a class component:
```TypeScript
export class ChildClassComponent extends React.Component {
public render() {
return (
<ThemeContext.Consumer>
{theme =>
<DefaultButton theme={theme}>Example Child Control</DefaultButton>
}
</ThemeContext.Consumer>
)
}
};
```

- Usage example using theme in child controls:
```TypeScript
<EnhancedThemeProvider applyTo="element" context={this.props.context} theme={this.props.themeVariant}>
<ChildFunctionComponent />
<ChildClassComponent />
</EnhancedThemeProvider>
```

## 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<HTMLElement> | 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)
1 change: 1 addition & 0 deletions docs/documentation/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions docs/documentation/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions src/EnhancedThemeProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './controls/EnhancedThemeProvider/index';
15 changes: 15 additions & 0 deletions src/common/fluentUIThemes/FluentUIDefaultTheme.ts
Original file line number Diff line number Diff line change
@@ -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;
};
29 changes: 29 additions & 0 deletions src/common/fluentUIThemes/FluentUITeamsDarkTheme.ts
Original file line number Diff line number Diff line change
@@ -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
});
29 changes: 29 additions & 0 deletions src/common/fluentUIThemes/FluentUITeamsDefaultTheme.ts
Original file line number Diff line number Diff line change
@@ -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
});
37 changes: 37 additions & 0 deletions src/common/fluentUIThemes/FluentUITeamsHighContrastTheme.ts
Original file line number Diff line number Diff line change
@@ -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"
}
});
4 changes: 4 additions & 0 deletions src/common/fluentUIThemes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './FluentUIDefaultTheme';
export * from './FluentUITeamsDarkTheme';
export * from './FluentUITeamsDefaultTheme';
export * from './FluentUITeamsHighContrastTheme';
91 changes: 91 additions & 0 deletions src/controls/EnhancedThemeProvider/EnhancedThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -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<string>(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 (
<ThemeProvider
{...props}
theme={themeToApply()}>
{props.children}
</ThemeProvider>
);
};

export { EnhancedThemeProvider, getDefaultTheme, useTheme, ThemeContext };
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 2 additions & 0 deletions src/controls/EnhancedThemeProvider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './EnhancedThemeProvider';
export * from './IEnhancedThemeProviderProps';
1 change: 0 additions & 1 deletion src/controls/treeView/TreeItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
11 changes: 11 additions & 0 deletions src/webparts/controlsTest/components/ControlsTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";



Expand Down Expand Up @@ -2265,6 +2267,15 @@ export default class ControlsTest extends React.Component<IControlsTestProps, IC
</Stack>
</VariantThemeProvider>
</div>

<div>
<h3>Enhanced Theme Provider</h3>
<EnhancedThemeProvider applyTo="element" context={this.props.context} theme={this.props.themeVariant}>
<ControlsTestEnhancedThemeProviderFunctionComponent />
<ControlsTestEnhancedThemeProvider />
</EnhancedThemeProvider>
</div>

</div>
);
}
Expand Down
Loading

0 comments on commit 81219ed

Please sign in to comment.