🐛 Fixed link contrast in editor with very light/dark accent colors (#1870)

refs https://github.com/TryGhost/Team/issues/551
refs https://github.com/TryGhost/Ghost/issues/12767#issuecomment-800177254

- calculate contrast color of accent color against light/dark mode background color
- lighten (dark mode) or darken (light mode) the accent color used in the editor to ensure it has enough contrast to be legible
This commit is contained in:
Kevin Ansfield 2021-03-18 16:46:38 +00:00 committed by GitHub
parent 6d324e31f1
commit 7286ae9fcf
3 changed files with 190 additions and 2 deletions

View File

@ -1,6 +1,13 @@
/* eslint-disable ghost/ember/alias-model-in-controller */
import Controller from '@ember/controller';
import {computed} from '@ember/object';
import {
contrast,
darkenToContrastThreshold,
hexToRgb,
lightenToContrastThreshold,
rgbToHex
} from 'ghost-admin/utils/color';
import {inject as service} from '@ember/service';
export default Controller.extend({
@ -8,6 +15,7 @@ export default Controller.extend({
customViews: service(),
config: service(),
dropdown: service(),
feature: service(),
router: service(),
session: service(),
settings: service(),
@ -30,5 +38,33 @@ export default Controller.extend({
return (router.currentRouteName !== 'error404' || session.isAuthenticated)
&& !router.currentRouteName.match(/(signin|signup|setup|reset)/);
}),
adjustedAccentColor: computed('settings.accentColor', 'feature.nightShift', function () {
const accentColor = this.settings.get('accentColor');
const nightShift = this.feature.get('nightShift');
// hardcoded background colors because
// grabbing color from .gh-main with getComputedStyle always returns #ffffff
const backgroundColor = nightShift ? '#151719' : '#ffffff';
const accentRgb = hexToRgb(accentColor);
const backgroundRgb = hexToRgb(backgroundColor);
// WCAG contrast. 1 = lowest contrast, 21 = highest contrast
const accentContrast = contrast(backgroundRgb, accentRgb);
if (accentContrast > 2) {
return accentColor;
}
let adjustedAccentRgb = accentRgb;
if (nightShift) {
adjustedAccentRgb = lightenToContrastThreshold(accentRgb, backgroundRgb, 2);
} else {
adjustedAccentRgb = darkenToContrastThreshold(accentRgb, backgroundRgb, 2);
}
return rgbToHex(adjustedAccentRgb);
})
});

View File

@ -37,10 +37,10 @@
{{#if this.settings.accentColor}}
<style>
.koenig-editor__editor a:not([class]) {
color: {{this.settings.accentColor}};
color: {{this.adjustedAccentColor}};
}
.koenig-editor__editor blockquote {
border-left: 0.25rem solid {{this.settings.accentColor}};
border-left: 0.25rem solid {{this.adjustedAccentColor}};
}
</style>
{{/if}}

View File

@ -0,0 +1,152 @@
export function hexToRgb(hex) {
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, function (m, r, g, b) {
return r + r + g + g + b + b;
});
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
export function rgbToHex({r, g, b}) {
function hex(x) {
return ('0' + parseInt(x).toString(16)).slice(-2);
}
return '#' + hex(r) + hex(g) + hex(b);
}
// returns {h,s,l} with range [0,1] for maximum precision in conversion
export function rgbToHsl({r, g, b}) {
r = r / 255;
g = g / 255;
b = b / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h = 0;
let s = 0;
const l = (max + min) / 2;
if (max === min) {
h = s = 0; // achromatic
} else {
const delta = max - min;
s = l > 0.5
? delta / (2 - max - min)
: delta / (max + min);
switch (max) {
case r: h = (g - b) / delta + (g < b ? 6 : 0); break;
case g: h = (b - r) / delta + 2; break;
case b: h = (r - g) / delta + 4; break;
}
h = h / 6;
}
return {h, s, l};
}
// expects {h,s,l} in range [0,1], returns {r,g,b} in the range [0,255]
export function hslToRgb({h, s, l}) {
let r, g, b;
function hue2rgb(p, q, t) {
if (t < 0) {
t += 1;
}
if (t > 1) {
t -= 1;
}
if (t < 1 / 6) {
return p + (q - p) * 6 * t;
}
if (t < 1 / 2) {
return q;
}
if (t < 2 / 3) {
return p + (q - p) * (2 / 3 - t) * 6;
}
return p;
}
if (s === 0) {
r = g = b = l;
} else {
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
r = r * 255;
g = g * 255;
b = b * 255;
return {r, g, b};
}
// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
export function luminance({r, g, b}) {
const a = [r, g, b].map(function (v) {
v = v / 255;
return v <= 0.03928
? v / 12.92
: Math.pow((v + 0.055) / 1.055, 2.4);
});
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
}
// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
export function contrast(rgb1, rgb2) {
const lum1 = luminance(rgb1);
const lum2 = luminance(rgb2);
const brightest = Math.max(lum1, lum2);
const darkest = Math.min(lum1, lum2);
return (brightest + 0.05) / (darkest + 0.05);
}
export function lightenToContrastThreshold(foregroundRgb, backgroundRgb, contrastThreshold) {
let newRgb = foregroundRgb;
while (contrast(newRgb, backgroundRgb) < contrastThreshold) {
let {h,s,l} = rgbToHsl(newRgb);
if (l >= 1) {
break;
}
l = Math.min(l + 0.05, 1);
newRgb = hslToRgb({h,s,l});
}
return newRgb;
}
export function darkenToContrastThreshold(foregroundRgb, backgroundRgb, contrastThreshold) {
let newRgb = foregroundRgb;
while (contrast(newRgb, backgroundRgb) < contrastThreshold) {
let {h,s,l} = rgbToHsl(newRgb);
if (l <= 0) {
break;
}
l = Math.max(l - 0.05, 0);
newRgb = hslToRgb({h,s,l});
}
return newRgb;
}