Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(palette): update adaptive-contrast and color functions #384

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
aeb80bb
feat(palette): update text-contrast and color functions
didimmova Feb 17, 2025
671f5b7
Merge branch 'master' into didimmova/text-contrast-function
didimmova Feb 17, 2025
40fe688
feat(color): update text-contrast function and palette mixin, remove …
didimmova Feb 17, 2025
c66fd1d
Merge branch 'didimmova/text-contrast-function' of https://github.com…
didimmova Feb 17, 2025
b2b7e1b
feat(palette): revert text-contrast function and add accessible-color…
didimmova Feb 18, 2025
85cde30
feat(palette): add fallback for contrast level
didimmova Feb 18, 2025
7bb476f
Merge branch 'master' into didimmova/text-contrast-function
simeonoff Feb 19, 2025
63512fb
refactor(colors): rename accsssible-colors function to adaptive-contrast
simeonoff Feb 19, 2025
d0aeaf7
docs(colors): update sassdoc for adaptive colors
simeonoff Feb 19, 2025
2eef76c
fix(colors): remove wrong parameter invocation
didimmova Feb 19, 2025
d483c76
refactor(colors): add adaptive-contrast mixin
simeonoff Feb 20, 2025
b234984
refactor(color): improve adaptive-contrast function
simeonoff Feb 21, 2025
7a17c72
spec(colors): refactor adaptive color tests
simeonoff Feb 21, 2025
58b6392
refactor(color): update color function and fix contrast shading
simeonoff Feb 21, 2025
1df28c5
Merge branch 'master' into didimmova/text-contrast-function
simeonoff Feb 21, 2025
92b5c87
Merge branch 'master' into didimmova/text-contrast-function
simeonoff Feb 21, 2025
bc1fd9e
refactor(colors): change where adaptive props are declared
simeonoff Feb 21, 2025
dda5c92
Update sass/color/_mixins.scss
simeonoff Feb 21, 2025
36f0c36
docs(adaptive-contrast): update mixin sassdoc
simeonoff Feb 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: hsl(from $s h s l / $_alpha);

@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
118 changes: 89 additions & 29 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 @@ -129,32 +154,40 @@ $_palette: palette(
}

@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 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 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),
hsl(from var(--ig-primary-500-contrast) h s l / 1)
);
@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'),
hsl(from var(--ig-primary-500-contrast) h s l / 1)
);
}

@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 @@ -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);
}
}
}
}
}
}
4 changes: 2 additions & 2 deletions test/_themes.spec.scss
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ $schema: (
type: 'light',
background: hsl(from var(--ig-primary-400) h s l / 1),
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: hsl(from var(--ig-primary-400-contrast) h s l / 1),
hover-foreground: hsl(from var(--ig-secondary-700-contrast) h s l / 1),
border-style: solid,
border-radius: .125rem,
brushes: var(--chart-brushes),
Expand Down