From b33f9e71a6995db9154307f9cda269c2c975c457 Mon Sep 17 00:00:00 2001 From: Yura Zenevich Date: Mon, 11 Feb 2019 20:46:23 +0000 Subject: [PATCH] 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 --- .../client/accessibility/accessibility.css | 141 ++++++++- .../components/ColorContrastAccessibility.js | 219 ++++++++++++++ devtools/client/accessibility/constants.js | 3 + .../accessibility/test/mochitest/chrome.ini | 3 + .../test/mochitest/contrast.snapshots.js | 272 ++++++++++++++++++ .../mochitest/test_accessible_contrast.html | 75 +++++ .../locales/en-US/accessibility.properties | 36 +++ 7 files changed, 740 insertions(+), 9 deletions(-) create mode 100644 devtools/client/accessibility/components/ColorContrastAccessibility.js create mode 100644 devtools/client/accessibility/test/mochitest/contrast.snapshots.js create mode 100644 devtools/client/accessibility/test/mochitest/test_accessible_contrast.html diff --git a/devtools/client/accessibility/accessibility.css b/devtools/client/accessibility/accessibility.css index 082b3b9ba922..7e5c5600fe08 100644 --- a/devtools/client/accessibility/accessibility.css +++ b/devtools/client/accessibility/accessibility.css @@ -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; +} diff --git a/devtools/client/accessibility/components/ColorContrastAccessibility.js b/devtools/client/accessibility/components/ColorContrastAccessibility.js new file mode 100644 index 000000000000..0d5d904ca3c0 --- /dev/null +++ b/devtools/client/accessibility/components/ColorContrastAccessibility.js @@ -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, +}; diff --git a/devtools/client/accessibility/constants.js b/devtools/client/accessibility/constants.js index 7a66876cc722..eda556ce92cf 100644 --- a/devtools/client/accessibility/constants.js +++ b/devtools/client/accessibility/constants.js @@ -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"; diff --git a/devtools/client/accessibility/test/mochitest/chrome.ini b/devtools/client/accessibility/test/mochitest/chrome.ini index 3aea7ea2878e..0691e4b13ed8 100644 --- a/devtools/client/accessibility/test/mochitest/chrome.ini +++ b/devtools/client/accessibility/test/mochitest/chrome.ini @@ -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] diff --git a/devtools/client/accessibility/test/mochitest/contrast.snapshots.js b/devtools/client/accessibility/test/mochitest/contrast.snapshots.js new file mode 100644 index 000000000000..f3a9b7a2b6e6 --- /dev/null +++ b/devtools/client/accessibility/test/mochitest/contrast.snapshots.js @@ -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", + ], + }, + "", + ], + }, + ], + }, +}; diff --git a/devtools/client/accessibility/test/mochitest/test_accessible_contrast.html b/devtools/client/accessibility/test/mochitest/test_accessible_contrast.html new file mode 100644 index 000000000000..22a74ecbf635 --- /dev/null +++ b/devtools/client/accessibility/test/mochitest/test_accessible_contrast.html @@ -0,0 +1,75 @@ + + + + + + + Color Contrast accessibility component test + + + + + +
+
+
+
+
+
+ + diff --git a/devtools/client/locales/en-US/accessibility.properties b/devtools/client/locales/en-US/accessibility.properties index 19359293dff8..40c1eff93ded 100644 --- a/devtools/client/locales/en-US/accessibility.properties +++ b/devtools/client/locales/en-US/accessibility.properties @@ -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