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