diff --git a/package.json b/package.json index e1b605ec..f203cd4c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/plugins/contrast/error-description.handlebars b/plugins/contrast/error-description.handlebars index d6eb2ac6..131cedbb 100644 --- a/plugins/contrast/error-description.handlebars +++ b/plugins/contrast/error-description.handlebars @@ -2,8 +2,8 @@ The color combination {{fgColorHex}}/{{bgColorHex}} has a contrast ratio of {{contrastRatio}}, which is not - sufficient. At this size, you will need a ratio of at least - {{requiredRatio}}. + sufficient for {{blindness}} users. At this size, you will need a ratio of at + least {{requiredRatio}}.

diff --git a/plugins/contrast/error-title.handlebars b/plugins/contrast/error-title.handlebars index 1593f153..9012f7c3 100644 --- a/plugins/contrast/error-title.handlebars +++ b/plugins/contrast/error-title.handlebars @@ -1,4 +1,9 @@ -Insufficient contrast ratio ({{contrastRatio}} < {{requiredRatio}}) +Insufficient contrast ratio ( +

+
+
+
+
{{contrastRatio}} < {{requiredRatio}}) diff --git a/plugins/contrast/index.js b/plugins/contrast/index.js index 21ee7df7..3975b657 100644 --- a/plugins/contrast/index.js +++ b/plugins/contrast/index.js @@ -6,6 +6,7 @@ let $ = require("jquery"); let Plugin = require("../base"); let annotate = require("../shared/annotate")("labels"); +let blinder = require("color-blind/lib/blind").Blind; let titleTemplate = require("./error-title.handlebars"); let descriptionTemplate = require("./error-description.handlebars"); @@ -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, @@ -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( @@ -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)`. @@ -63,6 +139,14 @@ class ContrastPlugin extends Plugin { // entry currently present in the info panel let combinations = {}; + // [, ] + 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)) { @@ -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]); } }); diff --git a/plugins/contrast/style.less b/plugins/contrast/style.less index df3673dd..2d8bfb3d 100644 --- a/plugins/contrast/style.less +++ b/plugins/contrast/style.less @@ -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; + } + } +} \ No newline at end of file