зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1473037 - Display contrast ratio for text nodes inside the accessibility infobar. r=pbro
Co-authored-by: Micah Tigley <mtigley@mozilla.com> MozReview-Commit-ID: 1KbcRG0bZA3 Differential Revision: https://phabricator.services.mozilla.com/D4954 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
50b54a512b
Коммит
d84869d27a
|
@ -21,6 +21,7 @@ const { isXUL } = require("devtools/server/actors/highlighters/utils/markup");
|
|||
const { isWindowIncluded } = require("devtools/shared/layout/utils");
|
||||
const { CustomHighlighterActor, register } =
|
||||
require("devtools/server/actors/highlighters");
|
||||
const { getContrastRatioFor } = require("devtools/server/actors/utils/accessibility");
|
||||
const PREF_ACCESSIBILITY_FORCE_DISABLED = "accessibility.force_disabled";
|
||||
|
||||
const nsIAccessibleEvent = Ci.nsIAccessibleEvent;
|
||||
|
@ -368,6 +369,36 @@ const AccessibleActor = ActorClassWithSpec(accessibleSpec, {
|
|||
actions: this.actions,
|
||||
attributes: this.attributes
|
||||
};
|
||||
},
|
||||
|
||||
_isValidTextLeaf(rawAccessible) {
|
||||
return !isDefunct(rawAccessible) &&
|
||||
rawAccessible.role === nsIAccessibleRole.ROLE_TEXT_LEAF &&
|
||||
rawAccessible.name && rawAccessible.name.trim().length > 0;
|
||||
},
|
||||
|
||||
get _nonEmptyTextLeafs() {
|
||||
return this.children().filter(child => this._isValidTextLeaf(child.rawAccessible));
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculate the contrast ratio of the given accessible.
|
||||
*/
|
||||
_getContrastRatio() {
|
||||
return getContrastRatioFor(this._isValidTextLeaf(this.rawAccessible) ?
|
||||
this.rawAccessible.DOMNode.parentNode : this.rawAccessible.DOMNode);
|
||||
},
|
||||
|
||||
/**
|
||||
* Audit the state of the accessible object.
|
||||
*
|
||||
* @return {Object|null}
|
||||
* Audit results for the accessible object.
|
||||
*/
|
||||
get audit() {
|
||||
return this.isDefunct ? null : {
|
||||
contrastRatio: this._getContrastRatio()
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -710,13 +741,14 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, {
|
|||
* True if highlighter shows the accessible object.
|
||||
*/
|
||||
highlightAccessible(accessible, options = {}) {
|
||||
const { bounds, name, role } = accessible;
|
||||
const { bounds } = accessible;
|
||||
if (!bounds) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { audit, name, role } = accessible;
|
||||
return this.highlighter.show({ rawNode: accessible.rawAccessible.DOMNode },
|
||||
{ ...options, ...bounds, name, role });
|
||||
{ ...options, ...bounds, name, role, audit });
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -803,15 +835,7 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, {
|
|||
}
|
||||
|
||||
if (this._currentAccessible !== accessible) {
|
||||
const { bounds, role, name } = accessible;
|
||||
if (bounds) {
|
||||
this.highlighter.show({ rawNode: event.originalTarget || event.target }, {
|
||||
...bounds,
|
||||
role,
|
||||
name
|
||||
});
|
||||
}
|
||||
|
||||
this.highlightAccessible(accessible);
|
||||
events.emit(this, "picker-accessible-hovered", accessible);
|
||||
this._currentAccessible = accessible;
|
||||
}
|
||||
|
|
|
@ -647,13 +647,32 @@
|
|||
fill: #6a5acd;
|
||||
}
|
||||
|
||||
:-moz-native-anonymous .accessible-infobar-name {
|
||||
:-moz-native-anonymous .accessible-infobar-name,
|
||||
:-moz-native-anonymous .accessible-infobar-audit {
|
||||
color: var(--highlighter-infobar-color);
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
:-moz-native-anonymous .accessible-infobar-name:not(:empty) {
|
||||
color: var(--highlighter-infobar-color);
|
||||
:-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;
|
||||
}
|
||||
|
||||
:-moz-native-anonymous .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).fail:after {
|
||||
color: #E57180;
|
||||
content: " ⚠️";
|
||||
}
|
||||
|
||||
:-moz-native-anonymous .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AA:after {
|
||||
content: " AA\2713";
|
||||
}
|
||||
|
||||
:-moz-native-anonymous .accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AAA:after {
|
||||
content: " AAA\2713";
|
||||
}
|
||||
|
||||
:-moz-native-anonymous .accessible-infobar-name:not(:empty),
|
||||
:-moz-native-anonymous .accessible-infobar-audit:not(:empty) {
|
||||
border-inline-start: 1px solid #5a6169;
|
||||
margin-inline-start: 6px;
|
||||
padding-inline-start: 6px;
|
||||
|
|
|
@ -4,10 +4,15 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
|
||||
const { getCurrentZoom, getViewportDimensions } = require("devtools/shared/layout/utils");
|
||||
const { moveInfobar, createNode } = require("./markup");
|
||||
const { truncateString } = require("devtools/shared/inspector/utils");
|
||||
|
||||
const STRINGS_URI = "devtools/shared/locales/accessibility.properties";
|
||||
loader.lazyRequireGetter(this, "LocalizationHelper", "devtools/shared/l10n", true);
|
||||
DevToolsUtils.defineLazyGetter(this, "L10N", () => new LocalizationHelper(STRINGS_URI));
|
||||
|
||||
// Max string length for truncating accessible name values.
|
||||
const MAX_STRING_LENGTH = 50;
|
||||
|
||||
|
@ -19,6 +24,7 @@ const MAX_STRING_LENGTH = 50;
|
|||
class Infobar {
|
||||
constructor(highlighter) {
|
||||
this.highlighter = highlighter;
|
||||
this.audit = new Audit(this);
|
||||
}
|
||||
|
||||
get document() {
|
||||
|
@ -110,6 +116,8 @@ class Infobar {
|
|||
},
|
||||
prefix: this.prefix,
|
||||
});
|
||||
|
||||
this.audit.buildMarkup(infobarText);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -117,6 +125,8 @@ class Infobar {
|
|||
*/
|
||||
destroy() {
|
||||
this.highlighter = null;
|
||||
this.audit.destroy();
|
||||
this.audit = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -168,10 +178,11 @@ class Infobar {
|
|||
* Update content of the infobar.
|
||||
*/
|
||||
update(container) {
|
||||
const { name, role } = this.options;
|
||||
const { audit, name, role } = this.options;
|
||||
|
||||
this.updateRole(role, this.getElement("infobar-role"));
|
||||
this.updateName(name, this.getElement("infobar-name"));
|
||||
this.audit.update(audit);
|
||||
|
||||
// Position the infobar.
|
||||
this._moveInfobar(container);
|
||||
|
@ -355,6 +366,145 @@ class XULWindowInfobar extends Infobar {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Audit component used within the accessible highlighter infobar. This component is
|
||||
* responsible for rendering and updating its containing AuditReport components that
|
||||
* display various audit information such as contrast ratio score.
|
||||
*/
|
||||
class Audit {
|
||||
constructor(infobar) {
|
||||
this.infobar = infobar;
|
||||
|
||||
// A list of audit reports to be shown on the fly when highlighting an accessible
|
||||
// object.
|
||||
this.reports = [
|
||||
new ContrastRatio(this)
|
||||
];
|
||||
}
|
||||
|
||||
get prefix() {
|
||||
return this.infobar.prefix;
|
||||
}
|
||||
|
||||
get win() {
|
||||
return this.infobar.win;
|
||||
}
|
||||
|
||||
buildMarkup(root) {
|
||||
const audit = createNode(this.win, {
|
||||
nodeType: "span",
|
||||
parent: root,
|
||||
attributes: {
|
||||
"class": "infobar-audit",
|
||||
"id": "infobar-audit",
|
||||
},
|
||||
prefix: this.prefix,
|
||||
});
|
||||
|
||||
this.reports.forEach(report => report.buildMarkup(audit));
|
||||
}
|
||||
|
||||
update(audit = {}) {
|
||||
const el = this.getElement("infobar-audit");
|
||||
el.setAttribute("hidden", true);
|
||||
|
||||
let updated = false;
|
||||
this.reports.forEach(report => {
|
||||
if (report.update(audit)) {
|
||||
updated = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (updated) {
|
||||
el.removeAttribute("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
getElement(id) {
|
||||
return this.infobar.getElement(id);
|
||||
}
|
||||
|
||||
setTextContent(el, text) {
|
||||
return this.infobar.setTextContent(el, text);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.infobar = null;
|
||||
this.reports.forEach(report => report.destroy());
|
||||
this.reports = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A common interface between audit report components used to render accessibility audit
|
||||
* information for the currently highlighted accessible object.
|
||||
*/
|
||||
class AuditReport {
|
||||
constructor(audit) {
|
||||
this.audit = audit;
|
||||
}
|
||||
|
||||
get prefix() {
|
||||
return this.audit.prefix;
|
||||
}
|
||||
|
||||
get win() {
|
||||
return this.audit.win;
|
||||
}
|
||||
|
||||
getElement(id) {
|
||||
return this.audit.getElement(id);
|
||||
}
|
||||
|
||||
setTextContent(el, text) {
|
||||
return this.audit.setTextContent(el, text);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.audit = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Contrast ratio audit report that is used to display contrast ratio score as part of the
|
||||
* inforbar,
|
||||
*/
|
||||
class ContrastRatio extends AuditReport {
|
||||
buildMarkup(root) {
|
||||
createNode(this.win, {
|
||||
nodeType: "span",
|
||||
parent: root,
|
||||
attributes: {
|
||||
"class": "contrast-ratio",
|
||||
"id": "contrast-ratio",
|
||||
},
|
||||
prefix: this.prefix,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update contrast ratio score infobar markup.
|
||||
* @param {Number}
|
||||
* Contrast ratio for an accessible object being highlighted.
|
||||
* @return {Boolean}
|
||||
* True if the contrast ratio markup was updated correctly and infobar audit
|
||||
* block should be visible.
|
||||
*/
|
||||
update({ contrastRatio }) {
|
||||
const el = this.getElement("contrast-ratio");
|
||||
["fail", "AA", "AAA"].forEach(style => el.classList.remove(style));
|
||||
|
||||
if (!contrastRatio) {
|
||||
return false;
|
||||
}
|
||||
|
||||
el.classList.add(getContrastRatioScoreStyle(contrastRatio));
|
||||
this.setTextContent(el,
|
||||
L10N.getFormatStr("accessibility.contrast.ratio", contrastRatio.ratio.toFixed(2)));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function that calculate accessible object bounds and positioning to
|
||||
* be used for highlighting.
|
||||
|
@ -410,6 +560,29 @@ function getBounds(win, { x, y, w, h, zoom }) {
|
|||
return { left, right, top, bottom, width, height };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contrast ratio score styling to be applied on the element that renders the contrast
|
||||
* ratio.
|
||||
* @param {Number} options.ratio
|
||||
* Value of the contrast ratio for a given accessible object.
|
||||
* @param {Boolean} options.largeText
|
||||
* 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 };
|
||||
|
||||
let style = "fail";
|
||||
if (ratio >= levels.AAA) {
|
||||
style = "AAA";
|
||||
} else if (ratio >= levels.AA) {
|
||||
style = "AA";
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
exports.MAX_STRING_LENGTH = MAX_STRING_LENGTH;
|
||||
exports.getBounds = getBounds;
|
||||
exports.Infobar = Infobar;
|
||||
|
|
|
@ -73,13 +73,32 @@ const ACCESSIBLE_BOUNDS_SHEET = "data:text/css;charset=utf-8," + encodeURICompon
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
.accessible-infobar-name {
|
||||
color: rgb(221, 0, 169);
|
||||
.accessible-infobar-name,
|
||||
.accessible-infobar-audit {
|
||||
color: hsl(210, 30%, 85%);
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.accessible-infobar-name:not(:empty) {
|
||||
color: hsl(210, 30%, 85%);
|
||||
.accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AA:after,
|
||||
.accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AAA:after {
|
||||
color: #90E274;
|
||||
}
|
||||
|
||||
.accessible-infobar-audit .accessible-contrast-ratio:not(:empty).fail:after {
|
||||
color: #E57180;
|
||||
content: " ⚠️";
|
||||
}
|
||||
|
||||
.accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AA:after {
|
||||
content: " AA\u2713";
|
||||
}
|
||||
|
||||
.accessible-infobar-audit .accessible-contrast-ratio:not(:empty).AAA:after {
|
||||
content: " AAA\u2713";
|
||||
}
|
||||
|
||||
.accessible-infobar-name:not(:empty),
|
||||
.accessible-infobar-audit:not(:empty) {
|
||||
border-inline-start: 1px solid #5a6169;
|
||||
margin-inline-start: 6px;
|
||||
padding-inline-start: 6px;
|
||||
|
|
|
@ -10,8 +10,6 @@ const InspectorUtils = require("InspectorUtils");
|
|||
const protocol = require("devtools/shared/protocol");
|
||||
const { nodeSpec, nodeListSpec } = require("devtools/shared/specs/node");
|
||||
|
||||
loader.lazyRequireGetter(this, "colorUtils", "devtools/shared/css/color", true);
|
||||
|
||||
loader.lazyRequireGetter(this, "getCssPath", "devtools/shared/inspector/css-logic", true);
|
||||
loader.lazyRequireGetter(this, "getXPath", "devtools/shared/inspector/css-logic", true);
|
||||
loader.lazyRequireGetter(this, "findCssSelector", "devtools/shared/inspector/css-logic", true);
|
||||
|
@ -686,26 +684,15 @@ const NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
|
|||
},
|
||||
|
||||
/**
|
||||
* Finds the computed background color of the closest parent with
|
||||
* a set background color.
|
||||
* Returns a string with the background color of the form
|
||||
* rgba(r, g, b, a). Defaults to rgba(255, 255, 255, 1) if no
|
||||
* background color is found.
|
||||
* Finds the computed background color of the closest parent with a set background
|
||||
* color.
|
||||
*
|
||||
* @return {String}
|
||||
* String with the background color of the form rgba(r, g, b, a). Defaults to
|
||||
* rgba(255, 255, 255, 1) if no background color is found.
|
||||
*/
|
||||
getClosestBackgroundColor: function() {
|
||||
let current = this.rawNode;
|
||||
while (current) {
|
||||
const computedStyle = CssLogic.getComputedStyle(current);
|
||||
const currentStyle = computedStyle.getPropertyValue("background-color");
|
||||
if (colorUtils.isValidCSSColor(currentStyle)) {
|
||||
const currentCssColor = new colorUtils.CssColor(currentStyle);
|
||||
if (!currentCssColor.isTransparent()) {
|
||||
return currentCssColor.rgba;
|
||||
}
|
||||
}
|
||||
current = current.parentNode;
|
||||
}
|
||||
return "rgba(255, 255, 255, 1)";
|
||||
return InspectorActorUtils.getClosestBackgroundColor(this.rawNode);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
const {Cu} = require("chrome");
|
||||
|
||||
loader.lazyRequireGetter(this, "colorUtils", "devtools/shared/css/color", true);
|
||||
loader.lazyRequireGetter(this, "AsyncUtils", "devtools/shared/async-utils");
|
||||
loader.lazyRequireGetter(this, "flags", "devtools/shared/flags");
|
||||
loader.lazyRequireGetter(this, "DevToolsUtils", "devtools/shared/DevToolsUtils");
|
||||
|
@ -14,6 +15,8 @@ loader.lazyRequireGetter(this, "nodeFilterConstants", "devtools/shared/dom-node-
|
|||
loader.lazyRequireGetter(this, "isNativeAnonymous", "devtools/shared/layout/utils", true);
|
||||
loader.lazyRequireGetter(this, "isXBLAnonymous", "devtools/shared/layout/utils", true);
|
||||
|
||||
loader.lazyRequireGetter(this, "CssLogic", "devtools/server/actors/inspector/css-logic", true);
|
||||
|
||||
const XHTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
const IMAGE_FETCHING_TIMEOUT = 500;
|
||||
|
@ -245,8 +248,67 @@ const imageToImageData = async function(node, maxDim) {
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the computed background color of the closest parent with a set background color.
|
||||
*
|
||||
* @param {DOMNode} node
|
||||
* Node for which we want to find closest background color.
|
||||
* @return {String}
|
||||
* String with the background color of the form rgba(r, g, b, a). Defaults to
|
||||
* rgba(255, 255, 255, 1) if no background color is found.
|
||||
*/
|
||||
function getClosestBackgroundColor(node) {
|
||||
let current = node;
|
||||
|
||||
while (current) {
|
||||
const computedStyle = CssLogic.getComputedStyle(current);
|
||||
if (computedStyle) {
|
||||
const currentStyle = computedStyle.getPropertyValue("background-color");
|
||||
if (colorUtils.isValidCSSColor(currentStyle)) {
|
||||
const currentCssColor = new colorUtils.CssColor(currentStyle);
|
||||
if (!currentCssColor.isTransparent()) {
|
||||
return currentCssColor.rgba;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
current = current.parentNode;
|
||||
}
|
||||
|
||||
return "rgba(255, 255, 255, 1)";
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the background image of the closest parent where it is set.
|
||||
*
|
||||
* @param {DOMNode} node
|
||||
* Node for which we want to find the background image.
|
||||
* @return {String}
|
||||
* String with the value of the background iamge property. Defaults to "none" if
|
||||
* no background image is found.
|
||||
*/
|
||||
function getClosestBackgroundImage(node) {
|
||||
let current = node;
|
||||
|
||||
while (current.ownerDocument) {
|
||||
const computedStyle = CssLogic.getComputedStyle(current);
|
||||
if (computedStyle) {
|
||||
const currentBackgroundImage = computedStyle.getPropertyValue("background-image");
|
||||
if (currentBackgroundImage !== "none") {
|
||||
return currentBackgroundImage;
|
||||
}
|
||||
}
|
||||
|
||||
current = current.parentNode;
|
||||
}
|
||||
|
||||
return "none";
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
allAnonymousContentTreeWalkerFilter,
|
||||
getClosestBackgroundColor,
|
||||
getClosestBackgroundImage,
|
||||
getNodeDisplayName,
|
||||
imageToImageData,
|
||||
isNodeDead,
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
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");
|
||||
|
||||
/**
|
||||
* Calculates the contrast ratio of the referenced DOM node.
|
||||
*
|
||||
* @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);
|
||||
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);
|
||||
|
||||
// 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.
|
||||
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") {
|
||||
return null;
|
||||
}
|
||||
|
||||
let { r: bgR, g: bgG, b: bgB, a: bgA} = backgroundRgbaColor.getRGBATuple();
|
||||
let { r: textR, g: textG, b: textB, a: textA } = textRgbaColor.getRGBATuple();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
return {
|
||||
ratio: colorUtils.calculateContrastRatio([ bgR, bgG, bgB, bgA ],
|
||||
[ textR, textG, textB, textA ]),
|
||||
largeText: Math.ceil(parseFloat(fontSize) * 72) / 96 >= (isBoldText ? 14 : 18)
|
||||
};
|
||||
}
|
||||
|
||||
exports.getContrastRatioFor = getContrastRatioFor;
|
|
@ -5,6 +5,7 @@
|
|||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DevToolsModules(
|
||||
'accessibility.js',
|
||||
'actor-registry-utils.js',
|
||||
'actor-registry.js',
|
||||
'audionodes.json',
|
||||
|
|
|
@ -1162,6 +1162,31 @@ function calculateLuminance(rgba) {
|
|||
return 0.2126 * rgba[0] + 0.7152 * rgba[1] + 0.0722 * rgba[2];
|
||||
}
|
||||
|
||||
/**
|
||||
* Blend background and foreground colors takign alpha into account.
|
||||
* @param {Array} foregroundColor
|
||||
* An array with [r,g,b,a] values containing the foreground color.
|
||||
* @param {Array} backgroundColor
|
||||
* An array with [r,g,b,a] values containing the background color. Defaults to
|
||||
* [ 255, 255, 255, 1 ].
|
||||
* @return {Array}
|
||||
* An array with combined [r,g,b,a] colors.
|
||||
*/
|
||||
function blendColors(foregroundColor, backgroundColor = [ 255, 255, 255, 1 ]) {
|
||||
const [ fgR, fgG, fgB, fgA ] = foregroundColor;
|
||||
const [ bgR, bgG, bgB, bgA ] = backgroundColor;
|
||||
if (fgA === 1) {
|
||||
return foregroundColor;
|
||||
}
|
||||
|
||||
return [
|
||||
(1 - fgA) * bgR + fgA * fgR,
|
||||
(1 - fgA) * bgG + fgA * fgG,
|
||||
(1 - fgA) * bgB + fgA * fgB,
|
||||
fgA + bgA * (1 - fgA)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the contrast ratio of 2 rgba tuples based on the formula in
|
||||
* https://www.w3.org/TR/2008/REC-WCAG20-20081211/#visual-audio-contrast7
|
||||
|
@ -1173,6 +1198,9 @@ function calculateLuminance(rgba) {
|
|||
* @return {Number} The calculated luminance.
|
||||
*/
|
||||
function calculateContrastRatio(backgroundColor, textColor) {
|
||||
backgroundColor = blendColors(backgroundColor);
|
||||
textColor = blendColors(textColor, backgroundColor);
|
||||
|
||||
const backgroundLuminance = calculateLuminance(backgroundColor);
|
||||
const textLuminance = calculateLuminance(textColor);
|
||||
const ratio = (textLuminance + 0.05) / (backgroundLuminance + 0.05);
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
# LOCALIZATION NOTE (accessibility.contrast.ratio): A title text for the color contrast
|
||||
# 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
|
Загрузка…
Ссылка в новой задаче