From e4079adb2ab4fb09f58c71192665ff0b81e5f5ec Mon Sep 17 00:00:00 2001 From: Chris Holt Date: Tue, 2 Feb 2021 14:47:52 -0800 Subject: [PATCH] feat: add color recipes to web component packages (#16755) * feat: move color recipes into fluent web component package * Change files * udpate recipe paths * ensure tests and syntax are correct * add more typedoc comments * update FAST deps * add generated file to prettier ignore * Change files --- .prettierignore | 1 + ...chhol-add-color-recipes-to-wc-package.json | 8 + packages/web-components/.eslintrc.js | 1 + .../build/generate-default-palettes.js | 29 + packages/web-components/color.ts | 31 -- packages/web-components/docs/api-report.md | 501 +++++++++++++++++- packages/web-components/index-rollup.ts | 1 - packages/web-components/package.json | 10 +- .../web-components/src/card/card.stories.ts | 2 +- packages/web-components/src/card/index.ts | 3 +- packages/web-components/src/color.ts | 31 -- .../src/color/accent-fill.spec.ts | 91 ++++ .../web-components/src/color/accent-fill.ts | 175 ++++++ .../src/color/accent-foreground-cut.spec.ts | 30 ++ .../src/color/accent-foreground-cut.ts | 42 ++ .../src/color/accent-foreground.spec.ts | 111 ++++ .../src/color/accent-foreground.ts | 154 ++++++ .../src/color/accessible-recipe.ts | 96 ++++ .../src/color/color-constants.js | 19 + .../web-components/src/color/common.spec.ts | 86 +++ packages/web-components/src/color/common.ts | 229 ++++++++ .../src/color/create-color-palette.js | 14 + packages/web-components/src/color/index.ts | 109 ++++ .../src/color/neutral-divider.spec.ts | 13 + .../src/color/neutral-divider.ts | 18 + .../src/color/neutral-fill-card.spec.ts | 44 ++ .../src/color/neutral-fill-card.ts | 32 ++ .../src/color/neutral-fill-input.spec.ts | 121 +++++ .../src/color/neutral-fill-input.ts | 75 +++ .../src/color/neutral-fill-stealth.spec.ts | 121 +++++ .../src/color/neutral-fill-stealth.ts | 86 +++ .../src/color/neutral-fill-toggle.ts | 60 +++ .../src/color/neutral-fill.spec.ts | 122 +++++ .../web-components/src/color/neutral-fill.ts | 76 +++ .../src/color/neutral-focus.spec.ts | 20 + .../web-components/src/color/neutral-focus.ts | 57 ++ .../src/color/neutral-foreground-hint.spec.ts | 71 +++ .../src/color/neutral-foreground-hint.ts | 31 ++ .../src/color/neutral-foreground-toggle.ts | 43 ++ .../src/color/neutral-foreground.spec.ts | 91 ++++ .../src/color/neutral-foreground.ts | 60 +++ .../src/color/neutral-layer.spec.ts | 173 ++++++ .../web-components/src/color/neutral-layer.ts | 173 ++++++ .../src/color/neutral-outline-contrast.ts | 70 +++ .../src/color/neutral-outline.spec.ts | 93 ++++ .../src/color/neutral-outline.ts | 77 +++ .../web-components/src/color/palette.spec.ts | 346 ++++++++++++ packages/web-components/src/color/palette.ts | 326 ++++++++++++ .../web-components/src/default-palette.ts | 196 +++++++ .../src/design-system-provider/index.ts | 44 +- .../src/fluent-design-system.ts | 453 ++++++++++++++++ packages/web-components/src/index-rollup.ts | 1 - packages/web-components/src/index.ts | 4 +- .../web-components/src/styles/behaviors.ts | 8 +- .../src/tree-item/tree-item.styles.ts | 2 +- packages/web-components/src/utilities/math.ts | 61 +++ yarn.lock | 101 ++-- 57 files changed, 4871 insertions(+), 172 deletions(-) create mode 100644 change/@fluentui-web-components-2021-02-02-11-55-41-users-chhol-add-color-recipes-to-wc-package.json create mode 100644 packages/web-components/build/generate-default-palettes.js delete mode 100644 packages/web-components/color.ts delete mode 100644 packages/web-components/src/color.ts create mode 100644 packages/web-components/src/color/accent-fill.spec.ts create mode 100644 packages/web-components/src/color/accent-fill.ts create mode 100644 packages/web-components/src/color/accent-foreground-cut.spec.ts create mode 100644 packages/web-components/src/color/accent-foreground-cut.ts create mode 100644 packages/web-components/src/color/accent-foreground.spec.ts create mode 100644 packages/web-components/src/color/accent-foreground.ts create mode 100644 packages/web-components/src/color/accessible-recipe.ts create mode 100644 packages/web-components/src/color/color-constants.js create mode 100644 packages/web-components/src/color/common.spec.ts create mode 100644 packages/web-components/src/color/common.ts create mode 100644 packages/web-components/src/color/create-color-palette.js create mode 100644 packages/web-components/src/color/index.ts create mode 100644 packages/web-components/src/color/neutral-divider.spec.ts create mode 100644 packages/web-components/src/color/neutral-divider.ts create mode 100644 packages/web-components/src/color/neutral-fill-card.spec.ts create mode 100644 packages/web-components/src/color/neutral-fill-card.ts create mode 100644 packages/web-components/src/color/neutral-fill-input.spec.ts create mode 100644 packages/web-components/src/color/neutral-fill-input.ts create mode 100644 packages/web-components/src/color/neutral-fill-stealth.spec.ts create mode 100644 packages/web-components/src/color/neutral-fill-stealth.ts create mode 100644 packages/web-components/src/color/neutral-fill-toggle.ts create mode 100644 packages/web-components/src/color/neutral-fill.spec.ts create mode 100644 packages/web-components/src/color/neutral-fill.ts create mode 100644 packages/web-components/src/color/neutral-focus.spec.ts create mode 100644 packages/web-components/src/color/neutral-focus.ts create mode 100644 packages/web-components/src/color/neutral-foreground-hint.spec.ts create mode 100644 packages/web-components/src/color/neutral-foreground-hint.ts create mode 100644 packages/web-components/src/color/neutral-foreground-toggle.ts create mode 100644 packages/web-components/src/color/neutral-foreground.spec.ts create mode 100644 packages/web-components/src/color/neutral-foreground.ts create mode 100644 packages/web-components/src/color/neutral-layer.spec.ts create mode 100644 packages/web-components/src/color/neutral-layer.ts create mode 100644 packages/web-components/src/color/neutral-outline-contrast.ts create mode 100644 packages/web-components/src/color/neutral-outline.spec.ts create mode 100644 packages/web-components/src/color/neutral-outline.ts create mode 100644 packages/web-components/src/color/palette.spec.ts create mode 100644 packages/web-components/src/color/palette.ts create mode 100644 packages/web-components/src/default-palette.ts create mode 100644 packages/web-components/src/fluent-design-system.ts create mode 100644 packages/web-components/src/utilities/math.ts diff --git a/.prettierignore b/.prettierignore index e8b8fdd35f220d..e9a98a21414e08 100644 --- a/.prettierignore +++ b/.prettierignore @@ -25,3 +25,4 @@ change # package specific files packages/web-components/**/*.spec.ts +packages/web-components/src/default-palette.ts diff --git a/change/@fluentui-web-components-2021-02-02-11-55-41-users-chhol-add-color-recipes-to-wc-package.json b/change/@fluentui-web-components-2021-02-02-11-55-41-users-chhol-add-color-recipes-to-wc-package.json new file mode 100644 index 00000000000000..6414faec2759db --- /dev/null +++ b/change/@fluentui-web-components-2021-02-02-11-55-41-users-chhol-add-color-recipes-to-wc-package.json @@ -0,0 +1,8 @@ +{ + "type": "minor", + "comment": "feat: move color recipes into fluent web component package", + "packageName": "@fluentui/web-components", + "email": "chhol@microsoft.com", + "dependentChangeType": "patch", + "date": "2021-02-02T19:55:41.615Z" +} diff --git a/packages/web-components/.eslintrc.js b/packages/web-components/.eslintrc.js index 3c638d2827f4af..c9c21b0384052b 100644 --- a/packages/web-components/.eslintrc.js +++ b/packages/web-components/.eslintrc.js @@ -5,6 +5,7 @@ module.exports = { '@typescript-eslint/no-use-before-define': 'off', '@typescript-eslint/typedef': 'off', '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/interface-name-prefix': 'off', }, diff --git a/packages/web-components/build/generate-default-palettes.js b/packages/web-components/build/generate-default-palettes.js new file mode 100644 index 00000000000000..22bf988d8a3b97 --- /dev/null +++ b/packages/web-components/build/generate-default-palettes.js @@ -0,0 +1,29 @@ +import fs from 'fs'; +import path from 'path'; +import { parseColorHexRGB } from '@microsoft/fast-colors'; +import { createColorPalette } from '../src/color/create-color-palette'; +import { accentBaseColor, neutralBaseColor } from '../src/color/color-constants'; + +const outpath = path.resolve(__dirname, '../src/default-palette.ts'); + +/** + * Define palettes from base colors + */ +const neutralPalette = createColorPalette(parseColorHexRGB(neutralBaseColor)); +const accentPalette = createColorPalette(parseColorHexRGB(accentBaseColor)); + +const file = `/** + * DO NOT EDIT THIS FILE DIRECTLY + * This file generated by web-components/build/${path.parse(__filename).name}${path.parse(__filename).ext} + */ +export const neutralPalette: string[] = ${JSON.stringify(neutralPalette, null, 4)}; +export const accentPalette: string[] = ${JSON.stringify(accentPalette, null, 4)}; +`; + +fs.writeFile(outpath, file, error => { + if (error) { + throw error; + } else { + console.log('\nPalette data written to', outpath, '\n'); + } +}); diff --git a/packages/web-components/color.ts b/packages/web-components/color.ts deleted file mode 100644 index 71eb19907c9c9e..00000000000000 --- a/packages/web-components/color.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ColorRGBA64, parseColorHexRGB, parseColorWebRGB } from '@microsoft/fast-colors'; - -const cache = new Map(); -/** - * Converts a color string into a ColorRGBA64 instance. - * Supports #RRGGBB and rgb(r, g, b) formats - * - * @public - */ -export function parseColorString(color: string): ColorRGBA64 { - const cached: ColorRGBA64 | void = cache.get(color); - - if (!cached) { - let parsed: ColorRGBA64 | null = parseColorHexRGB(color); - - if (parsed === null) { - parsed = parseColorWebRGB(color); - } - - if (parsed === null) { - throw new Error( - `${color} cannot be converted to a ColorRGBA64. Color strings must be one of the following formats: "#RGB", "#RRGGBB", or "rgb(r, g, b)"`, - ); - } - - cache.set(color, parsed); - return parsed; - } - - return cached; -} diff --git a/packages/web-components/docs/api-report.md b/packages/web-components/docs/api-report.md index 0bf712ffe034a9..f5e36c37bdbf4b 100644 --- a/packages/web-components/docs/api-report.md +++ b/packages/web-components/docs/api-report.md @@ -13,10 +13,7 @@ import { Breadcrumb } from '@microsoft/fast-foundation'; import { BreadcrumbItem } from '@microsoft/fast-foundation'; import { Button } from '@microsoft/fast-foundation'; import { Checkbox } from '@microsoft/fast-foundation'; -import { ColorRGBA64 } from '@microsoft/fast-colors'; import { CSSCustomPropertyBehavior } from '@microsoft/fast-foundation'; -import { DensityOffset } from '@microsoft/fast-components-styles-msft'; -import { DesignSystem } from '@microsoft/fast-components-styles-msft'; import { DesignSystemProvider } from '@microsoft/fast-foundation'; import { Dialog } from '@microsoft/fast-foundation'; import { Direction } from '@microsoft/fast-web-utilities'; @@ -47,60 +44,163 @@ import { TreeView } from '@microsoft/fast-foundation'; // @internal (undocumented) export const AccentButtonStyles: ElementStyles; +// Warning: (ae-forgotten-export) The symbol "SwatchFamilyResolver" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "FillSwatchFamily" needs to be exported by the entry point index.d.ts +// Warning: (ae-internal-missing-underscore) The name "accentFill" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const accentFill: SwatchFamilyResolver; + +// Warning: (ae-forgotten-export) The symbol "SwatchRecipe" needs to be exported by the entry point index.d.ts +// Warning: (ae-internal-missing-underscore) The name "accentFillActive" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const accentFillActive: SwatchRecipe; + // @public export const accentFillActiveBehavior: CSSCustomPropertyBehavior; // @public export const accentFillFocusBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "accentFillHover" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const accentFillHover: SwatchRecipe; + // @public export const accentFillHoverBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "accentFillLarge" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const accentFillLarge: SwatchFamilyResolver; + +// Warning: (ae-internal-missing-underscore) The name "accentFillLargeActive" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const accentFillLargeActive: SwatchRecipe; + // @public export const accentFillLargeActiveBehavior: CSSCustomPropertyBehavior; // @public export const accentFillLargeFocusBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "accentFillLargeHover" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const accentFillLargeHover: SwatchRecipe; + // @public export const accentFillLargeHoverBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "accentFillLargeRest" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const accentFillLargeRest: SwatchRecipe; + // @public export const accentFillLargeRestBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "accentFillLargeSelected" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const accentFillLargeSelected: SwatchRecipe; + // @public export const accentFillLargeSelectedBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "accentFillRest" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const accentFillRest: SwatchRecipe; + // @public export const accentFillRestBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "accentFillSelected" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const accentFillSelected: SwatchRecipe; + // @public export const accentFillSelectedBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "accentForeground" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const accentForeground: SwatchFamilyResolver; + +// Warning: (ae-internal-missing-underscore) The name "accentForegroundActive" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const accentForegroundActive: SwatchRecipe; + // @public export const accentForegroundActiveBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "accentForegroundCut" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal +export const accentForegroundCut: SwatchRecipe; + +// Warning: (ae-internal-missing-underscore) The name "accentForegroundCutLarge" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal +export const accentForegroundCutLarge: SwatchRecipe; + // @public export const accentForegroundCutRestBehavior: CSSCustomPropertyBehavior; // @public export const accentForegroundFocusBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "accentForegroundHover" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const accentForegroundHover: SwatchRecipe; + // @public export const accentForegroundHoverBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "accentForegroundLarge" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const accentForegroundLarge: SwatchFamilyResolver; + +// Warning: (ae-internal-missing-underscore) The name "accentForegroundLargeActive" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const accentForegroundLargeActive: SwatchRecipe; + // @public export const accentForegroundLargeActiveBehavior: CSSCustomPropertyBehavior; // @public export const accentForegroundLargeFocusBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "accentForegroundLargeHover" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const accentForegroundLargeHover: SwatchRecipe; + // @public export const accentForegroundLargeHoverBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "accentForegroundLargeRest" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const accentForegroundLargeRest: SwatchRecipe; + // @public export const accentForegroundLargeRestBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "accentForegroundRest" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const accentForegroundRest: SwatchRecipe; + // @public export const accentForegroundRestBehavior: CSSCustomPropertyBehavior; @@ -150,6 +250,129 @@ export const CardStyles: import("@microsoft/fast-element").ElementStyles; // @public export const CheckboxStyles: import("@microsoft/fast-element").ElementStyles; +// @public +export function createColorPalette(baseColor: any): string[]; + +// @public +export interface DesignSystem { + accentBaseColor: string; + // (undocumented) + accentFillActiveDelta: number; + // (undocumented) + accentFillFocusDelta: number; + // (undocumented) + accentFillHoverDelta: number; + accentFillRestDelta: number; + // (undocumented) + accentFillSelectedDelta: number; + // (undocumented) + accentForegroundActiveDelta: number; + // (undocumented) + accentForegroundFocusDelta: number; + // (undocumented) + accentForegroundHoverDelta: number; + accentForegroundRestDelta: number; + accentPalette: string[]; + backgroundColor: string; + baseHeightMultiplier: number; + baseHorizontalSpacingMultiplier: number; + baseLayerLuminance: number; + cornerRadius: number; + density: number; + designUnit: number; + direction: Direction; + disabledOpacity: number; + elevatedCornerRadius?: number; + focusOutlineWidth: number; + neutralDividerRestDelta: number; + // (undocumented) + neutralFillActiveDelta: number; + neutralFillCardDelta: number; + // (undocumented) + neutralFillFocusDelta: number; + // (undocumented) + neutralFillHoverDelta: number; + // (undocumented) + neutralFillInputActiveDelta: number; + // (undocumented) + neutralFillInputFocusDelta: number; + // (undocumented) + neutralFillInputHoverDelta: number; + neutralFillInputRestDelta: number; + // (undocumented) + neutralFillInputSelectedDelta: number; + // (undocumented) + neutralFillRestDelta: number; + // (undocumented) + neutralFillSelectedDelta: number; + // (undocumented) + neutralFillStealthActiveDelta: number; + // (undocumented) + neutralFillStealthFocusDelta: number; + // (undocumented) + neutralFillStealthHoverDelta: number; + neutralFillStealthRestDelta: number; + // (undocumented) + neutralFillStealthSelectedDelta: number; + // (undocumented) + neutralFillToggleActiveDelta: number; + // (undocumented) + neutralFillToggleFocusDelta: number; + neutralFillToggleHoverDelta: number; + // (undocumented) + neutralForegroundActiveDelta: number; + // (undocumented) + neutralForegroundFocusDelta: number; + neutralForegroundHoverDelta: number; + // (undocumented) + neutralOutlineActiveDelta: number; + // (undocumented) + neutralOutlineFocusDelta: number; + // (undocumented) + neutralOutlineHoverDelta: number; + neutralOutlineRestDelta: number; + neutralPalette: string[]; + outlineWidth: number; + // (undocumented) + typeRampBaseFontSize: string; + // (undocumented) + typeRampBaseLineHeight: string; + // (undocumented) + typeRampMinus1FontSize: string; + // (undocumented) + typeRampMinus1LineHeight: string; + typeRampMinus2FontSize: string; + // (undocumented) + typeRampMinus2LineHeight: string; + // (undocumented) + typeRampPlus1FontSize: string; + // (undocumented) + typeRampPlus1LineHeight: string; + // (undocumented) + typeRampPlus2FontSize: string; + // (undocumented) + typeRampPlus2LineHeight: string; + // (undocumented) + typeRampPlus3FontSize: string; + // (undocumented) + typeRampPlus3LineHeight: string; + // (undocumented) + typeRampPlus4FontSize: string; + // (undocumented) + typeRampPlus4LineHeight: string; + // (undocumented) + typeRampPlus5FontSize: string; + // (undocumented) + typeRampPlus5LineHeight: string; + // (undocumented) + typeRampPlus6FontSize: string; + // (undocumented) + typeRampPlus6LineHeight: string; +} + +// @public +export const DesignSystemDefaults: DesignSystem; + // @public export const DialogStyles: import("@microsoft/fast-element").ElementStyles; @@ -265,6 +488,8 @@ export class FluentDesignSystemProvider extends DesignSystemProvider implements baseLayerLuminance: number; // (undocumented) cornerRadius: number; + // Warning: (ae-forgotten-export) The symbol "DensityOffset" needs to be exported by the entry point index.d.ts + // // (undocumented) density: DensityOffset; // (undocumented) @@ -490,6 +715,9 @@ export const inlineEndBehavior: CSSCustomPropertyBehavior; // @public export const inlineStartBehavior: CSSCustomPropertyBehavior; +// @public (undocumented) +export function isDarkMode(designSystem: DesignSystem): boolean; + // Warning: (ae-internal-missing-underscore) The name "LightweightButtonStyles" should be prefixed with an underscore because the declaration is marked as @internal // // @internal (undocumented) @@ -504,129 +732,373 @@ export const MenuItemStyles: import("@microsoft/fast-element").ElementStyles; // @public export const MenuStyles: import("@microsoft/fast-element").ElementStyles; +// Warning: (ae-internal-missing-underscore) The name "neutralDividerRest" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralDividerRest: SwatchRecipe; + // @public export const neutralDividerRestBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-forgotten-export) The symbol "ColorRecipe" needs to be exported by the entry point index.d.ts +// Warning: (ae-internal-missing-underscore) The name "neutralFill" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralFill: ColorRecipe; + +// Warning: (ae-internal-missing-underscore) The name "neutralFillActive" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralFillActive: SwatchRecipe; + // @public export const neutralFillActiveBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-forgotten-export) The symbol "Swatch" needs to be exported by the entry point index.d.ts +// Warning: (ae-internal-missing-underscore) The name "neutralFillCard" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function neutralFillCard(designSystem: DesignSystem): Swatch; + +// Warning: (ae-forgotten-export) The symbol "SwatchResolver" needs to be exported by the entry point index.d.ts +// +// @internal (undocumented) +export function neutralFillCard(backgroundResolver: SwatchResolver): SwatchResolver; + // @public export const neutralFillCardRestBehavior: CSSCustomPropertyBehavior; // @public export const neutralFillFocusBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralFillHover" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralFillHover: SwatchRecipe; + // @public export const neutralFillHoverBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralFillInput" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralFillInput: ColorRecipe; + +// Warning: (ae-internal-missing-underscore) The name "neutralFillInputActive" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralFillInputActive: SwatchRecipe; + // @public export const neutralFillInputActiveBehavior: CSSCustomPropertyBehavior; // @public export const neutralFillInputFocusBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralFillInputHover" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralFillInputHover: SwatchRecipe; + // @public export const neutralFillInputHoverBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralFillInputRest" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralFillInputRest: SwatchRecipe; + // @public export const neutralFillInputRestBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralFillInputSelected" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralFillInputSelected: SwatchRecipe; + +// Warning: (ae-internal-missing-underscore) The name "neutralFillRest" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralFillRest: SwatchRecipe; + // @public export const neutralFillRestBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralFillSelected" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralFillSelected: SwatchRecipe; + // @public export const neutralFillSelectedBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralFillStealth" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralFillStealth: ColorRecipe; + +// Warning: (ae-internal-missing-underscore) The name "neutralFillStealthActive" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralFillStealthActive: ColorRecipe; + // @public export const neutralFillStealthActiveBehavior: CSSCustomPropertyBehavior; // @public export const neutralFillStealthFocusBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralFillStealthHover" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralFillStealthHover: ColorRecipe; + // @public export const neutralFillStealthHoverBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralFillStealthRest" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralFillStealthRest: ColorRecipe; + // @public export const neutralFillStealthRestBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralFillStealthSelected" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralFillStealthSelected: ColorRecipe; + // @public export const neutralFillStealthSelectedBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralFillToggle" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralFillToggle: SwatchFamilyResolver; + +// Warning: (ae-internal-missing-underscore) The name "neutralFillToggleActive" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralFillToggleActive: SwatchRecipe; + // @public export const neutralFillToggleActiveBehavior: CSSCustomPropertyBehavior; // @public export const neutralFillToggleFocusBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralFillToggleHover" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralFillToggleHover: SwatchRecipe; + // @public export const neutralFillToggleHoverBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralFillToggleRest" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralFillToggleRest: SwatchRecipe; + // @public export const neutralFillToggleRestBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralFocus" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralFocus: ColorRecipe; + // @public export const neutralFocusBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-forgotten-export) The symbol "DesignSystemResolver" needs to be exported by the entry point index.d.ts +// Warning: (ae-internal-missing-underscore) The name "neutralFocusInnerAccent" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function neutralFocusInnerAccent(accentFillColor: DesignSystemResolver): DesignSystemResolver; + // @public export const neutralFocusInnerAccentBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralForeground" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralForeground: SwatchFamilyResolver; + +// Warning: (ae-internal-missing-underscore) The name "neutralForegroundActive" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralForegroundActive: SwatchRecipe; + // @public export const neutralForegroundActiveBehavior: CSSCustomPropertyBehavior; // @public export const neutralForegroundFocusBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralForegroundHint" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal +export const neutralForegroundHint: SwatchRecipe; + // @public export const neutralForegroundHintBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralForegroundHintLarge" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal +export const neutralForegroundHintLarge: SwatchRecipe; + // @public export const neutralForegroundHintLargeBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralForegroundHover" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralForegroundHover: SwatchRecipe; + // @public export const neutralForegroundHoverBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralForegroundRest" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralForegroundRest: SwatchRecipe; + // @public export const neutralForegroundRestBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralForegroundToggle" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal +export const neutralForegroundToggle: SwatchRecipe; + // @public export const neutralForegroundToggleBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralForegroundToggleLarge" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal +export const neutralForegroundToggleLarge: SwatchRecipe; + // @public export const neutralForegroundToggleLargeBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralLayerCard" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal +export const neutralLayerCard: ColorRecipe; + // @public export const neutralLayerCardBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralLayerCardContainer" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal +export const neutralLayerCardContainer: ColorRecipe; + // @public export const neutralLayerCardContainerBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralLayerFloating" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal +export const neutralLayerFloating: ColorRecipe; + // @public export const neutralLayerFloatingBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralLayerL1" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal +export const neutralLayerL1: ColorRecipe; + +// Warning: (ae-internal-missing-underscore) The name "neutralLayerL1Alt" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal +export const neutralLayerL1Alt: ColorRecipe; + // @public export const neutralLayerL1AltBehavior: CSSCustomPropertyBehavior; // @public export const neutralLayerL1Behavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralLayerL2" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal +export const neutralLayerL2: ColorRecipe; + // @public export const neutralLayerL2Behavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralLayerL3" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal +export const neutralLayerL3: ColorRecipe; + // @public export const neutralLayerL3Behavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralLayerL4" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal +export const neutralLayerL4: ColorRecipe; + // @public export const neutralLayerL4Behavior: CSSCustomPropertyBehavior; +// Warning: (ae-forgotten-export) The symbol "SwatchFamily" needs to be exported by the entry point index.d.ts +// Warning: (ae-internal-missing-underscore) The name "neutralOutline" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralOutline: ColorRecipe; + +// Warning: (ae-internal-missing-underscore) The name "neutralOutlineActive" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralOutlineActive: SwatchRecipe; + // @public export const neutralOutlineActiveBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralOutlineContrast" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralOutlineContrast: ColorRecipe; + +// Warning: (ae-internal-missing-underscore) The name "neutralOutlineContrastActive" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralOutlineContrastActive: SwatchRecipe; + +// Warning: (ae-internal-missing-underscore) The name "neutralOutlineContrastHover" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralOutlineContrastHover: SwatchRecipe; + +// Warning: (ae-internal-missing-underscore) The name "neutralOutlineContrastRest" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralOutlineContrastRest: SwatchRecipe; + // @public export const neutralOutlineFocusBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralOutlineHover" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralOutlineHover: SwatchRecipe; + // @public export const neutralOutlineHoverBehavior: CSSCustomPropertyBehavior; +// Warning: (ae-internal-missing-underscore) The name "neutralOutlineRest" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const neutralOutlineRest: SwatchRecipe; + // @public export const neutralOutlineRestBehavior: CSSCustomPropertyBehavior; @@ -639,7 +1111,20 @@ export const OptionStyles: import("@microsoft/fast-element").ElementStyles; export const OutlineButtonStyles: ElementStyles; // @public -export function parseColorString(color: string): ColorRGBA64; +export type Palette = Swatch[]; + +// Warning: (ae-internal-missing-underscore) The name "palette" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal @deprecated +export function palette(paletteType: PaletteType): DesignSystemResolver; + +// @public @deprecated +export enum PaletteType { + // (undocumented) + accent = "accent", + // (undocumented) + neutral = "neutral" +} // @public export const ProgressRingStyles: import("@microsoft/fast-element").ElementStyles; @@ -665,6 +1150,14 @@ export const SliderLabelStyles: import("@microsoft/fast-element").ElementStyles; // @public export const SliderStyles: import("@microsoft/fast-element").ElementStyles; +// @public +export enum StandardLuminance { + // (undocumented) + DarkMode = 0.23, + // (undocumented) + LightMode = 1 +} + // Warning: (ae-internal-missing-underscore) The name "StealthButtonStyles" should be prefixed with an underscore because the declaration is marked as @internal // // @internal (undocumented) diff --git a/packages/web-components/index-rollup.ts b/packages/web-components/index-rollup.ts index ddca04ce3dbb01..b78f3062c71787 100644 --- a/packages/web-components/index-rollup.ts +++ b/packages/web-components/index-rollup.ts @@ -1,4 +1,3 @@ export * from './index'; export * from '@microsoft/fast-element'; export * from '@microsoft/fast-foundation'; -export { createColorPalette } from '@microsoft/fast-components-styles-msft'; diff --git a/packages/web-components/package.json b/packages/web-components/package.json index 00e9b15c04e4d7..e3dc9410b0a938 100644 --- a/packages/web-components/package.json +++ b/packages/web-components/package.json @@ -22,7 +22,7 @@ "clean": "node ./build/clean.js dist", "doc": "api-extractor run --local", "doc:ci": "api-extractor run --local", - "build": "tsc -p ./tsconfig.json && rollup -c && npm run doc", + "build": "yarn generate-default-palettes && tsc -p ./tsconfig.json && rollup -c && npm run doc", "dev": "tsc -p ./tsconfig.json -w", "tdd": "npm run dev & npm run test-chrome:watch", "prepare": "yarn clean && yarn build", @@ -33,6 +33,7 @@ "code-style": "npm run prettier && npm run lint", "start": "start-storybook -p 6006", "build-storybook": "build-storybook", + "generate-default-palettes": "node -r esm build/generate-default-palettes.js", "test": "yarn doc:ci && yarn test-chrome:verbose", "test-node": "mocha --reporter min --exit dist/esm/__test__/setup-node.js './dist/esm/**/*.spec.js'", "test-node:verbose": "mocha --reporter spec --exit dist/esm/__test__/setup-node.js './dist/esm/**/*.spec.js'", @@ -53,10 +54,12 @@ "@storybook/html": "^5.3.8", "@storybook/theming": "^5.3.8", "@types/chai": "^4.2.11", + "@types/chai-spies": "^1.0.1", "@types/karma": "^5.0.0", "@types/mocha": "^7.0.2", "@types/webpack-env": "1.16.0", "chai": "^4.2.0", + "chai-spies": "1.0.0", "circular-dependency-plugin": "^5.0.2", "esm": "^3.2.25", "ignore-loader": "^0.1.2", @@ -95,9 +98,8 @@ "dependencies": { "lodash-es": "^4.17.20", "@microsoft/fast-colors": "^5.1.0", - "@microsoft/fast-components-styles-msft": "^4.29.0", - "@microsoft/fast-element": "^0.21.1", - "@microsoft/fast-foundation": "^1.11.1", + "@microsoft/fast-element": "^0.22.0", + "@microsoft/fast-foundation": "^1.12.0", "tslib": "^1.13.0" }, "beachball": { diff --git a/packages/web-components/src/card/card.stories.ts b/packages/web-components/src/card/card.stories.ts index 92cfc35a50966f..39f027175e919f 100644 --- a/packages/web-components/src/card/card.stories.ts +++ b/packages/web-components/src/card/card.stories.ts @@ -1,5 +1,5 @@ -import { createColorPalette } from '@microsoft/fast-components-styles-msft'; import { ColorRGBA64 } from '@microsoft/fast-colors'; +import { createColorPalette } from '../color/create-color-palette'; import { FluentDesignSystemProvider } from '../design-system-provider'; import CardTemplate from './fixtures/card.html'; import { FluentCard } from './'; diff --git a/packages/web-components/src/card/index.ts b/packages/web-components/src/card/index.ts index 1caa1e22a2e7c6..35730b046be628 100644 --- a/packages/web-components/src/card/index.ts +++ b/packages/web-components/src/card/index.ts @@ -6,7 +6,8 @@ import { designSystemProvider, CardTemplate as template, } from '@microsoft/fast-foundation'; -import { createColorPalette, DesignSystem, neutralFillCard } from '@microsoft/fast-components-styles-msft'; +import { createColorPalette, neutralFillCard } from '../color'; +import { DesignSystem } from '../fluent-design-system'; import { CardStyles as styles } from './card.styles'; /** diff --git a/packages/web-components/src/color.ts b/packages/web-components/src/color.ts deleted file mode 100644 index 71eb19907c9c9e..00000000000000 --- a/packages/web-components/src/color.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ColorRGBA64, parseColorHexRGB, parseColorWebRGB } from '@microsoft/fast-colors'; - -const cache = new Map(); -/** - * Converts a color string into a ColorRGBA64 instance. - * Supports #RRGGBB and rgb(r, g, b) formats - * - * @public - */ -export function parseColorString(color: string): ColorRGBA64 { - const cached: ColorRGBA64 | void = cache.get(color); - - if (!cached) { - let parsed: ColorRGBA64 | null = parseColorHexRGB(color); - - if (parsed === null) { - parsed = parseColorWebRGB(color); - } - - if (parsed === null) { - throw new Error( - `${color} cannot be converted to a ColorRGBA64. Color strings must be one of the following formats: "#RGB", "#RRGGBB", or "rgb(r, g, b)"`, - ); - } - - cache.set(color, parsed); - return parsed; - } - - return cached; -} diff --git a/packages/web-components/src/color/accent-fill.spec.ts b/packages/web-components/src/color/accent-fill.spec.ts new file mode 100644 index 00000000000000..c213c48cec3eb3 --- /dev/null +++ b/packages/web-components/src/color/accent-fill.spec.ts @@ -0,0 +1,91 @@ +import { expect } from "chai"; +import { + accentBaseColor, + accentPalette as getAccentPalette, + DesignSystem, + DesignSystemDefaults, + neutralPalette as getNeutralPalette, +} from "../fluent-design-system"; +import { + accentFillActive, + accentFillHover, + accentFillLargeActive, + accentFillLargeHover, + accentFillLargeRest, + accentFillLargeSelected, + accentFillRest, + accentFillSelected, +} from "./accent-fill"; +import { findClosestSwatchIndex, Palette } from "./palette"; +import { contrast, Swatch } from "./common"; +import { accentForegroundCut } from "./accent-foreground-cut"; + +describe("accentFill", (): void => { + const neutralPalette: Palette = getNeutralPalette(DesignSystemDefaults); + const accentPalette: Palette = getAccentPalette(DesignSystemDefaults); + + const accentIndex: number = findClosestSwatchIndex( + getAccentPalette, + accentBaseColor(DesignSystemDefaults) + )(DesignSystemDefaults); + + it("should operate on design system defaults", (): void => { + [ + accentFillActive, + accentFillHover, + accentFillLargeActive, + accentFillLargeHover, + accentFillLargeRest, + accentFillLargeSelected, + accentFillRest, + accentFillSelected, + ].forEach(fn => { + expect(accentPalette).to.include(fn({} as DesignSystem)); + }); + }); + + it("should accept a function that resolves a background swatch", (): void => { + expect(typeof accentFillRest(() => "#FFF")).to.equal("function"); + expect(accentFillRest(() => "#000")({} as DesignSystem)).to.equal(accentPalette[63]); + }); + + it("should have accessible rest and hover colors against accentForegroundCut", (): void => { + const accentColors: Swatch[] = [ + "#0078D4", + "#107C10", + "#5C2D91", + "#D83B01", + "#F2C812", + ]; + + accentColors.forEach((accent: Swatch): void => { + neutralPalette.forEach((swatch: Swatch): void => { + const designSystem: DesignSystem = Object.assign( + {}, + DesignSystemDefaults, + { + backgroundColor: swatch, + accentPaletteSource: ["#FFF", accent, "#000"], + } + ); + + const accentForegroundCutColor: Swatch = accentForegroundCut( + designSystem + ); + + expect( + contrast(accentForegroundCutColor, accentFillRest(designSystem)) + ).to.be.gte(4.5); + expect( + contrast(accentForegroundCutColor, accentFillHover(designSystem)) + ).to.be.gte(4.5); + expect( + contrast(accentForegroundCutColor, accentFillLargeRest(designSystem)) + ).to.be.gte(3); + expect( + contrast(accentForegroundCutColor, accentFillLargeHover(designSystem)) + ).to.be.gte(3); + }); + }); + }); +}); diff --git a/packages/web-components/src/color/accent-fill.ts b/packages/web-components/src/color/accent-fill.ts new file mode 100644 index 00000000000000..c18032ba93faa7 --- /dev/null +++ b/packages/web-components/src/color/accent-fill.ts @@ -0,0 +1,175 @@ +import { inRange } from 'lodash-es'; +import { + accentBaseColor, + accentFillActiveDelta, + accentFillFocusDelta, + accentFillHoverDelta, + accentFillSelectedDelta, + accentPalette, + DesignSystem, + DesignSystemResolver, + neutralFillActiveDelta, + neutralFillHoverDelta, + neutralFillRestDelta, +} from '../fluent-design-system'; +import { accentForegroundCut } from './accent-foreground-cut'; +import { + colorRecipeFactory, + contrast, + designSystemResolverMax, + FillSwatchFamily, + Swatch, + SwatchFamilyResolver, + swatchFamilyToSwatchRecipeFactory, + SwatchFamilyType, + SwatchRecipe, +} from './common'; +import { findClosestBackgroundIndex, findClosestSwatchIndex, getSwatch, isDarkMode, Palette } from './palette'; + +const neutralFillThreshold: DesignSystemResolver = designSystemResolverMax( + neutralFillRestDelta, + neutralFillHoverDelta, + neutralFillActiveDelta, +); + +function accentFillAlgorithm(contrastTarget: number): DesignSystemResolver { + return (designSystem: DesignSystem): FillSwatchFamily => { + const palette: Palette = accentPalette(designSystem); + const paletteLength: number = palette.length; + const accent: Swatch = accentBaseColor(designSystem); + const textColor: Swatch = accentForegroundCut( + Object.assign({}, designSystem, { + backgroundColor: accent, + }), + ); + const hoverDelta: number = accentFillHoverDelta(designSystem); + + // Use the hover direction that matches the neutral fill recipe. + const backgroundIndex: number = findClosestBackgroundIndex(designSystem); + const swapThreshold: number = neutralFillThreshold(designSystem); + const direction: 1 | -1 = backgroundIndex >= swapThreshold ? -1 : 1; + const maxIndex: number = paletteLength - 1; + const accentIndex: number = findClosestSwatchIndex(accentPalette, accent)(designSystem); + + let accessibleOffset: number = 0; + + // Move the accent color the direction of hover, while maintaining the foreground color. + while ( + accessibleOffset < direction * hoverDelta && + inRange(accentIndex + accessibleOffset + direction, 0, paletteLength) && + contrast(palette[accentIndex + accessibleOffset + direction], textColor) >= contrastTarget && + inRange(accentIndex + accessibleOffset + direction + direction, 0, maxIndex) + ) { + accessibleOffset += direction; + } + + const hoverIndex: number = accentIndex + accessibleOffset; + const restIndex: number = hoverIndex + direction * -1 * hoverDelta; + const activeIndex: number = restIndex + direction * accentFillActiveDelta(designSystem); + const focusIndex: number = restIndex + direction * accentFillFocusDelta(designSystem); + + return { + rest: getSwatch(restIndex, palette), + hover: getSwatch(hoverIndex, palette), + active: getSwatch(activeIndex, palette), + focus: getSwatch(focusIndex, palette), + selected: getSwatch( + restIndex + + (isDarkMode(designSystem) + ? accentFillSelectedDelta(designSystem) * -1 + : accentFillSelectedDelta(designSystem)), + palette, + ), + }; + }; +} + +/** + * @internal + */ +export const accentFill: SwatchFamilyResolver = colorRecipeFactory(accentFillAlgorithm(4.5)); + +/** + * @internal + */ +export const accentFillLarge: SwatchFamilyResolver = colorRecipeFactory(accentFillAlgorithm(3)); + +/** + * @internal + */ +export const accentFillRest: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.rest, + accentFill, +); + +/** + * @internal + */ +export const accentFillHover: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.hover, + accentFill, +); + +/** + * @internal + */ +export const accentFillActive: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.active, + accentFill, +); + +/** + * @internal + */ +export const accentFillFocus: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.focus, + accentFill, +); + +/** + * @internal + */ +export const accentFillSelected: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.selected, + accentFill, +); + +/** + * @internal + */ +export const accentFillLargeRest: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.rest, + accentFillLarge, +); + +/** + * @internal + */ +export const accentFillLargeHover: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.hover, + accentFillLarge, +); + +/** + * @internal + */ +export const accentFillLargeActive: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.active, + accentFillLarge, +); + +/** + * @internal + */ +export const accentFillLargeFocus: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.focus, + accentFillLarge, +); + +/** + * @internal + */ +export const accentFillLargeSelected: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.selected, + accentFillLarge, +); diff --git a/packages/web-components/src/color/accent-foreground-cut.spec.ts b/packages/web-components/src/color/accent-foreground-cut.spec.ts new file mode 100644 index 00000000000000..c58e65cdf66399 --- /dev/null +++ b/packages/web-components/src/color/accent-foreground-cut.spec.ts @@ -0,0 +1,30 @@ +import { expect } from "chai"; +import { + DesignSystemDefaults, DesignSystem +} from "../fluent-design-system"; +import { accentForegroundCut, accentForegroundCutLarge } from "./accent-foreground-cut"; +import { Swatch } from "./common"; + +describe("Cut text", (): void => { + it("should return white by by default", (): void => { + expect(accentForegroundCut(undefined as any)).to.equal("#FFFFFF"); + expect(accentForegroundCutLarge(undefined as any)).to.equal("#FFFFFF"); + }); + it("should return black when background does not meet contrast ratio", (): void => { + expect(accentForegroundCut((): Swatch => "#FFF")({} as any)).to.equal("#000000"); + expect(accentForegroundCutLarge((): Swatch => "#FFF")({} as any)).to.equal("#000000"); + + expect( + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + accentForegroundCut((designSystem: DesignSystem) => "#FFF")( + DesignSystemDefaults + ) + ).to.equal("#000000"); + expect( + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + accentForegroundCutLarge((designSystem: DesignSystem) => "#FFF")( + DesignSystemDefaults + ) + ).to.equal("#000000"); + }); +}); diff --git a/packages/web-components/src/color/accent-foreground-cut.ts b/packages/web-components/src/color/accent-foreground-cut.ts new file mode 100644 index 00000000000000..087d07b4758679 --- /dev/null +++ b/packages/web-components/src/color/accent-foreground-cut.ts @@ -0,0 +1,42 @@ +import { accentBaseColor, DesignSystem } from '../fluent-design-system'; +import { black, white } from './color-constants'; +import { contrast, Swatch, SwatchRecipe, SwatchResolver } from './common'; + +/** + * Function to derive accentForegroundCut from an input background and target contrast ratio + */ +const accentForegroundCutAlgorithm: (backgroundColor: Swatch, targetContrast: number) => Swatch = ( + backgroundColor: Swatch, + targetContrast: number, +): Swatch => { + return contrast(white, backgroundColor) >= targetContrast ? white : black; +}; + +/** + * Factory to create a accent-foreground-cut function that operates on a target contrast ratio + */ +function accentForegroundCutFactory(targetContrast: number): SwatchRecipe { + function accentForegroundCutInternal(designSystem: DesignSystem): Swatch; + function accentForegroundCutInternal(backgroundResolver: SwatchResolver): SwatchResolver; + function accentForegroundCutInternal(arg: any): any { + return typeof arg === 'function' + ? (designSystem: DesignSystem): Swatch => { + return accentForegroundCutAlgorithm(arg(designSystem), targetContrast); + } + : accentForegroundCutAlgorithm(accentBaseColor(arg), targetContrast); + } + + return accentForegroundCutInternal; +} + +/** + * @internal + * Cut text for normal sized text, less than 18pt normal weight + */ +export const accentForegroundCut: SwatchRecipe = accentForegroundCutFactory(4.5); + +/** + * @internal + * Cut text for large sized text, greater than 18pt or 16pt and bold + */ +export const accentForegroundCutLarge: SwatchRecipe = accentForegroundCutFactory(3); diff --git a/packages/web-components/src/color/accent-foreground.spec.ts b/packages/web-components/src/color/accent-foreground.spec.ts new file mode 100644 index 00000000000000..8c47dae160320f --- /dev/null +++ b/packages/web-components/src/color/accent-foreground.spec.ts @@ -0,0 +1,111 @@ +import { parseColorHexRGB } from "@microsoft/fast-colors"; +import { expect } from "chai"; +import { + accentPalette as getAccentPalette, + DesignSystem, + DesignSystemDefaults, + neutralPalette as getNeutralPalette, +} from "../fluent-design-system"; +import { + accentForegroundActive, + accentForegroundHover, + accentForegroundLargeActive, + accentForegroundLargeHover, + accentForegroundLargeRest, + accentForegroundRest, +} from "./accent-foreground"; +import { Palette } from "./palette"; +import { contrast, Swatch } from "./common"; + +describe("accentForeground", (): void => { + const neutralPalette: Palette = getNeutralPalette(DesignSystemDefaults); + const accentPalette: Palette = getAccentPalette(DesignSystemDefaults); + + it("should operate on design system defaults", (): void => { + expect(accentForegroundRest({} as DesignSystem)).to.equal(accentPalette[59]); + expect(accentForegroundHover({} as DesignSystem)).to.equal(accentPalette[65]); + expect(accentForegroundActive({} as DesignSystem)).to.equal(accentPalette[55]); + expect(accentForegroundLargeRest({} as DesignSystem)).to.equal(accentPalette[59]); + expect(accentForegroundLargeHover({} as DesignSystem)).to.equal(accentPalette[65]); + expect(accentForegroundLargeActive({} as DesignSystem)).to.equal(accentPalette[55]); + }); + + it("should accept a function that resolves a background swatch", (): void => { + expect(typeof accentForegroundRest(() => "#FFF")).to.equal("function"); + expect(accentForegroundRest(() => "#000")({} as DesignSystem)).to.equal( + accentPalette[59] + ); + expect(typeof accentForegroundRest(() => "#FFFFFF")).to.equal("function"); + expect(accentForegroundRest(() => "#000000")({} as DesignSystem)).to.equal( + accentPalette[59] + ); + }); + + it("should increase contrast on hover state and decrease contrast on active state in either mode", (): void => { + expect( + accentPalette.indexOf(accentForegroundHover(DesignSystemDefaults)) + ).to.be.greaterThan( + accentPalette.indexOf(accentForegroundRest(DesignSystemDefaults)) + ); + expect( + accentPalette.indexOf(accentForegroundActive(DesignSystemDefaults)) + ).to.be.lessThan(accentPalette.indexOf(accentForegroundRest(DesignSystemDefaults))); + + const darkDesignSystem: DesignSystem = Object.assign({}, DesignSystemDefaults, { + backgroundColor: "#000", + }); + expect( + accentPalette.indexOf(accentForegroundHover(darkDesignSystem)) + ).to.be.lessThan(accentPalette.indexOf(accentForegroundRest(darkDesignSystem))); + expect( + accentPalette.indexOf(accentForegroundActive(darkDesignSystem)) + ).to.be.greaterThan(accentPalette.indexOf(accentForegroundRest(darkDesignSystem))); + }); + + it("should have accessible rest and hover colors against the background color", (): void => { + const accentColors: Swatch[] = [ + "#0078D4", + "#107C10", + "#5C2D91", + "#D83B01", + "#F2C812", + ]; + + accentColors.forEach( + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + (accent: Swatch): void => { + neutralPalette.forEach((swatch: Swatch): void => { + const designSystem: DesignSystem = Object.assign( + {}, + DesignSystemDefaults, + { + backgroundColor: swatch, + accentPaletteConfig: Object.assign({}, { + steps: 94, + clipLight: 0, + clipDark: 0, + }, { + baseColor: parseColorHexRGB(swatch), + }), + } + ); + + expect( + contrast(swatch, accentForegroundRest(designSystem)) + // There are a few states that are impossible to meet contrast on + ).to.be.gte(4.47); + expect( + contrast(swatch, accentForegroundHover(designSystem)) + // There are a few states that are impossible to meet contrast on + ).to.be.gte(3.7); + expect( + contrast(swatch, accentForegroundLargeRest(designSystem)) + ).to.be.gte(3); + expect( + contrast(swatch, accentForegroundLargeHover(designSystem)) + ).to.be.gte(3); + }); + } + ); + }); +}); diff --git a/packages/web-components/src/color/accent-foreground.ts b/packages/web-components/src/color/accent-foreground.ts new file mode 100644 index 00000000000000..86706f8e0d8bca --- /dev/null +++ b/packages/web-components/src/color/accent-foreground.ts @@ -0,0 +1,154 @@ +import { + accentBaseColor, + accentForegroundActiveDelta, + accentForegroundFocusDelta, + accentForegroundHoverDelta, + accentForegroundRestDelta, + accentPalette, + backgroundColor, + DesignSystem, + DesignSystemResolver, +} from '../fluent-design-system'; +import { findClosestSwatchIndex, findSwatchIndex, getSwatch, isDarkMode, Palette, swatchByContrast } from './palette'; +import { + colorRecipeFactory, + Swatch, + SwatchFamily, + SwatchFamilyResolver, + swatchFamilyToSwatchRecipeFactory, + SwatchFamilyType, + SwatchRecipe, +} from './common'; + +function accentForegroundAlgorithm(contrastTarget: number): DesignSystemResolver { + return (designSystem: DesignSystem): SwatchFamily => { + const palette: Palette = accentPalette(designSystem); + const accent: Swatch = accentBaseColor(designSystem); + const accentIndex: number = findClosestSwatchIndex(accentPalette, accent)(designSystem); + + const stateDeltas: any = { + rest: accentForegroundRestDelta(designSystem), + hover: accentForegroundHoverDelta(designSystem), + active: accentForegroundActiveDelta(designSystem), + focus: accentForegroundFocusDelta(designSystem), + }; + + const direction: 1 | -1 = isDarkMode(designSystem) ? -1 : 1; + + const startIndex: number = + accentIndex + + (direction === 1 + ? Math.min(stateDeltas.rest, stateDeltas.hover) + : Math.max(direction * stateDeltas.rest, direction * stateDeltas.hover)); + + const accessibleSwatch: Swatch = swatchByContrast( + backgroundColor, // Compare swatches against the background + )( + accentPalette, // Use the accent palette + )( + () => startIndex, // Begin searching based on accent index, direction, and deltas + )( + () => direction, // Search direction based on light/dark mode + )( + (swatchContrast: number) => swatchContrast >= contrastTarget, // A swatch is only valid if the contrast is greater than indicated + )( + designSystem, // Pass the design system + ); + + // One of these will be rest, the other will be hover. Depends on the offsets and the direction. + const accessibleIndex1: number = findSwatchIndex(accentPalette, accessibleSwatch)(designSystem); + const accessibleIndex2: number = accessibleIndex1 + direction * Math.abs(stateDeltas.rest - stateDeltas.hover); + + const indexOneIsRestState: boolean = + direction === 1 + ? stateDeltas.rest < stateDeltas.hover + : direction * stateDeltas.rest > direction * stateDeltas.hover; + + const restIndex: number = indexOneIsRestState ? accessibleIndex1 : accessibleIndex2; + const hoverIndex: number = indexOneIsRestState ? accessibleIndex2 : accessibleIndex1; + + const activeIndex: number = restIndex + direction * stateDeltas.active; + const focusIndex: number = restIndex + direction * stateDeltas.focus; + + return { + rest: getSwatch(restIndex, palette), + hover: getSwatch(hoverIndex, palette), + active: getSwatch(activeIndex, palette), + focus: getSwatch(focusIndex, palette), + }; + }; +} + +/** + * @internal + */ +export const accentForeground: SwatchFamilyResolver = colorRecipeFactory(accentForegroundAlgorithm(4.5)); + +/** + * @internal + */ +export const accentForegroundLarge: SwatchFamilyResolver = colorRecipeFactory(accentForegroundAlgorithm(3)); + +/** + * @internal + */ +export const accentForegroundRest: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.rest, + accentForeground, +); + +/** + * @internal + */ +export const accentForegroundHover: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.hover, + accentForeground, +); + +/** + * @internal + */ +export const accentForegroundActive: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.active, + accentForeground, +); + +/** + * @internal + */ +export const accentForegroundFocus: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.focus, + accentForeground, +); + +/** + * @internal + */ +export const accentForegroundLargeRest: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.rest, + accentForegroundLarge, +); + +/** + * @internal + */ +export const accentForegroundLargeHover: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.hover, + accentForegroundLarge, +); + +/** + * @internal + */ +export const accentForegroundLargeActive: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.active, + accentForegroundLarge, +); + +/** + * @internal + */ +export const accentForegroundLargeFocus: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.focus, + accentForegroundLarge, +); diff --git a/packages/web-components/src/color/accessible-recipe.ts b/packages/web-components/src/color/accessible-recipe.ts new file mode 100644 index 00000000000000..ae4a2927db901d --- /dev/null +++ b/packages/web-components/src/color/accessible-recipe.ts @@ -0,0 +1,96 @@ +import { + backgroundColor, + DesignSystem, + DesignSystemResolver, + evaluateDesignSystemResolver, +} from '../fluent-design-system'; +import { Swatch, SwatchFamily } from './common'; +import { + findSwatchIndex, + getSwatch, + isDarkMode, + minContrastTargetFactory, + Palette, + referenceColorInitialIndexResolver, + swatchByContrast, +} from './palette'; + +function indexToSwatchFamily( + accessibleIndex: number, + palette: Palette, + direction: number, + restDelta: number, + hoverDelta: number, + activeDelta: number, + focusDelta: number, +): SwatchFamily { + // One of the indexes will be rest, the other will be hover. Depends on the offsets and the direction. + const accessibleIndex2: number = accessibleIndex + direction * Math.abs(restDelta - hoverDelta); + + const indexOneIsRestState: boolean = + direction === 1 ? restDelta < hoverDelta : direction * restDelta > direction * hoverDelta; + + const restIndex: number = indexOneIsRestState ? accessibleIndex : accessibleIndex2; + const hoverIndex: number = indexOneIsRestState ? accessibleIndex2 : accessibleIndex; + + const activeIndex: number = restIndex + direction * activeDelta; + const focusIndex: number = restIndex + direction * focusDelta; + + return { + rest: getSwatch(restIndex, palette), + hover: getSwatch(hoverIndex, palette), + active: getSwatch(activeIndex, palette), + focus: getSwatch(focusIndex, palette), + }; +} + +/** + * @internal + * Function to derive accessible colors from contrast and delta configuration. + * Performs a simple contrast check against the colors and returns + * the color that has the most contrast against the background. If contrast + * cannot be retrieved correctly, function returns black. + */ +export function accessibleAlgorithm( + palette: Palette | DesignSystemResolver, + minContrast: number | DesignSystemResolver, + restDelta: number | DesignSystemResolver, + hoverDelta: number | DesignSystemResolver, + activeDelta: number | DesignSystemResolver, + focusDelta: number | DesignSystemResolver, +): DesignSystemResolver { + return (designSystem: DesignSystem): SwatchFamily => { + const resolvedPalette: Palette = evaluateDesignSystemResolver(palette, designSystem); + const direction: 1 | -1 = isDarkMode(designSystem) ? -1 : 1; + + const accessibleSwatch: Swatch = swatchByContrast( + backgroundColor, // Compare swatches against the background + )( + resolvedPalette, // Use the provided palette + )( + referenceColorInitialIndexResolver, // Begin searching from the background color + )( + () => direction, // Search direction based on light/dark mode + )( + minContrastTargetFactory(evaluateDesignSystemResolver(minContrast, designSystem)), // A swatch is only valid if the contrast is greater than indicated + )( + designSystem, // Pass the design system + ); + + const accessibleIndex: number = findSwatchIndex(palette, accessibleSwatch)(designSystem); + const resolvedRest: number = evaluateDesignSystemResolver(restDelta, designSystem); + const resolvedHover: number = evaluateDesignSystemResolver(hoverDelta, designSystem); + const resolvedActive: number = evaluateDesignSystemResolver(activeDelta, designSystem); + const resolvedFocus: number = evaluateDesignSystemResolver(focusDelta, designSystem); + + return indexToSwatchFamily( + accessibleIndex, + resolvedPalette, + direction, + resolvedRest, + resolvedHover, + resolvedActive, + resolvedFocus, + ); + }; +} diff --git a/packages/web-components/src/color/color-constants.js b/packages/web-components/src/color/color-constants.js new file mode 100644 index 00000000000000..e24ad5eca39659 --- /dev/null +++ b/packages/web-components/src/color/color-constants.js @@ -0,0 +1,19 @@ +/** + * @internal + */ +export const white = '#FFFFFF'; + +/** + * @internal + */ +export const black = '#000000'; + +/** + * @internal + */ +export const accentBaseColor = '#0078D4'; + +/** + * @internal + */ +export const neutralBaseColor = '#808080'; diff --git a/packages/web-components/src/color/common.spec.ts b/packages/web-components/src/color/common.spec.ts new file mode 100644 index 00000000000000..29b2ad61dc1ba4 --- /dev/null +++ b/packages/web-components/src/color/common.spec.ts @@ -0,0 +1,86 @@ +import { expect } from "chai"; +import { ColorRGBA64 } from "@microsoft/fast-colors"; +import { colorMatches, contrast, isValidColor, parseColorString } from "./common"; + +describe("isValidColor", (): void => { + it("should return true when input is a hex color", (): void => { + expect(isValidColor("#000")).to.be.ok; + expect(isValidColor("#000000")).to.be.ok; + }); + it("should return false when input is not a color", (): void => { + expect(isValidColor(undefined as any)).to.not.be.ok; + expect(isValidColor(null as any)).to.not.be.ok; + expect(isValidColor("ooggabooga")).to.not.be.ok; + }); +}); + +describe("colorMatches", (): void => { + it("should throw arguments are not colors", (): void => { + expect((): void => { + colorMatches("dksfjd", "weeeeeeee"); + }).to.throw(); + }); + + it("should return true if colors are the same", (): void => { + expect(colorMatches("#F00", "rgb(255, 0, 0)")).to.be.ok; + expect(colorMatches("#000", "rgb(0, 0, 0)")).to.be.ok; + expect(colorMatches("#FFF", "rgb(255, 255, 255)")).to.be.ok; + expect(colorMatches("#FF0000", "rgb(255, 0, 0)")).to.be.ok; + expect(colorMatches("#000000", "rgb(0, 0, 0)")).to.be.ok; + expect(colorMatches("#FFFFFF", "rgb(255, 255, 255)")).to.be.ok; + }); + + it("should return false if colors are not the same", (): void => { + expect(colorMatches("#000", "#023")).to.not.be.ok; + expect(colorMatches("#000", "#001")).to.not.be.ok; + expect(colorMatches("#F00", "rgb(255, 0, 1)")).to.not.be.ok; + expect(colorMatches("#000000", "#002233")).to.not.be.ok; + expect(colorMatches("#000000", "#000011")).to.not.be.ok; + expect(colorMatches("#FF0000", "rgb(255, 0, 1)")).to.not.be.ok; + }); +}); + +describe("parseColorString", (): void => { + it("should parse #RGB color strings", (): void => { + expect(parseColorString("#FFF") instanceof ColorRGBA64).to.equal(true); + }); + it("should parse #RRGGBB color strings", (): void => { + expect(parseColorString("#001122") instanceof ColorRGBA64).to.equal(true); + }); + it("should throw if the color is a malformed shorthand hex", (): void => { + expect((): void => { + parseColorString("#GGG"); + }).to.throw(); + }); + it("should throw if the color is a malformed hex", (): void => { + expect((): void => { + parseColorString("#zzzzzz"); + }).to.throw(); + }); + it("should throw if the color is a malformed rgb", (): void => { + expect((): void => { + parseColorString("rgb(256, 244, 30)"); + }).to.throw(); + }); + it("should throw if the color is a rgba", (): void => { + expect((): void => { + parseColorString("rgba(255, 244, 30, 1)"); + }).to.throw(); + }); +}); + +describe("contrast", (): void => { + it("should return the contrast between two colors", (): void => { + expect(contrast("#000", "#FFF")).to.equal(21); + expect(contrast("#000000", "#FFFFFF")).to.equal(21); + expect(contrast("rgb(0, 0, 0)", "rgb(255, 255, 255)")).to.equal(21); + }); + it("should throw if either color cannot be converted to a color", (): void => { + expect((): void => { + contrast("oogabooga", "#000"); + }).to.throw(); + expect((): void => { + contrast("#000", "oogabooga"); + }).to.throw(); + }); +}); diff --git a/packages/web-components/src/color/common.ts b/packages/web-components/src/color/common.ts new file mode 100644 index 00000000000000..926c90bd874275 --- /dev/null +++ b/packages/web-components/src/color/common.ts @@ -0,0 +1,229 @@ +import { + ColorRGBA64, + contrastRatio, + isColorStringHexRGB, + isColorStringWebRGB, + parseColorHexRGB, + parseColorWebRGB, + rgbToRelativeLuminance, +} from '@microsoft/fast-colors'; +import { memoize } from 'lodash-es'; +import { DesignSystem, DesignSystemResolver } from '../fluent-design-system'; + +/** + * Describes the format of a single color in a palette + */ +export type Swatch = string; + +/** + * Interface describing a family of swatches. + */ +export interface SwatchFamily { + /** + * The swatch to apply to the rest state + */ + rest: Swatch; + + /** + * The swatch to apply to the hover state + */ + hover: Swatch; + + /** + * The swatch to apply to the active state + */ + active: Swatch; + + /** + * The swatch to apply to the focus state + */ + focus: Swatch; +} + +/** + * Interface describing a family of swatches applied as fills + */ +export interface FillSwatchFamily extends SwatchFamily { + /** + * The swatch to apply to the selected state + */ + selected: Swatch; +} + +/** + * A DesignSystemResolver that resolves a Swatch + */ +export type SwatchResolver = DesignSystemResolver; + +/** + * A function that accepts a design system and resolves a SwatchFamily or FillSwatchFamily + */ +export type SwatchFamilyResolver = DesignSystemResolver; + +/** + * A function type that resolves a Swatch from a SwatchResolver + * and applies it to the backgroundColor property of the design system + * of the returned DesignSystemResolver + */ +export type DesignSystemResolverFromSwatchResolver = (resolver: SwatchResolver) => DesignSystemResolver; + +/** + * @internal + * The states that a swatch can have + */ +export enum SwatchFamilyType { + rest = 'rest', + hover = 'hover', + active = 'active', + focus = 'focus', + selected = 'selected', +} + +export type ColorRecipe = DesignSystemResolver & DesignSystemResolverFromSwatchResolver; + +export function colorRecipeFactory(recipe: DesignSystemResolver): ColorRecipe { + const memoizedRecipe: typeof recipe = memoize(recipe); + + function curryRecipe(designSystem: DesignSystem): T; + function curryRecipe(backgroundResolver: SwatchResolver): (designSystem: DesignSystem) => T; + function curryRecipe(arg: any): any { + if (typeof arg === 'function') { + return (designSystem: DesignSystem): T => { + return memoizedRecipe( + Object.assign({}, designSystem, { + backgroundColor: arg(designSystem), + }), + ); + }; + } else { + return memoizedRecipe(arg); + } + } + + return curryRecipe; +} + +/** + * A function to apply a named style or recipe. A ColorRecipe has several behaviors: + * 1. When provided a callback function, the color Recipe returns a function that expects a design-system. + * When called, the returned function will call the callback function with the input design-system and apply + * the result of that function as background to the recipe. This is useful for applying text recipes to colors + * other than the design system backgroundColor + * 2. When provided a design system, the recipe will use that design-system to generate the color + */ +export type SwatchRecipe = ColorRecipe; + +/** + * @internal + * Helper function to transform a SwatchFamilyResolver into simple ColorRecipe for simple use + * use in stylesheets. + */ +export function swatchFamilyToSwatchRecipeFactory( + type: keyof T, + callback: SwatchFamilyResolver, +): SwatchRecipe { + const memoizedRecipe: typeof callback = memoize(callback); + return (arg: DesignSystem | SwatchResolver): any => { + if (typeof arg === 'function') { + return (designSystem: DesignSystem): Swatch => { + return memoizedRecipe( + Object.assign({}, designSystem, { + backgroundColor: arg(designSystem), + }), + )[type as string]; + }; + } else { + return memoizedRecipe(arg)[type]; + } + }; +} + +const cache = new Map(); + +/** + * Converts a color string into a ColorRGBA64 instance. + * Supports #RRGGBB and rgb(r, g, b) formats + * + * @public + */ +export function parseColorString(color: string): ColorRGBA64 { + const cached: ColorRGBA64 | void = cache.get(color); + + if (!cached) { + let parsed: ColorRGBA64 | null = parseColorHexRGB(color); + + if (parsed === null) { + parsed = parseColorWebRGB(color); + } + + if (parsed === null) { + throw new Error( + `${color} cannot be converted to a ColorRGBA64. Color strings must be one of the following formats: "#RGB", "#RRGGBB", or "rgb(r, g, b)"`, + ); + } + + cache.set(color, parsed); + return parsed; + } + + return cached; +} + +/** + * @internal + * Determines if a string value represents a color + * Supports #RRGGBB and rgb(r, g, b) formats + */ +export function isValidColor(color: string): boolean { + return isColorStringHexRGB(color) || isColorStringWebRGB(color); +} + +/** + * @internal + * Determines if a color string matches another color. + * Supports #RRGGBB and rgb(r, g, b) formats + */ +export function colorMatches(a: string, b: string): boolean { + return parseColorString(a).equalValue(parseColorString(b)); +} + +/** + * @internal + * Returns the contrast value between two color strings. + * Supports #RRGGBB and rgb(r, g, b) formats. + */ +export const contrast: (a: string, b: string) => number = memoize( + (a: string, b: string): number => { + return contrastRatio(parseColorString(a), parseColorString(b)); + }, + (a: string, b: string): string => a + b, +); + +/** + * @internal + * Returns the relative luminance of a color. If the value is not a color, -1 will be returned + * Supports #RRGGBB and rgb(r, g, b) formats + */ +export function luminance(color: string): number { + return rgbToRelativeLuminance(parseColorString(color)); +} + +/** + * @internal + */ +export function designSystemResolverMax(...args: Array>): DesignSystemResolver { + return (designSystem: DesignSystem): number => + Math.max.apply( + null, + args.map((fn: DesignSystemResolver) => fn(designSystem)), + ); +} + +/** + * @internal + */ +export const clamp: (value: number, min: number, max: number) => number = ( + value: number, + min: number, + max: number, +): number => Math.min(Math.max(value, min), max); diff --git a/packages/web-components/src/color/create-color-palette.js b/packages/web-components/src/color/create-color-palette.js new file mode 100644 index 00000000000000..633bc8ff4dda2c --- /dev/null +++ b/packages/web-components/src/color/create-color-palette.js @@ -0,0 +1,14 @@ +import { ComponentStateColorPalette } from '@microsoft/fast-colors'; + +/** + * Creates a color palette from a provided source color + * @param baseColor - ColorRGBA64 + * @returns string[] + * + * @public + */ +export function createColorPalette(baseColor) { + return new ComponentStateColorPalette({ + baseColor, + }).palette.map(color => color.toStringHexRGB().toUpperCase()); +} diff --git a/packages/web-components/src/color/index.ts b/packages/web-components/src/color/index.ts new file mode 100644 index 00000000000000..d9eac0e6cd8ca5 --- /dev/null +++ b/packages/web-components/src/color/index.ts @@ -0,0 +1,109 @@ +/** + * Text exports + */ +export { + neutralForeground, + neutralForegroundRest, + neutralForegroundHover, + neutralForegroundActive, +} from './neutral-foreground'; + +export { neutralForegroundToggle, neutralForegroundToggleLarge } from './neutral-foreground-toggle'; + +export { accentForegroundCut, accentForegroundCutLarge } from './accent-foreground-cut'; + +export { neutralForegroundHint, neutralForegroundHintLarge } from './neutral-foreground-hint'; + +export { + accentForeground, + accentForegroundRest, + accentForegroundHover, + accentForegroundActive, + accentForegroundLarge, + accentForegroundLargeRest, + accentForegroundLargeHover, + accentForegroundLargeActive, +} from './accent-foreground'; + +/** + * Fill exports + */ +export { neutralFill, neutralFillRest, neutralFillHover, neutralFillActive, neutralFillSelected } from './neutral-fill'; + +export { + neutralFillStealth, + neutralFillStealthRest, + neutralFillStealthHover, + neutralFillStealthActive, + neutralFillStealthSelected, +} from './neutral-fill-stealth'; + +export { + neutralFillToggle, + neutralFillToggleRest, + neutralFillToggleHover, + neutralFillToggleActive, +} from './neutral-fill-toggle'; + +export { + neutralFillInput, + neutralFillInputRest, + neutralFillInputHover, + neutralFillInputActive, + neutralFillInputSelected, +} from './neutral-fill-input'; + +export { + accentFill, + accentFillRest, + accentFillHover, + accentFillActive, + accentFillSelected, + accentFillLarge, + accentFillLargeRest, + accentFillLargeHover, + accentFillLargeActive, + accentFillLargeSelected, +} from './accent-fill'; + +export { neutralFillCard } from './neutral-fill-card'; + +/** + * Border exports + */ +export { + neutralOutlineContrast, + neutralOutlineContrastRest, + neutralOutlineContrastHover, + neutralOutlineContrastActive, +} from './neutral-outline-contrast'; + +export { neutralOutline, neutralOutlineRest, neutralOutlineHover, neutralOutlineActive } from './neutral-outline'; + +export { neutralDividerRest } from './neutral-divider'; + +/** + * App layer exports + */ +export { + neutralLayerFloating, + neutralLayerCard, + neutralLayerCardContainer, + neutralLayerL1, + neutralLayerL1Alt, + neutralLayerL2, + neutralLayerL3, + neutralLayerL4, + StandardLuminance, +} from './neutral-layer'; + +/** + * Focus colors + */ +export { neutralFocus, neutralFocusInnerAccent } from './neutral-focus'; + +/** + * Export supporting types + */ +export { isDarkMode, palette, PaletteType, Palette } from './palette'; +export { createColorPalette } from './create-color-palette'; diff --git a/packages/web-components/src/color/neutral-divider.spec.ts b/packages/web-components/src/color/neutral-divider.spec.ts new file mode 100644 index 00000000000000..ce2ef1f8f43d8e --- /dev/null +++ b/packages/web-components/src/color/neutral-divider.spec.ts @@ -0,0 +1,13 @@ +import { expect } from "chai"; +import { DesignSystemDefaults } from "../fluent-design-system"; +import { neutralDividerRest } from "./neutral-divider"; + +describe("neutralDividerRest", (): void => { + it("should return a string when invoked with an object", (): void => { + expect(typeof neutralDividerRest(DesignSystemDefaults)).to.equal("string"); + }); + + it("should return a function when invoked with a function", (): void => { + expect(typeof neutralDividerRest(() => "#FFF")).to.equal("function"); + }); +}); diff --git a/packages/web-components/src/color/neutral-divider.ts b/packages/web-components/src/color/neutral-divider.ts new file mode 100644 index 00000000000000..32f17315ff4394 --- /dev/null +++ b/packages/web-components/src/color/neutral-divider.ts @@ -0,0 +1,18 @@ +import { DesignSystem, neutralDividerRestDelta, neutralPalette } from '../fluent-design-system'; +import { findClosestBackgroundIndex, getSwatch, isDarkMode, Palette } from './palette'; +import { colorRecipeFactory, Swatch, SwatchRecipe, SwatchResolver } from './common'; + +const neutralDividerAlgorithm: SwatchResolver = (designSystem: DesignSystem): Swatch => { + const palette: Palette = neutralPalette(designSystem); + const backgroundIndex: number = findClosestBackgroundIndex(designSystem); + const delta: number = neutralDividerRestDelta(designSystem); + const direction: 1 | -1 = isDarkMode(designSystem) ? -1 : 1; + + const index: number = backgroundIndex + direction * delta; + return getSwatch(index, palette); +}; + +/** + * @internal + */ +export const neutralDividerRest: SwatchRecipe = colorRecipeFactory(neutralDividerAlgorithm); diff --git a/packages/web-components/src/color/neutral-fill-card.spec.ts b/packages/web-components/src/color/neutral-fill-card.spec.ts new file mode 100644 index 00000000000000..f51f4f86839af2 --- /dev/null +++ b/packages/web-components/src/color/neutral-fill-card.spec.ts @@ -0,0 +1,44 @@ +import { expect } from "chai"; +import { DesignSystem, DesignSystemDefaults } from "../fluent-design-system"; +import { neutralFillCard } from "./neutral-fill-card"; + +describe("neutralFillCard", (): void => { + it("should operate on design system defaults", (): void => { + expect(neutralFillCard({} as DesignSystem)).to.equal( + DesignSystemDefaults.neutralPalette[DesignSystemDefaults.neutralFillCardDelta] + ); + }); + it("should get darker when the index of the backgroundColor is lower than the offset index", (): void => { + for (let i: number = 0; i < DesignSystemDefaults.neutralFillCardDelta; i++) { + expect( + DesignSystemDefaults.neutralPalette.indexOf( + neutralFillCard( + Object.assign({}, DesignSystemDefaults, { + backgroundColor: DesignSystemDefaults.neutralPalette[i], + }) + ) + ) + ).to.equal(DesignSystemDefaults.neutralFillCardDelta + i); + } + }); + it("should return the color at three steps lower than the background color", (): void => { + for (let i: number = 3; i < DesignSystemDefaults.neutralPalette.length; i++) { + expect( + DesignSystemDefaults.neutralPalette.indexOf( + neutralFillCard( + Object.assign({}, DesignSystemDefaults, { + backgroundColor: DesignSystemDefaults.neutralPalette[i], + }) + ) + ) + ).to.equal(i - 3); + } + }); + it("should generate a color based on the background color returned by a provided callback", (): void => { + expect( + neutralFillCard(() => DesignSystemDefaults.neutralPalette[4])( + DesignSystemDefaults + ) + ).to.equal(DesignSystemDefaults.neutralPalette[1]); + }); +}); diff --git a/packages/web-components/src/color/neutral-fill-card.ts b/packages/web-components/src/color/neutral-fill-card.ts new file mode 100644 index 00000000000000..54249992570014 --- /dev/null +++ b/packages/web-components/src/color/neutral-fill-card.ts @@ -0,0 +1,32 @@ +import { backgroundColor, DesignSystem, neutralFillCardDelta, neutralPalette } from '../fluent-design-system'; +import { Swatch, SwatchResolver } from './common'; +import { findClosestSwatchIndex, getSwatch } from './palette'; + +const neutralCardFillAlgorithm: SwatchResolver = (designSystem: DesignSystem): Swatch => { + const offset: number = neutralFillCardDelta(designSystem); + const index: number = findClosestSwatchIndex(neutralPalette, backgroundColor(designSystem))(designSystem); + return getSwatch(index - (index < offset ? offset * -1 : offset), neutralPalette(designSystem)); +}; + +/** + * @internal + */ +export function neutralFillCard(designSystem: DesignSystem): Swatch; + +/** + * @internal + */ +export function neutralFillCard(backgroundResolver: SwatchResolver): SwatchResolver; + +/** + * @internal + */ +export function neutralFillCard(arg: any): any { + if (typeof arg === 'function') { + return (designSystem: DesignSystem): Swatch => { + return neutralCardFillAlgorithm(Object.assign({}, designSystem, { backgroundColor: arg(designSystem) })); + }; + } else { + return neutralCardFillAlgorithm(arg); + } +} diff --git a/packages/web-components/src/color/neutral-fill-input.spec.ts b/packages/web-components/src/color/neutral-fill-input.spec.ts new file mode 100644 index 00000000000000..47e49e007cf7b6 --- /dev/null +++ b/packages/web-components/src/color/neutral-fill-input.spec.ts @@ -0,0 +1,121 @@ +import { expect } from "chai"; +import { + accentPalette as getAccentPalette, + DesignSystem, + DesignSystemDefaults, + neutralPalette as getNeutralPalette, +} from "../fluent-design-system"; +import { clamp, FillSwatchFamily, Swatch } from "./common"; +import { + neutralFillInput, + neutralFillInputActive, + neutralFillInputFocus, + neutralFillInputHover, + neutralFillInputRest, + neutralFillInputSelected, +} from "./neutral-fill-input"; +import { isDarkMode, Palette } from "./palette"; + +describe("neutralFillInput", (): void => { + const neutralPalette: Palette = getNeutralPalette(DesignSystemDefaults); + const accentPalette: Palette = getAccentPalette(DesignSystemDefaults); + + it("should operate on design system defaults", (): void => { + [ + neutralFillInputActive, + neutralFillInputFocus, + neutralFillInputHover, + neutralFillInputRest, + neutralFillInputSelected, + ].forEach(fn => { + expect(neutralPalette).to.include(fn({} as DesignSystem)); + }); + }); + + it("should always be lighter than the background by the delta in light mode and darker in dark mode", (): void => { + neutralPalette.forEach((swatch: Swatch, index: number): void => { + const designSystem: DesignSystem = { + backgroundColor: neutralPalette[index], + } as DesignSystem; + + expect(neutralFillInputSelected(designSystem)).to.equal( + neutralPalette[ + clamp( + index - + DesignSystemDefaults.neutralFillInputRestDelta * + (isDarkMode(designSystem) ? -1 : 1), + 0, + neutralPalette.length - 1 + ) + ] + ); + }); + }); + + it("should return the same color from both implementations", (): void => { + neutralPalette.concat(accentPalette).forEach((swatch: Swatch): void => { + expect(neutralFillInputRest(() => swatch)(DesignSystemDefaults)).to.equal( + neutralFillInputRest( + Object.assign({}, DesignSystemDefaults, { + backgroundColor: swatch, + }) + ) + ); + expect(neutralFillInputHover(() => swatch)(DesignSystemDefaults)).to.equal( + neutralFillInputHover( + Object.assign({}, DesignSystemDefaults, { + backgroundColor: swatch, + }) + ) + ); + expect(neutralFillInputActive(() => swatch)(DesignSystemDefaults)).to.equal( + neutralFillInputActive( + Object.assign({}, DesignSystemDefaults, { + backgroundColor: swatch, + }) + ) + ); + expect(neutralFillInputFocus(() => swatch)(DesignSystemDefaults)).to.equal( + neutralFillInputFocus( + Object.assign({}, DesignSystemDefaults, { + backgroundColor: swatch, + }) + ) + ); + expect(neutralFillInputSelected(() => swatch)(DesignSystemDefaults)).to.equal( + neutralFillInputSelected( + Object.assign({}, DesignSystemDefaults, { + backgroundColor: swatch, + }) + ) + ); + }); + }); + + it("should have consistent return values", (): void => { + neutralPalette.concat(accentPalette).forEach((swatch: Swatch): void => { + const backplates: FillSwatchFamily = neutralFillInput(() => swatch)( + DesignSystemDefaults + ); + const rest: Swatch = neutralFillInputRest(() => swatch)(DesignSystemDefaults); + const hover: Swatch = neutralFillInputHover(() => swatch)( + DesignSystemDefaults + ); + const active: Swatch = neutralFillInputActive(() => swatch)( + DesignSystemDefaults + ); + const focus: Swatch = neutralFillInputFocus(() => swatch)( + DesignSystemDefaults + ); + const selected: Swatch = neutralFillInputSelected(() => swatch)( + DesignSystemDefaults + ); + + expect(backplates.rest).to.equal(rest); + expect(backplates.hover).to.equal(hover); + expect(backplates.active).to.equal(active); + expect(backplates.focus).to.equal(focus); + expect(backplates.selected).to.equal(selected); + }); + }); +}); diff --git a/packages/web-components/src/color/neutral-fill-input.ts b/packages/web-components/src/color/neutral-fill-input.ts new file mode 100644 index 00000000000000..b1ee4a2c27c8db --- /dev/null +++ b/packages/web-components/src/color/neutral-fill-input.ts @@ -0,0 +1,75 @@ +import { + DesignSystem, + DesignSystemResolver, + neutralFillInputActiveDelta, + neutralFillInputFocusDelta, + neutralFillInputHoverDelta, + neutralFillInputRestDelta, + neutralFillInputSelectedDelta, + neutralPalette, +} from '../fluent-design-system'; +import { findClosestBackgroundIndex, getSwatch, isDarkMode } from './palette'; +import { ColorRecipe, colorRecipeFactory, FillSwatchFamily, Swatch, SwatchRecipe } from './common'; + +/** + * Algorithm for determining neutral backplate colors + */ +function neutralFillInputAlgorithm(indexResolver: DesignSystemResolver): DesignSystemResolver { + return (designSystem: DesignSystem): Swatch => { + const direction: 1 | -1 = isDarkMode(designSystem) ? -1 : 1; + return getSwatch( + findClosestBackgroundIndex(designSystem) - indexResolver(designSystem) * direction, + neutralPalette(designSystem), + ); + }; +} + +/** + * @internal + */ +export const neutralFillInputRest: SwatchRecipe = colorRecipeFactory( + neutralFillInputAlgorithm(neutralFillInputRestDelta), +); + +/** + * @internal + */ +export const neutralFillInputHover: SwatchRecipe = colorRecipeFactory( + neutralFillInputAlgorithm(neutralFillInputHoverDelta), +); + +/** + * @internal + */ +export const neutralFillInputActive: SwatchRecipe = colorRecipeFactory( + neutralFillInputAlgorithm(neutralFillInputActiveDelta), +); + +/** + * @internal + */ +export const neutralFillInputFocus: SwatchRecipe = colorRecipeFactory( + neutralFillInputAlgorithm(neutralFillInputFocusDelta), +); + +/** + * @internal + */ +export const neutralFillInputSelected: SwatchRecipe = colorRecipeFactory( + neutralFillInputAlgorithm(neutralFillInputSelectedDelta), +); + +/** + * @internal + */ +export const neutralFillInput: ColorRecipe = colorRecipeFactory( + (designSystem: DesignSystem): FillSwatchFamily => { + return { + rest: neutralFillInputRest(designSystem), + hover: neutralFillInputHover(designSystem), + active: neutralFillInputActive(designSystem), + focus: neutralFillInputFocus(designSystem), + selected: neutralFillInputSelected(designSystem), + }; + }, +); diff --git a/packages/web-components/src/color/neutral-fill-stealth.spec.ts b/packages/web-components/src/color/neutral-fill-stealth.spec.ts new file mode 100644 index 00000000000000..25abb4bd0b609b --- /dev/null +++ b/packages/web-components/src/color/neutral-fill-stealth.spec.ts @@ -0,0 +1,121 @@ +import { expect } from "chai"; +import { + accentPalette as getAccentPalette, + DesignSystem, + DesignSystemDefaults, + neutralPalette as getNeutralPalette, +} from "../fluent-design-system"; +import { + neutralFillStealth, + neutralFillStealthActive, + neutralFillStealthFocus, + neutralFillStealthHover, + neutralFillStealthRest, + neutralFillStealthSelected, +} from "./neutral-fill-stealth"; +import { Palette } from "./palette"; +import { FillSwatchFamily, Swatch } from "./common"; + +describe("neutralFillStealth", (): void => { + const neutralPalette: Palette = getNeutralPalette(DesignSystemDefaults); + const accentPalette: Palette = getAccentPalette(DesignSystemDefaults); + + it("should operate on design system defaults", (): void => { + [ + neutralFillStealthActive, + neutralFillStealthFocus, + neutralFillStealthHover, + neutralFillStealthRest, + neutralFillStealthSelected, + ].forEach(fn => { + expect(neutralPalette).to.include(fn({} as DesignSystem)); + }); + }); + + it("should switch from dark to light after 10 swatches", (): void => { + expect(neutralFillStealthHover(DesignSystemDefaults)).to.equal( + neutralPalette[DesignSystemDefaults.neutralFillStealthHoverDelta] + ); + expect( + neutralFillStealthHover(() => neutralPalette[1])(DesignSystemDefaults) + ).to.equal(neutralPalette[DesignSystemDefaults.neutralFillStealthHoverDelta + 1]); + expect( + neutralFillStealthHover(() => neutralPalette[2])(DesignSystemDefaults) + ).to.equal(neutralPalette[DesignSystemDefaults.neutralFillStealthHoverDelta + 2]); + expect( + neutralFillStealthHover(() => neutralPalette[9])(DesignSystemDefaults) + ).to.equal(neutralPalette[DesignSystemDefaults.neutralFillStealthHoverDelta + 9]); + expect( + neutralFillStealthHover(() => neutralPalette[10])(DesignSystemDefaults) + ).to.equal(neutralPalette[10 - DesignSystemDefaults.neutralFillStealthHoverDelta]); + }); + + it("should return the same color from both implementations", (): void => { + neutralPalette.concat(accentPalette).forEach((swatch: Swatch): void => { + expect(neutralFillStealthRest(() => swatch)(DesignSystemDefaults)).to.equal( + neutralFillStealthRest( + Object.assign({}, DesignSystemDefaults, { + backgroundColor: swatch, + }) + ) + ); + expect(neutralFillStealthHover(() => swatch)(DesignSystemDefaults)).to.equal( + neutralFillStealthHover( + Object.assign({}, DesignSystemDefaults, { + backgroundColor: swatch, + }) + ) + ); + expect(neutralFillStealthActive(() => swatch)(DesignSystemDefaults)).to.equal( + neutralFillStealthActive( + Object.assign({}, DesignSystemDefaults, { + backgroundColor: swatch, + }) + ) + ); + expect(neutralFillStealthFocus(() => swatch)(DesignSystemDefaults)).to.equal( + neutralFillStealthFocus( + Object.assign({}, DesignSystemDefaults, { + backgroundColor: swatch, + }) + ) + ); + expect(neutralFillStealthSelected(() => swatch)(DesignSystemDefaults)).to.equal( + neutralFillStealthSelected( + Object.assign({}, DesignSystemDefaults, { + backgroundColor: swatch, + }) + ) + ); + }); + }); + + it("should have consistent return values", (): void => { + neutralPalette.concat(accentPalette).forEach((swatch: Swatch): void => { + const backplates: FillSwatchFamily = neutralFillStealth(() => swatch)( + DesignSystemDefaults + ); + const rest: Swatch = neutralFillStealthRest(() => swatch)( + DesignSystemDefaults + ); + const hover: Swatch = neutralFillStealthHover(() => swatch)( + DesignSystemDefaults + ); + const active: Swatch = neutralFillStealthActive(() => swatch)( + DesignSystemDefaults + ); + const focus: Swatch = neutralFillStealthFocus(() => swatch)( + DesignSystemDefaults + ); + const selected: Swatch = neutralFillStealthSelected(() => swatch)( + DesignSystemDefaults + ); + + expect(backplates.rest).to.equal(rest); + expect(backplates.hover).to.equal(hover); + expect(backplates.active).to.equal(active); + expect(backplates.focus).to.equal(focus); + expect(backplates.selected).to.equal(selected); + }); + }); +}); diff --git a/packages/web-components/src/color/neutral-fill-stealth.ts b/packages/web-components/src/color/neutral-fill-stealth.ts new file mode 100644 index 00000000000000..3f3f8dbee02c1f --- /dev/null +++ b/packages/web-components/src/color/neutral-fill-stealth.ts @@ -0,0 +1,86 @@ +import { + DesignSystem, + DesignSystemResolver, + neutralFillActiveDelta, + neutralFillFocusDelta, + neutralFillHoverDelta, + neutralFillRestDelta, + neutralFillStealthActiveDelta, + neutralFillStealthFocusDelta, + neutralFillStealthHoverDelta, + neutralFillStealthRestDelta, + neutralFillStealthSelectedDelta, + neutralPalette, +} from '../fluent-design-system'; +import { ColorRecipe, colorRecipeFactory, designSystemResolverMax, FillSwatchFamily, Swatch } from './common'; +import { findClosestBackgroundIndex, getSwatch } from './palette'; + +const neutralFillStealthSwapThreshold: DesignSystemResolver = designSystemResolverMax( + neutralFillRestDelta, + neutralFillHoverDelta, + neutralFillActiveDelta, + neutralFillFocusDelta, + neutralFillStealthRestDelta, + neutralFillStealthHoverDelta, + neutralFillStealthActiveDelta, + neutralFillStealthFocusDelta, +); + +function neutralFillStealthAlgorithm(deltaResolver: DesignSystemResolver): DesignSystemResolver { + return (designSystem: DesignSystem): Swatch => { + const backgroundIndex: number = findClosestBackgroundIndex(designSystem); + const swapThreshold: number = neutralFillStealthSwapThreshold(designSystem); + + const direction: 1 | -1 = backgroundIndex >= swapThreshold ? -1 : 1; + + return getSwatch(backgroundIndex + direction * deltaResolver(designSystem), neutralPalette(designSystem)); + }; +} + +/** + * @internal + */ +export const neutralFillStealthRest: ColorRecipe = colorRecipeFactory( + neutralFillStealthAlgorithm(neutralFillStealthRestDelta), +); + +/** + * @internal + */ +export const neutralFillStealthHover: ColorRecipe = colorRecipeFactory( + neutralFillStealthAlgorithm(neutralFillStealthHoverDelta), +); + +/** + * @internal + */ +export const neutralFillStealthActive: ColorRecipe = colorRecipeFactory( + neutralFillStealthAlgorithm(neutralFillStealthActiveDelta), +); + +/** + * @internal + */ +export const neutralFillStealthFocus: ColorRecipe = colorRecipeFactory( + neutralFillStealthAlgorithm(neutralFillStealthFocusDelta), +); + +/** + * @internal + */ +export const neutralFillStealthSelected: ColorRecipe = colorRecipeFactory( + neutralFillStealthAlgorithm(neutralFillStealthSelectedDelta), +); + +/** + * @internal + */ +export const neutralFillStealth: ColorRecipe = colorRecipeFactory((designSystem: DesignSystem) => { + return { + rest: neutralFillStealthRest(designSystem), + hover: neutralFillStealthHover(designSystem), + active: neutralFillStealthActive(designSystem), + focus: neutralFillStealthFocus(designSystem), + selected: neutralFillStealthSelected(designSystem), + }; +}); diff --git a/packages/web-components/src/color/neutral-fill-toggle.ts b/packages/web-components/src/color/neutral-fill-toggle.ts new file mode 100644 index 00000000000000..74de0a42e7ec80 --- /dev/null +++ b/packages/web-components/src/color/neutral-fill-toggle.ts @@ -0,0 +1,60 @@ +import { + neutralFillToggleActiveDelta, + neutralFillToggleFocusDelta, + neutralFillToggleHoverDelta, + neutralPalette, +} from '../fluent-design-system'; +import { + colorRecipeFactory, + SwatchFamilyResolver, + swatchFamilyToSwatchRecipeFactory, + SwatchFamilyType, + SwatchRecipe, +} from './common'; +import { accessibleAlgorithm } from './accessible-recipe'; + +/** + * @internal + */ +export const neutralFillToggle: SwatchFamilyResolver = colorRecipeFactory( + accessibleAlgorithm( + neutralPalette, + 4.5, + 0, + neutralFillToggleHoverDelta, + neutralFillToggleActiveDelta, + neutralFillToggleFocusDelta, + ), +); + +/** + * @internal + */ +export const neutralFillToggleRest: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.rest, + neutralFillToggle, +); + +/** + * @internal + */ +export const neutralFillToggleHover: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.hover, + neutralFillToggle, +); + +/** + * @internal + */ +export const neutralFillToggleActive: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.active, + neutralFillToggle, +); + +/** + * @internal + */ +export const neutralFillToggleFocus: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.focus, + neutralFillToggle, +); diff --git a/packages/web-components/src/color/neutral-fill.spec.ts b/packages/web-components/src/color/neutral-fill.spec.ts new file mode 100644 index 00000000000000..1430444268ab70 --- /dev/null +++ b/packages/web-components/src/color/neutral-fill.spec.ts @@ -0,0 +1,122 @@ +import { expect } from "chai"; +import { + accentPalette as getAccentPalette, + DesignSystem, + DesignSystemDefaults, + neutralPalette as getNeutralPalette, +} from "../fluent-design-system"; +import { + neutralFill, + neutralFillActive, + neutralFillFocus, + neutralFillHover, + neutralFillRest, + neutralFillSelected, +} from "./neutral-fill"; +import { Palette } from "./palette"; +import { FillSwatchFamily, Swatch } from "./common"; + +describe("neutralFill", (): void => { + const neutralPalette: Palette = getNeutralPalette(DesignSystemDefaults); + const accentPalette: Palette = getAccentPalette(DesignSystemDefaults); + + it("should operate on design system defaults", (): void => { + [ + neutralFillActive, + neutralFillFocus, + neutralFillHover, + neutralFillRest, + neutralFillSelected, + ].forEach(fn => { + expect(neutralPalette).to.include(fn({} as DesignSystem)); + }); + }); + + it("should switch from dark to light after 10 swatches", (): void => { + expect(neutralFillRest(DesignSystemDefaults)).to.equal( + neutralPalette[DesignSystemDefaults.neutralFillRestDelta] + ); + expect(neutralFillHover(DesignSystemDefaults)).to.equal( + neutralPalette[DesignSystemDefaults.neutralFillHoverDelta] + ); + expect(neutralFillActive(DesignSystemDefaults)).to.equal( + neutralPalette[DesignSystemDefaults.neutralFillActiveDelta] + ); + expect(neutralFillFocus(DesignSystemDefaults)).to.equal( + neutralPalette[DesignSystemDefaults.neutralFillFocusDelta] + ); + expect(neutralFillRest(() => neutralPalette[1])(DesignSystemDefaults)).to.equal( + neutralPalette[DesignSystemDefaults.neutralFillRestDelta + 1] + ); + expect(neutralFillRest(() => neutralPalette[2])(DesignSystemDefaults)).to.equal( + neutralPalette[DesignSystemDefaults.neutralFillRestDelta + 2] + ); + expect(neutralFillRest(() => neutralPalette[9])(DesignSystemDefaults)).to.equal( + neutralPalette[DesignSystemDefaults.neutralFillRestDelta + 9] + ); + expect(neutralFillRest(() => neutralPalette[10])(DesignSystemDefaults)).to.equal( + neutralPalette[3] + ); + }); + + it("should return the same color from both implementations", (): void => { + neutralPalette.concat(accentPalette).forEach((swatch: Swatch): void => { + expect(neutralFillRest(() => swatch)(DesignSystemDefaults)).to.equal( + neutralFillRest( + Object.assign({}, DesignSystemDefaults, { + backgroundColor: swatch, + }) + ) + ); + expect(neutralFillHover(() => swatch)(DesignSystemDefaults)).to.equal( + neutralFillHover( + Object.assign({}, DesignSystemDefaults, { + backgroundColor: swatch, + }) + ) + ); + expect(neutralFillActive(() => swatch)(DesignSystemDefaults)).to.equal( + neutralFillActive( + Object.assign({}, DesignSystemDefaults, { + backgroundColor: swatch, + }) + ) + ); + expect(neutralFillFocus(() => swatch)(DesignSystemDefaults)).to.equal( + neutralFillFocus( + Object.assign({}, DesignSystemDefaults, { + backgroundColor: swatch, + }) + ) + ); + expect(neutralFillSelected(() => swatch)(DesignSystemDefaults)).to.equal( + neutralFillSelected( + Object.assign({}, DesignSystemDefaults, { + backgroundColor: swatch, + }) + ) + ); + }); + }); + + it("should have consistent return values", (): void => { + neutralPalette.concat(accentPalette).forEach((swatch: Swatch): void => { + const backplates: FillSwatchFamily = neutralFill(() => swatch)( + DesignSystemDefaults + ); + const rest: Swatch = neutralFillRest(() => swatch)(DesignSystemDefaults); + const hover: Swatch = neutralFillHover(() => swatch)(DesignSystemDefaults); + const active: Swatch = neutralFillActive(() => swatch)(DesignSystemDefaults); + const focus: Swatch = neutralFillFocus(() => swatch)(DesignSystemDefaults); + const selected: Swatch = neutralFillSelected(() => swatch)( + DesignSystemDefaults + ); + + expect(backplates.rest).to.equal(rest); + expect(backplates.hover).to.equal(hover); + expect(backplates.active).to.equal(active); + expect(backplates.focus).to.equal(focus); + expect(backplates.selected).to.equal(selected); + }); + }); +}); diff --git a/packages/web-components/src/color/neutral-fill.ts b/packages/web-components/src/color/neutral-fill.ts new file mode 100644 index 00000000000000..3d23ed21d79b75 --- /dev/null +++ b/packages/web-components/src/color/neutral-fill.ts @@ -0,0 +1,76 @@ +import { + DesignSystem, + DesignSystemResolver, + neutralFillActiveDelta, + neutralFillFocusDelta, + neutralFillHoverDelta, + neutralFillRestDelta, + neutralFillSelectedDelta, + neutralPalette, +} from '../fluent-design-system'; +import { + ColorRecipe, + colorRecipeFactory, + designSystemResolverMax, + FillSwatchFamily, + Swatch, + SwatchRecipe, +} from './common'; +import { findClosestBackgroundIndex, getSwatch } from './palette'; + +const neutralFillThreshold: DesignSystemResolver = designSystemResolverMax( + neutralFillRestDelta, + neutralFillHoverDelta, + neutralFillActiveDelta, + neutralFillFocusDelta, +); + +function neutralFillAlgorithm(deltaResolver: DesignSystemResolver): DesignSystemResolver { + return (designSystem: DesignSystem): Swatch => { + const backgroundIndex: number = findClosestBackgroundIndex(designSystem); + const swapThreshold: number = neutralFillThreshold(designSystem); + const direction: 1 | -1 = backgroundIndex >= swapThreshold ? -1 : 1; + + return getSwatch(backgroundIndex + direction * deltaResolver(designSystem), neutralPalette(designSystem)); + }; +} + +/** + * @internal + */ +export const neutralFillRest: SwatchRecipe = colorRecipeFactory(neutralFillAlgorithm(neutralFillRestDelta)); + +/** + * @internal + */ +export const neutralFillHover: SwatchRecipe = colorRecipeFactory(neutralFillAlgorithm(neutralFillHoverDelta)); + +/** + * @internal + */ +export const neutralFillActive: SwatchRecipe = colorRecipeFactory(neutralFillAlgorithm(neutralFillActiveDelta)); + +/** + * @internal + */ +export const neutralFillFocus: SwatchRecipe = colorRecipeFactory(neutralFillAlgorithm(neutralFillFocusDelta)); + +/** + * @internal + */ +export const neutralFillSelected: SwatchRecipe = colorRecipeFactory(neutralFillAlgorithm(neutralFillSelectedDelta)); + +/** + * @internal + */ +export const neutralFill: ColorRecipe = colorRecipeFactory( + (designSystem: DesignSystem): FillSwatchFamily => { + return { + rest: neutralFillRest(designSystem), + hover: neutralFillHover(designSystem), + active: neutralFillActive(designSystem), + focus: neutralFillFocus(designSystem), + selected: neutralFillSelected(designSystem), + }; + }, +); diff --git a/packages/web-components/src/color/neutral-focus.spec.ts b/packages/web-components/src/color/neutral-focus.spec.ts new file mode 100644 index 00000000000000..0289fb1bd760b5 --- /dev/null +++ b/packages/web-components/src/color/neutral-focus.spec.ts @@ -0,0 +1,20 @@ +import { expect } from "chai"; +import { DesignSystem, DesignSystemDefaults } from "../fluent-design-system"; +import { neutralFocus } from "./neutral-focus"; +import { contrast } from "./common"; + +describe("neutralFocus", (): void => { + it("should return a string when invoked with an object", (): void => { + expect(typeof neutralFocus(DesignSystemDefaults)).to.equal("string"); + }); + + it("should return a function when invoked with a function", (): void => { + expect(typeof neutralFocus(() => "#FFF")).to.equal("function"); + }); + + it("should operate on default design system if no design system is supplied", (): void => { + expect(contrast(neutralFocus({} as DesignSystem), "#FFF")).to.be.gte( + 3.5 + ); + }); +}); diff --git a/packages/web-components/src/color/neutral-focus.ts b/packages/web-components/src/color/neutral-focus.ts new file mode 100644 index 00000000000000..932e97f612f466 --- /dev/null +++ b/packages/web-components/src/color/neutral-focus.ts @@ -0,0 +1,57 @@ +import { + accentPalette, + backgroundColor, + DesignSystem, + DesignSystemResolver, + neutralPalette, +} from '../fluent-design-system'; +import { findClosestSwatchIndex, isDarkMode, Palette, swatchByContrast } from './palette'; +import { ColorRecipe, colorRecipeFactory, Swatch, SwatchResolver } from './common'; + +const targetRatio: number = 3.5; + +function neutralFocusIndexResolver(referenceColor: string, palette: Palette, designSystem: DesignSystem): number { + return findClosestSwatchIndex(neutralPalette, referenceColor)(designSystem); +} + +function neutralFocusDirectionResolver(index: number, palette: Palette, designSystem: DesignSystem): 1 | -1 { + return isDarkMode(designSystem) ? -1 : 1; +} + +function neutralFocusContrastCondition(contrastRatio: number): boolean { + return contrastRatio > targetRatio; +} + +const neutralFocusAlgorithm: SwatchResolver = swatchByContrast(backgroundColor)(neutralPalette)( + neutralFocusIndexResolver, +)(neutralFocusDirectionResolver)(neutralFocusContrastCondition); + +/** + * @internal + */ +export const neutralFocus: ColorRecipe = colorRecipeFactory(neutralFocusAlgorithm); + +function neutralFocusInnerAccentIndexResolver( + accentFillColor: DesignSystemResolver, +): (referenceColor: string, sourcePalette: Palette, designSystem: DesignSystem) => number { + return (referenceColor: string, sourcePalette: Palette, designSystem: DesignSystem): number => { + return sourcePalette.indexOf(accentFillColor(designSystem)); + }; +} + +function neutralFocusInnerAccentDirectionResolver( + referenceIndex: number, + palette: string[], + designSystem: DesignSystem, +): 1 | -1 { + return isDarkMode(designSystem) ? 1 : -1; +} + +/** + * @internal + */ +export function neutralFocusInnerAccent(accentFillColor: DesignSystemResolver): DesignSystemResolver { + return swatchByContrast(neutralFocus)(accentPalette)(neutralFocusInnerAccentIndexResolver(accentFillColor))( + neutralFocusInnerAccentDirectionResolver, + )(neutralFocusContrastCondition); +} diff --git a/packages/web-components/src/color/neutral-foreground-hint.spec.ts b/packages/web-components/src/color/neutral-foreground-hint.spec.ts new file mode 100644 index 00000000000000..1de1ed57bc8146 --- /dev/null +++ b/packages/web-components/src/color/neutral-foreground-hint.spec.ts @@ -0,0 +1,71 @@ +import { expect } from "chai"; +import { + accentPalette as getAccentPalette, + DesignSystemDefaults, + neutralPalette as getNeutralPalette, +} from "../fluent-design-system"; +import { + neutralForegroundHint, + neutralForegroundHintLarge, +} from "./neutral-foreground-hint"; +import { Palette } from "./palette"; +import { contrast, Swatch, SwatchRecipe } from "./common"; +describe("neutralForegroundHint", (): void => { + const neutralPalette: Palette = getNeutralPalette(DesignSystemDefaults); + const accentPalette: Palette = getAccentPalette(DesignSystemDefaults); + + it("should implement design system defaults", (): void => { + expect(neutralForegroundHint(undefined as any)).to.equal("#767676"); + }); + + neutralPalette.concat(accentPalette).forEach((swatch: Swatch): void => { + it(`${swatch} should resolve a color from the neutral palette`, (): void => { + expect( + neutralPalette.indexOf( + neutralForegroundHint( + Object.assign({}, DesignSystemDefaults, { + backgroundColor: swatch, + }) + ) + ) + ).not.to.equal(-1); + }); + }); + + it("should return the same color from both methods of setting the reference background", (): void => { + neutralPalette.concat(accentPalette).forEach((swatch: Swatch): void => { + expect( + neutralForegroundHint( + Object.assign({}, DesignSystemDefaults, { + backgroundColor: swatch, + }) + ) + ).to.equal(neutralForegroundHint(() => swatch)(DesignSystemDefaults)); + }); + }); + + function retrieveContrast(resolvedSwatch: Swatch, fn: SwatchRecipe): number { + return parseFloat( + contrast( + fn(() => resolvedSwatch)(DesignSystemDefaults), + resolvedSwatch + ).toPrecision(3) + ); + } + neutralPalette.concat(accentPalette).forEach((swatch: Swatch): void => { + it(`${swatch} should always be at least 4.5 : 1 against the background`, (): void => { + expect( + retrieveContrast(swatch, neutralForegroundHint) + // Because neutralForegroundHint follows the direction patterns of neutralForeground, + // a backgroundColor #777777 is impossible to hit 4.5 against. + ).to.be.gte(swatch === "#777777" ? 4.48 : 4.5); + expect(retrieveContrast(swatch, neutralForegroundHint)).to.be.lessThan(5); + expect( + retrieveContrast(swatch, neutralForegroundHintLarge) + ).to.be.gte(3); + expect(retrieveContrast(swatch, neutralForegroundHintLarge)).to.be.lessThan( + 3.3 + ); + }); + }); +}); diff --git a/packages/web-components/src/color/neutral-foreground-hint.ts b/packages/web-components/src/color/neutral-foreground-hint.ts new file mode 100644 index 00000000000000..06b602fe095b5d --- /dev/null +++ b/packages/web-components/src/color/neutral-foreground-hint.ts @@ -0,0 +1,31 @@ +import { DesignSystemResolver, neutralPalette } from '../fluent-design-system'; +import { + colorRecipeFactory, + SwatchFamily, + swatchFamilyToSwatchRecipeFactory, + SwatchFamilyType, + SwatchRecipe, +} from './common'; +import { accessibleAlgorithm } from './accessible-recipe'; + +function neutralForegroundHintAlgorithm(targetContrast: number): DesignSystemResolver { + return accessibleAlgorithm(neutralPalette, targetContrast, 0, 0, 0, 0); +} + +/** + * @internal + * Hint text for normal sized text, less than 18pt normal weight + */ +export const neutralForegroundHint: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.rest, + colorRecipeFactory(neutralForegroundHintAlgorithm(4.5)), +); + +/** + * @internal + * Hint text for large sized text, greater than 18pt or 16pt and bold + */ +export const neutralForegroundHintLarge: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.rest, + colorRecipeFactory(neutralForegroundHintAlgorithm(3)), +); diff --git a/packages/web-components/src/color/neutral-foreground-toggle.ts b/packages/web-components/src/color/neutral-foreground-toggle.ts new file mode 100644 index 00000000000000..ec45bfc318b866 --- /dev/null +++ b/packages/web-components/src/color/neutral-foreground-toggle.ts @@ -0,0 +1,43 @@ +import { DesignSystem } from '../fluent-design-system'; +import { black, white } from './color-constants'; +import { contrast, Swatch, SwatchRecipe, SwatchResolver } from './common'; +import { neutralFillToggleRest } from './neutral-fill-toggle'; + +/** + * Function to derive neutralForegroundToggle from an input background and target contrast ratio + */ +const neutralForegroundToggleAlgorithm: (backgroundColor: Swatch, targetContrast: number) => Swatch = ( + backgroundColor: Swatch, + targetContrast: number, +): Swatch => { + return contrast(white, backgroundColor) >= targetContrast ? white : black; +}; + +/** + * Factory to create a neutral-foreground-toggle function that operates on a target contrast ratio + */ +function neutralForegroundToggleFactory(targetContrast: number): SwatchRecipe { + function neutralForegroundToggleInternal(designSystem: DesignSystem): Swatch; + function neutralForegroundToggleInternal(backgroundResolver: SwatchResolver): SwatchResolver; + function neutralForegroundToggleInternal(arg: any): any { + return typeof arg === 'function' + ? (designSystem: DesignSystem): Swatch => { + return neutralForegroundToggleAlgorithm(arg(designSystem), targetContrast); + } + : neutralForegroundToggleAlgorithm(neutralFillToggleRest(arg), targetContrast); + } + + return neutralForegroundToggleInternal; +} + +/** + * @internal + * Toggle text for normal sized text, less than 18pt normal weight + */ +export const neutralForegroundToggle: SwatchRecipe = neutralForegroundToggleFactory(4.5); + +/** + * @internal + * Toggle text for large sized text, greater than 18pt or 16pt and bold + */ +export const neutralForegroundToggleLarge: SwatchRecipe = neutralForegroundToggleFactory(3); diff --git a/packages/web-components/src/color/neutral-foreground.spec.ts b/packages/web-components/src/color/neutral-foreground.spec.ts new file mode 100644 index 00000000000000..96c04c6c090291 --- /dev/null +++ b/packages/web-components/src/color/neutral-foreground.spec.ts @@ -0,0 +1,91 @@ +import { expect } from "chai"; +import { DesignSystemDefaults } from "../fluent-design-system"; +import { + neutralForegroundActive, + neutralForegroundHover, + neutralForegroundRest, +} from "./neutral-foreground"; +import { contrast } from "./common"; + +describe("neutralForeground", (): void => { + it("should return a string when invoked with an object", (): void => { + expect(typeof neutralForegroundRest(DesignSystemDefaults)).to.equal("string"); + expect(typeof neutralForegroundHover(DesignSystemDefaults)).to.equal("string"); + expect(typeof neutralForegroundActive(DesignSystemDefaults)).to.equal("string"); + }); + + it("should return a function when invoked with a function", (): void => { + expect(typeof neutralForegroundRest(() => "#FFF")).to.equal("function"); + expect(typeof neutralForegroundHover(() => "#FFF")).to.equal("function"); + expect(typeof neutralForegroundActive(() => "#FFF")).to.equal("function"); + }); + + it("should operate on default design system if no design system is supplied", (): void => { + expect( + contrast(neutralForegroundRest(undefined as any), "#FFF") + ).to.be.gte(14); + expect( + contrast( + neutralForegroundRest(() => undefined as any)(undefined as any), + "#FFF" + ) + ).to.be.gte(14); + expect( + contrast(neutralForegroundRest(() => "#FFF")(undefined as any), "#FFF") + ).to.be.gte(14); + expect( + contrast(neutralForegroundRest(() => "#FFFFFF")(undefined as any), "#FFF") + ).to.be.gte(14); + + expect( + contrast(neutralForegroundHover(undefined as any), "#FFF") + ).to.be.gte(14); + expect( + contrast( + neutralForegroundHover(() => undefined as any)(undefined as any), + "#FFF" + ) + ).to.be.gte(14); + expect( + contrast(neutralForegroundHover(() => "#FFF")(undefined as any), "#FFF") + ).to.be.gte(14); + expect( + contrast(neutralForegroundHover(() => "#FFFFFF")(undefined as any), "#FFF") + ).to.be.gte(14); + + expect( + contrast(neutralForegroundActive(undefined as any), "#FFF") + ).to.be.gte(14); + expect( + contrast( + neutralForegroundActive(() => undefined as any)(undefined as any), + "#FFF" + ) + ).to.be.gte(14); + expect( + contrast(neutralForegroundActive(() => "#FFF")(undefined as any), "#FFF") + ).to.be.gte(14); + expect( + contrast(neutralForegroundActive(() => "#FFFFFF")(undefined as any), "#FFF") + ).to.be.gte(14); + }); + + it("should return correct result with default design system values", (): void => { + expect( + contrast(neutralForegroundRest(DesignSystemDefaults), "#FFF") + ).to.be.gte(14); + }); + + it("should return #FFFFFF with a dark background", (): void => { + expect( + contrast( + neutralForegroundRest( + Object.assign({}, DesignSystemDefaults, { + backgroundColor: "#000", + }) + ), + "#000" + ) + ).to.be.gte(14); + }); +}); diff --git a/packages/web-components/src/color/neutral-foreground.ts b/packages/web-components/src/color/neutral-foreground.ts new file mode 100644 index 00000000000000..1f3ef078fc284e --- /dev/null +++ b/packages/web-components/src/color/neutral-foreground.ts @@ -0,0 +1,60 @@ +import { + neutralForegroundActiveDelta, + neutralForegroundFocusDelta, + neutralForegroundHoverDelta, + neutralPalette, +} from '../fluent-design-system'; +import { + colorRecipeFactory, + SwatchFamilyResolver, + swatchFamilyToSwatchRecipeFactory, + SwatchFamilyType, + SwatchRecipe, +} from './common'; +import { accessibleAlgorithm } from './accessible-recipe'; + +/** + * @internal + */ +export const neutralForeground: SwatchFamilyResolver = colorRecipeFactory( + accessibleAlgorithm( + neutralPalette, + 14, + 0, + neutralForegroundHoverDelta, + neutralForegroundActiveDelta, + neutralForegroundFocusDelta, + ), +); + +/** + * @internal + */ +export const neutralForegroundRest: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.rest, + neutralForeground, +); + +/** + * @internal + */ +export const neutralForegroundHover: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.hover, + neutralForeground, +); + +/** + * @internal + */ +export const neutralForegroundActive: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.active, + neutralForeground, +); + +/** + * @internal + */ +export const neutralForegroundFocus: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.focus, + neutralForeground, +); diff --git a/packages/web-components/src/color/neutral-layer.spec.ts b/packages/web-components/src/color/neutral-layer.spec.ts new file mode 100644 index 00000000000000..f2113cc5d2d622 --- /dev/null +++ b/packages/web-components/src/color/neutral-layer.spec.ts @@ -0,0 +1,173 @@ +import { expect } from "chai"; +import { DesignSystem, DesignSystemDefaults } from "../fluent-design-system"; +import { + neutralLayerCard, + neutralLayerCardContainer, + neutralLayerFloating, + neutralLayerL1, + neutralLayerL2, + neutralLayerL3, + neutralLayerL4, + StandardLuminance, +} from "./neutral-layer"; + +const lightModeDesignSystem: DesignSystem = Object.assign({}, DesignSystemDefaults, { + baseLayerLuminance: StandardLuminance.LightMode, +}); + +const darkModeDesignSystem: DesignSystem = Object.assign({}, DesignSystemDefaults, { + baseLayerLuminance: StandardLuminance.DarkMode, +}); + +const enum NeutralPaletteLightModeOffsets { + L1 = 0, + L2 = 10, + L3 = 13, + L4 = 16, +} + +const enum NeutralPaletteDarkModeOffsets { + L1 = 76, + L2 = 79, + L3 = 82, + L4 = 85, +} + +describe("neutralLayer", (): void => { + describe("L1", (): void => { + it("should return values from L1 when in light mode", (): void => { + expect(neutralLayerL1(lightModeDesignSystem)).to.equal( + DesignSystemDefaults.neutralPalette[NeutralPaletteLightModeOffsets.L1] + ); + }); + it("should return values from L1 when in dark mode", (): void => { + expect(neutralLayerL1(darkModeDesignSystem)).to.equal( + DesignSystemDefaults.neutralPalette[NeutralPaletteDarkModeOffsets.L1] + ); + }); + it("should operate on a provided background color", (): void => { + expect(neutralLayerL1((): string => "#000000")(DesignSystemDefaults)).to.equal( + DesignSystemDefaults.neutralPalette[NeutralPaletteDarkModeOffsets.L1] + ); + expect(neutralLayerL1((): string => "#FFFFFF")(DesignSystemDefaults)).to.equal( + DesignSystemDefaults.neutralPalette[NeutralPaletteLightModeOffsets.L1] + ); + }); + }); + + describe("L2", (): void => { + it("should return values from L2 when in light mode", (): void => { + expect(neutralLayerL2(lightModeDesignSystem)).to.equal( + DesignSystemDefaults.neutralPalette[NeutralPaletteLightModeOffsets.L2] + ); + }); + it("should return values from L2 when in dark mode", (): void => { + expect(neutralLayerL2(darkModeDesignSystem)).to.equal( + DesignSystemDefaults.neutralPalette[NeutralPaletteDarkModeOffsets.L2] + ); + }); + it("should operate on a provided background color", (): void => { + expect(neutralLayerL2((): string => "#000000")(DesignSystemDefaults)).to.equal( + DesignSystemDefaults.neutralPalette[NeutralPaletteDarkModeOffsets.L2] + ); + expect(neutralLayerL2((): string => "#FFFFFF")(DesignSystemDefaults)).to.equal( + DesignSystemDefaults.neutralPalette[NeutralPaletteLightModeOffsets.L2] + ); + }); + }); + + describe("L3", (): void => { + it("should return values from L3 when in light mode", (): void => { + expect(neutralLayerL3(lightModeDesignSystem)).to.equal( + DesignSystemDefaults.neutralPalette[NeutralPaletteLightModeOffsets.L3] + ); + }); + it("should return values from L3 when in dark mode", (): void => { + expect(neutralLayerL3(darkModeDesignSystem)).to.equal( + DesignSystemDefaults.neutralPalette[NeutralPaletteDarkModeOffsets.L3] + ); + }); + it("should operate on a provided background color", (): void => { + expect(neutralLayerL3((): string => "#000000")(DesignSystemDefaults)).to.equal( + DesignSystemDefaults.neutralPalette[NeutralPaletteDarkModeOffsets.L3] + ); + expect(neutralLayerL3((): string => "#FFFFFF")(DesignSystemDefaults)).to.equal( + DesignSystemDefaults.neutralPalette[NeutralPaletteLightModeOffsets.L3] + ); + }); + }); + + describe("L4", (): void => { + it("should return values from L4 when in light mode", (): void => { + expect(neutralLayerL4(lightModeDesignSystem)).to.equal( + DesignSystemDefaults.neutralPalette[NeutralPaletteLightModeOffsets.L4] + ); + }); + it("should return values from L4 when in dark mode", (): void => { + expect(neutralLayerL4(darkModeDesignSystem)).to.equal( + DesignSystemDefaults.neutralPalette[NeutralPaletteDarkModeOffsets.L4] + ); + }); + it("should operate on a provided background color", (): void => { + expect(neutralLayerL4((): string => "#000000")(DesignSystemDefaults)).to.equal( + DesignSystemDefaults.neutralPalette[NeutralPaletteDarkModeOffsets.L4] + ); + expect(neutralLayerL4((): string => "#FFFFFF")(DesignSystemDefaults)).to.equal( + DesignSystemDefaults.neutralPalette[NeutralPaletteLightModeOffsets.L4] + ); + }); + }); + + describe("neutralLayerFloating", (): void => { + it("should return a color from the neutral palette", (): void => { + expect( + DesignSystemDefaults.neutralPalette.includes( + neutralLayerFloating(DesignSystemDefaults) + ) + ).to.be.ok; + }); + + it("should operate on a provided background color", (): void => { + const color: string = neutralLayerFloating((): string => "#000000")( + DesignSystemDefaults + ); + + expect(color).not.to.equal(neutralLayerFloating(DesignSystemDefaults)); + expect(DesignSystemDefaults.neutralPalette.includes(color)).to.be.ok; + }); + }); + describe("neutralLayerCardContainer", (): void => { + it("should return a color from the neutral palette", (): void => { + expect( + DesignSystemDefaults.neutralPalette.includes( + neutralLayerCardContainer(DesignSystemDefaults) + ) + ).to.be.ok; + }); + it("should operate on a provided background color", (): void => { + const color: string = neutralLayerCardContainer((): string => "#000000")( + DesignSystemDefaults + ); + + expect(color).not.to.equal(neutralLayerCardContainer(DesignSystemDefaults)); + expect(DesignSystemDefaults.neutralPalette.includes(color)).to.be.ok; + }); + }); + describe("neutralLayerCard", (): void => { + it("should return a color from the neutral palette", (): void => { + expect( + DesignSystemDefaults.neutralPalette.includes( + neutralLayerCard(DesignSystemDefaults) + ) + ).to.be.ok; + }); + it("should operate on a provided background color", (): void => { + const color: string = neutralLayerCard((): string => "#000000")( + DesignSystemDefaults + ); + + expect(color).not.to.equal(neutralLayerCard(DesignSystemDefaults)); + expect(DesignSystemDefaults.neutralPalette.includes(color)).to.be.ok; + }); + }); +}); diff --git a/packages/web-components/src/color/neutral-layer.ts b/packages/web-components/src/color/neutral-layer.ts new file mode 100644 index 00000000000000..1251a5cc5fdce6 --- /dev/null +++ b/packages/web-components/src/color/neutral-layer.ts @@ -0,0 +1,173 @@ +import { clamp, ColorRGBA64 } from '@microsoft/fast-colors'; +import { add, multiply, subtract } from '../utilities/math'; +import { + baseLayerLuminance, + DesignSystem, + DesignSystemResolver, + neutralFillActiveDelta, + neutralFillCardDelta, + neutralFillHoverDelta, + neutralFillRestDelta, + neutralPalette, +} from '../fluent-design-system'; +import { findClosestSwatchIndex, getSwatch, swatchByMode } from './palette'; +import { ColorRecipe, colorRecipeFactory, designSystemResolverMax, Swatch } from './common'; + +/** + * @public + * Recommended values for light and dark mode for `baseLayerLuminance` in the design system. + */ +export enum StandardLuminance { + LightMode = 1, + DarkMode = 0.23, +} + +function luminanceOrBackgroundColor( + luminanceRecipe: DesignSystemResolver, + backgroundRecipe: DesignSystemResolver, +): DesignSystemResolver { + return (designSystem: DesignSystem): string => { + return baseLayerLuminance(designSystem) === -1 ? backgroundRecipe(designSystem) : luminanceRecipe(designSystem); + }; +} + +/** + * Find the palette color that's closest to the desired base layer luminance. + */ +const baseLayerLuminanceSwatch: DesignSystemResolver = (designSystem: DesignSystem): Swatch => { + const luminance: number = baseLayerLuminance(designSystem); + return new ColorRGBA64(luminance, luminance, luminance, 1).toStringHexRGB(); +}; + +/** + * Get the index of the base layer palette color. + */ +const baseLayerLuminanceIndex: DesignSystemResolver = findClosestSwatchIndex( + neutralPalette, + baseLayerLuminanceSwatch, +); + +/** + * Get the actual value of the card layer index, clamped so we can use it to base other layers from. + */ +const neutralLayerCardIndex: DesignSystemResolver = (designSystem: DesignSystem): number => + clamp( + subtract(baseLayerLuminanceIndex, neutralFillCardDelta)(designSystem), + 0, + neutralPalette(designSystem).length - 1, + ); + +/** + * Light mode L2 is significant because it happens at the same point as the neutral fill flip. Use this as the minimum index for L2. + */ +const lightNeutralLayerL2: DesignSystemResolver = designSystemResolverMax( + neutralFillRestDelta, + neutralFillHoverDelta, + neutralFillActiveDelta, +); + +/** + * The index for L2 based on luminance, adjusted for the flip in light mode if necessary. + */ +const neutralLayerL2Index: DesignSystemResolver = designSystemResolverMax( + add(baseLayerLuminanceIndex, neutralFillCardDelta), + lightNeutralLayerL2, +); + +/** + * Dark mode L4 is the darkest recommended background in the standard guidance, which is + * calculated based on luminance to work with variable sized ramps. + */ +const darkNeutralLayerL4: DesignSystemResolver = (designSystem: DesignSystem): number => { + const darkLum: number = 0.14; + const darkColor: ColorRGBA64 = new ColorRGBA64(darkLum, darkLum, darkLum, 1); + const darkRefIndex: number = findClosestSwatchIndex(neutralPalette, darkColor.toStringHexRGB())(designSystem); + return darkRefIndex; +}; + +/** + * @internal + * Used as the background color for floating layers like context menus and flyouts. + */ +export const neutralLayerFloating: ColorRecipe = colorRecipeFactory( + luminanceOrBackgroundColor( + getSwatch(subtract(neutralLayerCardIndex, neutralFillCardDelta), neutralPalette), + swatchByMode(neutralPalette)(0, subtract(darkNeutralLayerL4, multiply(neutralFillCardDelta, 5))), + ), +); + +/** + * @internal + * Used as the background color for cards. Pair with `neutralLayerCardContainer` for the container background. + */ +export const neutralLayerCard: ColorRecipe = colorRecipeFactory( + luminanceOrBackgroundColor( + getSwatch(neutralLayerCardIndex, neutralPalette), + swatchByMode(neutralPalette)(0, subtract(darkNeutralLayerL4, multiply(neutralFillCardDelta, 4))), + ), +); + +/** + * @internal + * Used as the background color for card containers. Pair with `neutralLayerCard` for the card backgrounds. + */ +export const neutralLayerCardContainer: ColorRecipe = colorRecipeFactory( + luminanceOrBackgroundColor( + getSwatch(add(neutralLayerCardIndex, neutralFillCardDelta), neutralPalette), + swatchByMode(neutralPalette)(neutralFillCardDelta, subtract(darkNeutralLayerL4, multiply(neutralFillCardDelta, 3))), + ), +); + +/** + * @internal + * Used as the background color for the primary content layer (L1). + */ +export const neutralLayerL1: ColorRecipe = colorRecipeFactory( + luminanceOrBackgroundColor( + getSwatch(baseLayerLuminanceIndex, neutralPalette), + swatchByMode(neutralPalette)(0, subtract(darkNeutralLayerL4, multiply(neutralFillCardDelta, 3))), + ), +); + +/** + * @internal + * Alternate darker color for L1 surfaces. Currently the same as card container, but use + * the most applicable semantic named recipe. + */ +export const neutralLayerL1Alt: ColorRecipe = neutralLayerCardContainer; + +/** + * @internal + * Used as the background for the top command surface, logically below L1. + */ +export const neutralLayerL2: ColorRecipe = colorRecipeFactory( + luminanceOrBackgroundColor( + getSwatch(neutralLayerL2Index, neutralPalette), + swatchByMode(neutralPalette)(lightNeutralLayerL2, subtract(darkNeutralLayerL4, multiply(neutralFillCardDelta, 2))), + ), +); + +/** + * @internal + * Used as the background for secondary command surfaces, logically below L2. + */ +export const neutralLayerL3: ColorRecipe = colorRecipeFactory( + luminanceOrBackgroundColor( + getSwatch(add(neutralLayerL2Index, neutralFillCardDelta), neutralPalette), + swatchByMode(neutralPalette)( + add(lightNeutralLayerL2, neutralFillCardDelta), + subtract(darkNeutralLayerL4, neutralFillCardDelta), + ), + ), +); + +/** + * @internal + * Used as the background for the lowest command surface or title bar, logically below L3. + */ +export const neutralLayerL4: ColorRecipe = colorRecipeFactory( + luminanceOrBackgroundColor( + getSwatch(add(neutralLayerL2Index, multiply(neutralFillCardDelta, 2)), neutralPalette), + swatchByMode(neutralPalette)(add(lightNeutralLayerL2, multiply(neutralFillCardDelta, 2)), darkNeutralLayerL4), + ), +); diff --git a/packages/web-components/src/color/neutral-outline-contrast.ts b/packages/web-components/src/color/neutral-outline-contrast.ts new file mode 100644 index 00000000000000..10864284f5fccd --- /dev/null +++ b/packages/web-components/src/color/neutral-outline-contrast.ts @@ -0,0 +1,70 @@ +import { subtract } from '../utilities/math'; +import { + neutralOutlineActiveDelta, + neutralOutlineFocusDelta, + neutralOutlineHoverDelta, + neutralOutlineRestDelta, + neutralPalette, +} from '../fluent-design-system'; +import { + ColorRecipe, + colorRecipeFactory, + SwatchFamily, + SwatchFamilyResolver, + swatchFamilyToSwatchRecipeFactory, + SwatchFamilyType, + SwatchRecipe, +} from './common'; + +import { accessibleAlgorithm } from './accessible-recipe'; + +/** + * @internal + */ +export const neutralOutlineContrastAlgorithm: SwatchFamilyResolver = colorRecipeFactory( + accessibleAlgorithm( + neutralPalette, + 3, + 0, + subtract(neutralOutlineHoverDelta, neutralOutlineRestDelta), + subtract(neutralOutlineActiveDelta, neutralOutlineRestDelta), + subtract(neutralOutlineFocusDelta, neutralOutlineRestDelta), + ), +); + +/** + * @internal + */ +export const neutralOutlineContrast: ColorRecipe = colorRecipeFactory(neutralOutlineContrastAlgorithm); + +/** + * @internal + */ +export const neutralOutlineContrastRest: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.rest, + neutralOutlineContrast, +); + +/** + * @internal + */ +export const neutralOutlineContrastHover: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.hover, + neutralOutlineContrast, +); + +/** + * @internal + */ +export const neutralOutlineContrastActive: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.active, + neutralOutlineContrast, +); + +/** + * @internal + */ +export const neutralOutlineContrastFocus: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.focus, + neutralOutlineContrast, +); diff --git a/packages/web-components/src/color/neutral-outline.spec.ts b/packages/web-components/src/color/neutral-outline.spec.ts new file mode 100644 index 00000000000000..95db385832f87f --- /dev/null +++ b/packages/web-components/src/color/neutral-outline.spec.ts @@ -0,0 +1,93 @@ +import { expect } from "chai"; +import { isColorStringHexRGB } from "@microsoft/fast-colors"; +import { + DesignSystem, + DesignSystemDefaults, + accentPalette as getAccentPalette, + neutralPalette as getNeutralPalette, +} from "../fluent-design-system"; +import { + neutralOutline, + neutralOutlineActive, + neutralOutlineFocus, + neutralOutlineHover, + neutralOutlineRest, +} from "./neutral-outline"; +import { Palette } from "./palette"; +import { Swatch, SwatchFamily } from "./common"; + +describe("neutralOutline", (): void => { + const neutralPalette: Palette = getNeutralPalette(DesignSystemDefaults); + const accentPalette: Palette = getAccentPalette(DesignSystemDefaults); + + it("should return by default", (): void => { + [ + neutralOutlineActive, + neutralOutlineFocus, + neutralOutlineHover, + neutralOutlineRest, + ].forEach(fn => { + expect(neutralPalette).to.include(fn({} as DesignSystem)); + }); + }); + + it("should always return a color", (): void => { + neutralPalette.concat(accentPalette).forEach((swatch: Swatch): void => { + expect( + isColorStringHexRGB(neutralOutlineRest(() => swatch)({} as DesignSystem)) + ).to.equal(true); + }); + }); + + it("should return the same color from both implementations", (): void => { + neutralPalette.concat(accentPalette).forEach((swatch: Swatch): void => { + expect(neutralOutlineRest(() => swatch)(DesignSystemDefaults)).to.equal( + neutralOutlineRest( + Object.assign({}, DesignSystemDefaults, { + backgroundColor: swatch, + }) + ) + ); + expect(neutralOutlineHover(() => swatch)(DesignSystemDefaults)).to.equal( + neutralOutlineHover( + Object.assign({}, DesignSystemDefaults, { + backgroundColor: swatch, + }) + ) + ); + expect(neutralOutlineActive(() => swatch)(DesignSystemDefaults)).to.equal( + neutralOutlineActive( + Object.assign({}, DesignSystemDefaults, { + backgroundColor: swatch, + }) + ) + ); + expect(neutralOutlineFocus(() => swatch)(DesignSystemDefaults)).to.equal( + neutralOutlineFocus( + Object.assign({}, DesignSystemDefaults, { + backgroundColor: swatch, + }) + ) + ); + }); + }); + + it("should have consistent return values", (): void => { + neutralPalette.concat(accentPalette).forEach((swatch: Swatch): void => { + const backplates: SwatchFamily = neutralOutline(() => swatch)( + DesignSystemDefaults + ); + const rest: Swatch = neutralOutlineRest(() => swatch)(DesignSystemDefaults); + const hover: Swatch = neutralOutlineHover(() => swatch)(DesignSystemDefaults); + const active: Swatch = neutralOutlineActive(() => swatch)( + DesignSystemDefaults + ); + const focus: Swatch = neutralOutlineFocus(() => swatch)(DesignSystemDefaults); + + expect(backplates.rest).to.equal(rest); + expect(backplates.hover).to.equal(hover); + expect(backplates.active).to.equal(active); + expect(backplates.focus).to.equal(focus); + }); + }); +}); diff --git a/packages/web-components/src/color/neutral-outline.ts b/packages/web-components/src/color/neutral-outline.ts new file mode 100644 index 00000000000000..170d347ee37649 --- /dev/null +++ b/packages/web-components/src/color/neutral-outline.ts @@ -0,0 +1,77 @@ +import { + DesignSystem, + neutralOutlineActiveDelta, + neutralOutlineFocusDelta, + neutralOutlineHoverDelta, + neutralOutlineRestDelta, + neutralPalette, +} from '../fluent-design-system'; +import { findClosestBackgroundIndex, getSwatch, isDarkMode } from './palette'; +import { + ColorRecipe, + colorRecipeFactory, + SwatchFamily, + SwatchFamilyResolver, + swatchFamilyToSwatchRecipeFactory, + SwatchFamilyType, + SwatchRecipe, +} from './common'; + +const neutralOutlineAlgorithm: SwatchFamilyResolver = (designSystem: DesignSystem): SwatchFamily => { + const palette: string[] = neutralPalette(designSystem); + const backgroundIndex: number = findClosestBackgroundIndex(designSystem); + const direction: 1 | -1 = isDarkMode(designSystem) ? -1 : 1; + + const restDelta: number = neutralOutlineRestDelta(designSystem); + const restIndex: number = backgroundIndex + direction * restDelta; + const hoverDelta: number = neutralOutlineHoverDelta(designSystem); + const hoverIndex: number = restIndex + direction * (hoverDelta - restDelta); + const activeDelta: number = neutralOutlineActiveDelta(designSystem); + const activeIndex: number = restIndex + direction * (activeDelta - restDelta); + const focusDelta: number = neutralOutlineFocusDelta(designSystem); + const focusIndex: number = restIndex + direction * (focusDelta - restDelta); + + return { + rest: getSwatch(restIndex, palette), + hover: getSwatch(hoverIndex, palette), + active: getSwatch(activeIndex, palette), + focus: getSwatch(focusIndex, palette), + }; +}; + +/** + * @internal + */ +export const neutralOutline: ColorRecipe = colorRecipeFactory(neutralOutlineAlgorithm); + +/** + * @internal + */ +export const neutralOutlineRest: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.rest, + neutralOutline, +); + +/** + * @internal + */ +export const neutralOutlineHover: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.hover, + neutralOutline, +); + +/** + * @internal + */ +export const neutralOutlineActive: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.active, + neutralOutline, +); + +/** + * @internal + */ +export const neutralOutlineFocus: SwatchRecipe = swatchFamilyToSwatchRecipeFactory( + SwatchFamilyType.focus, + neutralOutline, +); diff --git a/packages/web-components/src/color/palette.spec.ts b/packages/web-components/src/color/palette.spec.ts new file mode 100644 index 00000000000000..b2e8c964d73895 --- /dev/null +++ b/packages/web-components/src/color/palette.spec.ts @@ -0,0 +1,346 @@ +import chai, { expect } from "chai"; +import spies from "chai-spies"; +import { accentBaseColor, accentPalette, DesignSystem, DesignSystemDefaults, neutralPalette } from "../fluent-design-system"; +import { + findClosestSwatchIndex, + findSwatchIndex, + getSwatch, + palette, + Palette, + PaletteType, + swatchByContrast, + swatchByMode, +} from "./palette"; +import { Swatch } from "./common"; + +chai.use(spies); + +describe("palette", (): void => { + it("should return a function", (): void => { + expect(typeof palette(PaletteType.accent)).to.equal("function"); + expect(typeof palette(PaletteType.neutral)).to.equal("function"); + }); + + it("should return a function that returns a palette if the argument does not match a palette", (): void => { + expect((palette as any)()()).to.have.length(94); + }); + + it("should return a palette if no designSystem is provided", (): void => { + expect(palette(PaletteType.neutral)(undefined as any)).to.have.length(94); + expect(palette(PaletteType.accent)(undefined as any)).to.have.length(94); + }); + + it("should return upper-case hex values", (): void => { + (palette(PaletteType.neutral)(DesignSystemDefaults) as Palette).forEach( + (swatch: Swatch) => { + expect(swatch).to.equal(swatch.toUpperCase()); + } + ); + (palette(PaletteType.accent)(DesignSystemDefaults) as Palette).forEach( + (swatch: Swatch) => { + expect(swatch).to.equal(swatch.toUpperCase()); + } + ); + }); + + it("should return six-letter hex values", (): void => { + (palette(PaletteType.neutral)(DesignSystemDefaults) as Palette).forEach( + (swatch: Swatch) => { + expect(swatch.length).to.equal(7); + expect(swatch.charAt(0)).to.equal("#"); + } + ); + (palette(PaletteType.accent)(DesignSystemDefaults) as Palette).forEach( + (swatch: Swatch) => { + expect(swatch.length).to.equal(7); + expect(swatch.charAt(0)).to.equal("#"); + } + ); + }); +}); + +describe("findSwatchIndex", (): void => { + it("should implement design-system defaults", (): void => { + expect(findSwatchIndex(neutralPalette, "#FFF")({} as DesignSystem)).to.equal(0); + expect( + findSwatchIndex( + accentPalette, + accentBaseColor({} as DesignSystem) + )({} as DesignSystem) + ).to.equal(59); + }); + + it("should return -1 if the color is not found", (): void => { + expect(findSwatchIndex(neutralPalette, "#FF0000")(DesignSystemDefaults)).to.equal(-1); + expect(findSwatchIndex(accentPalette, "#FF0000")(DesignSystemDefaults)).to.equal(-1); + }); + + it("should find white", (): void => { + expect(findSwatchIndex(neutralPalette, "#FFFFFF")(DesignSystemDefaults)).to.equal(0); + expect(findSwatchIndex(neutralPalette, "#FFF")(DesignSystemDefaults)).to.equal(0); + expect( + findSwatchIndex(neutralPalette, "rgb(255, 255, 255)")(DesignSystemDefaults) + ).to.equal(0); + }); + + it("should find black", (): void => { + expect(findSwatchIndex(neutralPalette, "#000000")(DesignSystemDefaults)).to.equal(93); + expect(findSwatchIndex(neutralPalette, "#000")(DesignSystemDefaults)).to.equal(93); + expect( + findSwatchIndex(neutralPalette, "rgb(0, 0, 0)")(DesignSystemDefaults) + ).to.equal(93); + }); + + it("should find accent", (): void => { + expect( + findSwatchIndex( + accentPalette, + accentBaseColor(DesignSystemDefaults) + )(DesignSystemDefaults) + ).to.equal(59); + expect( + findSwatchIndex(accentPalette, "rgb(0, 120, 212)")(DesignSystemDefaults) + ).to.equal(59); + }); +}); + +describe("findClosestSwatchIndex", (): void => { + it("should return 0 if the input swatch cannot be converted to a color", (): void => { + expect( + findClosestSwatchIndex(neutralPalette, "pewpewpew")({} as DesignSystem) + ).to.equal(0); + }); + it("should operate on design system defaults", (): void => { + expect( + findClosestSwatchIndex(neutralPalette, "#FFFFFF")({} as DesignSystem) + ).to.equal(0); + expect( + findClosestSwatchIndex(neutralPalette, "#808080")({} as DesignSystem) + ).to.equal(49); + expect( + findClosestSwatchIndex(neutralPalette, "#000000")({} as DesignSystem) + ).to.equal(93); + }); + it("should return the index with the closest luminance to the input swatch if the swatch is not in the palette", (): void => { + expect( + findClosestSwatchIndex(neutralPalette, "#008000")({} as DesignSystem) + ).to.equal(56); + expect( + findClosestSwatchIndex(neutralPalette, "#F589FF")({} as DesignSystem) + ).to.equal(30); + }); +}); + +describe("getSwatch", (): void => { + const colorPalette: Palette = ["#FFF", "#F00", "#000"]; + + it("should return the first color when the input index is less than 0", (): void => { + expect(getSwatch(-1, colorPalette)).to.equal("#FFF"); + }); + + it("should return the last color when the input index is greater than the last index of the palette", (): void => { + expect(getSwatch(4, colorPalette)).to.equal("#000"); + }); + + it("should return the color at the provided index if the index is within the bounds of the array", (): void => { + expect(getSwatch(0, colorPalette)).to.equal("#FFF"); + expect(getSwatch(1, colorPalette)).to.equal("#F00"); + expect(getSwatch(2, colorPalette)).to.equal("#000"); + }); +}); + +describe("swatchByMode", (): void => { + it("should operate on DesignSystemDefaults", (): void => { + expect(swatchByMode(neutralPalette)(0, 0)({} as DesignSystem)).to.equal( + DesignSystemDefaults.neutralPalette[0] + ); + expect(swatchByMode(accentPalette)(0, 0)({} as DesignSystem)).to.equal( + DesignSystemDefaults.accentPalette[0] + ); + }); + it("should return the dark index color when the background color is dark", (): void => { + expect( + swatchByMode(neutralPalette)(0, 7)({ + backgroundColor: "#000", + } as DesignSystem) + ).to.equal(DesignSystemDefaults.neutralPalette[7]); + expect( + swatchByMode(accentPalette)(0, 7)({ + backgroundColor: "#000", + } as DesignSystem) + ).to.equal(DesignSystemDefaults.accentPalette[7]); + }); +}); + +describe("swatchByContrast", (): void => { + it("should return a function", (): void => { + expect(typeof swatchByContrast({} as any)).to.equal("function"); + }); + describe("indexResolver", (): void => { + it("should pass a reference color as the first argument", (): void => { + const indexResolver = chai.spy(() => 0); + const directionResolver = chai.spy(() => 1); + const contrastCondition = chai.spy(() => false); + + swatchByContrast("#FFF")(neutralPalette)(indexResolver as any)( + directionResolver as any + )(contrastCondition as any)({} as DesignSystem); + expect(indexResolver).to.have.been.called.once; + expect(indexResolver).to.have.been.called.with("#FFF"); + }); + it("should pass the palette as the second argument", (): void => { + const indexResolver = chai.spy(() => 0); + const directionResolver = chai.spy(() => 1); + const contrastCondition = chai.spy(() => false); + const colorPalette: string[] = ["foo"]; + + swatchByContrast("#FFF")(() => colorPalette)(indexResolver as any)( + directionResolver as any + )(contrastCondition as any)({} as DesignSystem); + expect(indexResolver).to.have.been.called.once; + expect(indexResolver).to.have.been.called.with(colorPalette); + }); + it("should pass the designSystem as the third argument", (): void => { + const indexResolver = chai.spy(() => 0); + const directionResolver = chai.spy(() => 1); + const contrastCondition = chai.spy(() => false); + const designSystem: DesignSystem = {} as DesignSystem; + + swatchByContrast("#FFF")(neutralPalette)(indexResolver as any)( + directionResolver as any + )(contrastCondition as any)(designSystem); + expect(indexResolver).to.have.been.called.once; + expect(indexResolver).to.have.been.called.with(designSystem); + }); + }); + describe("directionResolver", (): void => { + it("should pass the reference index as the first argument", (): void => { + const index: number = 20; + const indexResolver = chai.spy(() => index); + const directionResolver = chai.spy(() => 1); + const contrastCondition = chai.spy(() => false); + + swatchByContrast("#FFF")(neutralPalette)(indexResolver as any)( + directionResolver as any + )(contrastCondition as any)({} as DesignSystem); + expect(directionResolver).to.have.been.called.once; + expect(directionResolver).to.have.been.called.with(index); + }); + it("should receive the palette length - 1 if the resolved index is greater than the palette length", (): void => { + const index: number = 105; + const indexResolver = chai.spy(() => index); + const directionResolver = chai.spy(() => 1); + const contrastCondition = chai.spy(() => false); + + swatchByContrast("#FFF")(neutralPalette)(indexResolver as any)( + directionResolver as any + )(contrastCondition as any)({} as DesignSystem); + expect(directionResolver).to.have.been.called.once; + expect(directionResolver).to.have.been.called.with( + neutralPalette({} as DesignSystem).length - 1 + ); + }); + it("should receive 0 if the resolved index is less than 0", (): void => { + const index: number = -20; + const indexResolver = chai.spy(() => index); + const directionResolver = chai.spy(() => 1); + const contrastCondition = chai.spy(() => false); + + swatchByContrast("#FFF")(neutralPalette)(indexResolver as any)( + directionResolver as any + )(contrastCondition as any)({} as DesignSystem); + expect(directionResolver).to.have.been.called.once; + expect(directionResolver).to.have.been.called.with(0); + }); + it("should pass the palette as the second argument", (): void => { + const indexResolver = chai.spy(() => 0); + const directionResolver = chai.spy(() => 1); + const contrastCondition = chai.spy(() => false); + const colorPalette: string[] = ["foo"]; + + swatchByContrast("#FFF")(() => colorPalette)(indexResolver as any)( + directionResolver as any + )(contrastCondition as any)({} as DesignSystem); + expect(directionResolver).to.have.been.called.once; + expect(directionResolver).to.have.been.called.with(colorPalette); + }); + it("should pass the designSystem as the third argument", (): void => { + const indexResolver = chai.spy(() => 0); + const directionResolver = chai.spy(() => 1); + const contrastCondition = chai.spy(() => false); + const designSystem: DesignSystem = {} as DesignSystem; + + swatchByContrast("#FFF")(neutralPalette)(indexResolver as any)( + directionResolver as any + )(contrastCondition as any)(designSystem); + expect(directionResolver).to.have.been.called.once; + expect(directionResolver).to.have.been.called.with(designSystem); + }); + }); + + it("should return the color at the initial index if it satisfies the predicate", (): void => { + const indexResolver: () => number = (): number => 0; + const directionResolver: () => 1 | -1 = (): 1 | -1 => 1; + const contrastCondition: () => boolean = (): boolean => true; + const designSystem: DesignSystem = {} as DesignSystem; + const sourcePalette: string[] = ["#111", "#222", "#333"]; + + expect( + swatchByContrast("#FFF")(() => sourcePalette)(indexResolver)( + directionResolver + )(contrastCondition)(designSystem) + ).to.equal(sourcePalette[0]); + }); + it("should return the color at the last index when direction is 1 and no value satisfies the predicate", (): void => { + const indexResolver: () => number = (): number => 0; + const directionResolver: () => 1 | -1 = (): 1 | -1 => 1; + const contrastCondition: () => boolean = (): boolean => false; + const designSystem: DesignSystem = {} as DesignSystem; + const sourcePalette: string[] = ["#111", "#222", "#333"]; + + expect( + swatchByContrast("#FFF")(() => sourcePalette)(indexResolver)( + directionResolver + )(contrastCondition)(designSystem) + ).to.equal(sourcePalette[sourcePalette.length - 1]); + }); + it("should return the color at the first index when direction is -1 and no value satisfies the predicate", (): void => { + const sourcePalette: string[] = ["#111", "#222", "#333"]; + const indexResolver: () => number = (): number => sourcePalette.length - 1; + const directionResolver: () => 1 | -1 = (): 1 | -1 => 1; + const contrastCondition: () => boolean = (): boolean => false; + const designSystem: DesignSystem = {} as DesignSystem; + + expect( + swatchByContrast("#FFF")(() => sourcePalette)(indexResolver)( + directionResolver + )(contrastCondition)(designSystem) + ).to.equal(sourcePalette[sourcePalette.length - 1]); + }); + it("should return the color at the last index when initialIndex is greater than the last index", (): void => { + const sourcePalette: string[] = ["#111", "#222", "#333"]; + const indexResolver: () => number = (): number => sourcePalette.length; + const directionResolver: () => 1 | -1 = (): 1 | -1 => 1; + const contrastCondition: () => boolean = (): boolean => false; + const designSystem: DesignSystem = {} as DesignSystem; + + expect( + swatchByContrast("#FFF")(() => sourcePalette)(indexResolver)( + directionResolver + )(contrastCondition)(designSystem) + ).to.equal(sourcePalette[sourcePalette.length - 1]); + }); + it("should return the color at the first index when initialIndex is less than 0", (): void => { + const sourcePalette: string[] = ["#111", "#222", "#333"]; + const indexResolver: () => number = (): number => sourcePalette.length; + const directionResolver: () => 1 | -1 = (): 1 | -1 => -1; + const contrastCondition: () => boolean = (): boolean => false; + const designSystem: DesignSystem = {} as DesignSystem; + + expect( + swatchByContrast("#FFF")(() => sourcePalette)(indexResolver)( + directionResolver + )(contrastCondition)(designSystem) + ).to.equal(sourcePalette[0]); + }); +}); diff --git a/packages/web-components/src/color/palette.ts b/packages/web-components/src/color/palette.ts new file mode 100644 index 00000000000000..5b7809141dd3d5 --- /dev/null +++ b/packages/web-components/src/color/palette.ts @@ -0,0 +1,326 @@ +import { + accentPalette, + backgroundColor, + DesignSystem, + DesignSystemResolver, + evaluateDesignSystemResolver, + neutralPalette, +} from '../fluent-design-system'; +import { clamp, colorMatches, contrast, isValidColor, luminance, Swatch, SwatchResolver } from './common'; + +/** + * The named palettes of the MSFT design system + * @deprecated - use neutralPalette and accentPalette functions instead + * @public + */ +export enum PaletteType { + neutral = 'neutral', + accent = 'accent', +} + +/** + * The structure of a color palette + * + * @public + */ +export type Palette = Swatch[]; + +/** + * Retrieves a palette by name. This function returns a function that accepts + * a design system, returning a palette a palette or null + * @deprecated - use neutralPalette and accentPalette functions instead + * @internal + */ +export function palette(paletteType: PaletteType): DesignSystemResolver { + return (designSystem: DesignSystem | undefined): Palette => { + switch (paletteType) { + case PaletteType.accent: + return accentPalette(designSystem!); + case PaletteType.neutral: + default: + return neutralPalette(designSystem!); + } + }; +} + +/** + * A function to find the index of a swatch in a specified palette. If the color is found, + * otherwise it will return -1 + * + * @internal + */ +export function findSwatchIndex( + paletteResolver: Palette | DesignSystemResolver, + swatch: Swatch, +): DesignSystemResolver { + return (designSystem: DesignSystem): number => { + if (!isValidColor(swatch)) { + return -1; + } + + const colorPalette: Palette = evaluateDesignSystemResolver(paletteResolver, designSystem); + const index: number = colorPalette.indexOf(swatch); + + // If we don't find the string exactly, it might be because of color formatting differences + return index !== -1 + ? index + : colorPalette.findIndex((paletteSwatch: Swatch): boolean => { + return isValidColor(paletteSwatch) && colorMatches(swatch, paletteSwatch); + }); + }; +} + +/** + * Returns the closest swatch in a palette to an input swatch. + * If the input swatch cannot be converted to a color, 0 will be returned + * + * @internal + */ +export function findClosestSwatchIndex( + paletteResolver: Palette | DesignSystemResolver, + swatch: Swatch | DesignSystemResolver, +): DesignSystemResolver { + return (designSystem: DesignSystem): number => { + const resolvedPalette: Palette = evaluateDesignSystemResolver(paletteResolver, designSystem); + const resolvedSwatch: Swatch = evaluateDesignSystemResolver(swatch, designSystem); + const index: number = findSwatchIndex(resolvedPalette, resolvedSwatch)(designSystem); + let swatchLuminance: number; + + if (index !== -1) { + return index; + } + + try { + swatchLuminance = luminance(resolvedSwatch); + } catch (e) { + swatchLuminance = -1; + } + + if (swatchLuminance === -1) { + return 0; + } + + interface LuminanceMap { + luminance: number; + index: number; + } + + return resolvedPalette + .map( + (mappedSwatch: Swatch, mappedIndex: number): LuminanceMap => { + return { + luminance: luminance(mappedSwatch), + index: mappedIndex, + }; + }, + ) + .reduce( + (previousValue: LuminanceMap, currentValue: LuminanceMap): LuminanceMap => { + return Math.abs(currentValue.luminance - swatchLuminance) < + Math.abs(previousValue.luminance - swatchLuminance) + ? currentValue + : previousValue; + }, + ).index; + }; +} + +/** + * @public + * @privateRemarks + * Determines if the design-system should be considered in "dark mode". + * We're in dark mode if we have more contrast between #000000 and our background + * color than #FFFFFF and our background color. That threshold can be expressed as a relative luminance + * using the contrast formula as (1 + 0.5) / (bg + 0.05) === (bg + 0.05) / (0 + 0.05), + * which reduces to the following, where bg is the relative luminance of the background color + */ +export function isDarkMode(designSystem: DesignSystem): boolean { + return luminance(backgroundColor(designSystem)) <= (-0.1 + Math.sqrt(0.21)) / 2; +} + +/** + * @internal + * @deprecated + * Determines if the design-system should be considered in "light mode". + */ +export function isLightMode(designSystem: DesignSystem): boolean { + return !isDarkMode(designSystem); +} + +/** + * @internal + * Safely retrieves an index of a palette. The index is clamped to valid + * array indexes so that a swatch is always returned + */ +export function getSwatch(index: number, colorPalette: Palette): Swatch; +export function getSwatch( + index: DesignSystemResolver, + colorPalette: DesignSystemResolver, +): DesignSystemResolver; +export function getSwatch(index: any, colorPalette: any): any { + if (typeof index === 'function') { + return (designSystem: DesignSystem): Swatch => { + return colorPalette(designSystem)[clamp(index(designSystem), 0, colorPalette(designSystem).length - 1)]; + }; + } else { + return colorPalette[clamp(index, 0, colorPalette.length - 1)]; + } +} + +/** + * @internal + */ +export function swatchByMode( + paletteResolver: DesignSystemResolver, +): ( + a: number | DesignSystemResolver, + b: number | DesignSystemResolver, +) => DesignSystemResolver { + return ( + valueA: number | DesignSystemResolver, + valueB?: number | DesignSystemResolver, + ): DesignSystemResolver => { + return (designSystem: DesignSystem): Swatch => { + return getSwatch( + isDarkMode(designSystem) + ? evaluateDesignSystemResolver(valueB!, designSystem) + : evaluateDesignSystemResolver(valueA, designSystem), + paletteResolver(designSystem), + ); + }; + }; +} + +function binarySearch( + valuesToSearch: T[], + searchCondition: (value: T) => boolean, + startIndex: number = 0, + endIndex: number = valuesToSearch.length - 1, +): T { + if (endIndex === startIndex) { + return valuesToSearch[startIndex]; + } + + const middleIndex: number = Math.floor((endIndex - startIndex) / 2) + startIndex; + + // Check to see if this passes on the item in the center of the array + // if it does check the previous values + if (searchCondition(valuesToSearch[middleIndex])) { + return binarySearch( + valuesToSearch, + searchCondition, + startIndex, + middleIndex, // include this index because it passed the search condition + ); + } else { + return binarySearch( + valuesToSearch, + searchCondition, + middleIndex + 1, // exclude this index because it failed the search condition + endIndex, + ); + } +} + +// disable type-defs because this a deeply curried function and the call-signature is pretty complicated +// and typescript can work it out automatically for consumers +/** + * Retrieves a swatch from an input palette, where the swatch's contrast against the reference color + * passes an input condition. The direction to search in the palette is determined by an input condition. + * Where to begin the search in the palette will be determined another input function that should return the starting index. + * example: swatchByContrast( + * "#FFF" // compare swatches against "#FFF" + * )( + * neutralPalette // use the neutral palette from the DesignSystem - since this is a function, it will be evaluated with the DesignSystem + * )( + * () => 0 // begin searching for a swatch at the beginning of the neutral palette + * )( + * () => 1 // While searching, search in the direction toward the end of the array (-1 moves towards the beginning of the array) + * )( + * minContrastTargetFactory(4.5) // A swatch is only valid if the contrast is greater than 4.5 + * )( + * designSystem // Pass the design-system. The first swatch that passes the previous condition will be returned from this function + * ) + * @internal + */ +export function swatchByContrast(referenceColor: string | SwatchResolver) { + /** + * A function that expects a function that resolves a palette + */ + return (paletteResolver: Palette | DesignSystemResolver) => { + /** + * A function that expects a function that resolves the index + * of the palette that the algorithm should begin looking for a swatch at + */ + return (indexResolver: (referenceColor: string, palette: Palette, designSystem: DesignSystem) => number) => { + /** + * A function that expects a function that determines which direction in the + * palette we should look for a swatch relative to the initial index + */ + return (directionResolver: (referenceIndex: number, palette: Palette, designSystem: DesignSystem) => 1 | -1) => { + /** + * A function that expects a function that determines if the contrast + * between the reference color and color from the palette are acceptable + */ + return (contrastCondition: (contrastRatio: number) => boolean): DesignSystemResolver => { + /** + * A function that accepts a design-system. It resolves all of the curried arguments + * and loops over the palette until we reach the bounds of the palette or the condition + * is satisfied. Once either the condition is satisfied or we reach the end of the palette, + * we return the color + */ + return (designSystem: DesignSystem): Swatch => { + const color: Swatch = evaluateDesignSystemResolver(referenceColor, designSystem); + const sourcePalette: Palette = evaluateDesignSystemResolver(paletteResolver, designSystem); + const length: number = sourcePalette.length; + const initialSearchIndex: number = clamp(indexResolver(color, sourcePalette, designSystem), 0, length - 1); + const direction: 1 | -1 = directionResolver(initialSearchIndex, sourcePalette, designSystem); + + function contrastSearchCondition(valueToCheckAgainst: Swatch): boolean { + return contrastCondition(contrast(color, valueToCheckAgainst)); + } + + const constrainedSourcePalette: Palette = [].concat(sourcePalette as any); + const endSearchIndex: number = length - 1; + let startSearchIndex: number = initialSearchIndex; + + if (direction === -1) { + // reverse the palette array when the direction that + // the contrast resolves for is reversed + constrainedSourcePalette.reverse(); + startSearchIndex = endSearchIndex - startSearchIndex; + } + + return binarySearch(constrainedSourcePalette, contrastSearchCondition, startSearchIndex, endSearchIndex); + }; + }; + }; + }; + }; +} + +/** + * @internal + * Resolves the index that the contrast search algorithm should start at + */ +export function referenceColorInitialIndexResolver( + referenceColor: string, + sourcePalette: Palette, + designSystem: DesignSystem, +): number { + return findClosestSwatchIndex(sourcePalette, referenceColor)(designSystem); +} + +/** + * @internal + */ +export function findClosestBackgroundIndex(designSystem: DesignSystem): number { + return findClosestSwatchIndex(neutralPalette, backgroundColor(designSystem))(designSystem); +} + +/** + * @internal + */ +export function minContrastTargetFactory(targetContrast: number): (instanceContrast: number) => boolean { + return (instanceContrast: number): boolean => instanceContrast >= targetContrast; +} diff --git a/packages/web-components/src/default-palette.ts b/packages/web-components/src/default-palette.ts new file mode 100644 index 00000000000000..ba340464c54f63 --- /dev/null +++ b/packages/web-components/src/default-palette.ts @@ -0,0 +1,196 @@ +/** + * DO NOT EDIT THIS FILE DIRECTLY + * This file generated by web-components/build/generate-default-palettes.js + */ +export const neutralPalette: string[] = [ + "#FFFFFF", + "#FCFCFC", + "#FAFAFA", + "#F7F7F7", + "#F5F5F5", + "#F2F2F2", + "#EFEFEF", + "#EDEDED", + "#EAEAEA", + "#E8E8E8", + "#E5E5E5", + "#E2E2E2", + "#E0E0E0", + "#DDDDDD", + "#DBDBDB", + "#D8D8D8", + "#D6D6D6", + "#D3D3D3", + "#D0D0D0", + "#CECECE", + "#CBCBCB", + "#C9C9C9", + "#C6C6C6", + "#C3C3C3", + "#C1C1C1", + "#BEBEBE", + "#BCBCBC", + "#B9B9B9", + "#B6B6B6", + "#B4B4B4", + "#B1B1B1", + "#AFAFAF", + "#ACACAC", + "#A9A9A9", + "#A7A7A7", + "#A4A4A4", + "#A2A2A2", + "#9F9F9F", + "#9D9D9D", + "#9A9A9A", + "#979797", + "#959595", + "#929292", + "#909090", + "#8D8D8D", + "#8A8A8A", + "#888888", + "#858585", + "#838383", + "#808080", + "#7D7D7D", + "#7B7B7B", + "#787878", + "#767676", + "#737373", + "#717171", + "#6E6E6E", + "#6B6B6B", + "#696969", + "#666666", + "#646464", + "#616161", + "#5F5F5F", + "#5C5C5C", + "#5A5A5A", + "#575757", + "#545454", + "#525252", + "#4F4F4F", + "#4D4D4D", + "#4A4A4A", + "#484848", + "#454545", + "#424242", + "#404040", + "#3D3D3D", + "#3B3B3B", + "#383838", + "#363636", + "#333333", + "#313131", + "#2E2E2E", + "#2B2B2B", + "#292929", + "#262626", + "#242424", + "#212121", + "#1E1E1E", + "#1B1B1B", + "#181818", + "#151515", + "#121212", + "#101010", + "#000000" +]; +export const accentPalette: string[] = [ + "#FFFFFF", + "#FBFDFE", + "#F6FAFE", + "#F2F8FD", + "#EEF6FC", + "#E9F4FB", + "#E5F1FB", + "#E1EFFA", + "#DCEDF9", + "#D8EAF8", + "#D4E8F8", + "#CFE6F7", + "#CBE4F6", + "#C7E1F6", + "#C2DFF5", + "#BEDDF4", + "#BADAF3", + "#B6D8F3", + "#B1D6F2", + "#ADD4F1", + "#A9D1F0", + "#A4CFF0", + "#A0CDEF", + "#9CCAEE", + "#97C8EE", + "#93C6ED", + "#8FC4EC", + "#8AC1EB", + "#86BFEB", + "#82BDEA", + "#7DBAE9", + "#79B8E8", + "#75B6E8", + "#70B3E7", + "#6CB1E6", + "#68AFE5", + "#63ADE5", + "#5FAAE4", + "#5BA8E3", + "#56A6E3", + "#52A3E2", + "#4EA1E1", + "#499FE0", + "#459DE0", + "#419ADF", + "#3D98DE", + "#3896DD", + "#3493DD", + "#3091DC", + "#2B8FDB", + "#278DDB", + "#238ADA", + "#1E88D9", + "#1A86D8", + "#1683D8", + "#1181D7", + "#0D7FD6", + "#097DD5", + "#047AD5", + "#0078D4", + "#0075CF", + "#0072C9", + "#006FC4", + "#006CBE", + "#0069B9", + "#0066B4", + "#0063AE", + "#0060A9", + "#005CA3", + "#00599E", + "#005699", + "#005393", + "#00508E", + "#004D88", + "#004A83", + "#00477D", + "#004478", + "#004173", + "#003E6D", + "#003B68", + "#003862", + "#00355D", + "#003258", + "#002F52", + "#002B4D", + "#002847", + "#002542", + "#00223C", + "#001F36", + "#001B30", + "#00182B", + "#001525", + "#00121F", + "#000000" +]; diff --git a/packages/web-components/src/design-system-provider/index.ts b/packages/web-components/src/design-system-provider/index.ts index 173cdb7e21cc7b..813025b33e8a59 100644 --- a/packages/web-components/src/design-system-provider/index.ts +++ b/packages/web-components/src/design-system-provider/index.ts @@ -1,10 +1,4 @@ import { attr, css, nullableNumberConverter } from '@microsoft/fast-element'; -import { - DensityOffset, - DesignSystem, - DesignSystemDefaults, - neutralForegroundRest, -} from '@microsoft/fast-components-styles-msft'; import { Direction } from '@microsoft/fast-web-utilities'; import { CSSCustomPropertyBehavior, @@ -13,6 +7,8 @@ import { designSystemProvider, DesignSystemProviderTemplate as template, } from '@microsoft/fast-foundation'; +import { neutralForegroundRest } from '../color'; +import { DensityOffset, DesignSystem, DesignSystemDefaults } from '../fluent-design-system'; import { DesignSystemProviderStyles as styles } from './design-system-provider.styles'; const color = new CSSCustomPropertyBehavior( @@ -172,109 +168,109 @@ export class FluentDesignSystemProvider extends DesignSystemProvider @designSystemProperty({ attribute: 'type-ramp-minus-2-font-size', - default: '10px', + default: DesignSystemDefaults.typeRampMinus2FontSize, }) public typeRampMinus2FontSize: string; @designSystemProperty({ attribute: 'type-ramp-minus-2-line-height', - default: '16px', + default: DesignSystemDefaults.typeRampMinus2LineHeight, }) public typeRampMinus2LineHeight: string; @designSystemProperty({ attribute: 'type-ramp-minus-1-font-size', - default: '12px', + default: DesignSystemDefaults.typeRampMinus1FontSize, }) public typeRampMinus1FontSize: string; @designSystemProperty({ attribute: 'type-ramp-minus-1-line-height', - default: '16px', + default: DesignSystemDefaults.typeRampMinus1LineHeight, }) public typeRampMinus1LineHeight: string; @designSystemProperty({ attribute: 'type-ramp-base-font-size', - default: '14px', + default: DesignSystemDefaults.typeRampBaseFontSize, }) public typeRampBaseFontSize: string; @designSystemProperty({ attribute: 'type-ramp-base-line-height', - default: '20px', + default: DesignSystemDefaults.typeRampBaseLineHeight, }) public typeRampBaseLineHeight: string; @designSystemProperty({ attribute: 'type-ramp-plus-1-font-size', - default: '16px', + default: DesignSystemDefaults.typeRampPlus1FontSize, }) public typeRampPlus1FontSize: string; @designSystemProperty({ attribute: 'type-ramp-plus-1-line-height', - default: '24px', + default: DesignSystemDefaults.typeRampPlus1LineHeight, }) public typeRampPlus1LineHeight: string; @designSystemProperty({ attribute: 'type-ramp-plus-2-font-size', - default: '20px', + default: DesignSystemDefaults.typeRampPlus2FontSize, }) public typeRampPlus2FontSize: string; @designSystemProperty({ attribute: 'type-ramp-plus-2-line-height', - default: '28px', + default: DesignSystemDefaults.typeRampPlus2LineHeight, }) public typeRampPlus2LineHeight: string; @designSystemProperty({ attribute: 'type-ramp-plus-3-font-size', - default: '28px', + default: DesignSystemDefaults.typeRampPlus3FontSize, }) public typeRampPlus3FontSize: string; @designSystemProperty({ attribute: 'type-ramp-plus-3-line-height', - default: '36px', + default: DesignSystemDefaults.typeRampPlus3LineHeight, }) public typeRampPlus3LineHeight: string; @designSystemProperty({ attribute: 'type-ramp-plus-4-font-size', - default: '34px', + default: DesignSystemDefaults.typeRampPlus4FontSize, }) public typeRampPlus4FontSize: string; @designSystemProperty({ attribute: 'type-ramp-plus-4-line-height', - default: '44px', + default: DesignSystemDefaults.typeRampPlus4LineHeight, }) public typeRampPlus4LineHeight: string; @designSystemProperty({ attribute: 'type-ramp-plus-5-font-size', - default: '46px', + default: DesignSystemDefaults.typeRampPlus5FontSize, }) public typeRampPlus5FontSize: string; @designSystemProperty({ attribute: 'type-ramp-plus-5-line-height', - default: '56px', + default: DesignSystemDefaults.typeRampPlus5LineHeight, }) public typeRampPlus5LineHeight: string; @designSystemProperty({ attribute: 'type-ramp-plus-6-font-size', - default: '60px', + default: DesignSystemDefaults.typeRampPlus6FontSize, }) public typeRampPlus6FontSize: string; @designSystemProperty({ attribute: 'type-ramp-plus-6-line-height', - default: '72px', + default: DesignSystemDefaults.typeRampPlus6LineHeight, }) public typeRampPlus6LineHeight: string; diff --git a/packages/web-components/src/fluent-design-system.ts b/packages/web-components/src/fluent-design-system.ts new file mode 100644 index 00000000000000..d411356321d7c3 --- /dev/null +++ b/packages/web-components/src/fluent-design-system.ts @@ -0,0 +1,453 @@ +import { Direction } from '@microsoft/fast-web-utilities'; +import { accentPalette as defaultAccentPalette, neutralPalette as defaultNeutralPalette } from './default-palette'; + +export type DesignSystemResolver = (d: Y) => T; + +export type DensityOffset = -3 | -2 | -1 | 0 | 1 | 2 | 3; + +/** + * Defines the properties in the FAST Design System + * @public + */ +export interface DesignSystem { + /** + * Type-ramp font-size and line-height values + */ + typeRampMinus2FontSize: string; + typeRampMinus2LineHeight: string; + typeRampMinus1FontSize: string; + typeRampMinus1LineHeight: string; + typeRampBaseFontSize: string; + typeRampBaseLineHeight: string; + typeRampPlus1FontSize: string; + typeRampPlus1LineHeight: string; + typeRampPlus2FontSize: string; + typeRampPlus2LineHeight: string; + typeRampPlus3FontSize: string; + typeRampPlus3LineHeight: string; + typeRampPlus4FontSize: string; + typeRampPlus4LineHeight: string; + typeRampPlus5FontSize: string; + typeRampPlus5LineHeight: string; + typeRampPlus6FontSize: string; + typeRampPlus6LineHeight: string; + + /** + * The background color of the current context. + * May be used to draw an actual background or not. Color recipes evaluated within this context will use this as their basis. + */ + backgroundColor: string; + + /** + * The accent color, which the accent palette is based on. + * Keep this value in sync with accentPalette. + */ + accentBaseColor: string; + + /** + * An array of colors in a ramp from light to dark, used to look up values for neutral color recipes. + * Generate by calling createColorPalette. + */ + neutralPalette: string[]; + + /** + * An array of colors in a ramp from light to dark, used to lookup values for accent color recipes. + * Keep this value in sync with accentBaseColor. + * Generate by calling createColorPalette. + */ + accentPalette: string[]; + + /** + * The density offset, used with designUnit to calculate height and spacing. + */ + density: number; + + /** + * The grid-unit that UI dimensions are derived from in pixels. + */ + designUnit: number; + + /** + * The primary document direction. + */ + direction: Direction; + + /** + * The number of designUnits used for component height at the base density. + */ + baseHeightMultiplier: number; + + /** + * The number of designUnits used for horizontal spacing at the base density. + */ + baseHorizontalSpacingMultiplier: number; + + /** + * The corner radius applied to controls. + */ + cornerRadius: number; + + /** + * The corner radius applied to elevated surfaces or controls. + */ + elevatedCornerRadius?: number; + + /** + * The width of the standard outline applied to outline components in pixels. + */ + outlineWidth: number; + + /** + * The width of the standard focus outline in pixels. + */ + focusOutlineWidth: number; + + /** + * The opacity of a disabled control. + */ + disabledOpacity: number; + + /** + * Color swatch deltas for the accent-fill recipe. + */ + accentFillRestDelta: number; + accentFillHoverDelta: number; + accentFillActiveDelta: number; + accentFillFocusDelta: number; + accentFillSelectedDelta: number; + + /** + * Color swatch deltas for the accent-foreground recipe. + */ + accentForegroundRestDelta: number; + accentForegroundHoverDelta: number; + accentForegroundActiveDelta: number; + accentForegroundFocusDelta: number; + + /* + * Color swatch deltas for the neutral-fill recipe. + */ + neutralFillRestDelta: number; + neutralFillHoverDelta: number; + neutralFillActiveDelta: number; + neutralFillFocusDelta: number; + neutralFillSelectedDelta: number; + + /** + * Color swatch deltas for the neutral-fill-input recipe. + */ + neutralFillInputRestDelta: number; + neutralFillInputHoverDelta: number; + neutralFillInputActiveDelta: number; + neutralFillInputFocusDelta: number; + neutralFillInputSelectedDelta: number; + + /** + * Color swatch deltas for the neutral-fill-stealth recipe. + */ + neutralFillStealthRestDelta: number; + neutralFillStealthHoverDelta: number; + neutralFillStealthActiveDelta: number; + neutralFillStealthFocusDelta: number; + neutralFillStealthSelectedDelta: number; + + /** + * Configuration for the neutral-fill-toggle recipe. + */ + neutralFillToggleHoverDelta: number; + neutralFillToggleActiveDelta: number; + neutralFillToggleFocusDelta: number; + + /** + * The luminance value to base layer recipes on. + * Sets the luminance value for the L1 layer recipe in a manner that can adjust to variable contrast. + * + * Currently defaults to -1 to turn the feature off and use backgroundColor for layer colors instead. + */ + baseLayerLuminance: number; // 0...1 + + /** + * Color swatch deltas for the neutral-fill-card recipe. + */ + neutralFillCardDelta: number; + + /** + * Color swatch delta for neutral-foreground recipe. + */ + neutralForegroundHoverDelta: number; + neutralForegroundActiveDelta: number; + neutralForegroundFocusDelta: number; + + /** + * Color swatch delta for the neutral-divider recipe. + */ + neutralDividerRestDelta: number; + + /** + * Color swatch deltas for the neutral-outline recipe. + */ + neutralOutlineRestDelta: number; + neutralOutlineHoverDelta: number; + neutralOutlineActiveDelta: number; + neutralOutlineFocusDelta: number; +} + +/** + * The default values for {@link DesignSystem} + * @public + */ +export const DesignSystemDefaults: DesignSystem = { + typeRampMinus2FontSize: '10px', + typeRampMinus2LineHeight: '16px', + typeRampMinus1FontSize: '12px', + typeRampMinus1LineHeight: '16px', + typeRampBaseFontSize: '14px', + typeRampBaseLineHeight: '20px', + typeRampPlus1FontSize: '16px', + typeRampPlus1LineHeight: '24px', + typeRampPlus2FontSize: '20px', + typeRampPlus2LineHeight: '28px', + typeRampPlus3FontSize: '28px', + typeRampPlus3LineHeight: '36px', + typeRampPlus4FontSize: '34px', + typeRampPlus4LineHeight: '44px', + typeRampPlus5FontSize: '46px', + typeRampPlus5LineHeight: '56px', + typeRampPlus6FontSize: '60px', + typeRampPlus6LineHeight: '72px', + + accentBaseColor: '#0078D4', + accentPalette: defaultAccentPalette, + backgroundColor: '#FFFFFF', + baseHeightMultiplier: 8, + baseHorizontalSpacingMultiplier: 3, + cornerRadius: 2, + elevatedCornerRadius: 4, + density: 0, + designUnit: 4, + direction: Direction.ltr, + disabledOpacity: 0.3, + focusOutlineWidth: 2, + neutralPalette: defaultNeutralPalette, + outlineWidth: 1, + + /** + * Recipe Deltas + */ + accentFillRestDelta: 0, + accentFillHoverDelta: 4, + accentFillActiveDelta: -5, + accentFillFocusDelta: 0, + accentFillSelectedDelta: 12, + + accentForegroundRestDelta: 0, + accentForegroundHoverDelta: 6, + accentForegroundActiveDelta: -4, + accentForegroundFocusDelta: 0, + + neutralFillRestDelta: 7, + neutralFillHoverDelta: 10, + neutralFillActiveDelta: 5, + neutralFillFocusDelta: 0, + neutralFillSelectedDelta: 7, + + neutralFillInputRestDelta: 0, + neutralFillInputHoverDelta: 0, + neutralFillInputActiveDelta: 0, + neutralFillInputFocusDelta: 0, + neutralFillInputSelectedDelta: 0, + + neutralFillStealthRestDelta: 0, + neutralFillStealthHoverDelta: 5, + neutralFillStealthActiveDelta: 3, + neutralFillStealthFocusDelta: 0, + neutralFillStealthSelectedDelta: 7, + + neutralFillToggleHoverDelta: 8, + neutralFillToggleActiveDelta: -5, + neutralFillToggleFocusDelta: 0, + + baseLayerLuminance: -1, + neutralFillCardDelta: 3, + + neutralForegroundHoverDelta: 0, + neutralForegroundActiveDelta: 0, + neutralForegroundFocusDelta: 0, + + neutralDividerRestDelta: 8, + + neutralOutlineRestDelta: 25, + neutralOutlineHoverDelta: 40, + neutralOutlineActiveDelta: 16, + neutralOutlineFocusDelta: 25, +}; + +/** + * Returns the argument if basic, otherwise calls the DesignSystemResolver function. + * + * @param arg A value or a DesignSystemResolver function + * @param designSystem The design system config. + */ +export function evaluateDesignSystemResolver(arg: T | DesignSystemResolver, designSystem: DesignSystem): T { + return typeof arg === 'function' ? (arg as DesignSystemResolver)(designSystem) : arg; +} + +/** + * Safely retrieves the value from a key of the DesignSystem. + */ +export function getDesignSystemValue(key: K): (designSystem?: T) => T[K] { + return (designSystem?: T): T[K] => { + return designSystem && designSystem[key] !== undefined ? designSystem[key] : (DesignSystemDefaults as T)[key]; + }; +} + +/** + * Retrieve the backgroundColor when invoked with a DesignSystem + */ +export const backgroundColor: DesignSystemResolver = getDesignSystemValue('backgroundColor'); + +/** + * Retrieve the accentBaseColor when invoked with a DesignSystem + */ +export const accentBaseColor: DesignSystemResolver = getDesignSystemValue('accentBaseColor'); + +/** + * Retrieve the cornerRadius when invoked with a DesignSystem + */ +export const cornerRadius: DesignSystemResolver = getDesignSystemValue('cornerRadius'); + +/** + * Retrieve the neutral palette from the design system + */ +export const neutralPalette: DesignSystemResolver = getDesignSystemValue('neutralPalette'); + +/** + * Retrieve the accent palette from the design system + */ +export const accentPalette: DesignSystemResolver = getDesignSystemValue('accentPalette'); + +/** + * Retrieve the designUnit from the design system + */ +export const designUnit: DesignSystemResolver = getDesignSystemValue('designUnit'); + +/** + * Retrieve the baseHeightMultiplier from the design system + */ +export const baseHeightMultiplier: DesignSystemResolver = getDesignSystemValue('baseHeightMultiplier'); + +/** + * Retrieve the baseHorizontalSpacingMultiplier from the design system + */ +export const baseHorizontalSpacingMultiplier: DesignSystemResolver = getDesignSystemValue( + 'baseHorizontalSpacingMultiplier', +); + +/** + * Retrieve the outlineWidth from the design system + */ +export const outlineWidth: DesignSystemResolver = getDesignSystemValue('outlineWidth'); + +/** + * Retrieve the focusOutlineWidth from the design system + */ +export const focusOutlineWidth: DesignSystemResolver = getDesignSystemValue('focusOutlineWidth'); +/** + * Retrieve the disabledOpacity from the design system + */ +export const disabledOpacity: DesignSystemResolver = getDesignSystemValue('disabledOpacity'); + +/** + * Retrieve the disabledOpacity from the design system + */ +export const direction: DesignSystemResolver = getDesignSystemValue('direction'); + +export const accentFillRestDelta: DesignSystemResolver = getDesignSystemValue('accentFillRestDelta'); +export const accentFillHoverDelta: DesignSystemResolver = getDesignSystemValue('accentFillHoverDelta'); +export const accentFillActiveDelta: DesignSystemResolver = getDesignSystemValue('accentFillActiveDelta'); +export const accentFillFocusDelta: DesignSystemResolver = getDesignSystemValue('accentFillFocusDelta'); +export const accentFillSelectedDelta: DesignSystemResolver = getDesignSystemValue('accentFillSelectedDelta'); + +export const accentForegroundRestDelta: DesignSystemResolver = getDesignSystemValue( + 'accentForegroundRestDelta', +); +export const accentForegroundHoverDelta: DesignSystemResolver = getDesignSystemValue( + 'accentForegroundHoverDelta', +); +export const accentForegroundActiveDelta: DesignSystemResolver = getDesignSystemValue( + 'accentForegroundActiveDelta', +); +export const accentForegroundFocusDelta: DesignSystemResolver = getDesignSystemValue( + 'accentForegroundFocusDelta', +); + +export const neutralFillRestDelta: DesignSystemResolver = getDesignSystemValue('neutralFillRestDelta'); +export const neutralFillHoverDelta: DesignSystemResolver = getDesignSystemValue('neutralFillHoverDelta'); +export const neutralFillActiveDelta: DesignSystemResolver = getDesignSystemValue('neutralFillActiveDelta'); +export const neutralFillFocusDelta: DesignSystemResolver = getDesignSystemValue('neutralFillFocusDelta'); +export const neutralFillSelectedDelta: DesignSystemResolver = getDesignSystemValue('neutralFillSelectedDelta'); + +export const neutralFillInputRestDelta: DesignSystemResolver = getDesignSystemValue( + 'neutralFillInputRestDelta', +); +export const neutralFillInputHoverDelta: DesignSystemResolver = getDesignSystemValue( + 'neutralFillInputHoverDelta', +); +export const neutralFillInputActiveDelta: DesignSystemResolver = getDesignSystemValue( + 'neutralFillInputActiveDelta', +); +export const neutralFillInputFocusDelta: DesignSystemResolver = getDesignSystemValue( + 'neutralFillInputFocusDelta', +); +export const neutralFillInputSelectedDelta: DesignSystemResolver = getDesignSystemValue( + 'neutralFillInputSelectedDelta', +); + +export const neutralFillStealthRestDelta: DesignSystemResolver = getDesignSystemValue( + 'neutralFillStealthRestDelta', +); +export const neutralFillStealthHoverDelta: DesignSystemResolver = getDesignSystemValue( + 'neutralFillStealthHoverDelta', +); +export const neutralFillStealthActiveDelta: DesignSystemResolver = getDesignSystemValue( + 'neutralFillStealthActiveDelta', +); +export const neutralFillStealthFocusDelta: DesignSystemResolver = getDesignSystemValue( + 'neutralFillStealthFocusDelta', +); +export const neutralFillStealthSelectedDelta: DesignSystemResolver = getDesignSystemValue( + 'neutralFillStealthSelectedDelta', +); + +export const neutralFillToggleHoverDelta: DesignSystemResolver = getDesignSystemValue( + 'neutralFillToggleHoverDelta', +); +export const neutralFillToggleActiveDelta: DesignSystemResolver = getDesignSystemValue( + 'neutralFillToggleActiveDelta', +); +export const neutralFillToggleFocusDelta: DesignSystemResolver = getDesignSystemValue( + 'neutralFillToggleFocusDelta', +); + +export const baseLayerLuminance: DesignSystemResolver = getDesignSystemValue('baseLayerLuminance'); +export const neutralFillCardDelta: DesignSystemResolver = getDesignSystemValue('neutralFillCardDelta'); + +export const neutralForegroundHoverDelta: DesignSystemResolver = getDesignSystemValue( + 'neutralForegroundHoverDelta', +); +export const neutralForegroundActiveDelta: DesignSystemResolver = getDesignSystemValue( + 'neutralForegroundActiveDelta', +); +export const neutralForegroundFocusDelta: DesignSystemResolver = getDesignSystemValue( + 'neutralForegroundFocusDelta', +); + +export const neutralDividerRestDelta: DesignSystemResolver = getDesignSystemValue('neutralDividerRestDelta'); + +export const neutralOutlineRestDelta: DesignSystemResolver = getDesignSystemValue('neutralOutlineRestDelta'); +export const neutralOutlineHoverDelta: DesignSystemResolver = getDesignSystemValue('neutralOutlineHoverDelta'); +export const neutralOutlineActiveDelta: DesignSystemResolver = getDesignSystemValue( + 'neutralOutlineActiveDelta', +); + +export const neutralOutlineFocusDelta: DesignSystemResolver = getDesignSystemValue('neutralOutlineFocusDelta'); diff --git a/packages/web-components/src/index-rollup.ts b/packages/web-components/src/index-rollup.ts index ddca04ce3dbb01..b78f3062c71787 100644 --- a/packages/web-components/src/index-rollup.ts +++ b/packages/web-components/src/index-rollup.ts @@ -1,4 +1,3 @@ export * from './index'; export * from '@microsoft/fast-element'; export * from '@microsoft/fast-foundation'; -export { createColorPalette } from '@microsoft/fast-components-styles-msft'; diff --git a/packages/web-components/src/index.ts b/packages/web-components/src/index.ts index a5a97b343b5e14..a90874fc72489c 100644 --- a/packages/web-components/src/index.ts +++ b/packages/web-components/src/index.ts @@ -6,6 +6,7 @@ export * from './breadcrumb-item'; export * from './button/'; export * from './card/'; export * from './checkbox/'; +export * from './color/'; export * from './design-system-provider/'; export * from './dialog/'; export * from './divider/'; @@ -27,7 +28,8 @@ export * from './text-area/'; export * from './text-field/'; export * from './tree-item/'; export * from './tree-view/'; +export { DesignSystem, DesignSystemDefaults } from './fluent-design-system'; // export styles and utils export * from './styles'; -export { parseColorString } from './color'; +export * from './color'; diff --git a/packages/web-components/src/styles/behaviors.ts b/packages/web-components/src/styles/behaviors.ts index f48359b193224d..0bc56e7d19c0a6 100644 --- a/packages/web-components/src/styles/behaviors.ts +++ b/packages/web-components/src/styles/behaviors.ts @@ -1,13 +1,11 @@ import { CSSCustomPropertyBehavior, cssCustomPropertyBehaviorFactory } from '@microsoft/fast-foundation'; +import { Direction } from '@microsoft/fast-web-utilities'; import { - accentBaseColor, accentFill, accentFillLarge, accentForeground, accentForegroundCut, accentForegroundLarge, - DesignSystem, - direction, neutralDividerRest, neutralFill, neutralFillCard, @@ -30,8 +28,8 @@ import { neutralLayerL3, neutralLayerL4, neutralOutline, -} from '@microsoft/fast-components-styles-msft'; -import { Direction } from '@microsoft/fast-web-utilities'; +} from '../color'; +import { accentBaseColor, DesignSystem, direction } from '../fluent-design-system'; import { FluentDesignSystemProvider } from '../design-system-provider'; /** diff --git a/packages/web-components/src/tree-item/tree-item.styles.ts b/packages/web-components/src/tree-item/tree-item.styles.ts index 27af38bea4af34..c61731654053b6 100644 --- a/packages/web-components/src/tree-item/tree-item.styles.ts +++ b/packages/web-components/src/tree-item/tree-item.styles.ts @@ -8,7 +8,7 @@ import { forcedColorsStylesheetBehavior, } from '@microsoft/fast-foundation'; import { SystemColors } from '@microsoft/fast-web-utilities'; -import { neutralFillStealthHover, neutralFillStealthSelected } from '@microsoft/fast-components-styles-msft'; +import { neutralFillStealthHover, neutralFillStealthSelected } from '../color'; import { accentForegroundRestBehavior, heightNumber, diff --git a/packages/web-components/src/utilities/math.ts b/packages/web-components/src/utilities/math.ts new file mode 100644 index 00000000000000..f11509cd6e5786 --- /dev/null +++ b/packages/web-components/src/utilities/math.ts @@ -0,0 +1,61 @@ +function performOperation( + operation: (a: number, b: number) => number, +): (...args: Array number)>) => (designSystem?: T) => number { + return (...args: Array number)>): ((designSystem?: T) => number) => { + return (designSystem?: T): number => { + const firstArg: number | ((designSystem: T) => number) = args[0]; + let value: number = typeof firstArg === 'function' ? firstArg(designSystem as any) : firstArg; + + for (let i: number = 1; i < args.length; i++) { + const currentValue: number | ((designSystem: T) => number) = args[i]; + value = operation(value, typeof currentValue === 'function' ? currentValue(designSystem as any) : currentValue); + } + + return value; + }; + }; +} + +const _add: ( + ...args: Array number)> +) => (designSystem?: any) => number = performOperation((a: number, b: number): number => a + b); +const _subtract: ( + ...args: Array number)> +) => (designSystem?: any) => number = performOperation((a: number, b: number): number => a - b); +const _multiply: ( + ...args: Array number)> +) => (designSystem?: any) => number = performOperation((a: number, b: number): number => a * b); +const _divide: ( + ...args: Array number)> +) => (designSystem?: any) => number = performOperation((a: number, b: number): number => a / b); +/** + * Adds numbers or functions that accept a design system and return a number. + * @internal + */ +export function add(...args: Array number)>): (designSystem?: T) => number { + return _add.apply(this, args); +} + +/** + * Subtract numbers or functions that accept a design system and return a number. + * @internal + */ +export function subtract(...args: Array number)>): (designSystem?: T) => number { + return _subtract.apply(this, args); +} + +/** + * Multiplies numbers or functions that accept a design system and return a number. + * @internal + */ +export function multiply(...args: Array number)>): (designSystem?: T) => number { + return _multiply.apply(this, args); +} + +/** + * Divides numbers or functions that accept a design system and return a number. + * @internal + */ +export function divide(...args: Array number)>): (designSystem?: T) => number { + return _divide.apply(this, args); +} diff --git a/yarn.lock b/yarn.lock index cb0140f777be1d..f2fd57e021602b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2370,76 +2370,26 @@ resolved "https://registry.yarnpkg.com/@microsoft/fast-colors/-/fast-colors-5.1.0.tgz#f62ce25a800e6b413d3aa562e8416cdc1ed9131c" integrity sha512-u4R/sfF4SoKSAyDWJaBSDuVo4aGf1BXntlEWukC+1ubH36C6JmmdLSyyip5TQZiTqjQIy3uctcbepPi7oGI0Rw== -"@microsoft/fast-components-class-name-contracts-base@^4.8.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@microsoft/fast-components-class-name-contracts-base/-/fast-components-class-name-contracts-base-4.8.0.tgz#e59c85fd7ff88865d2c5bd6a8cf287232c1469f7" - integrity sha512-Dns5cmvh9zrFFeoy+hrqn+HvdtABi1uAXXbwudJCPQoBtnWqGeY7Y6iCLAq63h7xTbhi9SoFaOScTBkvpKXzYQ== - -"@microsoft/fast-components-class-name-contracts-msft@^4.9.0": - version "4.9.0" - resolved "https://registry.yarnpkg.com/@microsoft/fast-components-class-name-contracts-msft/-/fast-components-class-name-contracts-msft-4.9.0.tgz#d61fdc1ca2645f55cd3e0f76e2b171a15b0aafe9" - integrity sha512-RZCNylziTk+0G9wG1ngnGnojdyfXEAQTM6honXpXlvWz9b8YkN7aTsFP0xUzgMuKluSrMGQ1oG84p22DzieHxA== - dependencies: - "@microsoft/fast-components-class-name-contracts-base" "^4.8.0" - -"@microsoft/fast-components-styles-msft@^4.29.0": - version "4.29.0" - resolved "https://registry.yarnpkg.com/@microsoft/fast-components-styles-msft/-/fast-components-styles-msft-4.29.0.tgz#8549ae9d5f1bcbdcd60f174dde745ecf473fde5e" - integrity sha512-z5B/SAyffS7OONqja0isdp7ugE2L3+qK41e+6MlcYIaZ34h5x3ThOAjkSFWP4bAHXkYi2AOcTvfdnE+cIuwC/Q== - dependencies: - "@microsoft/fast-colors" "^5.1.0" - "@microsoft/fast-components-class-name-contracts-base" "^4.8.0" - "@microsoft/fast-components-class-name-contracts-msft" "^4.9.0" - "@microsoft/fast-jss-manager" "^4.2.0" - "@microsoft/fast-jss-utilities" "^4.8.0" - "@microsoft/fast-web-utilities" "^4.6.0" - -"@microsoft/fast-element@^0.21.1": - version "0.21.1" - resolved "https://registry.yarnpkg.com/@microsoft/fast-element/-/fast-element-0.21.1.tgz#366fb006608819edf8053e0fbdb31618957b86a4" - integrity sha512-DZVu9KGtwP+vg9z6fAHpiPntPPFNj4ex6qBSsRLP5RP8akqD54OiUokRTJ9/JodIF7R9w34XLG3EJRD75LQwDQ== - -"@microsoft/fast-foundation@^1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@microsoft/fast-foundation/-/fast-foundation-1.11.1.tgz#d3e04c55be7a51b7cf38ef2acfe6eb3cbb0aec27" - integrity sha512-tRgx6MbfbyArzRhBwkki+MjB98tSbTRjgZ+pq3HiPtRE8leoi0kkNqcj+9+xuPSvzxU+u8eG2ySuOtTzk7VskA== - dependencies: - "@microsoft/fast-element" "^0.21.1" - "@microsoft/fast-web-utilities" "^4.7.0" +"@microsoft/fast-element@^0.22.0": + version "0.22.0" + resolved "https://registry.yarnpkg.com/@microsoft/fast-element/-/fast-element-0.22.0.tgz#7d873e44df660dd37778d479dc710c56e04e1e65" + integrity sha512-4ziV51w/flQvucZq9tksQR3vAa9Obvfjooh9fIQ1ylPPRjEzV5RZeBQl7g5cSIRXz170jOo4VLuajeClmZpV9w== + +"@microsoft/fast-foundation@^1.12.0": + version "1.12.0" + resolved "https://registry.yarnpkg.com/@microsoft/fast-foundation/-/fast-foundation-1.12.0.tgz#4e93afec56f800a2720517daca95d6f924acc492" + integrity sha512-pSeeXmiGTX68GU3cyOFJqCCjgqK2fo0xJYqPhk0ik4A5RAbX25lyY66eDbxjX3tUSe24qTZs43MzvRIx8ozsrg== + dependencies: + "@microsoft/fast-element" "^0.22.0" + "@microsoft/fast-web-utilities" "^4.7.1" "@microsoft/tsdoc-config" "^0.13.4" tabbable "^4.0.0" tslib "^1.13.0" -"@microsoft/fast-jss-manager@^4.2.0": - version "4.2.0" - resolved "https://registry.yarnpkg.com/@microsoft/fast-jss-manager/-/fast-jss-manager-4.2.0.tgz#3d310b6f27435c4a57b6862c2b2273df6897ae06" - integrity sha512-2ubd6aOgBnF8CHzMnS9mzDAGPK9/3swgUf9fQ9tTWk6EIsdvwJw+auAzZEfM2xWWM6Tphyp+/XQFmMuvyO2ydw== - dependencies: - "@microsoft/fast-components-class-name-contracts-base" "^4.8.0" - csstype "^2.3.0" - -"@microsoft/fast-jss-utilities@^4.8.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@microsoft/fast-jss-utilities/-/fast-jss-utilities-4.8.0.tgz#aa2fce0f5ed29e0bc1bf4aaa8035d3fe6d2f6d06" - integrity sha512-gIkGUA07TjD2hlYJw79DChT+QkYtYLpdxquiGXYBfulI6JGkanAM7yRYYg9sSiKksYyZS7wgNfxA+DiVg5LYXA== - dependencies: - "@microsoft/fast-colors" "^5.1.0" - "@microsoft/fast-jss-manager" "^4.2.0" - "@microsoft/fast-web-utilities" "^4.6.0" - csstype "^2.3.0" - exenv-es6 "^1.0.0" - -"@microsoft/fast-web-utilities@^4.6.0": - version "4.6.0" - resolved "https://registry.yarnpkg.com/@microsoft/fast-web-utilities/-/fast-web-utilities-4.6.0.tgz#b81dcbac1ed0ae9cbb0d759d599df6ce8f187fae" - integrity sha512-VB1YwsN8OM9usmaBZVCg8fBxyhAIYTP2Dtu2IL0N2dO37Pu84I0hLXhacRps6BbZXGDMz3LQIg8xF893hqwVuw== - dependencies: - exenv-es6 "^1.0.0" - -"@microsoft/fast-web-utilities@^4.7.0": - version "4.7.0" - resolved "https://registry.yarnpkg.com/@microsoft/fast-web-utilities/-/fast-web-utilities-4.7.0.tgz#8ba4b3b790447fca715fd5d7c3f3779c035a7982" - integrity sha512-GnK0ofXnFg/n6DpRtzH2gDLxVRqVhgIyxUBKUOu/hBtnQyQ/0HHc5XxMgJxZXdWvCLluQVEFt93Wp+Hvolq40Q== +"@microsoft/fast-web-utilities@^4.7.1": + version "4.7.1" + resolved "https://registry.yarnpkg.com/@microsoft/fast-web-utilities/-/fast-web-utilities-4.7.1.tgz#c9ae712f2870152fded596a8d7edb97547331e96" + integrity sha512-o3oN15EdPKKBHBzcC2008eKjVBflMf44YPxkxEOwn7sFzZ2T6CNhTU7feUfSa4+97MeUI3GNn/Wb4YZK+sCeaw== dependencies: exenv-es6 "^1.0.0" @@ -3801,6 +3751,18 @@ "@types/connect" "*" "@types/node" "*" +"@types/chai-spies@^1.0.1": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/chai-spies/-/chai-spies-1.0.3.tgz#a52dc61af3853ec9b80965040811d15dfd401542" + integrity sha512-RBZjhVuK7vrg4rWMt04UF5zHYwfHnpk5mIWu3nQvU3AKGDixXzSjZ6v0zke6pBcaJqMv3IBZ5ibLWPMRDL0sLw== + dependencies: + "@types/chai" "*" + +"@types/chai@*": + version "4.2.14" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.14.tgz#44d2dd0b5de6185089375d976b4ec5caf6861193" + integrity sha512-G+ITQPXkwTrslfG5L/BksmbLUA0M1iybEsmCWPqzSxsRRhJZimBKJkoMi8fr/CPygPTj4zO5pJH7I2/cm9M7SQ== + "@types/chai@^4.2.11": version "4.2.12" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.12.tgz#6160ae454cd89dae05adc3bb97997f488b608201" @@ -7435,6 +7397,11 @@ ccount@^1.0.3: resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.5.tgz#ac82a944905a65ce204eb03023157edf29425c17" integrity sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw== +chai-spies@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/chai-spies/-/chai-spies-1.0.0.tgz#d16b39336fb316d03abf8c375feb23c0c8bb163d" + integrity sha512-elF2ZUczBsFoP07qCfMO/zeggs8pqCf3fZGyK5+2X4AndS8jycZYID91ztD9oQ7d/0tnS963dPkd0frQEThDsg== + chai@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/chai/-/chai-4.2.0.tgz#760aa72cf20e3795e84b12877ce0e83737aa29e5" @@ -8832,7 +8799,7 @@ cssstyle@^2.2.0: dependencies: cssom "~0.3.6" -csstype@^2.2.0, csstype@^2.3.0, csstype@^2.5.5, csstype@^2.5.7, csstype@^2.6.7: +csstype@^2.2.0, csstype@^2.5.5, csstype@^2.5.7, csstype@^2.6.7: version "2.6.8" resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.8.tgz#0fb6fc2417ffd2816a418c9336da74d7f07db431" integrity sha512-msVS9qTuMT5zwAGCVm4mxfrZ18BNc6Csd0oJAtiFMZ1FAx1CCvy2+5MDmYoix63LM/6NDbNtodCiGYGmFgO0dA==