PnP React Controls Part 2 – Variant Theme Provider

Introduction

Hi friends, this is part 2 where I will explain the usage of the Variant Theme Provider control. We will how we can leverage this control on our SPFx project and make use of the features provided by this control. Thanks to the community.

About the Control

This control is a superset of the Theme Provider control which allows us to pass the FluentUI theme, variants and also the custom theme colors. It also allows us to not only themes but also provides us different variants which can cause the change to all the controls in the web part.

Focus on the Code

You can refer Part 1 post on the creation of the SPFx project and installing all the dependencies.

Let us see the file webpart.ts

import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
	IPropertyPaneConfiguration,
	PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import { IReadonlyTheme } from '@microsoft/sp-component-base';

import * as strings from 'VariantThemeWebPartStrings';
import VariantTheme from './components/VariantTheme';
import { IVariantThemeProps } from './components/IVariantThemeProps';

export interface IVariantThemeWebPartProps {
	description: string;
}

export default class VariantThemeWebPart extends BaseClientSideWebPart<IVariantThemeWebPartProps> {

	private _isDarkTheme: boolean = false;
	private _environmentMessage: string = '';
	protected themeVariant: IReadonlyTheme;

	protected onInit(): Promise<void> {
		this._environmentMessage = this._getEnvironmentMessage();

		return super.onInit();
	}

	public render(): void {
		const element: React.ReactElement<IVariantThemeProps> = React.createElement(
			VariantTheme,
			{
				description: this.properties.description,
				isDarkTheme: this._isDarkTheme,
				environmentMessage: this._environmentMessage,
				hasTeamsContext: !!this.context.sdks.microsoftTeams,
				userDisplayName: this.context.pageContext.user.displayName,
				theme: this.themeVariant
			}
		);

		ReactDom.render(element, this.domElement);
	}

	private _getEnvironmentMessage(): string {
		if (!!this.context.sdks.microsoftTeams) { // running in Teams
			return this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentTeams : strings.AppTeamsTabEnvironment;
		}

		return this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentSharePoint : strings.AppSharePointEnvironment;
	}

	protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
		if (!currentTheme) {
			return;
		}
		this.themeVariant = currentTheme;
		this._isDarkTheme = !!currentTheme.isInverted;
		const {
			semanticColors
		} = currentTheme;
		this.domElement.style.setProperty('--bodyText', semanticColors.bodyText);
		this.domElement.style.setProperty('--link', semanticColors.link);
		this.domElement.style.setProperty('--linkHovered', semanticColors.linkHovered);

	}

	protected onDispose(): void {
		ReactDom.unmountComponentAtNode(this.domElement);
	}

	protected get dataVersion(): Version {
		return Version.parse('1.0');
	}

	protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
		return {
			pages: [
				{
					header: {
						description: strings.PropertyPaneDescription
					},
					groups: [
						{
							groupName: strings.BasicGroupName,
							groupFields: [
								PropertyPaneTextField('description', {
									label: strings.DescriptionFieldLabel
								})
							]
						}
					]
				}
			]
		};
	}
}

Below are the changes that are done to the default file

  • Created a private variable for themeVariant so that we could pass it to the component and use it in the control. Since the props has to be passed to the component, we need to create a new props variable in the webpart props file.
  • Since from SPFx version 1.14.0, there is an enhancement to the themeChanged event, we can use the built-in method and assign our variable with the current theme.

Let us the file webpart.tsx

import * as React from 'react';
import styles from './VariantTheme.module.scss';
import { IVariantThemeProps } from './IVariantThemeProps';
import { VariantThemeProvider, VariantType } from "@pnp/spfx-controls-react/lib/VariantThemeProvider";
import { ITheme, Separator } from 'office-ui-fabric-react';
import { SampleControls } from './SampleControls';

export default class VariantTheme extends React.Component<IVariantThemeProps, {}> {
	private themeVariant: ITheme = this.props.theme as ITheme;
	public render(): React.ReactElement<IVariantThemeProps> {
		const {
			description,
			isDarkTheme,
			environmentMessage,
			hasTeamsContext,
			userDisplayName
		} = this.props;

		return (
			<>
				<VariantThemeProvider theme={this.props.theme} variantType={VariantType.None}>
					<SampleControls />
				</VariantThemeProvider>
				<Separator />
				<VariantThemeProvider theme={this.props.theme} variantType={VariantType.Neutral}>
					<SampleControls />
				</VariantThemeProvider>
				<Separator />
				<VariantThemeProvider theme={this.props.theme} variantType={VariantType.Soft}>
					<SampleControls />
				</VariantThemeProvider>
				<Separator />
				<VariantThemeProvider theme={this.props.theme} variantType={VariantType.Strong}>
					<SampleControls />
				</VariantThemeProvider>
			</>
		);
	}
}

Below are the changes done

  • Import VariantThemeProvider and VariantType from pnp controls
  • Created a separate component with the different controls to show the theme update
  • The separate component is used inside the VariantThemeProvider control where we pass the actual theme and also the variants provided by the control.
  • Below are the 4 different variant types that is used
    • None
    • Neutral
    • Soft
    • Strong

Let us see the file SampleControls.tsx

import { ActionButton, CommandButton, DefaultButton, IconButton, PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox';
import { ChoiceGroup, IChoiceGroupOption } from 'office-ui-fabric-react/lib/ChoiceGroup';
import { IContextualMenuProps } from 'office-ui-fabric-react/lib/ContextualMenu';
import { IIconProps } from 'office-ui-fabric-react/lib/Icon';
import { Label } from 'office-ui-fabric-react/lib/Label';
import { Link } from 'office-ui-fabric-react/lib/Link';
import { Rating, RatingSize } from 'office-ui-fabric-react/lib/Rating';
import { Slider } from 'office-ui-fabric-react/lib/Slider';
import { Stack } from 'office-ui-fabric-react/lib/Stack';
import { Toggle } from 'office-ui-fabric-react/lib/Toggle';
import * as React from 'react';

const menuProps: IContextualMenuProps = {
	items: [
		{
			key: 'emailMessage',
			text: 'Email message',
			iconProps: { iconName: 'Mail' },
		},
		{
			key: 'calendarEvent',
			text: 'Calendar event',
			iconProps: { iconName: 'Calendar' },
		},
	],
};
const addIcon: IIconProps = { iconName: 'Add' };
const emojiIcon: IIconProps = { iconName: 'Emoji2' };
const addFriendIcon: IIconProps = { iconName: 'AddFriend' };

const options: IChoiceGroupOption[] = [
	{ key: 'A', text: 'Option A' },
	{ key: 'B', text: 'Option B' },
];
const options1: IChoiceGroupOption[] = [
	{ key: 'day', text: 'Day', iconProps: { iconName: 'CalendarDay' } },
	{ key: 'week', text: 'Week', iconProps: { iconName: 'CalendarWeek' } },
	{ key: 'month', text: 'Month', iconProps: { iconName: 'Calendar' }, disabled: true },
];

export const SampleControls: React.FC<{}> = (props) => {
	return (
		<Stack tokens={{ childrenGap: 20, padding: 15 }} verticalAlign={'start'}>
			<Stack.Item>
				<Stack horizontal horizontalAlign='space-evenly'>
					<Stack.Item>
						<PrimaryButton text='Button 1'></PrimaryButton>
					</Stack.Item>
					<Stack.Item>
						<DefaultButton text='Default Button 1'></DefaultButton>
					</Stack.Item>
					<Stack.Item>
						<CommandButton iconProps={addIcon} text="New item" menuProps={menuProps} />
					</Stack.Item>
					<Stack.Item>
						<IconButton iconProps={emojiIcon} title="Emoji" ariaLabel="Emoji" />
					</Stack.Item>
					<Stack.Item>
						<IconButton menuProps={menuProps} iconProps={emojiIcon} title="Emoji" ariaLabel="Emoji" />
					</Stack.Item>
					<Stack.Item>
						<ActionButton iconProps={addFriendIcon} allowDisabledFocus>Create account</ActionButton>
					</Stack.Item>
				</Stack>
				<Stack horizontal horizontalAlign='space-evenly'>
					<Stack.Item>
						<Checkbox label="Unchecked checkbox (uncontrolled)" />
						<Checkbox label="Disabled checked checkbox" disabled defaultChecked />
					</Stack.Item>
					<Stack.Item>
						<ChoiceGroup defaultSelectedKey="B" options={options} label="Pick one" required={true} />
					</Stack.Item>
					<Stack.Item>
						<ChoiceGroup label="Pick one icon" defaultSelectedKey="day" options={options1} />
					</Stack.Item>
				</Stack>
				<Stack horizontal horizontalAlign='space-evenly'>
					<Stack.Item>
						<Label>I'm a Label</Label>
					</Stack.Item>
					<Stack.Item>
						<Toggle label="Enabled and checked" defaultChecked onText="On" offText="Off" />
					</Stack.Item>
					<Stack.Item>
						<Slider label="Snapping slider example" min={0} max={50} step={10} defaultValue={20} showValue snapToStep />
					</Stack.Item>
				</Stack>
				<Stack horizontal horizontalAlign='space-evenly'>
					<Stack.Item>
						When a link has an href,{' '}
						<Link href="https://developer.microsoft.com/en-us/fluentui#/controls/web/link" underline>
							it renders as an anchor tag.
						</Link>{' '}
						Without an href,{' '}
						<Link underline>
							the link is rendered as a button
						</Link>
					</Stack.Item>
				</Stack>
				<Stack horizontal horizontalAlign='space-evenly'>
					<Stack.Item>
						<Rating max={5} size={RatingSize.Large} ariaLabel="Large stars" ariaLabelFormat="{0} of {1} stars" />
					</Stack.Item>
					<Stack.Item>
						<Rating max={5} disabled ariaLabel="Disabled" ariaLabelFormat="{0} of {1} stars" />
					</Stack.Item>
				</Stack>
			</Stack.Item>
		</Stack>
	);
};

The above code doesn’t have any unique code. All the controls used in the above file were taken from FluentUI for demo purpose. Since we are wrapping all our controls inside the VariantThemeProvider control, the theme and the variant is automatically applied to the controls.

Following are some of the key properties provided by the VariantThemeProvider control

  • theme – The oob theme or a custom theme that control should use
  • variantType – The different types of variant type that can be applied.
  • themeColors – Custom theme colors define based on the three colors mentioned below
    • primaryColor
    • textColor
    • backgroundColor

Reference URL

VariantThemeProvider – @pnp/spfx-controls-react

Conclussion

I hope you had learned something about one of the pnp control. There are lot to come in future.

Please free to post your comments and let me know if you want me to post article on any particular feature or plugins that relates to SharePoint. Thanks for your support.

sudharsank/pnpcontrols-demo: Demo project on different PnP React Controls. (github.com)

Happy Coding…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s