Initial release of the 'react-fluentui-theme-variant' sample

This commit is contained in:
Fabio Franzini 2021-08-09 11:54:07 +02:00
parent ec8162e8ea
commit 2f145ee1ba
8 changed files with 142 additions and 191 deletions

View File

@ -1,73 +1,68 @@
# react-fluentui-theme-variant
# React Fluent UI Theme Variant
## Summary
This web part provides an example of how to apply a custom theme or a variation of the current SharePoint theme directly to the web part.
In this way it is possible to implement the same mechanism that is currently implemented by default by the SharePoint page sections
Short summary on functionality and used technologies.
![picture of the web part in action](assets/preview.gif)
[picture of the solution in action, if possible]
## Compatibility
## Used SharePoint Framework Version
![version](https://img.shields.io/npm/v/@microsoft/sp-component-base/latest?color=green)
![SPFx 1.12.1](https://img.shields.io/badge/SPFx-1.12.1-green.svg)
![Node.js LTS v14 | LTS v12 | LTS v10](https://img.shields.io/badge/Node.js-LTS%20v14%20%7C%20LTS%20v12%20%7C%20LTS%20v10-green.svg)
![SharePoint Online](https://img.shields.io/badge/SharePoint-Online-yellow.svg)
![Teams N/A: Untested with Microsoft Teams](https://img.shields.io/badge/Teams-N%2FA-lightgrey.svg "Untested with Microsoft Teams")
![Workbench Local | Hosted](https://img.shields.io/badge/Workbench-Local%20%7C%20Hosted-green.svg)
## Applies to
- [SharePoint Framework](https://aka.ms/spfx)
- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram)
## Prerequisites
> Any special pre-requisites?
* [SharePoint Framework](https://docs.microsoft.com/sharepoint/dev/spfx/sharepoint-framework-overview)
* [Microsoft 365 tenant](https://docs.microsoft.com/sharepoint/dev/spfx/set-up-your-development-environment)
## Solution
Solution|Author(s)
--------|---------
folder name | Author details (name, company, twitter alias with link)
react-fluentui-theme-variant | [Fabio Franzini](https://www.linkedin.com/in/fabiofranzini/) ([@franzinifabio](https://twitter.com/franzinifabio)), fabiofranzini.com
## Version history
Version|Date|Comments
-------|----|--------
1.1|March 10, 2021|Update comment
1.0|January 29, 2021|Initial release
1.0|August 9, 2021|Initial release
## Minimal Path to Awesome
* Clone this repository
* in the command line run:
* `npm install`
* `gulp serve`
## Features
This example was born with the idea of overcoming the present default limit regarding the colors of the sections of the SharePoint pages.
Specifically, by default, it is only possible to change the color (also called Section Background Shading) of the sections but not of the individual Web Parts.
In this implementation it is instead possible to vary the "Background Shading" of the single Web Part in 3 ways:
* Use the colors applied to the section where the Web Part is present
* Select the color variations based on the theme applied at the Site level.
* Apply variations set to the json of a custom theme, created through the Fluent UI theme designer.
In all these cases, the component variation works automatically as much as the Fluent UI react controls are used, otherwise the variation will only work on the background and some HTML elements.
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Help
## Minimal Path to Awesome
We do not support samples, but we this community is always willing to help, and we want to improve these samples. We use GitHub to track issues, which makes it easy for community members to volunteer their time and help resolve issues.
- Clone this repository
- Ensure that you are at the solution folder
- in the command-line run:
- **npm install**
- **gulp serve**
If you encounter any issues while using this sample, [create a new issue](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Abug-suspected&template=bug-report.yml&sample=react-htm-templating&authors=@fabiofranzini&title=react-htm-templating%20-%20).
> Include any additional steps as needed.
For questions regarding this sample, [create a new question](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Abug-suspected&template=question.yml&sample=react-htm-templating&authors=@fabiofranzini&title=react-htm-templating%20-%20).
## Features
Finally, if you have an idea for improvement, [make a suggestion](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Abug-suspected&template=suggestion.yml&sample=react-htm-templating&authors=@fabiofranzini&title=react-htm-templating%20-%20).
Description of the extension that expands upon high-level summary above.
This extension illustrates the following concepts:
- topic 1
- topic 2
- topic 3
> Notice that better pictures and documentation will increase the sample usage and the value you are providing for others. Thanks for your submissions advance.
> Share your web part with others through Microsoft 365 Patterns and Practices program to get visibility and exposure. More details on the community, open-source projects and other activities from http://aka.ms/m365pnp.
## References
- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview)
- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis)
- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview)
- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-htm-templating" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 MiB

View File

@ -2,13 +2,7 @@ import { Theme } from "@fluentui/react-theme-provider";
import { getNeutralVariant, getSoftVariant, getStrongVariant } from "@fluentui/scheme-utilities/lib/variants";
import { IReadonlyTheme } from '@microsoft/sp-component-base';
import { ServiceKey, ServiceScope } from "@microsoft/sp-core-library";
import { BaseSlots, createTheme, FabricSlots, getColorFromString, getContrastRatio, getTheme, IColor, isDark, ThemeGenerator, themeRulesStandardCreator } from "office-ui-fabric-react";
export interface IContrastRatioPair {
contrastRatioValue: string;
contrastRatioPair: string;
colorPair: string;
}
import { createTheme, getTheme, IPalette } from "office-ui-fabric-react";
export enum ThemeType {
current = "current",
@ -24,9 +18,7 @@ export enum BackgroundShadingType {
}
export interface IThemeService {
setCustomColors(primaryColor: string, textColor: string, backgroundColor: string): void;
setThemeVariant(themeVariant: IReadonlyTheme | Theme): void;
generateTheme(themeType: ThemeType, backgroundShadingType: BackgroundShadingType): IReadonlyTheme | Theme;
generateTheme(themeType: ThemeType, backgroundShadingType: BackgroundShadingType, themeVariant: IReadonlyTheme | Theme, palette: Partial<IPalette>): IReadonlyTheme | Theme;
}
const ThemeService_ServiceKey = 'ReactFluentUIThemeVariant:ThemeService';
@ -34,49 +26,29 @@ const ThemeService_ServiceKey = 'ReactFluentUIThemeVariant:ThemeService';
export class ThemeService implements IThemeService {
public static ServiceKey: ServiceKey<IThemeService> = ServiceKey.create(ThemeService_ServiceKey, ThemeService);
private serviceScope: ServiceScope;
private themeRules = themeRulesStandardCreator();
private themeVariant: IReadonlyTheme | Theme;
private primaryColor: string;
private textColor: string;
private backgroundColor: string;
public constructor(serviceScope: ServiceScope) {
this.serviceScope = serviceScope;
}
public setCustomColors(primaryColor: string, textColor: string, backgroundColor: string): void {
if (!primaryColor)
throw 'primaryColor == null or undefined';
else
this.primaryColor = primaryColor;
if (!textColor)
throw 'textColor == null or undefined';
else
this.textColor = textColor;
if (!backgroundColor)
throw 'backgroundColor == null or undefined';
this.backgroundColor = backgroundColor;
}
public setThemeVariant(themeVariant: IReadonlyTheme | Theme): void {
this.themeVariant = themeVariant;
}
public generateTheme(themeType: ThemeType, backgroundShadingType: BackgroundShadingType): IReadonlyTheme | Theme {
public generateTheme(themeType: ThemeType,
backgroundShadingType: BackgroundShadingType,
themeVariant: IReadonlyTheme | Theme,
palette: Partial<IPalette>): IReadonlyTheme | Theme {
let currentTheme: IReadonlyTheme | Theme;
switch (themeType) {
case ThemeType.current: currentTheme = this.getDefaultTheme();
break;
case ThemeType.section: currentTheme = this.themeVariant;
case ThemeType.section: currentTheme = themeVariant;
break;
case ThemeType.custom: currentTheme = this.generateThemeFromColors();
case ThemeType.custom: currentTheme = this.generateThemeFromPalette(palette);
break;
}
if (themeType == ThemeType.section)
return currentTheme;
switch (backgroundShadingType) {
case BackgroundShadingType.none: currentTheme = currentTheme;
break;
@ -105,17 +77,14 @@ export class ThemeService implements IThemeService {
return currentTheme;
}
private generateThemeFromColors(): Theme {
ThemeGenerator.setSlot(this.themeRules[BaseSlots[BaseSlots.primaryColor]], getColorFromString(this.primaryColor), false, true, true);
ThemeGenerator.setSlot(this.themeRules[BaseSlots[BaseSlots.foregroundColor]], getColorFromString(this.textColor), false, true, true);
ThemeGenerator.setSlot(this.themeRules[BaseSlots[BaseSlots.backgroundColor]], getColorFromString(this.backgroundColor), false, true, true);
ThemeGenerator.insureSlots(this.themeRules, false);
private generateThemeFromPalette(palette: Partial<IPalette>): Theme {
let generatedTheme = createTheme({
...{ palette: ThemeGenerator.getThemeAsJson(this.themeRules) },
isInverted: isDark(this.themeRules[BaseSlots[BaseSlots.backgroundColor]].color!),
...{ palette: palette }
});
return generatedTheme;
}
}
}

View File

@ -15,8 +15,6 @@
"description": { "default": "Example on how to apply Fluent UI theme variant not only at Section level but at Web Part level" },
"officeFabricIconFontName": "BackgroundColor",
"properties": {
"themeType": "current",
"backgroundShadingType": "none"
}
}]
}

View File

@ -7,28 +7,29 @@ import {
PropertyPaneDropdown
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import { PropertyFieldColorPicker, PropertyFieldColorPickerStyle } from '@pnp/spfx-property-controls/lib/PropertyFieldColorPicker';
import { PropertyFieldMessage } from '@pnp/spfx-property-controls/lib/PropertyFieldMessage';
import * as strings from 'FluentUiThemeVariantWebPartStrings';
import { MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
import { MessageBarType } from 'office-ui-fabric-react/lib/components/MessageBar';
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { BackgroundShadingType, IContrastRatioPair, IThemeService, ThemeService, ThemeType } from '../../services/ThemeService';
import { BackgroundShadingType, IThemeService, ThemeService, ThemeType } from '../../services/ThemeService';
import FluentUiThemeVariant, { IFluentUiThemeVariantProps } from './components/FluentUiThemeVariant';
export interface IFluentUiThemeVariantWebPartProps {
themeType: ThemeType;
backgroundShadingType: BackgroundShadingType;
primaryColor: string;
textColor: string;
backgroundColor: string;
customPalette: string;
}
export default class FluentUiThemeVariantWebPart extends BaseClientSideWebPart<IFluentUiThemeVariantWebPartProps> {
private themeService: IThemeService;
protected themeProvider: ThemeProvider;
protected themeVariant: IReadonlyTheme;
protected propertyFieldCodeEditor;
protected propertyFieldCodeEditorLanguages;
protected propertyFieldMessage;
protected onInit(): Promise<void> {
this.themeService = this.context.serviceScope.consume<IThemeService>(ThemeService.ServiceKey);
@ -37,43 +38,67 @@ export default class FluentUiThemeVariantWebPart extends BaseClientSideWebPart<I
this.themeProvider.themeChangedEvent.add(this, (args: ThemeChangedEventArgs): void => {
if (!isEqual(this.themeVariant, args.theme)) {
this.themeVariant = args.theme;
this.themeService.setThemeVariant(this.themeVariant);
this.render();
}
});
if (!this.properties.primaryColor)
this.properties.primaryColor = "#0078d4";
if (!this.properties.themeType)
this.properties.themeType = ThemeType.section;
if (!this.properties.textColor)
this.properties.textColor = "#323130";
if (!this.properties.backgroundShadingType)
this.properties.backgroundShadingType = BackgroundShadingType.none;
if (!this.properties.backgroundColor)
this.properties.backgroundColor = "#ffffff";
this.themeService.setThemeVariant(this.themeVariant);
this.themeService.setCustomColors(this.properties.primaryColor, this.properties.textColor, this.properties.backgroundColor);
if (!this.properties.customPalette)
this.properties.customPalette =
JSON.stringify(
JSON.parse(
`{
"themePrimary": "#0078d4",
"themeLighterAlt": "#eff6fc",
"themeLighter": "#deecf9",
"themeLight": "#c7e0f4",
"themeTertiary": "#71afe5",
"themeSecondary": "#2b88d8",
"themeDarkAlt": "#106ebe",
"themeDark": "#005a9e",
"themeDarker": "#004578",
"neutralLighterAlt": "#faf9f8",
"neutralLighter": "#f3f2f1",
"neutralLight": "#edebe9",
"neutralQuaternaryAlt": "#e1dfdd",
"neutralQuaternary": "#d0d0d0",
"neutralTertiaryAlt": "#c8c6c4",
"neutralTertiary": "#a19f9d",
"neutralSecondary": "#605e5c",
"neutralPrimaryAlt": "#3b3a39",
"neutralPrimary": "#323130",
"neutralDark": "#201f1e",
"black": "#000000",
"white": "#ffffff"
}`
), null, 2);
return super.onInit();
}
public render(): void {
const currentTheme = this.themeService.generateTheme(
this.properties.themeType,
this.properties.backgroundShadingType,
this.themeVariant,
JSON.parse(this.properties.customPalette));
const element: React.ReactElement<IFluentUiThemeVariantProps> = React.createElement(
FluentUiThemeVariant,
{
themeVariant: this.themeService.generateTheme(
this.properties.themeType,
this.properties.backgroundShadingType)
themeVariant: currentTheme
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
@ -82,6 +107,21 @@ export default class FluentUiThemeVariantWebPart extends BaseClientSideWebPart<I
return Version.parse('1.0');
}
protected async loadPropertyPaneResources(): Promise<void> {
const { PropertyFieldCodeEditor, PropertyFieldCodeEditorLanguages } = await import(
/* webpackChunkName: 'pnp-controls-property-field-code-editor' */
'@pnp/spfx-property-controls/lib/PropertyFieldCodeEditor'
);
this.propertyFieldCodeEditor = PropertyFieldCodeEditor;
this.propertyFieldCodeEditorLanguages = PropertyFieldCodeEditorLanguages;
const { PropertyFieldMessage } = await import(
/* webpackChunkName: 'pnp-controls-property-field-message' */
'@pnp/spfx-property-controls/lib/PropertyFieldMessage'
);
this.propertyFieldMessage = PropertyFieldMessage;
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
let inlineSvgNone = this.generateInlineSvgForBackgroundShadingType(BackgroundShadingType.none);
@ -89,9 +129,6 @@ export default class FluentUiThemeVariantWebPart extends BaseClientSideWebPart<I
let inlineSvgSoft = this.generateInlineSvgForBackgroundShadingType(BackgroundShadingType.soft);
let inlineSvgStrong = this.generateInlineSvgForBackgroundShadingType(BackgroundShadingType.strong);
console.log(inlineSvgNone);
console.log(inlineSvgStrong);
return {
pages: [
{
@ -105,55 +142,29 @@ export default class FluentUiThemeVariantWebPart extends BaseClientSideWebPart<I
PropertyPaneDropdown('themeType', {
label: strings.ThemeTypeField,
options: [
{ key: ThemeType.current, text: strings.Texts.Current },
{ key: ThemeType.section, text: strings.Texts.Section },
{ key: ThemeType.current, text: strings.Texts.Current },
{ key: ThemeType.custom, text: strings.Texts.Custom }
]
}),
// PropertyFieldMessage("", {
// key: "colorPaletteAccessibilityErrors",
// text: this.getNonAccessiblePairsMessage(this.themeService.getNonAccessiblePairs()),
// multiline: true,
// messageType: MessageBarType.warning,
// isVisible: this.properties.themeType == ThemeType.custom && this.themeService.getNonAccessiblePairs().length > 0,
// }),
PropertyFieldColorPicker('primaryColor', {
label: strings.PrimaryColorField,
selectedColor: this.properties.primaryColor,
onPropertyChange: this.onCustomPropertyPaneFieldChanged,
this.properties.themeType == ThemeType.custom && this.propertyFieldMessage("", {
key: "fluentUiThemeDesignerMessage",
text: strings.CustomPaletteMessageField,
multiline: true,
messageType: MessageBarType.info,
isVisible: true,
}),
this.properties.themeType == ThemeType.custom && this.propertyFieldCodeEditor('customPalette', {
label: strings.CustomPaletteField,
panelTitle: strings.CustomPaletteField,
initialValue: this.properties.customPalette,
onPropertyChange: this.onPropertyPaneFieldChanged,
properties: this.properties,
disabled: false,
isHidden: this.properties.themeType != ThemeType.custom,
alphaSliderHidden: false,
style: PropertyFieldColorPickerStyle.Inline,
iconName: 'Color',
key: 'primaryColorFieldId'
key: 'customPaletteEditorFieldId',
language: this.propertyFieldCodeEditorLanguages.JSON
}),
PropertyFieldColorPicker('textColor', {
label: strings.TextColorField,
selectedColor: this.properties.textColor,
onPropertyChange: this.onCustomPropertyPaneFieldChanged,
properties: this.properties,
disabled: false,
isHidden: this.properties.themeType != ThemeType.custom,
alphaSliderHidden: false,
style: PropertyFieldColorPickerStyle.Inline,
iconName: 'Color',
key: 'textColorFieldId'
}),
PropertyFieldColorPicker('backgroundColor', {
label: strings.BackgroundColorField,
selectedColor: this.properties.backgroundColor,
onPropertyChange: this.onCustomPropertyPaneFieldChanged,
properties: this.properties,
disabled: false,
isHidden: this.properties.themeType != ThemeType.custom,
alphaSliderHidden: false,
style: PropertyFieldColorPickerStyle.Inline,
iconName: 'Color',
key: 'backgroundColorFieldId'
}),
PropertyPaneChoiceGroup('backgroundShadingType', {
this.properties.themeType != ThemeType.section && PropertyPaneChoiceGroup('backgroundShadingType', {
label: strings.BackgroundShadingTypeField,
options: [{
key: BackgroundShadingType.none,
@ -205,30 +216,11 @@ export default class FluentUiThemeVariantWebPart extends BaseClientSideWebPart<I
}
private generateInlineSvgForBackgroundShadingType(type: BackgroundShadingType): string {
let currentTheme = this.themeService.generateTheme(this.properties.themeType, type);
let currentTheme = this.themeService.generateTheme(this.properties.themeType, type, this.themeVariant, JSON.parse(this.properties.customPalette));
let backgroundColor = currentTheme.semanticColors.bodyBackground.replace("#", "%23");
let textColor = currentTheme.semanticColors.bodyText.replace("#", "%23");
return `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='45' height='45'%3E%3Crect x='0' y='0' width='45' height='45' stroke='black' stroke-width='2px' fill='${backgroundColor}'/%3E%3Ctext fill='${textColor}' font-family='Segoe UI, sans-serif' x='50%25' y='55%25' dominant-baseline='middle' text-anchor='middle'%3EAa%3C/text%3E%3C/svg%3E`;
}
private onCustomPropertyPaneFieldChanged = (targetProperty: string, oldValue: string, newValue: string) => {
if (targetProperty == "primaryColor" || targetProperty == "textColor" || targetProperty == "backgroundColor") {
this.themeService.setCustomColors(this.properties.primaryColor, this.properties.textColor, this.properties.backgroundColor);
//this.themeService.calculateContrastRatioPairs();
}
this.onPropertyPaneFieldChanged(targetProperty, oldValue, newValue);
}
// private getNonAccessiblePairsMessage(contrastRatios: IContrastRatioPair[]): string {
// let message = `Your color palette has ${this.themeService.getNonAccessiblePairs().length} accessibility errors:`;
// contrastRatios.forEach((element, index) => {
// message += (index > 0) ? `, ${element.colorPair}` : ` ${element.colorPair}`;
// });
// return message;
// }
}

View File

@ -1,9 +1,8 @@
import { ThemeProvider } from '@fluentui/react-theme-provider';
import { PartialTheme, Theme, ThemeProvider } from '@fluentui/react-theme-provider';
import { DefaultButton, DetailsList, DetailsListLayoutMode, Label, Link, PrimaryButton, Stack } from 'office-ui-fabric-react';
import * as React from 'react';
import { useEffect } from 'react';
import { PartialTheme, Theme } from '@fluentui/react-theme-provider';
export interface IFluentUiThemeVariantProps {
themeVariant: PartialTheme | Theme;

View File

@ -3,10 +3,9 @@ define([], function() {
"PropertyPaneDescription": "Web Part Configuration",
"BasicGroupName": "Core Settings",
"ThemeTypeField": "Theme Type",
"PrimaryColorField": "Primary Color",
"TextColorField": "Text Color",
"BackgroundColorField": "Background Color",
"BackgroundShadingTypeField": "Background Shading",
"CustomPaletteField": "Fluent UI Custom Palette",
"CustomPaletteMessageField": "Please use the 'Fluent UI Theme Designer' available at https://aka.ms/themedesigner to export the Theme you want to apply",
"Texts": {
"Current": "Current SharePoint Theme",
"Section": "Current Section Variation",

View File

@ -2,10 +2,9 @@ declare interface IFluentUiThemeVariantWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
ThemeTypeField: string;
PrimaryColorField: string;
TextColorField: string;
BackgroundColorField: string;
BackgroundShadingTypeField: string;
CustomPaletteField: string;
CustomPaletteMessageField: string;
Texts: {
Current: string;
Section: string;