зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1505848
- switch from CSS based approach to calculating contrast to canvas one, that also handles gradients and images. r=jdescottes,pbro
MozReview-Commit-ID: JS39hAY571f Differential Revision: https://phabricator.services.mozilla.com/D11368 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
8d7c6d41ca
Коммит
aa155585ea
|
@ -294,8 +294,15 @@ const AccessibleActor = ActorClassWithSpec(accessibleSpec, {
|
|||
* Calculate the contrast ratio of the given accessible.
|
||||
*/
|
||||
_getContrastRatio() {
|
||||
return getContrastRatioFor(this._isValidTextLeaf(this.rawAccessible) ?
|
||||
this.rawAccessible.DOMNode.parentNode : this.rawAccessible.DOMNode);
|
||||
if (!this._isValidTextLeaf(this.rawAccessible)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getContrastRatioFor(this.rawAccessible.DOMNode.parentNode, {
|
||||
bounds: this.bounds,
|
||||
contexts: this.walker.contexts,
|
||||
win: this.walker.rootWin,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -19,10 +19,22 @@ loader.lazyRequireGetter(this, "isDefunct", "devtools/server/actors/utils/access
|
|||
loader.lazyRequireGetter(this, "isTypeRegistered", "devtools/server/actors/highlighters", true);
|
||||
loader.lazyRequireGetter(this, "isWindowIncluded", "devtools/shared/layout/utils", true);
|
||||
loader.lazyRequireGetter(this, "isXUL", "devtools/server/actors/highlighters/utils/markup", true);
|
||||
loader.lazyRequireGetter(this, "loadSheet", "devtools/shared/layout/utils", true);
|
||||
loader.lazyRequireGetter(this, "register", "devtools/server/actors/highlighters", true);
|
||||
loader.lazyRequireGetter(this, "removeSheet", "devtools/shared/layout/utils", true);
|
||||
|
||||
const kStateHover = 0x00000004; // NS_EVENT_STATE_HOVER
|
||||
|
||||
const HIGHLIGHTER_STYLES_SHEET = `data:text/css;charset=utf-8,
|
||||
* {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
:-moz-devtools-highlighted {
|
||||
color: transparent !important;
|
||||
text-shadow: none !important;
|
||||
}`;
|
||||
|
||||
const nsIAccessibleEvent = Ci.nsIAccessibleEvent;
|
||||
const nsIAccessibleStateChangeEvent = Ci.nsIAccessibleStateChangeEvent;
|
||||
const nsIAccessibleRole = Ci.nsIAccessibleRole;
|
||||
|
@ -336,7 +348,7 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, {
|
|||
},
|
||||
|
||||
async getAncestry(accessible) {
|
||||
if (accessible.indexInParent === -1) {
|
||||
if (!accessible || accessible.indexInParent === -1) {
|
||||
return [];
|
||||
}
|
||||
const doc = await this.getDocument();
|
||||
|
@ -478,14 +490,23 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, {
|
|||
* True if highlighter shows the accessible object.
|
||||
*/
|
||||
highlightAccessible(accessible, options = {}) {
|
||||
this.unhighlight();
|
||||
const { bounds } = accessible;
|
||||
if (!bounds) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Disable potential mouse driven transitions (This is important because accessibility
|
||||
// highlighter temporarily modifies text color related CSS properties. In case where
|
||||
// there are transitions that affect them, there might be unexpected side effects when
|
||||
// taking a snapshot for contrast measurement)
|
||||
loadSheet(this.rootWin, HIGHLIGHTER_STYLES_SHEET);
|
||||
const { audit, name, role } = accessible;
|
||||
return this.highlighter.show({ rawNode: accessible.rawAccessible.DOMNode },
|
||||
{ ...options, ...bounds, name, role, audit });
|
||||
const shown = this.highlighter.show({ rawNode: accessible.rawAccessible.DOMNode },
|
||||
{ ...options, ...bounds, name, role, audit });
|
||||
// Re-enable transitions.
|
||||
removeSheet(this.rootWin, HIGHLIGHTER_STYLES_SHEET);
|
||||
return shown;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -744,7 +765,8 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, {
|
|||
},
|
||||
|
||||
/**
|
||||
* If content is still alive, stop picker content listeners.
|
||||
* If content is still alive, stop picker content listeners, reset the hover state for
|
||||
* last target element.
|
||||
*/
|
||||
_unsetPickerEnvironment: function() {
|
||||
const target = this.targetActor.chromeEventHandler;
|
||||
|
@ -799,9 +821,7 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, {
|
|||
* Cacncel picker pick. Remvoe all content listeners and hide the highlighter.
|
||||
*/
|
||||
cancelPick: function() {
|
||||
if (this._highlighter) {
|
||||
this.highlighter.hide();
|
||||
}
|
||||
this.unhighlight();
|
||||
|
||||
if (this._isPicking) {
|
||||
this._unsetPickerEnvironment();
|
||||
|
|
|
@ -653,6 +653,10 @@
|
|||
max-width: 90%;
|
||||
}
|
||||
|
||||
:-moz-native-anonymous .accessible-infobar-audit .accessible-contrast-ratio:not(:empty):after {
|
||||
margin-inline-start: 2px;
|
||||
}
|
||||
|
||||
:-moz-native-anonymous .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AA:after,
|
||||
:-moz-native-anonymous .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AAA:after {
|
||||
color: #90E274;
|
||||
|
@ -660,15 +664,28 @@
|
|||
|
||||
:-moz-native-anonymous .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).fail:after {
|
||||
color: #E57180;
|
||||
content: " ⚠️";
|
||||
content: "⚠️";
|
||||
}
|
||||
|
||||
:-moz-native-anonymous .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AA:after {
|
||||
content: " AA\2713";
|
||||
content: "AA\2713";
|
||||
}
|
||||
|
||||
:-moz-native-anonymous .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AAA:after {
|
||||
content: " AAA\2713";
|
||||
content: "AAA\2713";
|
||||
}
|
||||
|
||||
:-moz-native-anonymous .accessible-infobar-audit .accessible-contrast-ratio-label,
|
||||
:-moz-native-anonymous .accessible-infobar-audit #accessible-contrast-ratio-max:not(:empty):before {
|
||||
margin-inline-end: 3px;
|
||||
}
|
||||
|
||||
:-moz-native-anonymous .accessible-infobar-audit #accessible-contrast-ratio-max {
|
||||
margin-inline-start: 3px;
|
||||
}
|
||||
|
||||
:-moz-native-anonymous .accessible-infobar-audit #accessible-contrast-ratio-max:not(:empty):before {
|
||||
content: "-";
|
||||
}
|
||||
|
||||
:-moz-native-anonymous .accessible-infobar-name:not(:empty),
|
||||
|
|
|
@ -471,15 +471,55 @@ class AuditReport {
|
|||
*/
|
||||
class ContrastRatio extends AuditReport {
|
||||
buildMarkup(root) {
|
||||
createNode(this.win, {
|
||||
nodeType: "span",
|
||||
parent: root,
|
||||
attributes: {
|
||||
"class": "contrast-ratio-label",
|
||||
"id": "contrast-ratio-label",
|
||||
},
|
||||
prefix: this.prefix,
|
||||
text: L10N.getStr("accessibility.contrast.ratio.label"),
|
||||
});
|
||||
|
||||
createNode(this.win, {
|
||||
nodeType: "span",
|
||||
parent: root,
|
||||
attributes: {
|
||||
"class": "contrast-ratio",
|
||||
"id": "contrast-ratio",
|
||||
"id": "contrast-ratio-error",
|
||||
},
|
||||
prefix: this.prefix,
|
||||
text: L10N.getStr("accessibility.contrast.ratio.error"),
|
||||
});
|
||||
|
||||
createNode(this.win, {
|
||||
nodeType: "span",
|
||||
parent: root,
|
||||
attributes: {
|
||||
"class": "contrast-ratio",
|
||||
"id": "contrast-ratio-min",
|
||||
},
|
||||
prefix: this.prefix,
|
||||
});
|
||||
|
||||
createNode(this.win, {
|
||||
nodeType: "span",
|
||||
parent: root,
|
||||
attributes: {
|
||||
"class": "contrast-ratio",
|
||||
"id": "contrast-ratio-max",
|
||||
},
|
||||
prefix: this.prefix,
|
||||
});
|
||||
}
|
||||
|
||||
_fillAndStyleContrastValue(el, value, isLargeText, stringName) {
|
||||
value = value.toFixed(2);
|
||||
const style = getContrastRatioScoreStyle(value, isLargeText);
|
||||
this.setTextContent(el, stringName ? L10N.getFormatStr(stringName, value) : value);
|
||||
el.classList.add(style);
|
||||
el.removeAttribute("hidden");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -491,16 +531,36 @@ class ContrastRatio extends AuditReport {
|
|||
* block should be visible.
|
||||
*/
|
||||
update({ contrastRatio }) {
|
||||
const el = this.getElement("contrast-ratio");
|
||||
["fail", "AA", "AAA"].forEach(style => el.classList.remove(style));
|
||||
const els = {};
|
||||
for (const key of ["label", "min", "max", "error"]) {
|
||||
const el = els[key] = this.getElement(`contrast-ratio-${key}`);
|
||||
if (["min", "max"].includes(key)) {
|
||||
["fail", "AA", "AAA"].forEach(className => el.classList.remove(className));
|
||||
this.setTextContent(el, "");
|
||||
}
|
||||
|
||||
el.setAttribute("hidden", true);
|
||||
}
|
||||
|
||||
if (!contrastRatio) {
|
||||
return false;
|
||||
}
|
||||
|
||||
el.classList.add(getContrastRatioScoreStyle(contrastRatio));
|
||||
this.setTextContent(el,
|
||||
L10N.getFormatStr("accessibility.contrast.ratio", contrastRatio.ratio.toFixed(2)));
|
||||
const { isLargeText, error } = contrastRatio;
|
||||
els.label.removeAttribute("hidden");
|
||||
if (error) {
|
||||
els.error.removeAttribute("hidden");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (contrastRatio.value) {
|
||||
this._fillAndStyleContrastValue(els.min, contrastRatio.value, isLargeText);
|
||||
return true;
|
||||
}
|
||||
|
||||
this._fillAndStyleContrastValue(els.min, contrastRatio.min, isLargeText);
|
||||
this._fillAndStyleContrastValue(els.max, contrastRatio.max, isLargeText);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -563,15 +623,15 @@ function getBounds(win, { x, y, w, h, zoom }) {
|
|||
/**
|
||||
* Get contrast ratio score styling to be applied on the element that renders the contrast
|
||||
* ratio.
|
||||
* @param {Number} options.ratio
|
||||
* @param {Number} ratio
|
||||
* Value of the contrast ratio for a given accessible object.
|
||||
* @param {Boolean} options.largeText
|
||||
* @param {Boolean} isLargeText
|
||||
* True if the accessible object contains large text.
|
||||
* @return {String}
|
||||
* CSS class that represents the appropriate contrast ratio score styling.
|
||||
*/
|
||||
function getContrastRatioScoreStyle({ ratio, largeText }) {
|
||||
const levels = largeText ? { AA: 3, AAA: 4.5 } : { AA: 4.5, AAA: 7 };
|
||||
function getContrastRatioScoreStyle(ratio, isLargeText) {
|
||||
const levels = isLargeText ? { AA: 3, AAA: 4.5 } : { AA: 4.5, AAA: 7 };
|
||||
|
||||
let style = "fail";
|
||||
if (ratio >= levels.AAA) {
|
||||
|
|
|
@ -163,12 +163,14 @@ exports.createSVGNode = createSVGNode;
|
|||
* attributes.
|
||||
* - parent: if provided, the newly created element will be appended to this
|
||||
* node.
|
||||
* - text: if provided, set the text content of the element.
|
||||
*/
|
||||
function createNode(win, options) {
|
||||
const type = options.nodeType || "div";
|
||||
const namespace = options.namespace || XHTML_NS;
|
||||
const doc = win.document;
|
||||
|
||||
const node = win.document.createElementNS(namespace, type);
|
||||
const node = doc.createElementNS(namespace, type);
|
||||
|
||||
for (const name in options.attributes || {}) {
|
||||
let value = options.attributes[name];
|
||||
|
@ -182,6 +184,10 @@ function createNode(win, options) {
|
|||
options.parent.appendChild(node);
|
||||
}
|
||||
|
||||
if (options.text) {
|
||||
node.appendChild(doc.createTextNode(options.text));
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
exports.createNode = createNode;
|
||||
|
|
|
@ -79,6 +79,10 @@ const ACCESSIBLE_BOUNDS_SHEET = "data:text/css;charset=utf-8," + encodeURICompon
|
|||
max-width: 90%;
|
||||
}
|
||||
|
||||
.accessible-infobar-audit .accessible-contrast-ratio:not(:empty):after {
|
||||
margin-inline-start: 2px;
|
||||
}
|
||||
|
||||
.accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AA:after,
|
||||
.accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AAA:after {
|
||||
color: #90E274;
|
||||
|
@ -86,15 +90,28 @@ const ACCESSIBLE_BOUNDS_SHEET = "data:text/css;charset=utf-8," + encodeURICompon
|
|||
|
||||
.accessible-infobar-audit .accessible-contrast-ratio:not(:empty).fail:after {
|
||||
color: #E57180;
|
||||
content: " ⚠️";
|
||||
content: "⚠️";
|
||||
}
|
||||
|
||||
.accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AA:after {
|
||||
content: " AA\u2713";
|
||||
content: "AA\u2713";
|
||||
}
|
||||
|
||||
.accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AAA:after {
|
||||
content: " AAA\u2713";
|
||||
content: "AAA\u2713";
|
||||
}
|
||||
|
||||
.accessible-infobar-audit .accessible-contrast-ratio-label,
|
||||
.accessible-infobar-audit #accessible-contrast-ratio-max:not(:empty):before {
|
||||
margin-inline-end: 3px;
|
||||
}
|
||||
|
||||
.accessible-infobar-audit #accessible-contrast-ratio-max {
|
||||
margin-inline-start: 3px;
|
||||
}
|
||||
|
||||
.accessible-infobar-audit #accessible-contrast-ratio-max:not(:empty):before {
|
||||
content: "-";
|
||||
}
|
||||
|
||||
.accessible-infobar-name:not(:empty),
|
||||
|
|
|
@ -7,60 +7,211 @@
|
|||
loader.lazyRequireGetter(this, "Ci", "chrome", true);
|
||||
loader.lazyRequireGetter(this, "colorUtils", "devtools/shared/css/color", true);
|
||||
loader.lazyRequireGetter(this, "CssLogic", "devtools/server/actors/inspector/css-logic", true);
|
||||
loader.lazyRequireGetter(this, "InspectorActorUtils", "devtools/server/actors/inspector/utils");
|
||||
loader.lazyRequireGetter(this, "getBounds", "devtools/server/actors/highlighters/utils/accessibility", true);
|
||||
loader.lazyRequireGetter(this, "getCurrentZoom", "devtools/shared/layout/utils", true);
|
||||
loader.lazyRequireGetter(this, "Services");
|
||||
loader.lazyRequireGetter(this, "addPseudoClassLock", "devtools/server/actors/highlighters/utils/markup", true);
|
||||
loader.lazyRequireGetter(this, "removePseudoClassLock", "devtools/server/actors/highlighters/utils/markup", true);
|
||||
|
||||
const HIGHLIGHTED_PSEUDO_CLASS = ":-moz-devtools-highlighted";
|
||||
|
||||
/**
|
||||
* Calculates the contrast ratio of the referenced DOM node.
|
||||
*
|
||||
* Get text style properties for a given node, if possible.
|
||||
* @param {DOMNode} node
|
||||
* The node for which we want to calculate the contrast ratio.
|
||||
*
|
||||
* @return {Number|null} Contrast ratio value.
|
||||
*/
|
||||
function getContrastRatioFor(node) {
|
||||
const backgroundColor = InspectorActorUtils.getClosestBackgroundColor(node);
|
||||
const backgroundImage = InspectorActorUtils.getClosestBackgroundImage(node);
|
||||
* DOM node for which text styling information is to be calculated.
|
||||
* @return {Object}
|
||||
* Color and text size information for a given DOM node.
|
||||
*/
|
||||
function getTextProperties(node) {
|
||||
const computedStyles = CssLogic.getComputedStyle(node);
|
||||
if (!computedStyles) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { color, "font-size": fontSize, "font-weight": fontWeight } = computedStyles;
|
||||
const isBoldText = parseInt(fontWeight, 10) >= 600;
|
||||
const backgroundRgbaColor = new colorUtils.CssColor(backgroundColor, true);
|
||||
const textRgbaColor = new colorUtils.CssColor(color, true);
|
||||
const opacity = parseFloat(computedStyles.opacity);
|
||||
|
||||
let { r, g, b, a } = colorUtils.colorToRGBA(color, true);
|
||||
a = opacity * a;
|
||||
const textRgbaColor = new colorUtils.CssColor(`rgba(${r}, ${g}, ${b}, ${a})`, true);
|
||||
// TODO: For cases where text color is transparent, it likely comes from the color of
|
||||
// the background that is underneath it (commonly from background-clip: text property).
|
||||
// With some additional investigation it might be possible to calculate the color
|
||||
// contrast where the color of the background is used as text color and the color of
|
||||
// the ancestor's background is used as its background.
|
||||
// the background that is underneath it (commonly from background-clip: text
|
||||
// property). With some additional investigation it might be possible to calculate the
|
||||
// color contrast where the color of the background is used as text color and the
|
||||
// color of the ancestor's background is used as its background.
|
||||
if (textRgbaColor.isTransparent()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: these cases include handling gradient backgrounds and the actual image
|
||||
// backgrounds. Each one needs to be handled individually.
|
||||
if (backgroundImage !== "none") {
|
||||
const isBoldText = parseInt(fontWeight, 10) >= 600;
|
||||
const isLargeText = Math.ceil(parseFloat(fontSize) * 72) / 96 >= (isBoldText ? 14 : 18);
|
||||
|
||||
return {
|
||||
// Blend text color taking its alpha into account asuming white background.
|
||||
color: colorUtils.blendColors([r, g, b, a]),
|
||||
isLargeText,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get canvas rendering context for the current target window bound by the bounds of the
|
||||
* accessible objects.
|
||||
* @param {Object} win
|
||||
* Current target window.
|
||||
* @param {Object} bounds
|
||||
* Bounds for the accessible object.
|
||||
* @param {null|DOMNode} node
|
||||
* If not null, a node that corresponds to the accessible object to be used to
|
||||
* make its text color transparent.
|
||||
* @return {CanvasRenderingContext2D}
|
||||
* Canvas rendering context for the current window.
|
||||
*/
|
||||
function getImageCtx(win, bounds, node) {
|
||||
const doc = win.document;
|
||||
const canvas = doc.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
|
||||
const scale = getCurrentZoom(win);
|
||||
|
||||
const { left, top, width, height } = bounds;
|
||||
canvas.width = width / scale;
|
||||
canvas.height = height / scale;
|
||||
const ctx = canvas.getContext("2d", { alpha: false });
|
||||
|
||||
// If node is passed, make its color related text properties invisible.
|
||||
if (node) {
|
||||
addPseudoClassLock(node, HIGHLIGHTED_PSEUDO_CLASS);
|
||||
}
|
||||
|
||||
ctx.drawWindow(win, left / scale, top / scale, width / scale, height / scale, "#fff",
|
||||
ctx.DRAWWINDOW_USE_WIDGET_LAYERS);
|
||||
|
||||
// Restore all inline styling.
|
||||
if (node) {
|
||||
removePseudoClassLock(node, HIGHLIGHTED_PSEUDO_CLASS);
|
||||
}
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get RGBA or a range of RGBAs for the background pixels under the text. If luminance is
|
||||
* uniform, only return one value of RGBA, otherwise return values that correspond to the
|
||||
* min and max luminances.
|
||||
* @param {ImageData} dataText
|
||||
* pixel data for the accessible object with text visible.
|
||||
* @param {ImageData} dataBackground
|
||||
* pixel data for the accessible object with transparent text.
|
||||
* @return {Object}
|
||||
* RGBA or a range of RGBAs with min and max values.
|
||||
*/
|
||||
function getBgRGBA(dataText, dataBackground) {
|
||||
let min = [0, 0, 0, 1];
|
||||
let max = [255, 255, 255, 1];
|
||||
let minLuminance = 1;
|
||||
let maxLuminance = 0;
|
||||
const luminances = {};
|
||||
|
||||
let foundDistinctColor = false;
|
||||
for (let i = 0; i < dataText.length; i = i + 4) {
|
||||
const tR = dataText[i];
|
||||
const bgR = dataBackground[i];
|
||||
const tG = dataText[i + 1];
|
||||
const bgG = dataBackground[i + 1];
|
||||
const tB = dataText[i + 2];
|
||||
const bgB = dataBackground[i + 2];
|
||||
|
||||
// Ignore pixels that are the same where pixels that are different between the two
|
||||
// images are assumed to belong to the text within the node.
|
||||
if (tR === bgR && tG === bgG && tB === bgB) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foundDistinctColor = true;
|
||||
|
||||
const bgColor = `rgb(${bgR}, ${bgG}, ${bgB})`;
|
||||
let luminance = luminances[bgColor];
|
||||
|
||||
if (!luminance) {
|
||||
// Calculate luminance for the RGB value and store it to only measure once.
|
||||
luminance = colorUtils.calculateLuminance([bgR, bgG, bgB]);
|
||||
luminances[bgColor] = luminance;
|
||||
}
|
||||
|
||||
if (minLuminance >= luminance) {
|
||||
minLuminance = luminance;
|
||||
min = [bgR, bgG, bgB, 1];
|
||||
}
|
||||
|
||||
if (maxLuminance <= luminance) {
|
||||
maxLuminance = luminance;
|
||||
max = [bgR, bgG, bgB, 1];
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundDistinctColor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let { r: bgR, g: bgG, b: bgB, a: bgA} = backgroundRgbaColor.getRGBATuple();
|
||||
let { r: textR, g: textG, b: textB, a: textA } = textRgbaColor.getRGBATuple();
|
||||
return minLuminance === maxLuminance ? { value: max } : { min, max };
|
||||
}
|
||||
|
||||
// If the element has opacity in addition to text and background alpha values, take it
|
||||
// into account.
|
||||
const opacity = parseFloat(computedStyles.opacity);
|
||||
if (opacity < 1) {
|
||||
bgA = opacity * bgA;
|
||||
textA = opacity * textA;
|
||||
/**
|
||||
* Calculates the contrast ratio of the referenced DOM node.
|
||||
*
|
||||
* @param {DOMNode} node
|
||||
* The node for which we want to calculate the contrast ratio.
|
||||
* @param {Object} options
|
||||
* - bounds {Object}
|
||||
* Bounds for the accessible object.
|
||||
* - contexts {null|Object}
|
||||
* Canvas rendering contexts that have a window drawn as is and also
|
||||
* with the all text made transparent for contrast comparison.
|
||||
* - win {Object}
|
||||
* Target window.
|
||||
*
|
||||
* @return {Object}
|
||||
* An object that may contain one or more of the following fields: error,
|
||||
* isLargeText, value, min, max values for contrast.
|
||||
*/
|
||||
function getContrastRatioFor(node, options = {}) {
|
||||
const props = getTextProperties(node);
|
||||
if (!props) {
|
||||
return {
|
||||
error: true,
|
||||
};
|
||||
}
|
||||
|
||||
const bounds = getBounds(options.win, options.bounds);
|
||||
const textContext = getImageCtx(options.win, bounds);
|
||||
const backgroundContext = getImageCtx(options.win, bounds, node);
|
||||
|
||||
const { data: dataText } = textContext.getImageData(0, 0, bounds.width, bounds.height);
|
||||
const { data: dataBackground } = backgroundContext.getImageData(
|
||||
0, 0, bounds.width, bounds.height);
|
||||
|
||||
const rgba = getBgRGBA(dataText, dataBackground);
|
||||
if (!rgba) {
|
||||
return {
|
||||
error: true,
|
||||
};
|
||||
}
|
||||
|
||||
const { color, isLargeText } = props;
|
||||
if (rgba.value) {
|
||||
return {
|
||||
value: colorUtils.calculateContrastRatio(rgba.value, color),
|
||||
isLargeText,
|
||||
};
|
||||
}
|
||||
|
||||
// calculateContrastRatio modifies the array, since we need to use color array twice,
|
||||
// pass its copy to the method.
|
||||
const min = colorUtils.calculateContrastRatio(rgba.min, Array.from(color));
|
||||
const max = colorUtils.calculateContrastRatio(rgba.max, Array.from(color));
|
||||
|
||||
return {
|
||||
ratio: colorUtils.calculateContrastRatio([ bgR, bgG, bgB, bgA ],
|
||||
[ textR, textG, textB, textA ]),
|
||||
largeText: Math.ceil(parseFloat(fontSize) * 72) / 96 >= (isBoldText ? 14 : 18),
|
||||
min: min < max ? min : max,
|
||||
max: min < max ? max : min,
|
||||
isLargeText,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -6,3 +6,11 @@
|
|||
# ratio description, used by the accessibility highlighter to display the value. %S in the
|
||||
# content will be replaced by the contrast ratio numerical value.
|
||||
accessibility.contrast.ratio=Contrast: %S
|
||||
|
||||
# LOCALIZATION NOTE (accessibility.contrast.ratio.error): A title text for the color
|
||||
# contrast ratio, used when the tool is unable to calculate the contrast ratio value.
|
||||
accessibility.contrast.ratio.error=Unable to calculate
|
||||
|
||||
# LOCALIZATION NOTE (accessibility.contrast.ratio.label): A title text for the color
|
||||
# contrast ratio description, used together with the actual values.
|
||||
accessibility.contrast.ratio.label=Contrast:
|
||||
|
|
Загрузка…
Ссылка в новой задаче