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:
Yura Zenevich 2018-10-02 13:29:24 +00:00
Родитель 50b54a512b
Коммит d84869d27a
10 изменённых файлов: 426 добавлений и 40 удалений

Просмотреть файл

@ -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 {
color:var(--highlighter-infobar-color);
:-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