diff --git a/devtools/server/actors/highlighters.css b/devtools/server/actors/highlighters.css index 5c412a739dda..b21dfe489a0e 100644 --- a/devtools/server/actors/highlighters.css +++ b/devtools/server/actors/highlighters.css @@ -39,6 +39,8 @@ --highlighter-marker-color: #000; --grey-40: #b1b1b3; + --red-40: #ff3b6b; + --yellow-60: #d7b600; } /** @@ -737,6 +739,34 @@ margin-inline-start: 3px; } +:-moz-native-anonymous .accessible-infobar-audit .accessible-text-label:before { + display: inline-block; + width: 12px; + height: 12px; + content: ""; + margin-inline-end: 4px; + vertical-align: -2px; + background-image: none; + background-position: center; + background-repeat: no-repeat; + -moz-context-properties: fill; + fill: currentColor; +} + +:-moz-native-anonymous .accessible-infobar-audit .accessible-text-label.fail:before { + background-image: url(chrome://devtools/skin/images/error-small.svg); + fill: var(--red-40); +} + +:-moz-native-anonymous .accessible-infobar-audit .accessible-text-label.WARNING:before { + background-image: url(chrome://devtools/skin/images/alert-small.svg); + fill: var(--yellow-60); +} + +:-moz-native-anonymous .accessible-infobar-audit .accessible-text-label.BEST_PRACTICES:before { + background-image: url(chrome://devtools/skin/images/info-small.svg); +} + :-moz-native-anonymous .accessible-infobar-name:not(:empty) { border-inline-start: 1px solid #5a6169; margin-inline-start: 6px; diff --git a/devtools/server/actors/highlighters/utils/accessibility.js b/devtools/server/actors/highlighters/utils/accessibility.js index 6f4a76a68618..5ee218dffa64 100644 --- a/devtools/server/actors/highlighters/utils/accessibility.js +++ b/devtools/server/actors/highlighters/utils/accessibility.js @@ -9,13 +9,39 @@ const { getCurrentZoom, getViewportDimensions } = require("devtools/shared/layou const { moveInfobar, createNode } = require("./markup"); const { truncateString } = require("devtools/shared/inspector/utils"); -const { accessibility: { SCORES } } = require("devtools/shared/constants"); - const STRINGS_URI = "devtools/shared/locales/accessibility.properties"; loader.lazyRequireGetter(this, "LocalizationHelper", "devtools/shared/l10n", true); DevToolsUtils.defineLazyGetter(this, "L10N", () => new LocalizationHelper(STRINGS_URI)); -const { accessibility: { AUDIT_TYPE } } = require("devtools/shared/constants"); +const { + accessibility: { + AUDIT_TYPE, + ISSUE_TYPE: { + [AUDIT_TYPE.TEXT_LABEL]: { + AREA_NO_NAME_FROM_ALT, + DIALOG_NO_NAME, + DOCUMENT_NO_TITLE, + EMBED_NO_NAME, + FIGURE_NO_NAME, + FORM_FIELDSET_NO_NAME, + FORM_FIELDSET_NO_NAME_FROM_LEGEND, + FORM_NO_NAME, + FORM_NO_VISIBLE_NAME, + FORM_OPTGROUP_NO_NAME, + FORM_OPTGROUP_NO_NAME_FROM_LABEL, + FRAME_NO_NAME, + HEADING_NO_CONTENT, + HEADING_NO_NAME, + IFRAME_NO_NAME_FROM_TITLE, + IMAGE_NO_NAME, + INTERACTIVE_NO_NAME, + MATHML_GLYPH_NO_NAME, + TOOLBAR_NO_NAME, + }, + }, + SCORES, + }, +} = require("devtools/shared/constants"); // Max string length for truncating accessible name values. const MAX_STRING_LENGTH = 50; @@ -383,6 +409,7 @@ class Audit { // object. this.reports = [ new ContrastRatio(this), + new TextLabel(this), ]; } @@ -539,13 +566,13 @@ class ContrastRatio extends AuditReport { /** * Update contrast ratio score infobar markup. - * @param {Number} - * Contrast ratio for an accessible object being highlighted. + * @param {Object} + * Audit report for a given highlighted accessible. * @return {Boolean} * True if the contrast ratio markup was updated correctly and infobar audit * block should be visible. */ - update({ [AUDIT_TYPE.CONTRAST]: contrastRatio }) { + update(audit) { const els = {}; for (const key of ["label", "min", "max", "error", "separator"]) { const el = els[key] = this.getElement(`contrast-ratio-${key}`); @@ -559,6 +586,11 @@ class ContrastRatio extends AuditReport { el.removeAttribute("style"); } + if (!audit) { + return false; + } + + const contrastRatio = audit[AUDIT_TYPE.CONTRAST]; if (!contrastRatio) { return false; } @@ -592,6 +624,82 @@ class ContrastRatio extends AuditReport { } } +/** + * Text label audit report that is used to display a problem with text alternatives + * as part of the inforbar. + */ +class TextLabel extends AuditReport { + /** + * A map from text label issues to annotation component properties. + */ + static get ISSUE_TO_INFOBAR_LABEL_MAP() { + return { + [AREA_NO_NAME_FROM_ALT]: "accessibility.text.label.issue.area", + [DIALOG_NO_NAME]: "accessibility.text.label.issue.dialog", + [DOCUMENT_NO_TITLE]: "accessibility.text.label.issue.document.title", + [EMBED_NO_NAME]: "accessibility.text.label.issue.embed", + [FIGURE_NO_NAME]: "accessibility.text.label.issue.figure", + [FORM_FIELDSET_NO_NAME]: "accessibility.text.label.issue.fieldset", + [FORM_FIELDSET_NO_NAME_FROM_LEGEND]: + "accessibility.text.label.issue.fieldset.legend", + [FORM_NO_NAME]: "accessibility.text.label.issue.form", + [FORM_NO_VISIBLE_NAME]: "accessibility.text.label.issue.form.visible", + [FORM_OPTGROUP_NO_NAME]: "accessibility.text.label.issue.optgroup", + [FORM_OPTGROUP_NO_NAME_FROM_LABEL]: "accessibility.text.label.issue.optgroup.label", + [FRAME_NO_NAME]: "accessibility.text.label.issue.frame", + [HEADING_NO_CONTENT]: "accessibility.text.label.issue.heading.content", + [HEADING_NO_NAME]: "accessibility.text.label.issue.heading", + [IFRAME_NO_NAME_FROM_TITLE]: "accessibility.text.label.issue.iframe", + [IMAGE_NO_NAME]: "accessibility.text.label.issue.image", + [INTERACTIVE_NO_NAME]: "accessibility.text.label.issue.interactive", + [MATHML_GLYPH_NO_NAME]: "accessibility.text.label.issue.glyph", + [TOOLBAR_NO_NAME]: "accessibility.text.label.issue.toolbar", + }; + } + + buildMarkup(root) { + createNode(this.win, { + nodeType: "span", + parent: root, + attributes: { + "class": "text-label", + "id": "text-label", + }, + prefix: this.prefix, + }); + } + + /** + * Update text label audit infobar markup. + * @param {Object} + * Audit report for a given highlighted accessible. + * @return {Boolean} + * True if the text label markup was updated correctly and infobar + * audit block should be visible. + */ + update(audit) { + const el = this.getElement("text-label"); + el.setAttribute("hidden", true); + Object.values(SCORES).forEach(className => el.classList.remove(className)); + + if (!audit) { + return false; + } + + const textLabelAudit = audit[AUDIT_TYPE.TEXT_LABEL]; + if (!textLabelAudit) { + return false; + } + + const { issue, score } = textLabelAudit; + this.setTextContent(el, L10N.getStr(TextLabel.ISSUE_TO_INFOBAR_LABEL_MAP[issue])); + el.classList.add(score); + el.removeAttribute("hidden"); + + return true; + } +} + /** * A helper function that calculate accessible object bounds and positioning to * be used for highlighting. diff --git a/devtools/server/actors/highlighters/xul-accessible.js b/devtools/server/actors/highlighters/xul-accessible.js index 6d11aa1d5341..206a2067b4c6 100644 --- a/devtools/server/actors/highlighters/xul-accessible.js +++ b/devtools/server/actors/highlighters/xul-accessible.js @@ -20,6 +20,8 @@ const ACCESSIBLE_BOUNDS_SHEET = "data:text/css;charset=utf-8," + encodeURICompon --highlighter-bubble-arrow-size: 8px; --grey-40: #b1b1b3; + --red-40: #ff3b6b; + --yellow-60: #d7b600; } .accessible-bounds { @@ -147,6 +149,34 @@ const ACCESSIBLE_BOUNDS_SHEET = "data:text/css;charset=utf-8," + encodeURICompon border-inline-start: 1px solid #5a6169; margin-inline-start: 6px; padding-inline-start: 6px; + } + + .accessible-infobar-audit .accessible-text-label:before { + display: inline-block; + width: 12px; + height: 12px; + content: ""; + margin-inline-end: 4px; + vertical-align: -2px; + background-image: none; + background-position: center; + background-repeat: no-repeat; + -moz-context-properties: fill; + fill: currentColor; + } + + .accessible-infobar-audit .accessible-text-label.fail:before { + background-image: url(chrome://devtools/skin/images/error-small.svg); + fill: var(--red-40); + } + + .accessible-infobar-audit .accessible-text-label.WARNING:before { + background-image: url(chrome://devtools/skin/images/alert-small.svg); + fill: var(--yellow-60); + } + + .accessible-infobar-audit .accessible-text-label.BEST_PRACTICES:before { + background-image: url(chrome://devtools/skin/images/info-small.svg); }`); /** diff --git a/devtools/server/tests/browser/browser.ini b/devtools/server/tests/browser/browser.ini index 3320414e4097..5e2d5a45bc1e 100644 --- a/devtools/server/tests/browser/browser.ini +++ b/devtools/server/tests/browser/browser.ini @@ -43,6 +43,7 @@ support-files = [browser_accessibility_highlighter_infobar.js] skip-if = (os == 'win' && processor == 'aarch64') # bug 1533184 [browser_accessibility_infobar_show.js] +[browser_accessibility_infobar_audit_text_label.js] [browser_accessibility_node.js] skip-if = (os == 'win' && processor == 'aarch64') # bug 1533184 [browser_accessibility_node_audit.js] diff --git a/devtools/server/tests/browser/browser_accessibility_infobar_audit_text_label.js b/devtools/server/tests/browser/browser_accessibility_infobar_audit_text_label.js new file mode 100644 index 000000000000..0b346918e68f --- /dev/null +++ b/devtools/server/tests/browser/browser_accessibility_infobar_audit_text_label.js @@ -0,0 +1,127 @@ +/* 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"; + +// Checks for the AccessibleHighlighter's infobar component and its text label +// audit. + +add_task(async function() { + await BrowserTestUtils.withNewTab({ + gBrowser, + url: MAIN_DOMAIN + "doc_accessibility_infobar.html", + }, async function(browser) { + await ContentTask.spawn(browser, null, async function() { + const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm"); + const { HighlighterEnvironment } = require("devtools/server/actors/highlighters"); + const { AccessibleHighlighter } = require("devtools/server/actors/highlighters/accessible"); + const { LocalizationHelper } = require("devtools/shared/l10n"); + const L10N = new LocalizationHelper( + "devtools/shared/locales/accessibility.properties"); + + const { + accessibility: { + AUDIT_TYPE, + ISSUE_TYPE: { + [AUDIT_TYPE.TEXT_LABEL]: { + DIALOG_NO_NAME, + FORM_NO_VISIBLE_NAME, + TOOLBAR_NO_NAME, + }, + }, + SCORES: { BEST_PRACTICES, FAIL, WARNING }, + }, + } = require("devtools/shared/constants"); + + /** + * Checks for updated content for an infobar. + * + * @param {Object} infobar + * Accessible highlighter's infobar component. + * @param {Object} audit + * Audit information that is passed on highlighter show. + */ + function checkTextLabel(infobar, audit) { + const { issue, score } = audit || {}; + let expected = ""; + if (issue) { + const { ISSUE_TO_INFOBAR_LABEL_MAP } = infobar.audit.reports[1].constructor; + expected = L10N.getStr(ISSUE_TO_INFOBAR_LABEL_MAP[issue]); + } + + is(infobar.getTextContent("text-label"), expected, + "infobar text label audit text content is correct"); + if (score) { + ok(infobar.getElement("text-label").classList.contains( + score === FAIL ? "fail" : score)); + } + } + + // Start testing. First, create highlighter environment and initialize. + const env = new HighlighterEnvironment(); + env.initFromWindow(content.window); + + // Wait for loading highlighter environment content to complete before creating the + // highlighter. + await new Promise(resolve => { + const doc = env.document; + + function onContentLoaded() { + if (doc.readyState === "interactive" || doc.readyState === "complete") { + resolve(); + } else { + doc.addEventListener("DOMContentLoaded", onContentLoaded, { once: true }); + } + } + + onContentLoaded(); + }); + + // Now, we can test the Infobar's audit content. + const node = content.document.createElement("div"); + content.document.body.append(node); + const highlighter = new AccessibleHighlighter(env); + const infobar = highlighter.accessibleInfobar; + const bounds = { + x: 0, + y: 0, + w: 250, + h: 100, + }; + + const tests = [{ + desc: "Infobar is shown with no text label audit content when no audit.", + }, { + desc: "Infobar is shown with no text label audit content when audit is null.", + audit: null, + }, { + desc: "Infobar is shown with no text label audit content when empty " + + "text label audit.", + audit: { [AUDIT_TYPE.TEXT_LABEL]: null }, + }, { + desc: "Infobar is shown with text label audit content for an error.", + audit: { [AUDIT_TYPE.TEXT_LABEL]: { score: FAIL, issue: TOOLBAR_NO_NAME } }, + }, { + desc: "Infobar is shown with text label audit content for a warning.", + audit: { [AUDIT_TYPE.TEXT_LABEL]: { + score: WARNING, issue: FORM_NO_VISIBLE_NAME, + }}, + }, { + desc: "Infobar is shown with text label audit content for best practices.", + audit: { [AUDIT_TYPE.TEXT_LABEL]: { + score: BEST_PRACTICES, issue: DIALOG_NO_NAME, + }}, + }]; + + for (const test of tests) { + const { desc, audit } = test; + + info(desc); + highlighter.show(node, { ...bounds, audit }); + checkTextLabel(infobar, audit && audit[AUDIT_TYPE.TEXT_LABEL]); + highlighter.hide(); + } + }); + }); +}); diff --git a/devtools/shared/locales/en-US/accessibility.properties b/devtools/shared/locales/en-US/accessibility.properties index 8131c3997eb9..34e3d2d55b39 100644 --- a/devtools/shared/locales/en-US/accessibility.properties +++ b/devtools/shared/locales/en-US/accessibility.properties @@ -19,3 +19,98 @@ accessibility.contrast.ratio.label=Contrast: # contrast ratio description that also specifies that the color contrast criteria used is # if for large text. accessibility.contrast.ratio.label.large=Contrast (large text): + +# LOCALIZATION NOTE (accessibility.text.label.issue.area): A title text that +# describes that currently selected accessible object for an element must have +# its name provided via the alt attribute. +accessibility.text.label.issue.area = Use “alt” attribute to label “area” elements that have the “href” attribute. + +# LOCALIZATION NOTE (accessibility.text.label.issue.dialog): A title text that +# describes that currently selected accessible object for a dialog should have a name +# provided. +accessibility.text.label.issue.dialog = Dialogs should be labeled. + +# LOCALIZATION NOTE (accessibility.text.label.issue.document.title): A title text that +# describes that currently selected accessible object for a document must have a name +# provided via title. +accessibility.text.label.issue.document.title = Documents must have a title. + +# LOCALIZATION NOTE (accessibility.text.label.issue.embed): A title text that +# describes that currently selected accessible object for an must have a name +# provided. +accessibility.text.label.issue.embed = Embedded content must be labeled. + +# LOCALIZATION NOTE (accessibility.text.label.issue.figure): A title text that +# describes that currently selected accessible object for a figure should have a name +# provided. +accessibility.text.label.issue.figure = Figures with optional captions should be labeled. + +# LOCALIZATION NOTE (accessibility.text.label.issue.fieldset): A title text that +# describes that currently selected accessible object for a
must have a name +# provided. +accessibility.text.label.issue.fieldset = “fieldset” elements must be labeled. + +# LOCALIZATION NOTE (accessibility.text.label.issue.fieldset.legend): A title text that +# describes that currently selected accessible object for a
must have a name +# provided via element. +accessibility.text.label.issue.fieldset.legend = Use “legend” element to label “fieldset” elements. + +# LOCALIZATION NOTE (accessibility.text.label.issue.form): A title text that +# describes that currently selected accessible object for a form element must have a name +# provided. +accessibility.text.label.issue.form = Form elements must be labeled. + +# LOCALIZATION NOTE (accessibility.text.label.issue.form.visible): A title text that +# describes that currently selected accessible object for a form element should have a name +# provided via a visible label/element. +accessibility.text.label.issue.form.visible = Form elements should have a visible text label. + +# LOCALIZATION NOTE (accessibility.text.label.issue.frame): A title text that +# describes that currently selected accessible object for a must have a name +# provided. +accessibility.text.label.issue.frame = “frame” elements must be labeled. + +# LOCALIZATION NOTE (accessibility.text.label.issue.glyph): A title text that +# describes that currently selected accessible object for a must have a name +# provided via alt attribute. +accessibility.text.label.issue.glyph = Use “alt” attribute to label “mglyph” elements. + +# LOCALIZATION NOTE (accessibility.text.label.issue.heading): A title text that +# describes that currently selected accessible object for a heading must have a name +# provided. +accessibility.text.label.issue.heading = Headings must be labeled. + +# LOCALIZATION NOTE (accessibility.text.label.issue.heading.content): A title text that +# describes that currently selected accessible object for a heading must have visible +# content. +accessibility.text.label.issue.heading.content = Headings should have visible text content. + +# LOCALIZATION NOTE (accessibility.text.label.issue.iframe): A title text that +# describes that currently selected accessible object for an