Bug 1518487 - implement ColorContrast component to display a11y audit information for text color contrast. r=gl

MozReview-Commit-ID: DoOp2JhaQyD

Differential Revision: https://phabricator.services.mozilla.com/D19053

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Yura Zenevich 2019-02-11 20:46:23 +00:00
Родитель 2a58419670
Коммит b33f9e71a6
7 изменённых файлов: 740 добавлений и 9 удалений

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

@ -10,6 +10,7 @@
--accessibility-toolbar-focus-alpha30: rgba(10, 132, 255, 0.3);
--accessibility-full-length-minus-splitter: calc(100% - 1px);
--accessibility-horizontal-padding: 5px;
--accessibility-horizontal-indent: 14px;
--accessibility-properties-item-width: calc(100% - var(--accessibility-horizontal-padding));
--accessibility-arrow-horizontal-padding: 4px;
--accessibility-tree-row-height: 21px;
@ -19,9 +20,9 @@
--accessibility-link-color-active: var(--blue-70);
--accessible-role-active-background-color: var(--blue-50);
--accessible-role-active-border-color: #FFFFFFB3;
--accessible-role-background-color: white;
--accessible-role-border-color: #CACAD1;
--accessible-role-color: var(--grey-60);
--accessible-label-background-color: white;
--accessible-label-border-color: #CACAD1;
--accessible-label-color: var(--grey-60);
}
:root.theme-dark {
@ -31,9 +32,9 @@
--accessibility-link-color-active: var(--blue-40);
--accessible-role-active-background-color: var(--blue-60);
--accessible-role-active-border-color: #FFF6;
--accessible-role-background-color: var(--grey-80);
--accessible-role-border-color: var(--grey-50);
--accessible-role-color: var(--grey-40);
--accessible-label-background-color: var(--grey-80);
--accessible-label-border-color: var(--grey-50);
--accessible-label-color: var(--grey-40);
}
/* General */
@ -412,9 +413,9 @@ body {
}
.accessible .tree .objectBox-accessible .accessible-role {
background-color: var(--accessible-role-background-color);
color: var(--accessible-role-color);
border: 1px solid var(--accessible-role-border-color);
background-color: var(--accessible-label-background-color);
color: var(--accessible-label-color);
border: 1px solid var(--accessible-label-border-color);
border-radius: 3px;
padding: 0px 2px;
margin-inline-start: 5px;
@ -476,3 +477,125 @@ body {
height: var(--accessibility-toolbar-height-tall);
line-height: var(--accessibility-toolbar-height-tall);
}
/* Color Contrast */
.accessibility-color-contrast-check,
.accessibility-color-contrast {
position: relative;
display: flex;
cursor: default;
height: inherit;
}
.accessibility-color-contrast-check {
flex-direction: column;
padding: 4px var(--accessibility-horizontal-indent);
line-height: 20px;
}
.accessibility-color-contrast {
align-items: baseline;
}
.accessibility-color-contrast-header {
margin: 0;
font-weight: bold;
font-size: var(--accessibility-font-size);
line-height: var(--accessibility-toolbar-height);
}
.accessibility-color-contrast-annotation {
margin: 0;
white-space: normal;
color: var(--accessible-label-color);
}
.accessibility-color-contrast-annotation .link {
color: var(--accessibility-link-color);
cursor: pointer;
outline: 0;
white-space: nowrap;
font-style: normal;
}
.accessibility-color-contrast-annotation .link:hover:not(:focus) {
text-decoration: underline;
}
.accessibility-color-contrast-annotation .link:focus:not(:active) {
box-shadow: 0 0 0 2px var(--accessibility-toolbar-focus), 0 0 0 4px var(--accessibility-toolbar-focus-alpha30);
border-radius: 2px;
}
.accessibility-color-contrast-annotation .link:active {
color: var(--accessibility-link-color-active);
text-decoration: underline;
}
.accessibility-color-contrast-large-text {
background-color: var(--accessible-label-background-color);
color: var(--accessible-label-color);
outline: 1px solid var(--accessible-label-border-color);
-moz-outline-radius: 3px;
padding: 0px 2px;
margin-inline-start: 6px;
line-height: initial;
}
.accessibility-color-contrast .accessibility-contrast-value:not(:empty) {
margin-block-end: 4px;
}
.accessibility-color-contrast .accessibility-contrast-value:not(:empty):before {
content: "";
height: 14px;
width: 14px;
display: inline-flex;
background-color: var(--accessibility-contrast-color);
box-shadow: 0 0 0 1px var(--grey-40), 6px 5px var(--accessibility-contrast-bg), 6px 5px 0 1px var(--grey-40);
margin-inline-end: 11px;
}
.accessibility-color-contrast .accessibility-contrast-value:first-child:not(:empty):before {
margin-inline-start: 1px;
}
.accessibility-color-contrast .accessibility-contrast-value:not(:first-child):not(:empty):before {
margin-inline-start: 4px;
}
.accessibility-color-contrast .accessibility-contrast-value:not(:empty):after {
margin-inline-start: 4px;
}
.accessibility-color-contrast .accessibility-contrast-value:not(:empty).AA:after,
.accessibility-color-contrast .accessibility-contrast-value:not(:empty).AAA:after {
color: var(--theme-highlight-green);
}
.accessibility-color-contrast .accessibility-contrast-value:not(:empty).fail:after {
color: #E57180;
content: "⚠️";
}
.accessibility-color-contrast .accessibility-contrast-value:not(:empty).AA:after {
content: "AA\2713";
}
.accessibility-color-contrast .accessibility-contrast-value:not(:empty).AAA:after {
content: "AAA\2713";
}
.accessibility-color-contrast .accessibility-color-contrast-label:after {
content: ":";
}
.accessibility-color-contrast .accessibility-color-contrast-label,
.accessibility-color-contrast .accessibility-color-contrast-separator:before {
margin-inline-end: 3px;
}
.accessibility-color-contrast .accessibility-color-contrast-separator:before {
content: "-";
margin-inline-start: 4px;
}

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

@ -0,0 +1,219 @@
/* 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";
const { Component, createFactory } = require("devtools/client/shared/vendor/react");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const { div, span, h3 } = require("devtools/client/shared/vendor/react-dom-factories");
const LearnMoreLink = createFactory(require("./LearnMoreLink"));
const { A11Y_CONTRAST_LEARN_MORE_LINK } = require("../constants");
const { L10N } = require("../utils/l10n");
/**
* Component that renders a colour contrast value along with a swatch preview of what the
* text and background colours are.
*/
class ContrastValueClass extends Component {
static get propTypes() {
return {
backgroundColor: PropTypes.array.isRequired,
color: PropTypes.array.isRequired,
isLargeText: PropTypes.bool.isRequired,
value: PropTypes.number.isRequired,
};
}
render() {
const {
backgroundColor,
color,
isLargeText,
value,
} = this.props;
const className = [
"accessibility-contrast-value",
getContrastRatioScore(value, isLargeText),
].join(" ");
return (
span({
className,
role: "presentation",
style: {
"--accessibility-contrast-color": `rgba(${color})`,
"--accessibility-contrast-bg": `rgba(${backgroundColor})`,
},
}, value.toFixed(2))
);
}
}
const ContrastValue = createFactory(ContrastValueClass);
/**
* Component that renders labeled colour contrast values together with the large text
* indiscator.
*/
class ColorContrastAccessibilityClass extends Component {
static get propTypes() {
return {
error: PropTypes.string,
isLargeText: PropTypes.bool.isRequired,
color: PropTypes.array.isRequired,
value: PropTypes.number,
min: PropTypes.number,
max: PropTypes.number,
backgroundColor: PropTypes.array,
backgroundColorMin: PropTypes.array,
backgroundColorMax: PropTypes.array,
};
}
render() {
const {
error,
isLargeText,
color,
value, backgroundColor,
min, backgroundColorMin,
max, backgroundColorMax,
} = this.props;
const children = [];
if (error) {
children.push(span({
className: "accessibility-color-contrast-error",
role: "presentation",
}, L10N.getStr("accessibility.contrast.error")));
return (div({
role: "presentation",
className: "accessibility-color-contrast",
}, ...children));
}
if (value) {
children.push(ContrastValue({ isLargeText, color, backgroundColor, value }));
} else {
children.push(
ContrastValue(
{ isLargeText, color, backgroundColor: backgroundColorMin, value: min }),
div({
role: "presentation",
className: "accessibility-color-contrast-separator",
}),
ContrastValue(
{ isLargeText, color, backgroundColor: backgroundColorMax, value: max }),
);
}
if (isLargeText) {
children.push(
span({
className: "accessibility-color-contrast-large-text",
role: "presentation",
title: L10N.getStr("accessibility.contrast.large.title"),
}, L10N.getStr("accessibility.contrast.large.text"))
);
}
return (
div(
{
role: "presentation",
className: "accessibility-color-contrast",
},
...children
)
);
}
}
const ColorContrastAccessibility = createFactory(ColorContrastAccessibilityClass);
class ContrastAnnotationClass extends Component {
static get propTypes() {
return {
isLargeText: PropTypes.bool.isRequired,
value: PropTypes.number,
min: PropTypes.number,
};
}
render() {
const { isLargeText, min, value } = this.props;
const score = getContrastRatioScore(value || min, isLargeText);
return (
LearnMoreLink(
{
className: "accessibility-color-contrast-annotation",
href: A11Y_CONTRAST_LEARN_MORE_LINK,
learnMoreStringKey: "accessibility.learnMore",
l10n: L10N,
messageStringKey: `accessibility.contrast.annotation.${score}`,
}
)
);
}
}
const ContrastAnnotation = createFactory(ContrastAnnotationClass);
class ColorContrastCheck extends Component {
static get propTypes() {
return {
error: PropTypes.string.isRequired,
};
}
render() {
const { error } = this.props;
return (
div({
role: "presentation",
className: "accessibility-color-contrast-check",
},
h3({
className: "accessibility-color-contrast-header",
}, L10N.getStr("accessibility.contrast.header")),
ColorContrastAccessibility(this.props),
!error && ContrastAnnotation(this.props)
)
);
}
}
/**
* Get contrast ratio score.
* ratio.
* @param {Number} value
* Value of the contrast ratio for a given accessible object.
* @param {Boolean} isLargeText
* True if the accessible object contains large text.
* @return {String}
* Represents the appropriate contrast ratio score.
*/
function getContrastRatioScore(value, isLargeText) {
const levels = isLargeText ? { AA: 3, AAA: 4.5 } : { AA: 4.5, AAA: 7 };
let score = "fail";
if (value >= levels.AAA) {
score = "AAA";
} else if (value >= levels.AA) {
score = "AA";
}
return score;
}
module.exports = {
ColorContrastAccessibility: ColorContrastAccessibilityClass,
ColorContrastCheck,
};

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

@ -68,3 +68,6 @@ exports.A11Y_SERVICE_ENABLED_COUNT = "devtools.accessibility.service_enabled_cou
// URL constants
exports.A11Y_LEARN_MORE_LINK =
"https://developer.mozilla.org/docs/Tools/Accessibility_inspector";
exports.A11Y_CONTRAST_LEARN_MORE_LINK =
"https://developer.mozilla.org/docs/Web/Accessibility/Understanding_WCAG/Perceivable/" +
"Color_contrast?utm_source=devtools&utm_medium=a11y-panel-checks-color-contrast";

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

@ -1,7 +1,10 @@
[DEFAULT]
support-files =
head.js
contrast.snapshots.js
!/devtools/client/shared/components/test/mochitest/head.js
[test_accessible_contrast.html]
[test_accessible_learnMoreLink.html]
[test_accessible_openLink.html]
[test_accessible_relations.html]

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

@ -0,0 +1,272 @@
/* 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";
window._snapshots = {
"ColorContrastAccessibility error render.": {
"type": "div",
"props": {
"role": "presentation",
"className": "accessibility-color-contrast-check",
},
"children": [
{
"type": "h3",
"props": {
"className": "accessibility-color-contrast-header",
},
"children": [
"Color and Contrast",
],
},
{
"type": "div",
"props": {
"role": "presentation",
"className": "accessibility-color-contrast",
},
"children": [
{
"type": "span",
"props": {
"className": "accessibility-color-contrast-error",
"role": "presentation",
},
"children": [
"Unable to calculate",
],
},
],
},
],
},
"ColorContrastAccessibility basic render.": {
"type": "div",
"props": {
"role": "presentation",
"className": "accessibility-color-contrast-check",
},
"children": [
{
"type": "h3",
"props": {
"className": "accessibility-color-contrast-header",
},
"children": [
"Color and Contrast",
],
},
{
"type": "div",
"props": {
"role": "presentation",
"className": "accessibility-color-contrast",
},
"children": [
{
"type": "span",
"props": {
"className": "accessibility-contrast-value fail",
"role": "presentation",
"style": {
"--accessibility-contrast-color": "rgba(255,0,0,1)",
"--accessibility-contrast-bg": "rgba(255,255,255,1)",
},
},
"children": [
"4.00",
],
},
],
},
{
"type": "p",
"props": {
"className": "accessibility-color-contrast-annotation",
},
"children": [
"Does not meet WCAG standards for accessible text. ",
{
"type": "a",
"props": {
"className": "link",
"href": "https://developer.mozilla.org/docs/Web/Accessibility/" +
"Understanding_WCAG/Perceivable/Color_contrast?utm_source=" +
"devtools&utm_medium=a11y-panel-checks-color-contrast",
"onClick": "openDocOnClick(event) {\n event.preventDefault();\n " +
"openDocLink(event.target.href);\n }",
},
"children": [
"Learn more",
],
},
"",
],
},
],
},
"ColorContrastAccessibility range render.": {
"type": "div",
"props": {
"role": "presentation",
"className": "accessibility-color-contrast-check",
},
"children": [
{
"type": "h3",
"props": {
"className": "accessibility-color-contrast-header",
},
"children": [
"Color and Contrast",
],
},
{
"type": "div",
"props": {
"role": "presentation",
"className": "accessibility-color-contrast",
},
"children": [
{
"type": "span",
"props": {
"className": "accessibility-contrast-value fail",
"role": "presentation",
"style": {
"--accessibility-contrast-color": "rgba(128,128,128,1)",
"--accessibility-contrast-bg": "rgba(219,106,116,1)",
},
},
"children": [
"1.19",
],
},
{
"type": "div",
"props": {
"role": "presentation",
"className": "accessibility-color-contrast-separator",
},
"children": null,
},
{
"type": "span",
"props": {
"className": "accessibility-contrast-value fail",
"role": "presentation",
"style": {
"--accessibility-contrast-color": "rgba(128,128,128,1)",
"--accessibility-contrast-bg": "rgba(156,145,211,1)",
},
},
"children": [
"1.39",
],
},
],
},
{
"type": "p",
"props": {
"className": "accessibility-color-contrast-annotation",
},
"children": [
"Does not meet WCAG standards for accessible text. ",
{
"type": "a",
"props": {
"className": "link",
"href": "https://developer.mozilla.org/docs/Web/Accessibility/" +
"Understanding_WCAG/Perceivable/Color_contrast?utm_source=" +
"devtools&utm_medium=a11y-panel-checks-color-contrast",
"onClick": "openDocOnClick(event) {\n event.preventDefault();\n " +
"openDocLink(event.target.href);\n }",
},
"children": [
"Learn more",
],
},
"",
],
},
],
},
"ColorContrastAccessibility large text render.": {
"type": "div",
"props": {
"role": "presentation",
"className": "accessibility-color-contrast-check",
},
"children": [
{
"type": "h3",
"props": {
"className": "accessibility-color-contrast-header",
},
"children": [
"Color and Contrast",
],
},
{
"type": "div",
"props": {
"role": "presentation",
"className": "accessibility-color-contrast",
},
"children": [
{
"type": "span",
"props": {
"className": "accessibility-contrast-value AA",
"role": "presentation",
"style": {
"--accessibility-contrast-color": "rgba(255,0,0,1)",
"--accessibility-contrast-bg": "rgba(255,255,255,1)",
},
},
"children": [
"4.00",
],
},
{
"type": "span",
"props": {
"className": "accessibility-color-contrast-large-text",
"role": "presentation",
"title": "Text is 14 point and bold or larger, or 18 point or larger.",
},
"children": [
"large text",
],
},
],
},
{
"type": "p",
"props": {
"className": "accessibility-color-contrast-annotation",
},
"children": [
"Meets WCAG AA standards for accessible text. ",
{
"type": "a",
"props": {
"className": "link",
"href": "https://developer.mozilla.org/docs/Web/Accessibility/" +
"Understanding_WCAG/Perceivable/Color_contrast?utm_source=" +
"devtools&utm_medium=a11y-panel-checks-color-contrast",
"onClick": "openDocOnClick(event) {\n event.preventDefault();\n " +
"openDocLink(event.target.href);\n }",
},
"children": [
"Learn more",
],
},
"",
],
},
],
},
};

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

@ -0,0 +1,75 @@
<!-- 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/. -->
<!DOCTYPE HTML>
<html>
<!--
Test that Color Contrast component renders correctly.
-->
<head>
<meta charset="utf-8">
<title>Color Contrast accessibility component test</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
</head>
<body>
<pre id="test">
<script src="head.js" type="application/javascript"></script>
<script src="chrome://mochitests/content/chrome/devtools/client/shared/components/test/mochitest/head.js" type="application/javascript"/>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" type="application/javascript"></script>
<script src="contrast.snapshots.js" type="application/javascript"></script>
<script type="application/javascript">
"use strict";
/* global matchSnapshot */
window.onload = async function() {
try {
const React = browserRequire("devtools/client/shared/vendor/react");
const { ColorContrastCheck } = browserRequire(
"devtools/client/accessibility/components/ColorContrastAccessibility");
matchSnapshot("ColorContrastAccessibility error render.",
React.createElement(ColorContrastCheck, { error: true })
);
matchSnapshot("ColorContrastAccessibility basic render.",
React.createElement(ColorContrastCheck, {
"value": 4.00,
"color": [255, 0, 0, 1],
"backgroundColor": [255, 255, 255, 1],
"isLargeText": false,
})
);
matchSnapshot("ColorContrastAccessibility range render.",
React.createElement(ColorContrastCheck, {
"min": 1.19,
"max": 1.39,
"color": [128, 128, 128, 1],
"backgroundColorMin": [219, 106, 116, 1],
"backgroundColorMax": [156, 145, 211, 1],
"isLargeText": false,
})
);
matchSnapshot("ColorContrastAccessibility large text render.",
React.createElement(ColorContrastCheck, {
"value": 4.00,
"color": [255, 0, 0, 1],
"backgroundColor": [255, 255, 255, 1],
"isLargeText": true,
})
);
} catch (e) {
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
} finally {
SimpleTest.finish();
}
};
</script>
</pre>
</body>
</html>

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

@ -106,3 +106,39 @@ accessibility.description.oldVersion=You are connected to a debugger server that
# context menu item for printing an accessible tree to JSON is rendered after triggering a
# context menu for an accessible tree row.
accessibility.tree.menu.printToJSON=Print to JSON
# LOCALIZATION NOTE (accessibility.contrast.header): A title text used for header for
# checks related to color and contrast.
accessibility.contrast.header=Color and Contrast
# LOCALIZATION NOTE (accessibility.contrast.error): A title text for the color
# contrast ratio, used when the tool is unable to calculate the contrast ratio value.
accessibility.contrast.error=Unable to calculate
# LOCALIZATION NOTE (accessibility.contrast.large.text): A title text for the color
# contrast ratio label indicating that the color contrast criteria used is if for large
# text. This is lower case because it's used as a label for a tree item in accessibility
# tree.
accessibility.contrast.large.text=large text
# LOCALIZATION NOTE (accessibility.contrast.large.title): A title text for the tooltip
# used for the large text label (see accessibility.contrast.large.text).
accessibility.contrast.large.title=Text is 14 point and bold or larger, or 18 point or larger.
# LOCALIZATION NOTE (accessibility.contrast.annotation.AA): A title text for the paragraph
# describing that the given colour contrast satisfies AA standard from Web Content
# Accessibility Guidelines. %S in the content will be replaced by a link at run time
# with the accessibility.learnMore string.
accessibility.contrast.annotation.AA=Meets WCAG AA standards for accessible text. %S
# LOCALIZATION NOTE (accessibility.contrast.annotation.AAA): A title text for the
# paragraph describing that the given colour contrast satisfies AAA standard from Web
# Content Accessibility Guidelines. %S in the content will be replaced by a link at run
# time with the accessibility.learnMore string.
accessibility.contrast.annotation.AAA=Meets WCAG AAA standards for accessible text. %S
# LOCALIZATION NOTE (accessibility.contrast.annotation.fail): A title text for the
# paragraph describing that the given colour contrast fails to meet the minimum level from
# Web Content Accessibility Guidelines. %S in the content will be replaced by a link at
# run time with the accessibility.learnMore string.
accessibility.contrast.annotation.fail=Does not meet WCAG standards for accessible text. %S