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

Color Blindness Contrast #43

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -27,7 +27,8 @@
"script-loader": "^0.6.1",
"style-loader": "^0.10.1",
"webpack": "^1.8.4",
"webpack-dev-server": "^1.9.0"
"webpack-dev-server": "^1.9.0",
"color-blind": "^0.1.0"
},
"scripts": {
"build": "npm run prod && npm run dev",
Expand Down
4 changes: 2 additions & 2 deletions plugins/contrast/error-description.handlebars
Expand Up @@ -2,8 +2,8 @@
The color combination
<span class="tota11y-color-hexes">{{fgColorHex}}/{{bgColorHex}}</span>
has a contrast ratio of <strong>{{contrastRatio}}</strong>, which is not
sufficient. At this size, you will need a ratio of at least
<strong>{{requiredRatio}}</strong>.
sufficient for {{blindness}} users. At this size, you will need a ratio of at
least <strong>{{requiredRatio}}</strong>.
</p>

<p>
Expand Down
7 changes: 6 additions & 1 deletion plugins/contrast/error-title.handlebars
@@ -1,4 +1,9 @@
Insufficient contrast ratio ({{contrastRatio}} &lt; {{requiredRatio}})
Insufficient contrast ratio (
<div class="tota11y-blindness tota11y-{{blindness}}" title="{{blindness}}">
<div class="tota11y-cone tota11y-short"></div>
<div class="tota11y-cone tota11y-long"></div>
<div class="tota11y-cone tota11y-medium"></div>
</div> {{contrastRatio}} &lt; {{requiredRatio}})

<span class="tota11y-swatches">
<span class="tota11y-swatch" style="background-color: {{fgColorHex}}"></span>
Expand Down
140 changes: 89 additions & 51 deletions plugins/contrast/index.js
Expand Up @@ -6,6 +6,7 @@
let $ = require("jquery");
let Plugin = require("../base");
let annotate = require("../shared/annotate")("labels");
let blinder = require("../../node_modules/color-blind/lib/blind").Blind;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You just call require("color-blind").Blind here and webpack will know where to look :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fancy! Updated.

I can't use only "color-blind" as I don't use the index js from the package, but I updated it to "color-blind/lib/blind".

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, you're absolutely right! I was looking at the "main" field in package.json, but that points to lib/color-blind. Anyway - thanks.


let titleTemplate = require("./error-title.handlebars");
let descriptionTemplate = require("./error-description.handlebars");
Expand All @@ -21,7 +22,7 @@ class ContrastPlugin extends Plugin {
return "Labels elements with insufficient contrast";
}

addError({fgColor, bgColor, contrastRatio, requiredRatio}, el) {
addError({fgColor, bgColor, blindness, contrastRatio, requiredRatio}, el) {
// Suggest colors at an "AA" level
let suggestedColors = axs.utils.suggestColors(
bgColor,
Expand All @@ -36,7 +37,8 @@ class ContrastPlugin extends Plugin {
requiredRatio: requiredRatio,
suggestedFgColorHex: suggestedColors.fg,
suggestedBgColorHex: suggestedColors.bg,
suggestedColorsRatio: suggestedColors.contrast
suggestedColorsRatio: suggestedColors.contrast,
blindness: blindness
};

return this.error(
Expand All @@ -45,6 +47,80 @@ class ContrastPlugin extends Plugin {
$(el));
}

blind(color, blindness) {
let alpha = color.alpha;
color = blinder({
R: color.red,
G: color.green,
B: color.blue
}, blindness, false);
return {
red: color.R | 0,
green: color.G | 0,
blue: color.B | 0,
alpha: alpha
};
}

runOneType(el, combinations, fgColor, bgColor, style, blindness, blindnessName) {
// Calculate required ratio based on size
// Using strings to prevent rounding
let requiredRatio = axs.utils.isLargeFont(style) ?
"3.0" : "4.5";

// Build a key for our `combinations` map and report the color
// if we have not seen it yet
let key = axs.utils.colorToString(fgColor) + "/" +
axs.utils.colorToString(bgColor) + "/" +
// blindness + "/" + // Overwhelming: report one at a time
requiredRatio;

if (blindness !== null)
{
fgColor = this.blind(fgColor, blindness);
bgColor = this.blind(bgColor, blindness);
}
let contrastRatio = axs.utils.calculateContrastRatio(
fgColor, bgColor).toFixed(2);

if (!axs.utils.isLowContrast(contrastRatio, style)) {
// For acceptable contrast values, we don't show ratios if
// they have been presented already
if (!combinations[key]) {
annotate
.label($(el), contrastRatio)
.addClass("tota11y-label-success");

// Add the key to the combinations map. We don't have an
// error to associate it with, so we'll just give it the
// value of `true`.
combinations[key] = true;
}
} else {
if (!combinations[key]) {
// We do not show duplicates in the errors panel, however,
// to keep the output from being overwhelming
let error = this.addError(
{fgColor, bgColor, blindness: blindnessName, contrastRatio, requiredRatio},
el);

combinations[key] = error;
}

// We display errors multiple times for emphasis. Each error
// will point back to the entry in the info panel for that
// particular color combination.
//
// TODO: The error entry in the info panel will only highlight
// the first element with that color combination
annotate.errorLabel(
$(el),
contrastRatio,
"This contrast is insufficient at this size.",
combinations[key]);
}
}

run() {
// Temporary parseColor proxy for FF, which offers "transparent" as a
// default computed backgroundColor instead of `rgba(0, 0, 0, 0)`.
Expand All @@ -63,6 +139,14 @@ class ContrastPlugin extends Plugin {
// entry currently present in the info panel
let combinations = {};

// [<internal name>, <name used on UI and in styles>]
let blindnesses = [
[null, "trichromat"],
["protan", "protanopia"],
["deutan", "deuteranopia"],
["tritan", "tritanopia"]
];

$("*").each((i, el) => {
// Only check elements with a direct text descendant
if (!axs.properties.hasDirectTextDescendant(el)) {
Expand All @@ -83,55 +167,9 @@ class ContrastPlugin extends Plugin {
let style = getComputedStyle(el);
let bgColor = axs.utils.getBgColor(style, el);
let fgColor = axs.utils.getFgColor(style, el, bgColor);
let contrastRatio = axs.utils.calculateContrastRatio(
fgColor, bgColor).toFixed(2);

// Calculate required ratio based on size
// Using strings to prevent rounding
let requiredRatio = axs.utils.isLargeFont(style) ?
"3.0" : "4.5";

// Build a key for our `combinations` map and report the color
// if we have not seen it yet
let key = axs.utils.colorToString(fgColor) + "/" +
axs.utils.colorToString(bgColor) + "/" +
requiredRatio;

if (!axs.utils.isLowContrast(contrastRatio, style)) {
// For acceptable contrast values, we don't show ratios if
// they have been presented already
if (!combinations[key]) {
annotate
.label($(el), contrastRatio)
.addClass("tota11y-label-success");

// Add the key to the combinations map. We don't have an
// error to associate it with, so we'll just give it the
// value of `true`.
combinations[key] = true;
}
} else {
if (!combinations[key]) {
// We do not show duplicates in the errors panel, however,
// to keep the output from being overwhelming
let error = this.addError(
{fgColor, bgColor, contrastRatio, requiredRatio},
el);

combinations[key] = error;
}

// We display errors multiple times for emphasis. Each error
// will point back to the entry in the info panel for that
// particular color combination.
//
// TODO: The error entry in the info panel will only highlight
// the first element with that color combination
annotate.errorLabel(
$(el),
contrastRatio,
"This contrast is insufficient at this size.",
combinations[key]);

for (var blindness of blindnesses) {
this.runOneType(el, combinations, fgColor, bgColor, style, blindness[0], blindness[1]);
}
});

Expand Down
52 changes: 52 additions & 0 deletions plugins/contrast/style.less
Expand Up @@ -20,3 +20,55 @@
.tota11y-color-hexes {
font-family: monospace;
}

.tota11y-blindness {
width: 11px !important;
height: 11px !important;
display: inline-block;

> .tota11y-cone {
border-radius: 50%;
width: 5px !important;
height: 5px !important;
position: relative;
border: 1px solid rgba(0, 0, 0, 0.5);
}

> .tota11y-short {
background-color: #00F;
left: 0px;
top: 0px;
}

> .tota11y-medium {
background-color: #0F0;
margin-top: -5px;
left: 3px;
top: 5px;
}

> .tota11y-long {
background-color: #F00;
margin-top: -5px;
top: 0px;
left: 6px;
}

&.tota11y-protanopia {
> .tota11y-long {
background-color: transparent;
}
}

&.tota11y-deuteranopia {
> .tota11y-medium {
background-color: transparent;
}
}

&.tota11y-tritanopia {
> .tota11y-short {
background-color: transparent;
}
}
}