import * as React from 'react';
import { loadTheme } from '@fluentui/react/lib/Styling';
import { IColor, getContrastRatio, isDark } from '@fluentui/react/lib/Color';
import {
ThemeGenerator,
themeRulesStandardCreator,
BaseSlots,
FabricSlots,
IThemeSlotRule,
IThemeRules,
} from '@fluentui/react/lib/ThemeGenerator';
import { Callout } from '@fluentui/react/lib/Callout';
import { ColorPicker } from '@fluentui/react/lib/ColorPicker';
import { ChoiceGroup } from '@fluentui/react/lib/ChoiceGroup';
import { TeachingBubbleBasicExample } from '../TeachingBubble/TeachingBubble.Basic.Example';
import { TextFieldBasicExample } from '../TextField/TextField.Basic.Example';
import { ToggleBasicExample } from '../../react/Toggle/Toggle.Basic.Example';
import { ProgressIndicatorBasicExample } from '../ProgressIndicator/ProgressIndicator.Basic.Example';
import { Async } from '@fluentui/utilities';
export interface IThemeGeneratorPageState {
themeRules: IThemeRules;
colorPickerSlotRule: IThemeSlotRule | null;
colorPickerElement: HTMLElement | null;
colorPickerVisible: boolean;
}
export class ThemeGeneratorPage extends React.Component<{}, IThemeGeneratorPageState> {
private _semanticSlotColorChangeTimeout: number;
private _async: Async;
constructor(props: {}) {
super(props);
this._async = new Async(this);
const themeRules = themeRulesStandardCreator();
ThemeGenerator.insureSlots(themeRules, isDark(themeRules[BaseSlots[BaseSlots.backgroundColor]].color!));
this.state = {
themeRules: themeRules,
colorPickerSlotRule: null,
colorPickerElement: null,
colorPickerVisible: false,
};
}
public componentWillUnmount(): void {
// remove temp styles
const root = document.querySelector('.App-content') as HTMLElement;
if (root) {
root.style.backgroundColor = '';
root.style.color = '';
}
document.body.style.backgroundColor = '';
document.body.style.color = '';
// and apply the default theme to overwrite any existing custom theme
loadTheme({});
this._async.dispose();
}
public render(): JSX.Element {
const { colorPickerVisible, colorPickerSlotRule, colorPickerElement } = this.state;
const fabricThemeSlots = [
this._fabricSlotWidget(FabricSlots.themeDarker),
this._fabricSlotWidget(FabricSlots.themeDark),
this._fabricSlotWidget(FabricSlots.themeDarkAlt),
this._fabricSlotWidget(FabricSlots.themePrimary),
this._fabricSlotWidget(FabricSlots.themeSecondary),
this._fabricSlotWidget(FabricSlots.themeTertiary),
this._fabricSlotWidget(FabricSlots.themeLight),
this._fabricSlotWidget(FabricSlots.themeLighter),
this._fabricSlotWidget(FabricSlots.themeLighterAlt),
];
const fabricNeutralForegroundSlots = [
this._fabricSlotWidget(FabricSlots.black),
this._fabricSlotWidget(FabricSlots.neutralDark),
this._fabricSlotWidget(FabricSlots.neutralPrimary),
this._fabricSlotWidget(FabricSlots.neutralPrimaryAlt),
this._fabricSlotWidget(FabricSlots.neutralSecondary),
this._fabricSlotWidget(FabricSlots.neutralTertiary),
];
const fabricNeutralBackgroundSlots = [
this._fabricSlotWidget(FabricSlots.neutralTertiaryAlt),
this._fabricSlotWidget(FabricSlots.neutralQuaternary),
this._fabricSlotWidget(FabricSlots.neutralQuaternaryAlt),
this._fabricSlotWidget(FabricSlots.neutralLight),
this._fabricSlotWidget(FabricSlots.neutralLighter),
this._fabricSlotWidget(FabricSlots.neutralLighterAlt),
this._fabricSlotWidget(FabricSlots.white),
];
const stylingUrl = 'https://github.com/microsoft/fluentui/tree/master/packages/style-utilities';
return (
Overview
{/* eslint-disable-next-line @fluentui/max-len */}
This tool helps you easily create all the shades and slots for a custom theme. The theme can be used by the{' '}
@fluentui/style-utilities package; see the{' '}
documentation
.
As you modify one of the three base colors, the theme will update automatically based on predefined rules.
You can modify each individual slot below as well.
{/* the shared popup color picker for slots */}
{colorPickerVisible && colorPickerSlotRule && colorPickerElement && (
)}
{/* the three base slots, prominently displayed at the top of the page */}
{[
this._baseColorSlotPicker(BaseSlots.primaryColor, 'Primary theme color'),
this._baseColorSlotPicker(BaseSlots.foregroundColor, 'Body text color'),
this._baseColorSlotPicker(BaseSlots.backgroundColor, 'Body background color'),
]}
{this._outputSection()}
Fabric palette
The original Fabric palette slots. These are raw colors with no prescriptive uses. Each one is a shade or tint
of a base color.
{fabricThemeSlots}
generally used for text and foregrounds
{fabricNeutralForegroundSlots}
generally used for backgrounds
{fabricNeutralBackgroundSlots}
Samples
Accessibility
Each pair of colors below should produce legible text and have a minimum contrast ratio of 4.5.
| Sample text |
Contrast ratio |
Slot pair |
{this._accessibilityTableBody()}
);
}
private _colorPickerOnDismiss = (): void => {
this.setState({ colorPickerVisible: false });
};
private _semanticSlotRuleChanged = (slotRule: IThemeSlotRule, ev: any, color: IColor): void => {
if (this._semanticSlotColorChangeTimeout) {
clearTimeout(this._semanticSlotColorChangeTimeout);
}
this._semanticSlotColorChangeTimeout = this._async.setTimeout(() => {
const { themeRules } = this.state;
ThemeGenerator.setSlot(
slotRule,
color.str,
isDark(themeRules[BaseSlots[BaseSlots.backgroundColor]].color!),
true,
true,
);
this.setState({ themeRules: themeRules }, this._makeNewTheme);
}, 20);
// 20ms is low enough that you can slowly drag to change color and see that theme,
// but high enough that quick changes don't get bogged down by a million changes in-between
};
private _onSwatchClick = (slotRule: IThemeSlotRule, ev: React.MouseEvent): void => {
const { colorPickerSlotRule, colorPickerElement } = this.state;
if (
colorPickerSlotRule !== null &&
colorPickerSlotRule !== undefined &&
!!colorPickerElement &&
colorPickerSlotRule === slotRule &&
colorPickerElement === ev.target
) {
// same one, close it
this.setState({ colorPickerVisible: false, colorPickerSlotRule: null, colorPickerElement: null });
} else {
// new one, open it
this.setState({
colorPickerVisible: true,
colorPickerSlotRule: slotRule,
colorPickerElement: ev.target as HTMLElement,
});
}
};
private _slotWidget = (slotRule: IThemeSlotRule): JSX.Element => {
return (
{this._colorSquareSwatchWidget(slotRule)}
{slotRule.name}
{!slotRule.isCustomized ?
Inherits from: {slotRule.inherits!.name}
:
Customized
}
);
};
private _fabricSlotWidget = (fabricSlot: FabricSlots): JSX.Element => {
return this._slotWidget(this.state.themeRules[FabricSlots[fabricSlot]]);
};
private _colorSquareSwatchWidget(slotRule: IThemeSlotRule): JSX.Element {
return (
);
}
private _accessibilityRow = (foreground: FabricSlots, background: FabricSlots): JSX.Element => {
const themeRules = this.state.themeRules;
const bgc: IColor = themeRules[FabricSlots[background]].color!;
const fgc: IColor = themeRules[FabricSlots[foreground]].color!;
const contrastRatio = getContrastRatio(bgc, fgc);
let contrastRatioString = String(contrastRatio);
contrastRatioString = contrastRatioString.substr(0, contrastRatioString.indexOf('.') + 3);
if (contrastRatio < 4.5) {
contrastRatioString = '**' + contrastRatioString + '**';
}
return (
| How vexingly quick daft zebras jump. |
{contrastRatioString} |
{FabricSlots[foreground] + ' + ' + FabricSlots[background]} |
);
};
private _accessibilityTableBody = (): JSX.Element => {
const accessibilityRows: JSX.Element[] = [
this._accessibilityRow(FabricSlots.neutralPrimary, FabricSlots.white), // default
// primary color also needs to be accessible, this is also strong variant default
this._accessibilityRow(FabricSlots.white, FabricSlots.themePrimary),
this._accessibilityRow(FabricSlots.neutralPrimary, FabricSlots.neutralLighter), // neutral variant default
this._accessibilityRow(FabricSlots.themeDark, FabricSlots.neutralLighter),
this._accessibilityRow(FabricSlots.neutralPrimary, FabricSlots.themeLighter),
]; // neutral variant with primary color
// these are the text and primary colors on top of the soft variant, whose bg depends on invertedness of
// the original theme
if (!isDark(this.state.themeRules[BaseSlots[BaseSlots.backgroundColor]].color!)) {
// is not inverted
accessibilityRows.push(
this._accessibilityRow(FabricSlots.neutralPrimary, FabricSlots.themeLighterAlt),
this._accessibilityRow(FabricSlots.themeDarkAlt, FabricSlots.themeLighterAlt),
);
} else {
// is inverted
accessibilityRows.push(
this._accessibilityRow(FabricSlots.neutralPrimary, FabricSlots.themeLight),
this._accessibilityRow(FabricSlots.themeDarkAlt, FabricSlots.themeLight),
);
}
return {accessibilityRows};
};
private _outputSection = (): JSX.Element => {
const themeRules = this.state.themeRules;
// strip out the unnecessary shade slots from the final output theme
const abridgedTheme: IThemeRules = {};
for (const ruleName in themeRules) {
if (themeRules.hasOwnProperty(ruleName)) {
if (
ruleName.indexOf('ColorShade') === -1 &&
ruleName !== 'primaryColor' &&
ruleName !== 'backgroundColor' &&
ruleName !== 'foregroundColor' &&
ruleName.indexOf('body') === -1
) {
abridgedTheme[ruleName] = themeRules[ruleName];
}
}
}
return (
);
};
private _makeNewTheme = (): void => {
const themeAsJson: { [key: string]: string } = ThemeGenerator.getThemeAsJson(this.state.themeRules);
// eslint-disable-next-line no-console
console.log('New theme...', themeAsJson);
const finalTheme = loadTheme({
...{ palette: themeAsJson },
isInverted: isDark(this.state.themeRules[BaseSlots[BaseSlots.backgroundColor]].color!),
});
const root = document.querySelector('.App-content') as HTMLElement;
if (root) {
root.style.backgroundColor = finalTheme.semanticColors.bodyBackground;
root.style.color = finalTheme.semanticColors.bodyText;
}
document.body.style.backgroundColor = finalTheme.semanticColors.bodyBackground;
document.body.style.color = finalTheme.semanticColors.bodyText;
// eslint-disable-next-line no-console
console.log('New theme:', finalTheme);
};
private _baseColorSlotPicker = (baseSlot: BaseSlots, title: string): JSX.Element => {
let colorChangeTimeout: number;
const onChange = (ev: any, newColor: IColor): void => {
if (colorChangeTimeout) {
clearTimeout(colorChangeTimeout);
}
colorChangeTimeout = this._async.setTimeout(() => {
const themeRules = this.state.themeRules;
const currentIsDark = isDark(themeRules[BaseSlots[BaseSlots.backgroundColor]].color!);
ThemeGenerator.setSlot(themeRules[BaseSlots[baseSlot]], newColor.str, currentIsDark, true, true);
if (currentIsDark !== isDark(themeRules[BaseSlots[BaseSlots.backgroundColor]].color!)) {
// isInverted got swapped, so need to refresh slots with new shading rules
ThemeGenerator.insureSlots(themeRules, !currentIsDark);
}
this.setState({ themeRules: themeRules }, this._makeNewTheme);
}, 20);
// 20ms is low enough that you can slowly drag to change color and see that theme,
// but high enough that quick changes don't get bogged down by a million changes in-between
};
return (
{title}
{[
this._colorSquareSwatchWidget(this.state.themeRules[BaseSlots[baseSlot] + 'Shade1']),
this._colorSquareSwatchWidget(this.state.themeRules[BaseSlots[baseSlot] + 'Shade2']),
this._colorSquareSwatchWidget(this.state.themeRules[BaseSlots[baseSlot] + 'Shade3']),
this._colorSquareSwatchWidget(this.state.themeRules[BaseSlots[baseSlot] + 'Shade4']),
this._colorSquareSwatchWidget(this.state.themeRules[BaseSlots[baseSlot] + 'Shade5']),
this._colorSquareSwatchWidget(this.state.themeRules[BaseSlots[baseSlot] + 'Shade6']),
this._colorSquareSwatchWidget(this.state.themeRules[BaseSlots[baseSlot] + 'Shade7']),
this._colorSquareSwatchWidget(this.state.themeRules[BaseSlots[baseSlot] + 'Shade8']),
]}
);
};
}