diff --git a/.changeset/upset-ties-spend.md b/.changeset/upset-ties-spend.md new file mode 100644 index 00000000..06375183 --- /dev/null +++ b/.changeset/upset-ties-spend.md @@ -0,0 +1,5 @@ +--- +"@adaptive-web/adaptive-ui": patch +--- + +AUI: Updated layer fill interactive to invert colors to avoid white on white scenario diff --git a/packages/adaptive-ui-explorer/src/components/color-block.ts b/packages/adaptive-ui-explorer/src/components/color-block.ts index 15bb6ed0..cfcea335 100644 --- a/packages/adaptive-ui-explorer/src/components/color-block.ts +++ b/packages/adaptive-ui-explorer/src/components/color-block.ts @@ -19,6 +19,7 @@ import { highlightFillSubtleInverseControlStyles, highlightForegroundReadableControlStyles, highlightOutlineDiscernibleControlStyles, + layerFillInteractiveControlStyles, neutralDividerDiscernibleElementStyles, neutralDividerSubtleElementStyles, neutralFillDiscernibleControlStyles, @@ -82,6 +83,10 @@ const backplateComponents = html` Accent subtle inverse + x.disabledState} :showSwatches=${x => x.showSwatches} :styles="${x => layerFillInteractiveControlStyles}"> + Layer interactive + + x.disabledState} :showSwatches=${x => x.showSwatches} :styles="${x => neutralFillIdealControlStyles}"> Neutral ideal diff --git a/packages/adaptive-ui/docs/api-report.md b/packages/adaptive-ui/docs/api-report.md index 9b532dbc..8aafc2e0 100644 --- a/packages/adaptive-ui/docs/api-report.md +++ b/packages/adaptive-ui/docs/api-report.md @@ -466,6 +466,9 @@ export type InteractivityDefinition = { interactive?: string; }; +// @public +export function invertingPaletteDeltasForSet(palette: Palette, reference: RelativeLuminance, restDelta: number, hoverDelta: number, activeDelta: number, focusDelta: number, disabledDelta: number): InteractiveValues; + // @public export function isDark(color: RelativeLuminance): boolean; diff --git a/packages/adaptive-ui/src/core/color/recipes/index.ts b/packages/adaptive-ui/src/core/color/recipes/index.ts index fdac8633..60074b53 100644 --- a/packages/adaptive-ui/src/core/color/recipes/index.ts +++ b/packages/adaptive-ui/src/core/color/recipes/index.ts @@ -4,6 +4,7 @@ export * from "./contrast-and-delta-swatch-set.js"; export * from "./contrast-swatch.js"; export * from "./delta-swatch-set.js"; export * from "./delta-swatch.js"; +export * from "./inverting-palette-deltas-for-set.js"; export * from "./hue-shift-gradient.js"; export * from "./ideal-color-delta-swatch-set.js"; export * from "./two-palette-gradient.js"; diff --git a/packages/adaptive-ui/src/core/color/recipes/inverting-palette-deltas-for-set.ts b/packages/adaptive-ui/src/core/color/recipes/inverting-palette-deltas-for-set.ts new file mode 100644 index 00000000..4b500adf --- /dev/null +++ b/packages/adaptive-ui/src/core/color/recipes/inverting-palette-deltas-for-set.ts @@ -0,0 +1,58 @@ +import { InteractiveValues } from "../../types.js"; +import { Palette } from "../palette.js"; +import { RelativeLuminance } from "../utilities/relative-luminance.js"; + +/** + * Checks if the supplied delta set for palette access is valid, or flips sign if not to keep deltas in bounds. + * + * Returns a new set of deltas so the indices for all states (rest, hover, active, focus) are in bounds. + * Flipping is always based on the rest delta; the other deltas are adjusted to preserve their *relative* difference to rest. + * + * @param palette - The Palette used to find the Colors + * @param reference - The reference color + * @param restDelta - The rest state offset from `reference` + * @param hoverDelta - The hover state offset from `reference` + * @param activeDelta - The active state offset from `reference` + * @param focusDelta - The focus state offset from `reference` + * @param disabledDelta - The disabled state offset from `reference` + * @returns An interactive set of deltas with possibly flipped and shifted values + * + * @public + */ +export function invertingPaletteDeltasForSet(palette: Palette, reference: RelativeLuminance, restDelta: number, hoverDelta: number, activeDelta: number, focusDelta: number, disabledDelta: number): InteractiveValues { + const referenceIndex = palette.closestIndexOf(reference); + const deltas = [restDelta, hoverDelta, activeDelta, focusDelta]; + + // Compute the indices they'll hit + const indices = deltas.map(d => referenceIndex + d); + + // Check if all indices are within palette bounds + const withinBounds = indices.every(idx => idx >= 0 && idx < palette.swatches.length); + + if (withinBounds) { + // All are in bounds as-is + return { + rest: restDelta, + hover: hoverDelta, + active: activeDelta, + focus: focusDelta, + disabled: disabledDelta, + }; + } + + // The offset of other deltas from restDelta + const hoverOffset = hoverDelta - restDelta; + const activeOffset = activeDelta - restDelta; + const focusOffset = focusDelta - restDelta; + const disabledOffset = disabledDelta - restDelta; + + const flippedRestDelta = restDelta * -1; + + return { + rest: flippedRestDelta, + hover: flippedRestDelta + hoverOffset, + active: flippedRestDelta + activeOffset, + focus: flippedRestDelta + focusOffset, + disabled: flippedRestDelta + disabledOffset, + }; +} diff --git a/packages/adaptive-ui/src/reference/layer.ts b/packages/adaptive-ui/src/reference/layer.ts index 20e1b7ff..ffcc40c1 100644 --- a/packages/adaptive-ui/src/reference/layer.ts +++ b/packages/adaptive-ui/src/reference/layer.ts @@ -2,7 +2,7 @@ import { DesignTokenResolver } from "@microsoft/fast-foundation"; import { DesignTokenType } from "../core/adaptive-design-tokens.js"; import { Palette, PaletteDirectionValue } from "../core/color/palette.js"; import { ColorRecipeParams, InteractivePaintSet } from "../core/color/recipe.js"; -import { deltaSwatch, deltaSwatchSet } from "../core/color/recipes/index.js"; +import { deltaSwatch, deltaSwatchSet, invertingPaletteDeltasForSet } from "../core/color/recipes/index.js"; import { Color } from "../core/color/color.js"; import { luminanceSwatch } from "../core/color/utilities/luminance-swatch.js"; import { StyleProperty } from "../core/modules/types.js"; @@ -50,7 +50,7 @@ export const layerFillRestDelta = createTokenDelta(layerFillName, "layer", -2); /** * @public - * @deprecated Use `layerFillLayerDelta` instead. + * @deprecated Use `layerFillRestDelta` instead. */ export const layerFillDelta = layerFillRestDelta; @@ -246,18 +246,27 @@ export const layerFillDisabledDelta = createTokenDelta(layerFillInteractiveName, * @public */ export const layerFillInteractiveRecipe = createTokenColorRecipe(layerFillInteractiveName, StyleProperty.backgroundFill, - (resolve: DesignTokenResolver, params?: ColorRecipeParams): InteractivePaintSet => - deltaSwatchSet( - resolve(layerPalette), - params?.reference || resolve(colorContext), - resolve(layerFillRestDelta), - resolve(layerFillHoverDelta), - resolve(layerFillActiveDelta), - resolve(layerFillFocusDelta), - resolve(layerFillDisabledDelta), + (resolve: DesignTokenResolver, params?: ColorRecipeParams): InteractivePaintSet => { + const palette = resolve(layerPalette); + const reference = params?.reference || resolve(colorContext); + const restDelta = resolve(layerFillRestDelta); + const hoverDelta = resolve(layerFillHoverDelta); + const activeDelta = resolve(layerFillActiveDelta); + const focusDelta = resolve(layerFillFocusDelta); + const disabledDelta = resolve(layerFillDisabledDelta); + const deltas = invertingPaletteDeltasForSet(palette, reference, restDelta, hoverDelta, activeDelta, focusDelta, disabledDelta); + return deltaSwatchSet( + palette, + reference, + deltas.rest, + deltas.hover, + deltas.active, + deltas.focus, + deltas.disabled, undefined, PaletteDirectionValue.darker, - ), + ); + } ); export const layerFillInteractive = createTokenColorSet(layerFillInteractiveRecipe); diff --git a/packages/adaptive-ui/src/reference/modules.ts b/packages/adaptive-ui/src/reference/modules.ts index 97ce81dc..c0d29601 100644 --- a/packages/adaptive-ui/src/reference/modules.ts +++ b/packages/adaptive-ui/src/reference/modules.ts @@ -81,7 +81,7 @@ import { } from "./color.js"; import { densityControl, densityControlList, densityItemContainer, densityLayer, densityText } from "./density.js"; import { elevationCardInteractive, elevationCardRest, elevationDialog, elevationFlyout, elevationTooltip } from "./elevation.js"; -import { layerFillFixedPlus1 } from "./layer.js"; +import { layerFillFixedPlus1, layerFillInteractive } from "./layer.js"; import { fontFamily, fontWeight, @@ -1137,6 +1137,24 @@ export const criticalForegroundReadableControlStyles: Styles = Styles.fromProper "color.critical-foreground-readable-control", ); +/** + * Convenience style module for a layer-filled control (interactive). + * + * By default, only the foreground color meets accessibility, useful for a button or similar: + * - layer interactive background + * - neutral strong foreground (a11y) + * - transparent border + * + * @public + */ +export const layerFillInteractiveControlStyles: Styles = Styles.fromProperties( + { + ...Fill.backgroundAndForeground(layerFillInteractive, neutralStrokeStrongRecipe), + ...densityBorderStyles(transparent), + }, + "color.layer-fill-interactive-control", +); + /** * Convenience style module for a neutral-filled stealth control (interactive). *