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

Add warning for low contrast and equal colors #86

Closed
wants to merge 13 commits into from
Closed
1 change: 1 addition & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ The icons `assets/qr-icon*.svg` (used in `src/icons/icon-small-*.svg`) are made
* [`common/img/info-dark.svg`](common/img/info-dark.svg) by Mozilla, license: [MPL v2.0](https://www.mozilla.org/en-US/MPL/2.0/)
* [`common/img/info-light.svg`](common/img/info-light.svg) by Mozilla, license: [MPL v2.0](https://www.mozilla.org/en-US/MPL/2.0/)
* [`common/img/open-in-new.svg`](common/img/open-in-new.svg) by Mozilla, license: [MPL v2.0](https://www.mozilla.org/en-US/MPL/2.0/)
* [`common/img/warning-dark.svg`](common/img/warning-dark.svg) by Mozilla, license: [MPL v2.0](https://www.mozilla.org/en-US/MPL/2.0/)

The MPLv2.0 license is reproduced below:

Expand Down
16 changes: 16 additions & 0 deletions src/_locales/de/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,22 @@
"message": "Wir benötigen die Download-Berechtigung, um das Speichern des QR-Codes fortzusetzen.",
"description": "Shown, when the user is asked to allow the download permission to save the QR code."
},
"lowContrastRatioInfo": {
"message": "Der QR-Code kann wahrscheinlich nicht von allen Scannern erkannt werden, weil der Kontrast zu niedrig ist.",
"desription": "The message shown when the contrast ratio is too low"
},
"lowContrastRatioWarning": {
"message": "Das Kontrastverhältnis zwischen der Farbe des QR-Codes und dem Hintergrund ist wahrscheinlich zu niedrig um von den meisten QR-Code-Scannern erkannt zu werden",
Copy link
Owner

@rugk rugk May 22, 2018

Choose a reason for hiding this comment

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

shorter:
das Kontrastverhältnis -> der Kontrast
is "wahrscheinlich" neccessary? Maybe just use subjunctive ("Konjunktiv") here, too (as in the English version).

grammar:
dem Hintergrund -> dessen Hintergrund

In general you may reword it. Keep the fact that it may not be scanned by many QR code readers.

"desription": "The warning shown when the contrast ratio is too low"
},
"lowContrastRatioError": {
"message": "Die Farbe des QR-Codes und des Hintergrunds ist zu ähnlich.",
"description": "The message shown when the color for the QR code and the background are equal"
},
"messageAutoSelectColorButton": {
"message": "Besten Kontrast berechnen",
"description": "The text of a button that sets a new color with a sufficient contrast."
},

// tips
"tipYouLikeAddon": {
Expand Down
16 changes: 16 additions & 0 deletions src/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,22 @@
"message": "To continue to save the QR code, we need the download permission.",
"description": "Shown, when the user is asked to allow the download permission to save the QR code."
},
"lowContrastRatioInfo": {
"message": "Your QR code may be hard to scan by some QR code readers because the contrast is too low.",
"desription": "The message shown when the contrast ratio is too low"
},
"lowContrastRatioWarning": {
"message": "The contrast ratio between the QR code color and the background might be too low to be recognized by the most QR code readers",
"desription": "The warning shown when the contrast ratio is too low"
},
"lowContrastRatioError": {
"message": "The background and the foreground color are too similar.",
"description": "The message shown when the color for the QR code and the background are equal"
},
"messageAutoSelectColorButton": {
"message": "Calculate best contrast",
Copy link
Owner

Choose a reason for hiding this comment

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

MAybe you can try to shorten this a bit?

Instead of calculate maybe just "Use". (That the JS has to calculate this is a technical detail the user does not even want to know. 😄)
And instead of contrast maybe contrasting color? Or something else like "best color"? Remember it does not matter to be technically 100% accurate here, the user should just know what happens when they click the button.

Theoretically it could e.g. also be okay for the user to get some color variation (i.e. not 100%ly the contrast color, but a color, which has enough contrast, so the message goes away.)

"description": "The text of a button that sets a new color with a sufficient contrast."
},

// tips
"tipYouLikeAddon": {
Expand Down
30 changes: 30 additions & 0 deletions src/common/common.css
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ a:active {
/* buttons https://design.firefox.com/photon/components/buttons.html */
.micro-button {
height: 24px;
height: auto;
border-radius: 2px;

padding-left: 8px;
Expand Down Expand Up @@ -87,6 +88,27 @@ a:active {
background-color: var(--green-80);
}

.micro-button.warning {
background-color: var(--yellow-60);
}
.micro-button:hover.warning {
background-color: var(--yellow-70);
}
.micro-button:active.warning {
background-color: var(--yellow-80);
}

.micro-button.error {
background-color: var(--red-70);
color: var(--white-100);
}
.micro-button:hover.error {
background-color: var(--red-80);
}
.micro-button:active.error {
background-color: var(--red-90);
}

.micro-button:focus {
box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3)
}
Expand Down Expand Up @@ -150,6 +172,11 @@ a:active {
background-color: var(--green-50);
}

.warning {
color: var(--yellow-90);
background-color: var(--yellow-50);
}

/* message box action button */
.message-action-button {
margin-left: 8px;
Expand Down Expand Up @@ -188,6 +215,9 @@ a:active {
.success::before {
background-image: url('/common/img/check.svg');
}
.warning::before {
background-image: url('/common/img/warning-dark.svg');
}

.icon-dismiss {
box-sizing: content-box;
Expand Down
98 changes: 98 additions & 0 deletions src/common/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -1372,6 +1372,104 @@ const RandomTips = (function () {// eslint-disable-line no-unused-vars
return me;
})();

const Colors = (() => { // eslint-disable-line no-unused-vars
Copy link
Owner

Choose a reason for hiding this comment

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

Yeah, this module looks great! So for doing #20 later, everything is set.

const me = {};

/**
* Calculates the contrast between two colors
*
* @name Colors.contrastRatio
* @function
* @param {Array} rgb1
* @param {Array} rgb2
* @returns {int}
*/
me.contrastRatio = function(rgb1, rgb2) {
const l1 = luminance(rgb1);
const l2 = luminance(rgb2);
// Formula: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
};

/**
* Calculates the luminance of a given RGB color.
*
* @name Colors.luminance
* @function
* @private
* @param {Array} rgb
* @returns {Array|null}
*/
function luminance(rgb) {
// Formula: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
let r = rgb[0] / 255;
let g = rgb[1] / 255;
let b = rgb[2] / 255;
// Note: I'm using 0.04045 here (instead of 0.03928) because 0.04045 is the
// number used in the actual sRGB standard.
// See also: https://github.com/w3c/wcag21/issues/815
r = r < 0.04045 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4);
g = g < 0.04045 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4);
b = b < 0.04045 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4);
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

/**
* Returns the complementary color of a given RGB array.
*
* @name Colors.invertColor
* @function
* @param {Array} rgb
* @returns {string}
*/
me.invertColor = function(rgb) {
// invert color components
const r = (255 - rgb[0]).toString(16);
const g = (255 - rgb[1]).toString(16);
const b = (255 - rgb[2]).toString(16);
// pad each with zeros and return
return `#${padZero(r)}${padZero(g)}${padZero(b)}`;
};

/**
* Adds missing zeros in front of a string.
*
* @name Colors.padZero
* @function
* @private
* @param {string} string
* @param {int} length
* @returns {string}
*/
function padZero(string, length) {
length = length || 2;
const zeros = new Array(length).join("0");
return (zeros + string).slice(-length);
}

/**
* Converts a hex color string to RGB.
*
* @name Colors.hexToRgb
* @function
* @param {string} hex
* @returns {Array|null}
*/
me.hexToRgb = function(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (result) {
return [
parseInt(result[1], 16),
parseInt(result[2], 16),
parseInt(result[3], 16)
];
}
return null;
};

return me;
})();

// init modules
AddonSettings.loadOptions();
Logger.init();
Expand Down
6 changes: 6 additions & 0 deletions src/common/img/warning-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions src/common/variables.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@
--green-80: #006504;
--green-90: #003706;

--yellow-50: #ffe900;
--yellow-60: #d7b600;
--yellow-70: #a47f00;
--yellow-80: #715100;
--yellow-90: #3e2800;

--red-50: #ff0039;
--red-60: #d70022;
--red-70: #a4000f;
--red-80: #5a0002;
--red-90: #3e0200;

--red-60: #d70022;

--grey-20: #ededf0;
Expand Down
13 changes: 13 additions & 0 deletions src/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
</div>
<div id="messageInfo" class="message-box info invisible fade-hide">
<span class="message-text">Some settings are managed by your administrator and cannot be changed.</span>
<a href="#">
<button class="message-action-button micro-button info invisible"></button>
</a>
<img class="icon-dismiss invisible" src="/common/img/close.svg" width="24" height="24" tabindex="0" data-i18n data-i18n-aria-label="__MSG_dismissIconDescription__"></span>
</div>
<div id="messageSuccess" class="message-box success invisible fade-hide">
Expand All @@ -33,6 +36,16 @@
</div>
<div id="messageError" class="message-box error invisible fade-hide">
<span class="message-text">An error happened.</span>
<a href="#">
<button class="message-action-button micro-button error invisible"></button>
</a>
<img class="icon-dismiss invisible" src="/common/img/close.svg" width="24" height="24" tabindex="0" data-i18n data-i18n-aria-label="__MSG_dismissIconDescription__"></span>
</div>
<div id="messageWarning" class="message-box warning invisible fade-hide">
<span class="message-text">There were some difficulties.</span>
<a href="#">
<button class="message-action-button micro-button warning invisible"></button>
</a>
<img class="icon-dismiss invisible" src="/common/img/close.svg" width="24" height="24" tabindex="0" data-i18n data-i18n-aria-label="__MSG_dismissIconDescription__"></span>
</div>
</div>
Expand Down
55 changes: 51 additions & 4 deletions src/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
/* globals AddonSettings */
/* globals MESSAGE_LEVEL, MessageHandler */
/* globals RandomTips */
/* globals Colors */

const OptionHandler = (function () {
const me = {};
Expand All @@ -19,7 +20,7 @@ const OptionHandler = (function () {
/**
* Applies option to element.
*
* @name OptionHandler.applyOptionToElementToElement
* @name OptionHandler.applyOptionToElement
* @function
* @private
* @param {string} option string ob object ID
Expand Down Expand Up @@ -151,6 +152,8 @@ const OptionHandler = (function () {
// run for each option, which we know to handle
applyOptionLive("popupIconColored", res.popupIconColored);
applyOptionLive("qrCodeSize", res.qrCodeSize);
applyOptionLive("qrColor", res.qrColor);
applyOptionLive("qrBackgroundColor", res.qrBackgroundColor);
});
}

Expand Down Expand Up @@ -200,6 +203,50 @@ const OptionHandler = (function () {

case "debugMode":
Logger.setDebugMode(optionValue);
break;

case "qrColor":
case "qrBackgroundColor": {
const elQrColor = document.getElementById("qrColor");
const elQrBackgroundColor = document.getElementById("qrBackgroundColor");

const qrCodeColor = Colors.hexToRgb(elQrColor.value);
const qrCodeBackgroundColor = Colors.hexToRgb(elQrBackgroundColor.value);

const colorContrast = Colors.contrastRatio(qrCodeColor, qrCodeBackgroundColor);

const actionButton = {
text: "messageAutoSelectColorButton",
action: () => {
const invertedColor = Colors.invertColor(qrCodeColor);
browser.storage.sync.set({
"qrBackgroundColor": invertedColor
}).catch((error) => {
Logger.logError("could not save option", option, ":", error);
MessageHandler.showError("couldNotSaveOption", true);
}).finally(() => {
applyOptionLive();
elQrBackgroundColor.value = invertedColor;
});
}
};

// breakpoints: https://github.com/rugk/offline-qr-code/pull/86#issuecomment-390426286
Copy link
Owner

Choose a reason for hiding this comment

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

Oh and once we are at it. You can link to my comment, yes, but please still add comments for the colors, i.e. the accessibility guidelines I mentioned.

Or even better remember the "no magic numbers" rules. So put these numbers into constants. As they are color stuff not (only) related to the options handler, the best would be to use these as constants in the Colors module, i.e. make them "public constants". (add to me in that module)

if (colorContrast <= 2) {
// show an error when nearly no QR code scanner can read it
MessageHandler.showError("lowContrastRatioError", false, actionButton);
} else if (colorContrast <= 3) {
// show a warning when approx. 50% of the QR code scanners can read it
MessageHandler.showWarning("lowContrastRatioWarning", false, actionButton);
} else if (colorContrast <= 4.5) {
// show only an info when the contrast is low but most of the scanners can still read it
Copy link
Owner

Choose a reason for hiding this comment

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

Okay, I rather thought about "WCAG 2.0 level AA" (so please add that), but this information is also good.

Still prefer them as constants though. 😄

MessageHandler.showInfo("lowContrastRatioInfo", false, actionButton);
} else {
MessageHandler.hideInfo();
Copy link
Owner

Choose a reason for hiding this comment

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

Hmm, this could get problematic, because we also show a permanent "managed setting" info message when this is used (see OptionHandler.showManagedInfo).

What we could do about this:

  • check managedInfoIsShown and do not show or hide the info message 8easy, but not really nice)
  • use a custom element for that managed info thing – this is an information, which stays there, anyway. (the way to go, IMHO)
  • implement Extend MessageHandler for showing multiple messages #9 (too hard and not needed for this, IMHO)

So I can do the second version after your PR is merged.

MessageHandler.hideWarning();
MessageHandler.hideError();
}
}
}

return null;
Expand Down Expand Up @@ -263,7 +310,7 @@ const OptionHandler = (function () {
browser.storage.sync.set({
[option]: optionValue
}).catch((error) => {
Logger.logError("could not save option", option, ": ", error);
Logger.logError("could not save option", option, ":", error);
MessageHandler.showError("couldNotSaveOption", true);
});
}
Expand Down Expand Up @@ -475,9 +522,9 @@ const OptionHandler = (function () {
}

/**
* Localizes static strings in the HTML file.
* Initializes the options.
*
* @name Localizer.init
* @name OptionHandler.init
* @function
* @returns {void}
*/
Expand Down