diff --git a/change/@microsoft-fast-components-11d3d100-a9cd-47cf-adda-e2b76137e8fa.json b/change/@microsoft-fast-components-11d3d100-a9cd-47cf-adda-e2b76137e8fa.json new file mode 100644 index 00000000000..b5e1d3b7e8b --- /dev/null +++ b/change/@microsoft-fast-components-11d3d100-a9cd-47cf-adda-e2b76137e8fa.json @@ -0,0 +1,6 @@ +{ + "type": "minor", + "packageName": "@microsoft/fast-components", + "email": "nicholasrice@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/fast-components/src/color-vNext/README.md b/packages/web-components/fast-components/src/color-vNext/README.md new file mode 100644 index 00000000000..aeeb715d94f --- /dev/null +++ b/packages/web-components/fast-components/src/color-vNext/README.md @@ -0,0 +1,28 @@ +# FAST Color Recipes + +Color recipes are named colors who's value is algorithmically defined from a variety of inputs. `@microsoft/fast-components` relies on these recipes heavily to achieve expressive theming options while maintaining color accessability targets. + + +## Swatch +A Swatch is a representation of a color that has a `relativeLuminance` value and a method to convert the swatch to a color string. It is used by recipes to determine which colors to use for UI. + +### SwatchRGB +A concrete implementation of `Swatch`, it is a swatch with red, green, and blue 64bit color channels . + +**Example: Creating a SwatchRGB** +```ts +import { SwatchRGB } from "@microsoft/fast-components"; + +const red = new SwatchRGB(1, 0, 0); +``` + +## Palette +A palette is a collection `Swatch` instances, ordered by relative luminance, and provides mechanisms to safely retrieve swatches by index and by target contrast ratios. It also contains a `source` color, which is the color from which the palette is + +### PaletteRGB +An implementation of `Palette` of `SwatchRGB` instances. + +```ts +// Create a palette from the red swatch +const palette = PaletteRGB.from(red): +``` \ No newline at end of file diff --git a/packages/web-components/fast-components/src/color-vNext/palette.ts b/packages/web-components/fast-components/src/color-vNext/palette.ts new file mode 100644 index 00000000000..449527c6811 --- /dev/null +++ b/packages/web-components/fast-components/src/color-vNext/palette.ts @@ -0,0 +1,145 @@ +import { + clamp, + ComponentStateColorPalette, + parseColorHexRGB, +} from "@microsoft/fast-colors"; +import { Swatch, SwatchRGB } from "./swatch"; +import { binarySearch } from "./utilities/binary-search"; +import { directionByIsDark } from "./utilities/direction-by-is-dark"; +import { contrast, RelativeLuminance } from "./utilities/relative-luminance"; + +/** + * A collection of {@link Swatch} instances + * @public + */ +export interface Palette { + readonly source: T; + readonly swatches: ReadonlyArray; + + /** + * Returns a swatch from the palette that most closely matches + * the contrast ratio provided to a provided reference. + */ + colorContrast( + reference: Swatch, + contrast: number, + initialIndex?: number, + direction?: 1 | -1 + ): Swatch; + + /** + * Returns the index of the palette that most closely matches + * the relativeLuminance of the provided swatch + */ + closestIndexOf(reference: RelativeLuminance): number; + + /** + * Gets a swatch by index. Index is clamped to the limits + * of the palette so a Swatch will always be returned. + */ + get(index: number): T; +} + +/** + * A {@link Palette} representing RGB swatch values. + * @public + */ +export class PaletteRGB implements Palette { + /** + * {@inheritdoc Palette.source} + */ + public readonly source: SwatchRGB; + public readonly swatches: ReadonlyArray; + private lastIndex: number; + private reversedSwatches: ReadonlyArray; + /** + * + * @param source - The source color for the palette + * @param swatches - All swatches in the palette + */ + constructor(source: SwatchRGB, swatches: ReadonlyArray) { + this.source = source; + this.swatches = swatches; + + this.reversedSwatches = Object.freeze([...this.swatches].reverse()); + this.lastIndex = this.swatches.length - 1; + } + + /** + * {@inheritdoc Palette.colorContrast} + */ + public colorContrast( + reference: Swatch, + contrastTarget: number, + initialSearchIndex?: number, + direction?: 1 | -1 + ): SwatchRGB { + if (initialSearchIndex === undefined) { + initialSearchIndex = this.closestIndexOf(reference); + } + + let source: ReadonlyArray = this.swatches; + const endSearchIndex = this.lastIndex; + let startSearchIndex = initialSearchIndex; + + if (direction === undefined) { + direction = directionByIsDark(reference); + } + + const condition = (value: SwatchRGB) => + contrast(reference, value) >= contrastTarget; + + if (direction === -1) { + source = this.reversedSwatches; + startSearchIndex = endSearchIndex - startSearchIndex; + } + + return binarySearch(source, condition, startSearchIndex, endSearchIndex); + } + + /** + * {@inheritdoc Palette.get} + */ + public get(index: number): SwatchRGB { + return this.swatches[index] || this.swatches[clamp(index, 0, this.lastIndex)]; + } + + /** + * {@inheritdoc Palette.closestIndexOf} + */ + public closestIndexOf(reference: Swatch): number { + const index = this.swatches.indexOf(reference as SwatchRGB); + + if (index !== -1) { + return index; + } + + const closest = this.swatches.reduce((previous, next) => + Math.abs(next.relativeLuminance - reference.relativeLuminance) < + Math.abs(previous.relativeLuminance - reference.relativeLuminance) + ? next + : previous + ); + + return this.swatches.indexOf(closest); + } + + /** + * Create a color palette from a provided swatch + * @param source - The source swatch to create a palette from + * @returns + */ + static from(source: SwatchRGB) { + return new PaletteRGB( + source, + Object.freeze( + new ComponentStateColorPalette({ + baseColor: source, + }).palette.map(x => { + const _x = parseColorHexRGB(x.toStringHexRGB())!; + return new SwatchRGB(_x.r, _x.g, _x.b); + }) + ) + ); + } +} diff --git a/packages/web-components/fast-components/src/color-vNext/recipes/accent-fill.ts b/packages/web-components/fast-components/src/color-vNext/recipes/accent-fill.ts new file mode 100644 index 00000000000..c20463a8e60 --- /dev/null +++ b/packages/web-components/fast-components/src/color-vNext/recipes/accent-fill.ts @@ -0,0 +1,60 @@ +import { inRange } from "lodash"; +import { Palette } from "../palette"; +import { Swatch } from "../swatch"; +import { isDark } from "../utilities/is-dark"; + +/** + * @internal + */ +export function accentFill( + palette: Palette, + neutralPalette: Palette, + reference: Swatch, + textColor: Swatch, + contrastTarget: number, + hoverDelta: number, + activeDelta: number, + focusDelta: number, + selectedDelta: number, + neutralFillRestDelta: number, + neutralFillHoverDelta: number, + neutralFillActiveDelta: number +) { + const accent = palette.source; + const referenceIndex = neutralPalette.closestIndexOf(reference); + const swapThreshold = Math.max( + neutralFillRestDelta, + neutralFillHoverDelta, + neutralFillActiveDelta + ); + const direction = referenceIndex >= swapThreshold ? -1 : 1; + const paletteLength = palette.swatches.length; + const maxIndex = paletteLength - 1; + const accentIndex = palette.closestIndexOf(accent); + let accessibleOffset = 0; + + while ( + accessibleOffset < direction * hoverDelta && + inRange(accentIndex + accessibleOffset + direction, 0, paletteLength) && + textColor.contrast(palette.get(accentIndex + accessibleOffset + direction)) >= + contrastTarget && + inRange(accentIndex + accessibleOffset + direction + direction, 0, maxIndex) + ) { + accessibleOffset += direction; + } + + const hoverIndex = accentIndex + accessibleOffset; + const restIndex = hoverIndex + direction * -1 * hoverDelta; + const activeIndex = restIndex + direction * activeDelta; + const focusIndex = restIndex + direction * focusDelta; + const selectedIndex = + restIndex + (isDark(reference) ? selectedDelta * -1 : selectedDelta); + + return { + rest: palette.get(restIndex), + hover: palette.get(hoverIndex), + active: palette.get(activeIndex), + focus: palette.get(focusIndex), + selected: palette.get(selectedIndex), + }; +} diff --git a/packages/web-components/fast-components/src/color-vNext/recipes/accent-foreground-cut.ts b/packages/web-components/fast-components/src/color-vNext/recipes/accent-foreground-cut.ts new file mode 100644 index 00000000000..088cf539841 --- /dev/null +++ b/packages/web-components/fast-components/src/color-vNext/recipes/accent-foreground-cut.ts @@ -0,0 +1,9 @@ +import { Swatch } from "../swatch"; +import { black, white } from "../utilities/color-constants"; + +/** + * @internal + */ +export function accentForegroundCut(reference: Swatch, contrastTarget: number) { + return reference.contrast(white) >= contrastTarget ? white : black; +} diff --git a/packages/web-components/fast-components/src/color-vNext/recipes/accent-foreground.ts b/packages/web-components/fast-components/src/color-vNext/recipes/accent-foreground.ts new file mode 100644 index 00000000000..a1d1e5ce5e2 --- /dev/null +++ b/packages/web-components/fast-components/src/color-vNext/recipes/accent-foreground.ts @@ -0,0 +1,56 @@ +import { Palette } from "../palette"; +import { Swatch } from "../swatch"; +import { directionByIsDark } from "../utilities/direction-by-is-dark"; + +/** + * @internal + */ +export function accentForeground( + palette: Palette, + reference: Swatch, + contrastTarget: number, + restDelta: number, + hoverDelta: number, + activeDelta: number, + focusDelta: number +) { + const accent = palette.source; + const accentIndex = palette.closestIndexOf(accent); + const direction = directionByIsDark(reference); + const startIndex = + accentIndex + + (direction === 1 + ? Math.min(restDelta, hoverDelta) + : Math.max(direction * restDelta, direction * hoverDelta)); + const accessibleSwatch = palette.colorContrast( + reference, + contrastTarget, + startIndex, + direction + ); + const accessibleIndex1 = palette.closestIndexOf(accessibleSwatch); + const accessibleIndex2 = + accessibleIndex1 + direction * Math.abs(restDelta - hoverDelta); + const indexOneIsRestState = + direction === 1 + ? restDelta < hoverDelta + : direction * restDelta > direction * hoverDelta; + + let restIndex: number; + let hoverIndex: number; + + if (indexOneIsRestState) { + restIndex = accessibleIndex1; + hoverIndex = accessibleIndex2; + } else { + restIndex = accessibleIndex2; + hoverIndex = accessibleIndex1; + } + + return { + rest: palette.get(restIndex), + hover: palette.get(hoverIndex), + active: palette.get(restIndex + direction * activeDelta), + focus: palette.get(restIndex + direction * focusDelta), + }; +} diff --git a/packages/web-components/fast-components/src/color-vNext/recipes/neutral-divider.ts b/packages/web-components/fast-components/src/color-vNext/recipes/neutral-divider.ts new file mode 100644 index 00000000000..3739fd5d464 --- /dev/null +++ b/packages/web-components/fast-components/src/color-vNext/recipes/neutral-divider.ts @@ -0,0 +1,17 @@ +import { Swatch } from "../swatch"; +import { Palette } from "../palette"; +import { directionByIsDark } from "../utilities/direction-by-is-dark"; + +/** + * The neutralDivider color recipe + * @param palette - The palette to operate on + * @param reference - The reference color + * @param delta - The offset from the reference + * + * @internal + */ +export function neutralDivider(palette: Palette, reference: Swatch, delta: number) { + return palette.get( + palette.closestIndexOf(reference) + directionByIsDark(reference) * delta + ); +} diff --git a/packages/web-components/fast-components/src/color-vNext/recipes/neutral-fill-card.ts b/packages/web-components/fast-components/src/color-vNext/recipes/neutral-fill-card.ts new file mode 100644 index 00000000000..a57c510ce0b --- /dev/null +++ b/packages/web-components/fast-components/src/color-vNext/recipes/neutral-fill-card.ts @@ -0,0 +1,11 @@ +import { Palette } from "../palette"; +import { Swatch } from "../swatch"; + +/** + * @internal + */ +export function neutralFillCard(palette: Palette, reference: Swatch, delta: number) { + const referenceIndex = palette.closestIndexOf(reference); + + return palette.get(referenceIndex - (referenceIndex < delta ? delta * -1 : delta)); +} diff --git a/packages/web-components/fast-components/src/color-vNext/recipes/neutral-fill-input.ts b/packages/web-components/fast-components/src/color-vNext/recipes/neutral-fill-input.ts new file mode 100644 index 00000000000..3616035d808 --- /dev/null +++ b/packages/web-components/fast-components/src/color-vNext/recipes/neutral-fill-input.ts @@ -0,0 +1,27 @@ +import { Palette } from "../palette"; +import { Swatch } from "../swatch"; +import { directionByIsDark } from "../utilities/direction-by-is-dark"; + +/** + * @internal + */ +export function neutralFillInput( + palette: Palette, + reference: Swatch, + restDelta: number, + hoverDelta: number, + activeDelta: number, + focusDelta: number, + selectedDelta: number +) { + const direction = directionByIsDark(reference); + const referenceIndex = palette.closestIndexOf(reference); + + return { + rest: palette.get(referenceIndex - direction * restDelta), + hover: palette.get(referenceIndex - direction * hoverDelta), + active: palette.get(referenceIndex - direction * activeDelta), + focus: palette.get(referenceIndex - direction * focusDelta), + selected: palette.get(referenceIndex - direction * selectedDelta), + }; +} diff --git a/packages/web-components/fast-components/src/color-vNext/recipes/neutral-fill-stealth.ts b/packages/web-components/fast-components/src/color-vNext/recipes/neutral-fill-stealth.ts new file mode 100644 index 00000000000..676b631cd31 --- /dev/null +++ b/packages/web-components/fast-components/src/color-vNext/recipes/neutral-fill-stealth.ts @@ -0,0 +1,41 @@ +import { Palette } from "../palette"; +import { Swatch } from "../swatch"; + +/** + * @internal + */ +export function neutralFillStealth( + palette: Palette, + reference: Swatch, + restDelta: number, + hoverDelta: number, + activeDelta: number, + focusDelta: number, + selectedDelta: number, + fillRestDelta: number, + fillHoverDelta: number, + fillActiveDelta: number, + fillFocusDelta: number +) { + const swapThreshold = Math.max( + restDelta, + hoverDelta, + activeDelta, + focusDelta, + fillRestDelta, + fillHoverDelta, + fillActiveDelta, + fillFocusDelta + ); + + const referenceIndex = palette.closestIndexOf(reference); + const direction: 1 | -1 = referenceIndex >= swapThreshold ? -1 : 1; + + return { + rest: palette.get(referenceIndex + direction * restDelta), + hover: palette.get(referenceIndex + direction * hoverDelta), + active: palette.get(referenceIndex + direction * activeDelta), + focus: palette.get(referenceIndex + direction * focusDelta), + selected: palette.get(referenceIndex + direction * selectedDelta), + }; +} diff --git a/packages/web-components/fast-components/src/color-vNext/recipes/neutral-fill-toggle.ts b/packages/web-components/fast-components/src/color-vNext/recipes/neutral-fill-toggle.ts new file mode 100644 index 00000000000..a30f07d0287 --- /dev/null +++ b/packages/web-components/fast-components/src/color-vNext/recipes/neutral-fill-toggle.ts @@ -0,0 +1,41 @@ +import { Palette } from "../palette"; +import { Swatch } from "../swatch"; +import { directionByIsDark } from "../utilities/direction-by-is-dark"; + +/** + * @internal + */ +export function neutralFillToggle( + palette: Palette, + reference: Swatch, + restDelta: number, + hoverDelta: number, + activeDelta: number, + focusDelta: number +) { + const direction = directionByIsDark(reference); + const accessibleIndex = palette.closestIndexOf(palette.colorContrast(reference, 4.5)); + const accessibleIndex2 = + accessibleIndex + direction * Math.abs(restDelta - hoverDelta); + const indexOneIsRest = + direction === 1 + ? restDelta < hoverDelta + : direction * restDelta > direction * hoverDelta; + let restIndex: number; + let hoverIndex: number; + + if (indexOneIsRest) { + restIndex = accessibleIndex; + hoverIndex = accessibleIndex2; + } else { + restIndex = accessibleIndex2; + hoverIndex = accessibleIndex; + } + + return { + rest: palette.get(restIndex), + hover: palette.get(hoverIndex), + active: palette.get(restIndex + direction * activeDelta), + focus: palette.get(restIndex + direction * focusDelta), + }; +} diff --git a/packages/web-components/fast-components/src/color-vNext/recipes/neutral-fill.ts b/packages/web-components/fast-components/src/color-vNext/recipes/neutral-fill.ts new file mode 100644 index 00000000000..e71ed44a372 --- /dev/null +++ b/packages/web-components/fast-components/src/color-vNext/recipes/neutral-fill.ts @@ -0,0 +1,34 @@ +import { Palette } from "../palette"; +import { Swatch } from "../swatch"; + +/** + * + * @param palette - The palette to operate on + * @param reference - The reference color to calculate a color for + * @param delta - The offset from the reference's location + * @param threshold - Determines if a lighter or darker color than the reference will be picked. + * @returns + * + * @internal + */ +export function neutralFill( + palette: Palette, + reference: Swatch, + restDelta: number, + hoverDelta: number, + activeDelta: number, + focusDelta: number, + selectedDelta: number +) { + const referenceIndex = palette.closestIndexOf(reference); + const threshold = Math.max(restDelta, hoverDelta, activeDelta, focusDelta); + const direction = referenceIndex >= threshold ? -1 : 1; + + return { + rest: palette.get(referenceIndex + direction * restDelta), + hover: palette.get(referenceIndex + direction * hoverDelta), + active: palette.get(referenceIndex + direction * activeDelta), + focus: palette.get(referenceIndex + direction * focusDelta), + selected: palette.get(referenceIndex + direction * selectedDelta), + }; +} diff --git a/packages/web-components/fast-components/src/color-vNext/recipes/neutral-focus.ts b/packages/web-components/fast-components/src/color-vNext/recipes/neutral-focus.ts new file mode 100644 index 00000000000..5874fbc5ab5 --- /dev/null +++ b/packages/web-components/fast-components/src/color-vNext/recipes/neutral-focus.ts @@ -0,0 +1,9 @@ +import { Palette } from "../palette"; +import { Swatch } from "../swatch"; + +/** + * @internal + */ +export function neutralFocus(palette: Palette, reference: Swatch) { + return palette.colorContrast(reference, 3.5); +} diff --git a/packages/web-components/fast-components/src/color-vNext/recipes/neutral-foreground-hint.ts b/packages/web-components/fast-components/src/color-vNext/recipes/neutral-foreground-hint.ts new file mode 100644 index 00000000000..d1f11b53b7c --- /dev/null +++ b/packages/web-components/fast-components/src/color-vNext/recipes/neutral-foreground-hint.ts @@ -0,0 +1,13 @@ +import { Swatch } from "../swatch"; +import { Palette } from "../palette"; + +/** + * The neutralForegroundHint color recipe + * @param palette - The palette to operate on + * @param reference - The reference color + * + * @internal + */ +export function neutralForegroundHint(palette: Palette, reference: Swatch) { + return palette.colorContrast(reference, 4.5); +} diff --git a/packages/web-components/fast-components/src/color-vNext/recipes/neutral-foreground.ts b/packages/web-components/fast-components/src/color-vNext/recipes/neutral-foreground.ts new file mode 100644 index 00000000000..0da040cbdd2 --- /dev/null +++ b/packages/web-components/fast-components/src/color-vNext/recipes/neutral-foreground.ts @@ -0,0 +1,9 @@ +import { Palette } from "../palette"; +import { Swatch } from "../swatch"; + +/** + * @internal + */ +export function neutralForeground(palette: Palette, reference: Swatch) { + return palette.colorContrast(reference, 14); +} diff --git a/packages/web-components/fast-components/src/color-vNext/recipes/neutral-layer-card.ts b/packages/web-components/fast-components/src/color-vNext/recipes/neutral-layer-card.ts new file mode 100644 index 00000000000..aa870f4ce05 --- /dev/null +++ b/packages/web-components/fast-components/src/color-vNext/recipes/neutral-layer-card.ts @@ -0,0 +1,17 @@ +import { Palette } from "../palette"; +import { SwatchRGB } from "../swatch"; + +/** + * @internal + */ +export function neutralLayerCard( + palette: Palette, + relativeLuminance: number, + cardDelta: number +) { + return palette.get( + palette.closestIndexOf( + new SwatchRGB(relativeLuminance, relativeLuminance, relativeLuminance) + ) - cardDelta + ); +} diff --git a/packages/web-components/fast-components/src/color-vNext/recipes/neutral-layer-floating.ts b/packages/web-components/fast-components/src/color-vNext/recipes/neutral-layer-floating.ts new file mode 100644 index 00000000000..27a32783284 --- /dev/null +++ b/packages/web-components/fast-components/src/color-vNext/recipes/neutral-layer-floating.ts @@ -0,0 +1,16 @@ +import { Palette } from "../palette"; +import { neutralLayerCard } from "./neutral-layer-card"; + +/** + * @internal + */ +export function neutralLayerFloating( + palette: Palette, + relativeLuminance: number, + cardDelta: number +) { + const cardIndex = palette.closestIndexOf( + neutralLayerCard(palette, relativeLuminance, cardDelta) + ); + return palette.get(cardIndex - cardDelta); +} diff --git a/packages/web-components/fast-components/src/color-vNext/recipes/neutral-outline.ts b/packages/web-components/fast-components/src/color-vNext/recipes/neutral-outline.ts new file mode 100644 index 00000000000..d0faca00fb5 --- /dev/null +++ b/packages/web-components/fast-components/src/color-vNext/recipes/neutral-outline.ts @@ -0,0 +1,30 @@ +import { Palette } from "../palette"; +import { Swatch } from "../swatch"; +import { directionByIsDark } from "../utilities/direction-by-is-dark"; + +/** + * @internal + */ +export function neutralOutline( + palette: Palette, + reference: Swatch, + restDelta: number, + hoverDelta: number, + activeDelta: number, + focusDelta: number +) { + const referenceIndex = palette.closestIndexOf(reference); + const direction = directionByIsDark(reference); + + const restIndex = referenceIndex + direction * restDelta; + const hoverIndex = restIndex + direction * (hoverDelta - restDelta); + const activeIndex = restIndex + direction * (activeDelta - restDelta); + const focusIndex = restIndex + direction * (focusDelta - restDelta); + + return { + rest: palette.get(restIndex), + hover: palette.get(hoverIndex), + active: palette.get(activeIndex), + focus: palette.get(focusIndex), + }; +} diff --git a/packages/web-components/fast-components/src/color-vNext/swatch.ts b/packages/web-components/fast-components/src/color-vNext/swatch.ts new file mode 100644 index 00000000000..af959230af8 --- /dev/null +++ b/packages/web-components/fast-components/src/color-vNext/swatch.ts @@ -0,0 +1,33 @@ +import { ColorRGBA64, rgbToRelativeLuminance } from "@microsoft/fast-colors"; +import { contrast, RelativeLuminance } from "./utilities/relative-luminance"; + +/** + * Represents a color in a {@link Palette} + * @public + */ +export interface Swatch extends RelativeLuminance { + toColorString(): string; + contrast(target: RelativeLuminance): number; +} + +/** + * A RGB implementation of {@link Swatch} + * @public + */ +export class SwatchRGB extends ColorRGBA64 implements Swatch { + readonly relativeLuminance: number; + + /** + * + * @param red - Red channel expressed as a number between 0 and 1 + * @param green - Green channel expressed as a number between 0 and 1 + * @param blue - Blue channel expressed as a number between 0 and 1 + */ + constructor(red: number, green: number, blue: number) { + super(red, green, blue, 1); + this.relativeLuminance = rgbToRelativeLuminance(this); + } + + public toColorString = this.toStringHexRGB; + public contrast = contrast.bind(null, this); +} diff --git a/packages/web-components/fast-components/src/color-vNext/utilities/binary-search.ts b/packages/web-components/fast-components/src/color-vNext/utilities/binary-search.ts new file mode 100644 index 00000000000..25f3b4ccc98 --- /dev/null +++ b/packages/web-components/fast-components/src/color-vNext/utilities/binary-search.ts @@ -0,0 +1,31 @@ +/** + * @internal + */ +export function binarySearch( + valuesToSearch: T[] | ReadonlyArray, + 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 + return searchCondition(valuesToSearch[middleIndex]) + ? binarySearch( + valuesToSearch, + searchCondition, + startIndex, + middleIndex // include this index because it passed the search condition + ) + : binarySearch( + valuesToSearch, + searchCondition, + middleIndex + 1, // exclude this index because it failed the search condition + endIndex + ); +} diff --git a/packages/web-components/fast-components/src/color-vNext/utilities/color-constants.ts b/packages/web-components/fast-components/src/color-vNext/utilities/color-constants.ts new file mode 100644 index 00000000000..28e47af7fdf --- /dev/null +++ b/packages/web-components/fast-components/src/color-vNext/utilities/color-constants.ts @@ -0,0 +1,10 @@ +import { SwatchRGB } from "../swatch"; + +/** + * @internal + */ +export const white = new SwatchRGB(1, 1, 1); +/** + * @internal + */ +export const black = new SwatchRGB(0, 0, 0); diff --git a/packages/web-components/fast-components/src/color-vNext/utilities/direction-by-is-dark.ts b/packages/web-components/fast-components/src/color-vNext/utilities/direction-by-is-dark.ts new file mode 100644 index 00000000000..9c55072fdc5 --- /dev/null +++ b/packages/web-components/fast-components/src/color-vNext/utilities/direction-by-is-dark.ts @@ -0,0 +1,9 @@ +import { Swatch } from "../swatch"; +import { isDark } from "./is-dark"; + +/** + * @internal + */ +export function directionByIsDark(color: Swatch): 1 | -1 { + return isDark(color) ? -1 : 1; +} diff --git a/packages/web-components/fast-components/src/color-vNext/utilities/is-dark.ts b/packages/web-components/fast-components/src/color-vNext/utilities/is-dark.ts new file mode 100644 index 00000000000..6f759bc17a1 --- /dev/null +++ b/packages/web-components/fast-components/src/color-vNext/utilities/is-dark.ts @@ -0,0 +1,20 @@ +import { Swatch } from "../swatch"; + +/* + * A color is in "dark" if there is more contrast between #000000 and a reference + * color than #FFFFFF and the reference color. That threshold can be expressed as a relative luminance + * using the contrast formula as (1 + 0.5) / (R + 0.05) === (R + 0.05) / (0 + 0.05), + * which reduces to the following, where 'R' is the relative luminance of the reference color + */ +const target = (-0.1 + Math.sqrt(0.21)) / 2; + +/** + * Determines if a color should be considered Dark Mode + * @param color - The color to check to mode of + * @returns boolean + * + * @internal + */ +export function isDark(color: Swatch): boolean { + return color.relativeLuminance <= target; +} diff --git a/packages/web-components/fast-components/src/color-vNext/utilities/relative-luminance.ts b/packages/web-components/fast-components/src/color-vNext/utilities/relative-luminance.ts new file mode 100644 index 00000000000..115c1faa685 --- /dev/null +++ b/packages/web-components/fast-components/src/color-vNext/utilities/relative-luminance.ts @@ -0,0 +1,19 @@ +/** + * @public + */ +export interface RelativeLuminance { + /** + * A number between 0 and 1, calculated by {@link https://www.w3.org/WAI/GL/wiki/Relative_luminance} + */ + readonly relativeLuminance: number; +} + +/** + * @internal + */ +export function contrast(a: RelativeLuminance, b: RelativeLuminance): number { + const L1 = a.relativeLuminance > b.relativeLuminance ? a : b; + const L2 = a.relativeLuminance > b.relativeLuminance ? b : a; + + return (L1.relativeLuminance + 0.05) / (L2.relativeLuminance + 0.05); +} diff --git a/packages/web-components/fast-components/src/color/accent-fill.spec.ts b/packages/web-components/fast-components/src/color/accent-fill.spec.ts index bc50cee4bc3..002a3444010 100644 --- a/packages/web-components/fast-components/src/color/accent-fill.spec.ts +++ b/packages/web-components/fast-components/src/color/accent-fill.spec.ts @@ -1,11 +1,11 @@ import { expect } from "chai"; import { FASTDesignSystem, fastDesignSystemDefaults } from "../fast-design-system"; import { - accentBaseColor, accentPalette as getAccentPalette, neutralPalette as getNeutralPalette, } from "../fast-design-system"; import { + accentFill, accentFillActive, accentFillHover, accentFillLargeActive, @@ -17,6 +17,12 @@ import { } from "./accent-fill"; import { findClosestSwatchIndex, Palette } from "./palette"; import { contrast, Swatch } from "./common"; +import { parseColorHexRGB } from "@microsoft/fast-colors"; +import { neutralBaseColor, accentBaseColor } from "./color-constants"; +import { PaletteRGB } from "../color-vNext/palette"; +import { SwatchRGB } from "../color-vNext/swatch"; +import { accentFill as accentFillNew } from "../color-vNext/recipes/accent-fill"; +import { accentForegroundCut as accentForegroundCutNew } from '../color-vNext/recipes/accent-foreground-cut'; import { accentForegroundCut } from "./accent-foreground-cut"; describe("accentFill", (): void => { @@ -25,7 +31,7 @@ describe("accentFill", (): void => { const accentIndex: number = findClosestSwatchIndex( getAccentPalette, - accentBaseColor(fastDesignSystemDefaults) + accentBaseColor )(fastDesignSystemDefaults); it("should operate on design system defaults", (): void => { @@ -90,3 +96,43 @@ describe("accentFill", (): void => { }); }); }); +describe("ensure parity between old and new recipe implementation", () => { + const neutralColor = (parseColorHexRGB(neutralBaseColor)!) + const neutralPalette = PaletteRGB.from(new SwatchRGB(neutralColor.r, neutralColor.g, neutralColor.b)); + const accentColor = (parseColorHexRGB(accentBaseColor)!) + const accentPalette = PaletteRGB.from(new SwatchRGB(accentColor.r, accentColor.g, accentColor.b)); + neutralPalette.swatches.forEach(( newSwatch, index ) => { + const { + accentFillHoverDelta, + accentFillActiveDelta, + accentFillFocusDelta, + accentFillSelectedDelta, + neutralFillRestDelta, + neutralFillHoverDelta, + neutralFillActiveDelta, + } = fastDesignSystemDefaults; + + const oldValues = accentFill({...fastDesignSystemDefaults, backgroundColor: fastDesignSystemDefaults.neutralPalette[index]}); + const textColor = accentForegroundCutNew(accentPalette.source, 4.5); + const newValues = accentFillNew( + accentPalette, + neutralPalette, + newSwatch, + textColor, + 4.5, + accentFillHoverDelta, + accentFillActiveDelta, + accentFillFocusDelta, + accentFillSelectedDelta, + neutralFillRestDelta, + neutralFillHoverDelta, + neutralFillActiveDelta + ) + + for (let key in oldValues) { + it(`${newSwatch.toColorString()}old value for ${key} at ${oldValues[key]} should be equal to new value`, () => { + expect(oldValues[key]).to.equal(newValues[key].toColorString().toUpperCase()) + } ) + } + }) +}) \ No newline at end of file diff --git a/packages/web-components/fast-components/src/color/accent-foreground-cut.spec.ts b/packages/web-components/fast-components/src/color/accent-foreground-cut.spec.ts index 3c6ef36ec0e..40cc8044b5d 100644 --- a/packages/web-components/fast-components/src/color/accent-foreground-cut.spec.ts +++ b/packages/web-components/fast-components/src/color/accent-foreground-cut.spec.ts @@ -1,7 +1,12 @@ +import { parseColorHexRGB } from "@microsoft/fast-colors"; import { expect } from "chai"; +import { PaletteRGB } from "../color-vNext/palette"; +import { SwatchRGB } from "../color-vNext/swatch"; import { FASTDesignSystem, fastDesignSystemDefaults } from "../fast-design-system"; import { accentForegroundCut, accentForegroundCutLarge } from "./accent-foreground-cut"; +import { neutralBaseColor, accentBaseColor } from "./color-constants"; import { Swatch } from "./common"; +import { accentForegroundCut as accentForegroundCutNew } from "../color-vNext/recipes/accent-foreground-cut"; describe("Cut text", (): void => { it("should return white by by default", (): void => { @@ -28,3 +33,19 @@ describe("Cut text", (): void => { ).to.equal("#000000"); }); }); +describe("ensure parity between old and new recipe implementation", () => { + const color = (parseColorHexRGB(accentBaseColor)!) + const palette = PaletteRGB.from(new SwatchRGB(color.r, color.g, color.b)); + it( + `should be the same for ${palette.source}`, + () => { + expect( + accentForegroundCut( + { ...fastDesignSystemDefaults, backgroundColor: fastDesignSystemDefaults.accentBaseColor } + ) + ).to.be.equal( + accentForegroundCutNew(palette.source, 4.5).toColorString().toUpperCase() + ) + } + ) +}) \ No newline at end of file diff --git a/packages/web-components/fast-components/src/color/accent-foreground.spec.ts b/packages/web-components/fast-components/src/color/accent-foreground.spec.ts index 6bd0c341ae0..67f6af14c80 100644 --- a/packages/web-components/fast-components/src/color/accent-foreground.spec.ts +++ b/packages/web-components/fast-components/src/color/accent-foreground.spec.ts @@ -12,9 +12,14 @@ import { accentForegroundLargeHover, accentForegroundLargeRest, accentForegroundRest, + accentForeground } from "./accent-foreground"; import { Palette } from "./palette"; import { contrast, Swatch } from "./common"; +import { accentBaseColor, neutralBaseColor } from "./color-constants"; +import { PaletteRGB } from "../color-vNext/palette"; +import { SwatchRGB } from "../color-vNext/swatch"; +import { accentForeground as accentForegroundNew } from "../color-vNext/recipes/accent-foreground"; describe("accentForeground", (): void => { const neutralPalette: Palette = getNeutralPalette(fastDesignSystemDefaults); @@ -121,3 +126,40 @@ describe("accentForeground", (): void => { ); }); }); + +describe("ensure parity between old and new recipe implementation", () => { + const neutralBase = parseColorHexRGB(neutralBaseColor)!; + const accentBase = parseColorHexRGB(accentBaseColor)!; + + const neutralPalette = PaletteRGB.from(new SwatchRGB(neutralBase.r, neutralBase.g, neutralBase.b)); + const accentPalette = PaletteRGB.from(new SwatchRGB(accentBase.r, accentBase.g, accentBase.b)); + + neutralPalette.swatches.forEach((newSwatch, index) => { + const { + accentForegroundRestDelta, + accentForegroundFocusDelta, + accentForegroundActiveDelta, + accentForegroundHoverDelta + } = fastDesignSystemDefaults; + const oldValues = accentForeground({ + ...fastDesignSystemDefaults, + backgroundColor: fastDesignSystemDefaults.neutralPalette[index], + }); + const newValues = accentForegroundNew( + accentPalette, + newSwatch, + 4.5, + accentForegroundRestDelta, + accentForegroundHoverDelta, + accentForegroundActiveDelta, + accentForegroundFocusDelta, + ); + it(`should be the same for ${newSwatch}`, () => { + for (let key in newValues) { + expect(oldValues[key]).to.equal( + newValues[key].toColorString().toUpperCase() + ); + } + }); + }); +}); \ No newline at end of file diff --git a/packages/web-components/fast-components/src/color/accent-foreground.ts b/packages/web-components/fast-components/src/color/accent-foreground.ts index 38dcd692332..e9ba0093784 100644 --- a/packages/web-components/fast-components/src/color/accent-foreground.ts +++ b/packages/web-components/fast-components/src/color/accent-foreground.ts @@ -66,7 +66,6 @@ function accentForegroundAlgorithm( 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 diff --git a/packages/web-components/fast-components/src/color/neutral-divider.spec.ts b/packages/web-components/fast-components/src/color/neutral-divider.spec.ts index e5d3efb4cbd..4a1bb895768 100644 --- a/packages/web-components/fast-components/src/color/neutral-divider.spec.ts +++ b/packages/web-components/fast-components/src/color/neutral-divider.spec.ts @@ -1,5 +1,10 @@ +import { ColorRGBA64, parseColorHexRGB } from "@microsoft/fast-colors"; import { expect } from "chai"; +import { PaletteRGB } from "../color-vNext/palette"; +import { neutralDivider } from "../color-vNext/recipes/neutral-divider"; +import { SwatchRGB } from "../color-vNext/swatch"; import { fastDesignSystemDefaults } from "../fast-design-system"; +import { neutralBaseColor } from "./color-constants"; import { neutralDividerRest } from "./neutral-divider"; describe("neutralDividerRest", (): void => { @@ -11,3 +16,16 @@ describe("neutralDividerRest", (): void => { expect(typeof neutralDividerRest(() => "#FFF")).to.equal("function"); }); }); + + +describe("ensure parity between old and new recipe implementation", () => { + const color = (parseColorHexRGB(neutralBaseColor)!) + const palette = PaletteRGB.from(new SwatchRGB(color.r, color.g, color.b)); + palette.swatches.forEach(( newSwatch, index ) => { + it(`should be the same for ${newSwatch}`, () => { + expect(neutralDivider(palette, newSwatch, fastDesignSystemDefaults.neutralDividerRestDelta).toColorString().toUpperCase()).to.equal( + neutralDividerRest({...fastDesignSystemDefaults, backgroundColor: fastDesignSystemDefaults.neutralPalette[index]}) + ) + }) + }) +}) \ No newline at end of file diff --git a/packages/web-components/fast-components/src/color/neutral-fill-card.spec.ts b/packages/web-components/fast-components/src/color/neutral-fill-card.spec.ts index ed627c9e841..d1f34b77542 100644 --- a/packages/web-components/fast-components/src/color/neutral-fill-card.spec.ts +++ b/packages/web-components/fast-components/src/color/neutral-fill-card.spec.ts @@ -1,6 +1,11 @@ +import { parseColorHexRGB } from "@microsoft/fast-colors"; import { expect } from "chai"; +import { PaletteRGB } from "../color-vNext/palette"; +import { SwatchRGB } from "../color-vNext/swatch"; import { FASTDesignSystem, fastDesignSystemDefaults } from "../fast-design-system"; +import { neutralBaseColor } from "./color-constants"; import { neutralFillCard } from "./neutral-fill-card"; +import { neutralFillCard as neutralFillCardNew } from "../color-vNext/recipes/neutral-fill-card" describe("neutralFillCard", (): void => { it("should operate on design system defaults", (): void => { @@ -42,3 +47,15 @@ describe("neutralFillCard", (): void => { ).to.equal(fastDesignSystemDefaults.neutralPalette[1]); }); }); +describe("ensure parity between old and new recipe implementation", () => { + const color = (parseColorHexRGB(neutralBaseColor)!) + const palette = PaletteRGB.from(new SwatchRGB(color.r, color.g, color.b)); + const { neutralFillCardDelta } = fastDesignSystemDefaults; + palette.swatches.forEach(( newSwatch, index ) => { + it(`should be the same for ${newSwatch}`, () => { + expect( + neutralFillCard({...fastDesignSystemDefaults, backgroundColor: fastDesignSystemDefaults.neutralPalette[index]}) + ).to.be.equal(neutralFillCardNew( palette, newSwatch, neutralFillCardDelta).toColorString().toUpperCase()) + }); + }) +}) \ No newline at end of file diff --git a/packages/web-components/fast-components/src/color/neutral-fill-input.spec.ts b/packages/web-components/fast-components/src/color/neutral-fill-input.spec.ts index d46b4b082d4..22bbfadd5f4 100644 --- a/packages/web-components/fast-components/src/color/neutral-fill-input.spec.ts +++ b/packages/web-components/fast-components/src/color/neutral-fill-input.spec.ts @@ -1,9 +1,13 @@ +import { parseColorHexRGB } from "@microsoft/fast-colors"; import { expect } from "chai"; +import { PaletteRGB } from "../color-vNext/palette"; +import { SwatchRGB } from "../color-vNext/swatch"; import { FASTDesignSystem, fastDesignSystemDefaults } from "../fast-design-system"; import { accentPalette as getAccentPalette, neutralPalette as getNeutralPalette, } from "../fast-design-system"; +import { neutralBaseColor } from "./color-constants"; import { clamp, FillSwatchFamily, Swatch } from "./common"; import { neutralFillInput, @@ -14,6 +18,7 @@ import { neutralFillInputSelected, } from "./neutral-fill-input"; import { isDarkMode, Palette } from "./palette"; +import { neutralFillInput as neutralFillInputNew } from "../color-vNext/recipes/neutral-fill-input"; describe("neutralFillInput", (): void => { const neutralPalette: Palette = getNeutralPalette(fastDesignSystemDefaults); @@ -128,3 +133,31 @@ describe("neutralFillInput", (): void => { }); }); }); +describe("ensure parity between old and new recipe implementation", () => { + const color = (parseColorHexRGB(neutralBaseColor)!) + const palette = PaletteRGB.from(new SwatchRGB(color.r, color.g, color.b)); + palette.swatches.forEach(( newSwatch, index ) => { + const { + neutralFillInputRestDelta, + neutralFillInputHoverDelta, + neutralFillInputActiveDelta, + neutralFillInputFocusDelta, + neutralFillInputSelectedDelta + } = fastDesignSystemDefaults; + const oldValues = neutralFillInput({...fastDesignSystemDefaults, backgroundColor: fastDesignSystemDefaults.neutralPalette[index]}); + const newValues = neutralFillInputNew( + palette, + newSwatch, + neutralFillInputRestDelta, + neutralFillInputHoverDelta, + neutralFillInputActiveDelta, + neutralFillInputFocusDelta, + neutralFillInputSelectedDelta + ); + it(`should be the same for ${newSwatch}`, () => { + for (let key in oldValues) { + expect(oldValues[key]).to.equal(newValues[key].toColorString().toUpperCase()) + } + }); + }) +}) \ No newline at end of file diff --git a/packages/web-components/fast-components/src/color/neutral-fill-stealth.spec.ts b/packages/web-components/fast-components/src/color/neutral-fill-stealth.spec.ts index 04cbe4ba40f..ee77774399b 100644 --- a/packages/web-components/fast-components/src/color/neutral-fill-stealth.spec.ts +++ b/packages/web-components/fast-components/src/color/neutral-fill-stealth.spec.ts @@ -14,6 +14,11 @@ import { } from "./neutral-fill-stealth"; import { Palette } from "./palette"; import { FillSwatchFamily, Swatch } from "./common"; +import { parseColorHexRGB } from "@microsoft/fast-colors"; +import { PaletteRGB } from "../color-vNext/palette"; +import { SwatchRGB } from "../color-vNext/swatch"; +import { neutralBaseColor } from "./color-constants"; +import { neutralFillStealth as neutralFillStealthNew } from "../color-vNext/recipes/neutral-fill-stealth"; describe("neutralFillStealth", (): void => { const neutralPalette: Palette = getNeutralPalette(fastDesignSystemDefaults); @@ -110,3 +115,39 @@ describe("neutralFillStealth", (): void => { }); }); }); +describe("ensure parity between old and new recipe implementation", () => { + const color = (parseColorHexRGB(neutralBaseColor)!) + const palette = PaletteRGB.from(new SwatchRGB(color.r, color.g, color.b)); + palette.swatches.forEach(( newSwatch, index ) => { + const { + neutralFillStealthRestDelta, + neutralFillStealthHoverDelta, + neutralFillStealthActiveDelta, + neutralFillStealthFocusDelta, + neutralFillStealthSelectedDelta, + neutralFillRestDelta, + neutralFillHoverDelta, + neutralFillActiveDelta, + neutralFillFocusDelta + } = fastDesignSystemDefaults; + const oldValues = neutralFillStealth({...fastDesignSystemDefaults, backgroundColor: fastDesignSystemDefaults.neutralPalette[index]}); + const newValues = neutralFillStealthNew( + palette, + newSwatch, + neutralFillStealthRestDelta, + neutralFillStealthHoverDelta, + neutralFillStealthActiveDelta, + neutralFillStealthFocusDelta, + neutralFillStealthSelectedDelta, + neutralFillRestDelta, + neutralFillHoverDelta, + neutralFillActiveDelta, + neutralFillFocusDelta + ); + it(`should be the same for ${newSwatch}`, () => { + for (let key in oldValues) { + expect(oldValues[key]).to.equal(newValues[key].toColorString().toUpperCase()) + } + }); + }) +}) \ No newline at end of file diff --git a/packages/web-components/fast-components/src/color/neutral-fill-toggle.spec.ts b/packages/web-components/fast-components/src/color/neutral-fill-toggle.spec.ts new file mode 100644 index 00000000000..13a3a726dbe --- /dev/null +++ b/packages/web-components/fast-components/src/color/neutral-fill-toggle.spec.ts @@ -0,0 +1,30 @@ +import { parseColorHexRGB } from "@microsoft/fast-colors"; +import { expect } from "chai"; +import { PaletteRGB } from "../color-vNext/palette"; +import { neutralFillToggle as neutralFillToggleNew } from "../color-vNext/recipes/neutral-fill-toggle"; +import { SwatchRGB } from "../color-vNext/swatch"; +import { fastDesignSystemDefaults } from "../fast-design-system"; +import { neutralBaseColor } from "./color-constants"; +import { neutralFillToggle } from "./neutral-fill-toggle"; + +describe("ensure parity between old and new recipe implementation", () => { + const color = (parseColorHexRGB(neutralBaseColor)!) + const palette = PaletteRGB.from(new SwatchRGB(color.r, color.g, color.b)); + palette.swatches.forEach(( newSwatch, index ) => { + const { neutralFillToggleHoverDelta, neutralFillToggleActiveDelta, neutralFillToggleFocusDelta} = fastDesignSystemDefaults; + const oldValues = neutralFillToggle({...fastDesignSystemDefaults, backgroundColor: fastDesignSystemDefaults.neutralPalette[index]}); + const newValues = neutralFillToggleNew( + palette, + newSwatch, + 0, + neutralFillToggleHoverDelta, + neutralFillToggleActiveDelta, + neutralFillToggleFocusDelta, + ); + it(`should be the same for ${newSwatch}`, () => { + for (let key in oldValues) { + expect(oldValues[key]).to.equal(newValues[key].toColorString().toUpperCase()) + } + }); + }) +}) \ No newline at end of file diff --git a/packages/web-components/fast-components/src/color/neutral-fill.spec.ts b/packages/web-components/fast-components/src/color/neutral-fill.spec.ts index 80a37297fca..684076d9470 100644 --- a/packages/web-components/fast-components/src/color/neutral-fill.spec.ts +++ b/packages/web-components/fast-components/src/color/neutral-fill.spec.ts @@ -14,6 +14,11 @@ import { } from "./neutral-fill"; import { Palette } from "./palette"; import { FillSwatchFamily, Swatch } from "./common"; +import { parseColorHexRGB } from "@microsoft/fast-colors"; +import { neutralBaseColor } from "./color-constants"; +import { SwatchRGB } from "../color-vNext/swatch"; +import { PaletteRGB } from "../color-vNext/palette"; +import { neutralFill as neutralFillNew } from "../color-vNext/recipes/neutral-fill" describe("neutralFill", (): void => { const neutralPalette: Palette = getNeutralPalette(fastDesignSystemDefaults); @@ -98,3 +103,17 @@ describe("neutralFill", (): void => { }); }); }); +describe("ensure parity between old and new recipe implementation", () => { + const color = (parseColorHexRGB(neutralBaseColor)!) + const palette = PaletteRGB.from(new SwatchRGB(color.r, color.g, color.b)); + palette.swatches.forEach(( newSwatch, index ) => { + const { neutralFillRestDelta, neutralFillHoverDelta, neutralFillActiveDelta, neutralFillFocusDelta, neutralFillSelectedDelta } = fastDesignSystemDefaults; + const oldValues = neutralFill({...fastDesignSystemDefaults, backgroundColor: fastDesignSystemDefaults.neutralPalette[index]}); + const newValues = neutralFillNew(palette, newSwatch, neutralFillRestDelta, neutralFillHoverDelta, neutralFillActiveDelta, neutralFillFocusDelta, neutralFillSelectedDelta ); + it(`should be the same for ${newSwatch}`, () => { + for (let key in oldValues) { + expect(oldValues[key]).to.equal(newValues[key].toColorString().toUpperCase()) + } + }); + }) +}) \ No newline at end of file diff --git a/packages/web-components/fast-components/src/color/neutral-focus.spec.ts b/packages/web-components/fast-components/src/color/neutral-focus.spec.ts index a84cb9b7b8f..322226ed3ee 100644 --- a/packages/web-components/fast-components/src/color/neutral-focus.spec.ts +++ b/packages/web-components/fast-components/src/color/neutral-focus.spec.ts @@ -2,6 +2,11 @@ import { expect } from "chai"; import { FASTDesignSystem, fastDesignSystemDefaults } from "../fast-design-system"; import { neutralFocus } from "./neutral-focus"; import { contrast } from "./common"; +import { parseColorHexRGB } from "@microsoft/fast-colors"; +import { neutralBaseColor } from "./color-constants"; +import { PaletteRGB } from "../color-vNext/palette"; +import { SwatchRGB } from "../color-vNext/swatch"; +import { neutralFocus as neutralFocusNew } from "../color-vNext/recipes/neutral-focus"; describe("neutralFocus", (): void => { it("should return a string when invoked with an object", (): void => { @@ -16,3 +21,13 @@ describe("neutralFocus", (): void => { expect(contrast(neutralFocus({} as FASTDesignSystem), "#FFF")).to.be.gte(3.5); }); }); + +describe("ensure parity between old and new recipe implementation", () => { + const color = (parseColorHexRGB(neutralBaseColor)!) + const palette = PaletteRGB.from(new SwatchRGB(color.r, color.g, color.b)); + palette.swatches.forEach(( newSwatch, index ) => { + it(`should be the same for ${newSwatch}`, () => { + expect(neutralFocus({...fastDesignSystemDefaults, backgroundColor: fastDesignSystemDefaults.neutralPalette[index]})).to.be.equal(neutralFocusNew( palette, newSwatch).toColorString().toUpperCase()) + }); + }) +}) \ No newline at end of file diff --git a/packages/web-components/fast-components/src/color/neutral-foreground-hint.spec.ts b/packages/web-components/fast-components/src/color/neutral-foreground-hint.spec.ts index 491b7f8b3fb..7063b05dc3e 100644 --- a/packages/web-components/fast-components/src/color/neutral-foreground-hint.spec.ts +++ b/packages/web-components/fast-components/src/color/neutral-foreground-hint.spec.ts @@ -10,6 +10,11 @@ import { } from "./neutral-foreground-hint"; import { Palette } from "./palette"; import { contrast, Swatch, SwatchRecipe } from "./common"; +import { parseColorHexRGB } from "@microsoft/fast-colors"; +import { neutralBaseColor } from "./color-constants"; +import { PaletteRGB } from "../color-vNext/palette"; +import { SwatchRGB } from "../color-vNext/swatch"; +import { neutralForegroundHint as neutralForegroundHintNew } from "../color-vNext/recipes/neutral-foreground-hint"; describe("neutralForegroundHint", (): void => { const neutralPalette: Palette = getNeutralPalette(fastDesignSystemDefaults); @@ -69,3 +74,15 @@ describe("neutralForegroundHint", (): void => { }); }); }); + +describe("ensure parity between old and new recipe implementation", () => { + const color = (parseColorHexRGB(neutralBaseColor)!) + const palette = PaletteRGB.from(new SwatchRGB(color.r, color.g, color.b)); + palette.swatches.forEach(( newSwatch, index ) => { + it(`should be the same for ${newSwatch}`, () => { + expect(neutralForegroundHintNew(palette, newSwatch).toColorString().toUpperCase()).to.equal( + neutralForegroundHint({...fastDesignSystemDefaults, backgroundColor: fastDesignSystemDefaults.neutralPalette[index]}) + ) + }) + }) +}) \ No newline at end of file diff --git a/packages/web-components/fast-components/src/color/neutral-foreground.spec.ts b/packages/web-components/fast-components/src/color/neutral-foreground.spec.ts index a6e791fcf87..4c3a759e79c 100644 --- a/packages/web-components/fast-components/src/color/neutral-foreground.spec.ts +++ b/packages/web-components/fast-components/src/color/neutral-foreground.spec.ts @@ -1,11 +1,16 @@ +import { parseColorHexRGB } from "@microsoft/fast-colors"; import { expect } from "chai"; +import { PaletteRGB } from "../color-vNext/palette"; +import { neutralForeground } from "../color-vNext/recipes/neutral-foreground"; +import { SwatchRGB } from "../color-vNext/swatch"; import { fastDesignSystemDefaults } from "../fast-design-system"; +import { neutralBaseColor } from "./color-constants"; +import { contrast } from "./common"; import { neutralForegroundActive, neutralForegroundHover, - neutralForegroundRest, + neutralForegroundRest } from "./neutral-foreground"; -import { contrast } from "./common"; describe("neutralForeground", (): void => { it("should return a string when invoked with an object", (): void => { @@ -125,3 +130,13 @@ describe("neutralForeground", (): void => { ).to.be.gte(14); }); }); + +describe("ensure parity between old and new recipe implementation", () => { + const color = (parseColorHexRGB(neutralBaseColor)!) + const palette = PaletteRGB.from(new SwatchRGB(color.r, color.g, color.b)); + palette.swatches.forEach(( newSwatch, index ) => { + it(`should be the same for ${newSwatch}`, () => { + expect(neutralForegroundRest({...fastDesignSystemDefaults, backgroundColor: fastDesignSystemDefaults.neutralPalette[index]})).to.be.equal(neutralForeground( palette, newSwatch).toColorString().toUpperCase()) + }); + }) +}) \ No newline at end of file diff --git a/packages/web-components/fast-components/src/color/neutral-foreground.ts b/packages/web-components/fast-components/src/color/neutral-foreground.ts index 63eb8a12f94..45fb0fede74 100644 --- a/packages/web-components/fast-components/src/color/neutral-foreground.ts +++ b/packages/web-components/fast-components/src/color/neutral-foreground.ts @@ -4,6 +4,7 @@ import { neutralForegroundHoverDelta, neutralPalette, } from "../fast-design-system"; +import { accessibleAlgorithm } from "./accessible-recipe"; import { colorRecipeFactory, SwatchFamilyResolver, @@ -11,7 +12,6 @@ import { SwatchFamilyType, SwatchRecipe, } from "./common"; -import { accessibleAlgorithm } from "./accessible-recipe"; /** * @internal diff --git a/packages/web-components/fast-components/src/color/neutral-layer.spec.ts b/packages/web-components/fast-components/src/color/neutral-layer.spec.ts index 1f0ffcb4ea2..90f2cf40ebd 100644 --- a/packages/web-components/fast-components/src/color/neutral-layer.spec.ts +++ b/packages/web-components/fast-components/src/color/neutral-layer.spec.ts @@ -10,6 +10,16 @@ import { neutralLayerL4, StandardLuminance, } from "./neutral-layer"; +import { + neutralLayerFloating as neutralLayerFloatingNew +} from '../color-vNext/recipes/neutral-layer-floating'; +import { + neutralLayerCard as neutralLayerCardNew +} from '../color-vNext/recipes/neutral-layer-card'; +import { parseColorHexRGB } from "@microsoft/fast-colors"; +import { neutralBaseColor } from "./color-constants"; +import { PaletteRGB } from "../color-vNext/palette"; +import { SwatchRGB } from "../color-vNext/swatch"; const lightModeDesignSystem: FASTDesignSystem = Object.assign( {}, @@ -159,6 +169,13 @@ describe("neutralLayer", (): void => { expect(color).not.to.equal(neutralLayerFloating(fastDesignSystemDefaults)); expect(fastDesignSystemDefaults.neutralPalette.includes(color)).to.be.ok; }); + + it("should have a new implementation that matches the old implementation", () => { + const color = (parseColorHexRGB(neutralBaseColor)!) + const palette = PaletteRGB.from(new SwatchRGB(color.r, color.g, color.b)); + expect(neutralLayerFloating(lightModeDesignSystem)).to.equal(neutralLayerFloatingNew(palette, StandardLuminance.LightMode, lightModeDesignSystem.neutralFillCardDelta).toColorString().toUpperCase()) + expect(neutralLayerFloating(darkModeDesignSystem)).to.equal(neutralLayerFloatingNew(palette, StandardLuminance.DarkMode, lightModeDesignSystem.neutralFillCardDelta).toColorString().toUpperCase()) + }) }); describe("neutralLayerCardContainer", (): void => { it("should return a color from the neutral palette", (): void => { @@ -195,5 +212,11 @@ describe("neutralLayer", (): void => { expect(color).not.to.equal(neutralLayerCard(fastDesignSystemDefaults)); expect(fastDesignSystemDefaults.neutralPalette.includes(color)).to.be.ok; }); + it("should have a new implementation that matches the old implementation", () => { + const color = (parseColorHexRGB(neutralBaseColor)!) + const palette = PaletteRGB.from(new SwatchRGB(color.r, color.g, color.b)); + expect(neutralLayerCard(lightModeDesignSystem)).to.equal(neutralLayerCardNew(palette, StandardLuminance.LightMode, lightModeDesignSystem.neutralFillCardDelta).toColorString().toUpperCase()) + expect(neutralLayerCard(darkModeDesignSystem)).to.equal(neutralLayerCardNew(palette, StandardLuminance.DarkMode, lightModeDesignSystem.neutralFillCardDelta).toColorString().toUpperCase()) + }) }); }); diff --git a/packages/web-components/fast-components/src/color/neutral-outline.spec.ts b/packages/web-components/fast-components/src/color/neutral-outline.spec.ts index a612ee51299..73344338d90 100644 --- a/packages/web-components/fast-components/src/color/neutral-outline.spec.ts +++ b/packages/web-components/fast-components/src/color/neutral-outline.spec.ts @@ -1,4 +1,4 @@ -import { isColorStringHexRGB } from "@microsoft/fast-colors"; +import { isColorStringHexRGB, parseColorHexRGB } from "@microsoft/fast-colors"; import { expect } from "chai"; import { FASTDesignSystem, fastDesignSystemDefaults } from "../fast-design-system"; import { @@ -14,6 +14,10 @@ import { } from "./neutral-outline"; import { Palette } from "./palette"; import { Swatch, SwatchFamily } from "./common"; +import { neutralBaseColor } from "./color-constants"; +import { PaletteRGB } from "../color-vNext/palette"; +import { SwatchRGB } from "../color-vNext/swatch"; +import { neutralOutline as neutralOutlineNew } from "../color-vNext/recipes/neutral-outline" describe("neutralOutline", (): void => { const neutralPalette: Palette = getNeutralPalette(fastDesignSystemDefaults); @@ -98,3 +102,25 @@ describe("neutralOutline", (): void => { }); }); }); + +describe("ensure parity between old and new recipe implementation", () => { + const color = (parseColorHexRGB(neutralBaseColor)!) + const palette = PaletteRGB.from(new SwatchRGB(color.r, color.g, color.b)); + palette.swatches.forEach(( newSwatch, index ) => { + const { neutralOutlineRestDelta, neutralOutlineHoverDelta, neutralOutlineFocusDelta, neutralOutlineActiveDelta } = fastDesignSystemDefaults; + const oldValues = neutralOutline({...fastDesignSystemDefaults, backgroundColor: fastDesignSystemDefaults.neutralPalette[index]}); + const newValues = neutralOutlineNew( + palette, + newSwatch, + neutralOutlineRestDelta, + neutralOutlineHoverDelta, + neutralOutlineActiveDelta, + neutralOutlineFocusDelta, + ); + it(`should be the same for ${newSwatch}`, () => { + for (let key in oldValues) { + expect(oldValues[key]).to.equal(newValues[key].toColorString().toUpperCase()) + } + }); + }) +}) \ No newline at end of file