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

fix(color-contrast): support CSS 4 color spaces #4020

Merged
merged 4 commits into from
May 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
169 changes: 41 additions & 128 deletions lib/commons/color/color.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import standards from '../../standards';
import { Colorjs } from '../../core/imports';

const hexRegex = /^#[0-9a-f]{3,8}$/i;
const colorFnRegex = /^((?:rgb|hsl)a?)\s*\(([^\)]*)\)/i;
const hslRegex = /hsl\(\s*([\d.]+)(rad|turn)/;

/**
* @class Color
Expand Down Expand Up @@ -57,26 +57,35 @@ export default class Color {
* @instance
*/
parseString(colorString) {
// IE occasionally returns named colors instead of RGB(A) values
if (standards.cssColors[colorString] || colorString === 'transparent') {
const [red, green, blue] = standards.cssColors[colorString] || [0, 0, 0];
this.red = red;
this.green = green;
this.blue = blue;
this.alpha = colorString === 'transparent' ? 0 : 1;
return this;
}
// Colorjs currently does not support rad or turn angle values
// @see https://github.com/LeaVerou/color.js/issues/311
colorString = colorString.replace(hslRegex, (match, angle, unit) => {
const value = angle + unit;

switch (unit) {
case 'rad':
return match.replace(value, radToDeg(angle));
case 'turn':
return match.replace(value, turnToDeg(angle));
}
});

if (colorString.match(colorFnRegex)) {
this.parseColorFnString(colorString);
return this;
try {
// srgb values are between 0 and 1
const color = new Colorjs(colorString).to('srgb');
// when converting from one color space to srgb
// the values of rgb may be above 1 so we need to clamp them
// we also need to round the final value as rgb values don't have decimals
this.red = Math.round(clamp(color.r, 0, 1) * 255);
this.green = Math.round(clamp(color.g, 0, 1) * 255);
this.blue = Math.round(clamp(color.b, 0, 1) * 255);
// color.alpha is a Number object so convert it to a number
this.alpha = +color.alpha;
} catch (err) {
throw new Error(`Unable to parse color "${colorString}"`);
}

if (colorString.match(hexRegex)) {
this.parseHexString(colorString);
return this;
}
throw new Error(`Unable to parse color "${colorString}"`);
return this;
}

/**
Expand All @@ -88,15 +97,7 @@ export default class Color {
* @param {string} rgb The string value
*/
parseRgbString(colorString) {
// IE can pass transparent as value instead of rgba
if (colorString === 'transparent') {
this.red = 0;
this.green = 0;
this.blue = 0;
this.alpha = 0;
return;
}
this.parseColorFnString(colorString);
this.parseString(colorString);
}

/**
Expand All @@ -111,24 +112,8 @@ export default class Color {
if (!colorString.match(hexRegex) || [6, 8].includes(colorString.length)) {
return;
}
colorString = colorString.replace('#', '');
if (colorString.length < 6) {
const [r, g, b, a] = colorString;
colorString = r + r + g + g + b + b;
if (a) {
colorString += a + a;
}
}

var aRgbHex = colorString.match(/.{1,2}/g);
this.red = parseInt(aRgbHex[0], 16);
this.green = parseInt(aRgbHex[1], 16);
this.blue = parseInt(aRgbHex[2], 16);
if (aRgbHex[3]) {
this.alpha = parseInt(aRgbHex[3], 16) / 255;
} else {
this.alpha = 1;
}
this.parseString(colorString);
}

/**
Expand All @@ -140,30 +125,7 @@ export default class Color {
* @param {string} rgb The string value
*/
parseColorFnString(colorString) {
const [, colorFunc, colorValStr] = colorString.match(colorFnRegex) || [];
if (!colorFunc || !colorValStr) {
return;
}

// Get array of color number strings from the string:
const colorVals = colorValStr
.split(/\s*[,\/\s]\s*/)
.map(str => str.replace(',', '').trim())
.filter(str => str !== '');

// Convert to numbers
let colorNums = colorVals.map((val, index) => {
return convertColorVal(colorFunc, val, index);
});

if (colorFunc.substr(0, 3) === 'hsl') {
colorNums = hslToRgb(colorNums);
}

this.red = colorNums[0];
this.green = colorNums[1];
this.blue = colorNums[2];
this.alpha = typeof colorNums[3] === 'number' ? colorNums[3] : 1;
this.parseString(colorString);
}

/**
Expand All @@ -190,66 +152,17 @@ export default class Color {
}
}

/**
* Convert a CSS color value into a number
*/
function convertColorVal(colorFunc, value, index) {
if (/%$/.test(value)) {
//<percentage>
if (index === 3) {
// alpha
return parseFloat(value) / 100;
}
return (parseFloat(value) * 255) / 100;
}
if (colorFunc[index] === 'h') {
// hue
if (/turn$/.test(value)) {
return parseFloat(value) * 360;
}
if (/rad$/.test(value)) {
return parseFloat(value) * 57.3;
}
}
return parseFloat(value);
// clamp a value between two numbers (inclusive)
function clamp(value, min, max) {
return Math.min(Math.max(min, value), max);
}

/**
* Convert HSL to RGB
*/
function hslToRgb([hue, saturation, lightness, alpha]) {
// Must be fractions of 1
saturation /= 255;
lightness /= 255;

const high = (1 - Math.abs(2 * lightness - 1)) * saturation;
const low = high * (1 - Math.abs(((hue / 60) % 2) - 1));
const base = lightness - high / 2;

let colors;
if (hue < 60) {
// red - yellow
colors = [high, low, 0];
} else if (hue < 120) {
// yellow - green
colors = [low, high, 0];
} else if (hue < 180) {
// green - cyan
colors = [0, high, low];
} else if (hue < 240) {
// cyan - blue
colors = [0, low, high];
} else if (hue < 300) {
// blue - purple
colors = [low, 0, high];
} else {
// purple - red
colors = [high, 0, low];
}
// convert radians to degrees
function radToDeg(rad) {
return (rad * 180) / Math.PI;
}

return colors
.map(color => {
return Math.round((color + base) * 255);
})
.concat(alpha);
// convert turn to degrees
function turnToDeg(turn) {
return turn * 360;
}
3 changes: 2 additions & 1 deletion lib/core/imports/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CssSelectorParser } from 'css-selector-parser';
import doT from '@deque/dot';
import emojiRegexText from 'emoji-regex';
import memoize from 'memoizee';
import Color from 'colorjs.io';

import es6promise from 'es6-promise';
import { Uint32Array } from 'typedarray';
Expand Down Expand Up @@ -40,4 +41,4 @@ if (window.Uint32Array) {
* @namespace imports
* @memberof axe
*/
export { CssSelectorParser, doT, emojiRegexText, memoize };
export { CssSelectorParser, doT, emojiRegexText, memoize, Color as Colorjs };
14 changes: 12 additions & 2 deletions lib/standards/aria-roles.js
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,12 @@ const ariaRoles = {
type: 'widget',
requiredContext: ['menu', 'menubar', 'group'],
requiredAttrs: ['aria-checked'],
allowedAttrs: ['aria-expanded', 'aria-posinset', 'aria-readonly', 'aria-setsize'],
allowedAttrs: [
'aria-expanded',
'aria-posinset',
'aria-readonly',
'aria-setsize'
],
superclassRole: ['checkbox', 'menuitem'],
accessibleNameRequired: true,
nameFromContent: true,
Expand All @@ -388,7 +393,12 @@ const ariaRoles = {
type: 'widget',
requiredContext: ['menu', 'menubar', 'group'],
requiredAttrs: ['aria-checked'],
allowedAttrs: ['aria-expanded', 'aria-posinset', 'aria-readonly', 'aria-setsize'],
allowedAttrs: [
'aria-expanded',
'aria-posinset',
'aria-readonly',
'aria-setsize'
],
superclassRole: ['menuitemcheckbox', 'radio'],
accessibleNameRequired: true,
nameFromContent: true,
Expand Down
15 changes: 14 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
"chalk": "^4.x",
"chromedriver": "latest",
"clone": "^2.1.2",
"colorjs.io": "^0.4.3",
"conventional-commits-parser": "^3.2.4",
"core-js": "^3.27.1",
"css-selector-parser": "^1.4.1",
Expand Down
Loading