Skip to content

Commit

Permalink
feat(palette): update adaptive-contrast and color functions (#384)
Browse files Browse the repository at this point in the history
  • Loading branch information
didimmova authored Feb 24, 2025
1 parent ae39296 commit 12e4a71
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 47 deletions.
33 changes: 23 additions & 10 deletions sass/color/_functions.scss
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,7 @@ $_enhanced-accessibility: false;
$result,
(
$variant: map.get($shade, 'hsl'),
'#{$variant}-contrast':
text-contrast(
$background: map.get($shade, 'raw'),
$contrast: 'AA',
),
'#{$variant}-contrast': adaptive-contrast(#{var(--ig-#{$name}-#{$variant})}),
'#{$variant}-raw': map.get($shade, 'raw'),
)
);
Expand Down Expand Up @@ -189,18 +185,17 @@ $_enhanced-accessibility: false;
$s: #{var(--ig-#{$color}-#{$variant})};
$contrast: if(meta.type-of($variant) == string, string.index($variant, 'contrast'), false);
$_alpha: if($opacity, $opacity, 1);
$_hsl-alpha: hsl(from $s h s l / $_alpha);
$_mix-alpha: color-mix(in oklch, $s #{$_alpha * 100%}, transparent);
$_relative-color: if($opacity, hsl(from $s h s l / $opacity), $s);

@if $palette {
$s: map.get($palette, #{$color});
$base: map.get($s, #{$variant});
$raw: if($contrast, map.get($s, #{$variant}-contrast), map.get($s, #{$variant}-raw));
$raw: map.get($s, #{$variant}-raw);

@return if($raw and $variant != '500', rgba($raw, $_alpha), rgba($base, $_alpha));
@return if($contrast, $_relative-color, if($raw and $variant != '500', rgba($raw, $_alpha), $base));
}

@return if($contrast, $_mix-alpha, $_hsl-alpha);
@return $_relative-color;
}

/// Retrieves a contrast text color for a given color variant from a color palette.
Expand All @@ -221,6 +216,24 @@ $_enhanced-accessibility: false;
@return color($palette, $color, #{$variant}-contrast, $opacity);
}

/// Returns a CSS runtime calculated relative color(black or white) for a given color.
/// @access public
/// @group Color
/// @param {Color} $color - The base color used in the calculation.
/// @returns {string} - Returns a relative syntax OKLCH color where the lightness is adjusted
/// based on the specified contrast level, resulting in either black or white.
/// @example scss
/// .my-component {
/// --bg: #09f;
/// background: var(--bg);
/// color: adaptive-contrast(var(--bg));
/// }
@function adaptive-contrast($color) {
$fn: meta.get-function('color', $css: true);

@return hsl(from meta.call($fn, from $color var(--y-contrast)) h 0 l);
}

/// Returns a contrast color for a passed color.
/// @access public
/// @group Color
Expand Down
42 changes: 41 additions & 1 deletion sass/color/_mixins.scss
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,42 @@ $_added: () !default;
}
}

/// Sets up CSS custom properties for WCAG contrast calculations.
/// These properties are used to determine the appropriate text color contrast
/// based on WCAG accessibility guidelines.
/// @access public
/// @group Color
/// @param {String} $level ['aa'] - WCAG contrast level ('a', 'aa', or 'aaa')
/// @example scss - Using the mixin with default AA level
/// .my-component {
/// @include adaptive-contrast();
/// }
/// @example scss - Using the mixin with AAA level
/// .my-component {
/// @include adaptive-contrast('aaa');
/// }
/// @example scss - Generated CSS custom properties
/// :root {
/// --ig-wcag-a: 0.31; // Level A threshold
/// --ig-wcag-aa: 0.185; // Level AA threshold
/// --ig-wcag-aaa: 0.178; // Level AAA threshold
/// --ig-contrast-level: var(--ig-wcag-aa);
/// --y: clamp(0, (y / var(--ig-contrast-level) - 1) * -infinity, 1);
/// --y-contrast: xyz-d65 var(--y) var(--y) var(--y);
/// }
@mixin adaptive-contrast($level: 'aa') {
$scope: if(is-root(), ':root', '&');

#{$scope} {
--ig-wcag-a: 0.31;
--ig-wcag-aa: 0.185;
--ig-wcag-aaa: 0.178;
--ig-contrast-level: var(--ig-wcag-#{$level});
--y: clamp(0, (y / var(--ig-contrast-level) - 1) * -infinity, 1);
--y-contrast: xyz-d65 var(--y) var(--y) var(--y);
}
}

/// Generates CSS variables for a given palette.
/// @access public
/// @group Palettes
Expand All @@ -38,9 +74,13 @@ $_added: () !default;
/// $palette: palette($primary: red, $secondary: blue, $gray: #000);
/// @include palette($palette);
/// @require {function} is-root
@mixin palette($palette, $contrast: true) {
@mixin palette($palette, $contrast: true, $contrast-level: 'aa') {
$scope: if(is-root(), ':root', '&');

@if $contrast {
@include adaptive-contrast($contrast-level);
}

#{$scope} {
@each $color, $shades in map.remove($palette, '_meta') {
@each $shade, $value in $shades {
Expand Down
126 changes: 93 additions & 33 deletions test/_color.spec.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,14 @@ $_palette: palette(
$info: $_info,
$warn: $_warn,
$error: $_error,
$variant: 'material'
$variant: 'material',
);

@include describe('Color') {
@include describe('base') {
@include it('should calculate the contrast ratio between two colors') {
@include assert-equal(contrast($_primary, $_secondary), 1.19);
}

@include it('should mix two colors to produce an opaque color') {
@include assert-equal(to-opaque(rgba(255, 255, 255, .32), #fff), #fff);
@include assert-equal(to-opaque(rgba(233, 233, 233, .32), rgba(255, 255, 255, 0)), #f7f7f7);
@include assert-equal(to-opaque(rgba(255, 255, 255, 0.32), #fff), #fff);
@include assert-equal(to-opaque(rgba(233, 233, 233, 0.32), rgba(255, 255, 255, 0)), #f7f7f7);
}

@include it('converts a color to a list of HSL values') {
Expand All @@ -57,6 +53,35 @@ $_palette: palette(
}

@include describe('contrast') {
$fn: meta.get-function('color', $css: true);

@include it('should return an adaptive contrast color from a hex value') {
$color: #09f;

@include assert-equal(
adaptive-contrast($color),
hsl(from meta.call($fn, from #09f var(--y-contrast)) h 0 l)
);
}

@include it('should return an adaptive contrast color from an hsl value') {
$color: hsl(204deg 100% 50%);

@include assert-equal(
adaptive-contrast($color),
hsl(from meta.call($fn, from hsl(204deg 100% 50%) var(--y-contrast)) h 0 l)
);
}

@include it('should return an adaptive contrast color from a CSS variable value') {
$color: var(--ig-primary-500);

@include assert-equal(
adaptive-contrast($color),
hsl(from meta.call($fn, from var(--ig-primary-500) var(--y-contrast)) h 0 l)
);
}

@include it('should return the passed background value if no valid colors are provided') {
$value: 'not a color';

Expand Down Expand Up @@ -125,36 +150,44 @@ $_palette: palette(
$value: color();

@include assert-equal(type-of($value), string);
@include assert-equal($value, hsl(from (var(--ig-primary-500)) h s l / 1));
@include assert-equal($value, var(--ig-primary-500));
}

@include it('should return a shade as CSS variable w/ color as only argument') {
$value: color($color: secondary);
$value: color(
$color: secondary,
);

@include assert-equal(type-of($value), string);
@include assert-equal($value, hsl(from (var(--ig-secondary-500)) h s l / 1));
@include assert-equal($value, var(--ig-secondary-500));
}

@include it('should return a shade of type string as CSS var w/ color and variant as only arguments') {
$value: color($color: secondary, $variant: 'A400');
$value: color(
$color: secondary,
$variant: 'A400',
);

@include assert-equal(type-of($value), string);
@include assert-equal($value, hsl(from (var(--ig-secondary-A400)) h s l / 1));
@include assert-equal($value, var(--ig-secondary-A400));
}

@include it('should return a contrast shade of type color w/ palette as only argument') {
$value: contrast-color($_palette, $opacity: .5);
$expected: rgba(0 0 0 / .5);
@include it('should return a contrast shade w/ palette as only argument') {
$value: contrast-color($_palette, $opacity: 0.5);
$expected: hsl(from var(--ig-primary-500-contrast) h s l / 0.5);

@include assert-equal(type-of($value), color);
@include assert-equal($expected, $value);
}

@include it('should return a contrast shade of type string as CSS var w/ color and variant as only arguments') {
$value: contrast-color($color: secondary, $variant: 'A400', $opacity: .25);
$value: contrast-color(
$color: secondary,
$variant: 'A400',
$opacity: 0.25,
);

@include assert-equal(type-of($value), string);
@include assert-equal($value, color-mix(in oklch, var(--ig-secondary-A400-contrast) 25%, transparent));
@include assert-equal($value, hsl(from var(--ig-secondary-A400-contrast) h s l / 0.25));
}

@include it('should retrieve colors from a palette regadless of type of key') {
Expand All @@ -163,9 +196,15 @@ $_palette: palette(
@include assert-true(color($_palette, 'primary', '500'));
@include assert-equal(color($_palette, 'primary', '500'), $_primary);
@include assert-true(contrast-color($_palette, primary, 500));
@include assert-equal(contrast-color($_palette, primary, 500), black);
@include assert-equal(
contrast-color($_palette, primary, 500),
var(--ig-primary-500-contrast)
);
@include assert-true(contrast-color($_palette, 'primary', '500'));
@include assert-equal(contrast-color($_palette, 'primary', '500'), black);
@include assert-equal(
contrast-color($_palette, 'primary', '500'),
var(--ig-primary-500-contrast)
);
}

@include it('should generate an HSL color shade from a given base color') {
Expand All @@ -174,7 +213,7 @@ $_palette: palette(
$shade: shade($color, $_primary, $variant, null);
$expected: (
raw: hsl(204deg 100% 44.5%),
hsl: #{hsl(from var(--ig-primary-500) h calc(s * 1.26) calc(l * 0.89))}
hsl: #{hsl(from var(--ig-primary-500) h calc(s * 1.26) calc(l * 0.89))},
);

@include assert-equal($shade, $expected);
Expand All @@ -187,18 +226,20 @@ $_palette: palette(
$shade: shade($color, null, $variant, $surface);
$expected: (
raw: hsl(0deg 0% 98%),
hsl: #{hsl(from var(--ig-gray-500) h s 98%)}
hsl: #{hsl(from var(--ig-gray-500) h s 98%)},
);

// $surface is bright, return a darker shade of gray
@include assert-equal($shade, $expected);

$surface: #444;
$shade: shade($color, null, $variant, $surface);
$expected: #{var(--ig-#{$color}-h), var(--ig-#{$color}-s), 13%};
$expected: #{var(--ig-#{$color}-h),
var(--ig-#{$color}-s),
13%};
$expected: (
raw: hsl(0deg 0% 13%),
hsl: #{hsl(from var(--ig-gray-500) h s 13%)}
hsl: #{hsl(from var(--ig-gray-500) h s 13%)},
);

// $surface is dark, return a lighter shade of gray
Expand Down Expand Up @@ -237,11 +278,11 @@ $_palette: palette(
@include contains($selector: false) {
:root {
@each $color, $shades in map.remove($IPalette, '_meta') {
@each $shade in $shades {
$value: map.get($_palette, $color, $shade);
@each $shade in $shades {
$value: map.get($_palette, $color, $shade);

--ig-#{$color}-#{$shade}: #{$value};
}
--ig-#{$color}-#{$shade}: #{$value};
}
}
}
}
Expand Down Expand Up @@ -285,11 +326,11 @@ $_palette: palette(
@include contains($selector: false) {
:root {
@each $color, $shades in map.remove($IPalette, '_meta') {
@each $shade in $shades {
$value: map.get($_palette, $color, $shade);
@each $shade in $shades {
$value: map.get($_palette, $color, $shade);

--ig-#{$color}-#{$shade}: #{$value};
}
--ig-#{$color}-#{$shade}: #{$value};
}
}
}
}
Expand All @@ -300,7 +341,7 @@ $_palette: palette(
$_palette: mocks.$handmade-palette;
$_ref: color(null, primary, 800);

@include assert-equal($_ref, hsl(from var(--ig-primary-800) h s l / 1));
@include assert-equal($_ref, var(--ig-primary-800));

@include assert() {
@include output() {
Expand All @@ -316,5 +357,24 @@ $_palette: palette(
@include it('should convert a color to a list of HSL values') {
@include assert-equal(to-hsl(black), (0deg, 0%, 0%));
}

@include it('should include all necessarry CSS custom properties for adaptive contrast to work') {
@include assert() {
@include output($selector: false) {
@include adaptive-contrast('aaa');
}

@include contains($selector: false) {
:root {
--ig-wcag-a: 0.31;
--ig-wcag-aa: 0.185;
--ig-wcag-aaa: 0.178;
--ig-contrast-level: var(--ig-wcag-aaa);
--y: clamp(0, (y / var(--ig-contrast-level) - 1) * -infinity, 1);
--y-contrast: xyz-d65 var(--y) var(--y) var(--y);
}
}
}
}
}
}
6 changes: 3 additions & 3 deletions test/_themes.spec.scss
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,10 @@ $schema: (
@include it('should output theme maps from schema definitions') {
$theme: (
type: 'light',
background: hsl(from var(--ig-primary-400) h s l / 1),
background: var(--ig-primary-400),
hover-background: hsl(from var(--ig-secondary-700) h s l / .26),
foreground: color-mix(in oklch, var(--ig-primary-400-contrast) 100%, transparent),
hover-foreground: color-mix(in oklch, var(--ig-secondary-700-contrast) 100%, transparent),
foreground: var(--ig-primary-400-contrast),
hover-foreground: var(--ig-secondary-700-contrast),
border-style: solid,
border-radius: .125rem,
brushes: var(--chart-brushes),
Expand Down

0 comments on commit 12e4a71

Please sign in to comment.