Bug 1587809: Give the CFR address bar button a more descriptive tooltip/a11y label. Announce its appearance to screen reader users. r=andreio,fluent-reviewers,flod
authorJames Teh <jteh@mozilla.com>
Mon, 21 Oct 2019 07:13:14 +0000
changeset 498314 82a89b1ae03ef722acc5088a215cd28297efbebe
parent 498309 28171f948a0874143e59c9551712b8fa08e50f55
child 498315 99b184e791f9717ae0cef21aecc4dc02dd9938b6
push id36717
push usernbeleuzu@mozilla.com
push dateMon, 21 Oct 2019 21:51:55 +0000
treeherdermozilla-central@563f437f24b9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersandreio, fluent-reviewers, flod
bugs1587809
milestone71.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1587809: Give the CFR address bar button a more descriptive tooltip/a11y label. Announce its appearance to screen reader users. r=andreio,fluent-reviewers,flod 1. Previously, the label and tooltip for all recommendations was just "Recommendation", even though the icon was different for extension and feature recommendations. Because users might not be able to see the icon and/or determine its meaning, it is important that this is communicated in the tooltip and a11y label. 2. Screen reader users won't know this has appeared, even though this attracts some attention visually. Therefore, provide a specific announcement for screen reader users when the recommendation appears. Differential Revision: https://phabricator.services.mozilla.com/D47718
browser/components/newtab/lib/CFRMessageProvider.jsm
browser/components/newtab/lib/CFRPageActions.jsm
browser/components/newtab/lib/PanelTestProvider.jsm
browser/components/newtab/test/browser/browser_asrouter_cfr.js
browser/components/newtab/test/unit/asrouter/ASRouter.test.js
browser/components/newtab/test/unit/asrouter/CFRPageActions.test.js
browser/components/newtab/test/unit/asrouter/constants.js
browser/locales/en-US/browser/newtab/asrouter.ftl
--- a/browser/components/newtab/lib/CFRMessageProvider.jsm
+++ b/browser/components/newtab/lib/CFRMessageProvider.jsm
@@ -124,17 +124,19 @@ const PINNED_TABS_TARGET_LOCALES = [
 const CFR_MESSAGES = [
   {
     id: "FACEBOOK_CONTAINER_3",
     template: "cfr_doorhanger",
     content: {
       layout: "addon_recommendation",
       category: "cfrAddons",
       bucket_id: "CFR_M1",
-      notification_text: { string_id: "cfr-doorhanger-extension-notification" },
+      notification_text: {
+        string_id: "cfr-doorhanger-extension-notification2",
+      },
       heading_text: { string_id: "cfr-doorhanger-extension-heading" },
       info_icon: {
         label: { string_id: "cfr-doorhanger-extension-sumo-link" },
         sumo_path: FACEBOOK_CONTAINER_PARAMS.sumo_path,
       },
       addon: {
         id: "954390",
         title: "Facebook Container",
@@ -193,17 +195,19 @@ const CFR_MESSAGES = [
   },
   {
     id: "GOOGLE_TRANSLATE_3",
     template: "cfr_doorhanger",
     content: {
       layout: "addon_recommendation",
       category: "cfrAddons",
       bucket_id: "CFR_M1",
-      notification_text: { string_id: "cfr-doorhanger-extension-notification" },
+      notification_text: {
+        string_id: "cfr-doorhanger-extension-notification2",
+      },
       heading_text: { string_id: "cfr-doorhanger-extension-heading" },
       info_icon: {
         label: { string_id: "cfr-doorhanger-extension-sumo-link" },
         sumo_path: GOOGLE_TRANSLATE_PARAMS.sumo_path,
       },
       addon: {
         id: "445852",
         title: "To Google Translate",
@@ -263,17 +267,19 @@ const CFR_MESSAGES = [
   },
   {
     id: "YOUTUBE_ENHANCE_3",
     template: "cfr_doorhanger",
     content: {
       layout: "addon_recommendation",
       category: "cfrAddons",
       bucket_id: "CFR_M1",
-      notification_text: { string_id: "cfr-doorhanger-extension-notification" },
+      notification_text: {
+        string_id: "cfr-doorhanger-extension-notification2",
+      },
       heading_text: { string_id: "cfr-doorhanger-extension-heading" },
       info_icon: {
         label: { string_id: "cfr-doorhanger-extension-sumo-link" },
         sumo_path: YOUTUBE_ENHANCE_PARAMS.sumo_path,
       },
       addon: {
         id: "700308",
         title: "Enhancer for YouTube\u2122",
@@ -334,17 +340,19 @@ const CFR_MESSAGES = [
   {
     id: "WIKIPEDIA_CONTEXT_MENU_SEARCH_3",
     template: "cfr_doorhanger",
     exclude: true,
     content: {
       layout: "addon_recommendation",
       category: "cfrAddons",
       bucket_id: "CFR_M1",
-      notification_text: { string_id: "cfr-doorhanger-extension-notification" },
+      notification_text: {
+        string_id: "cfr-doorhanger-extension-notification2",
+      },
       heading_text: { string_id: "cfr-doorhanger-extension-heading" },
       info_icon: {
         label: { string_id: "cfr-doorhanger-extension-sumo-link" },
         sumo_path: WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS.sumo_path,
       },
       addon: {
         id: "659026",
         title: "Wikipedia Context Menu Search",
@@ -408,17 +416,19 @@ const CFR_MESSAGES = [
   {
     id: "REDDIT_ENHANCEMENT_3",
     template: "cfr_doorhanger",
     exclude: true,
     content: {
       layout: "addon_recommendation",
       category: "cfrAddons",
       bucket_id: "CFR_M1",
-      notification_text: { string_id: "cfr-doorhanger-extension-notification" },
+      notification_text: {
+        string_id: "cfr-doorhanger-extension-notification2",
+      },
       heading_text: { string_id: "cfr-doorhanger-extension-heading" },
       info_icon: {
         label: { string_id: "cfr-doorhanger-extension-sumo-link" },
         sumo_path: REDDIT_ENHANCEMENT_PARAMS.sumo_path,
       },
       addon: {
         id: "387429",
         title: "Reddit Enhancement Suite",
@@ -478,17 +488,17 @@ const CFR_MESSAGES = [
   },
   {
     id: "PIN_TAB",
     template: "cfr_doorhanger",
     content: {
       layout: "message_and_animation",
       category: "cfrFeatures",
       bucket_id: "CFR_PIN_TAB",
-      notification_text: { string_id: "cfr-doorhanger-extension-notification" },
+      notification_text: { string_id: "cfr-doorhanger-feature-notification" },
       heading_text: { string_id: "cfr-doorhanger-pintab-heading" },
       info_icon: {
         label: { string_id: "cfr-doorhanger-extension-sumo-link" },
         sumo_path: REDDIT_ENHANCEMENT_PARAMS.sumo_path,
       },
       text: { string_id: "cfr-doorhanger-pintab-description" },
       descriptionDetails: {
         steps: [
@@ -592,17 +602,17 @@ const CFR_MESSAGES = [
       },
       info_icon: {
         label: {
           string_id: "cfr-doorhanger-extension-sumo-link",
         },
         sumo_path: "extensionrecommendations",
       },
       notification_text: {
-        string_id: "cfr-doorhanger-extension-notification",
+        string_id: "cfr-doorhanger-feature-notification",
       },
       category: "cfrFeatures",
     },
     trigger: {
       id: "newSavedLogin",
     },
   },
   {
--- a/browser/components/newtab/lib/CFRPageActions.jsm
+++ b/browser/components/newtab/lib/CFRPageActions.jsm
@@ -177,23 +177,28 @@ class PageAction {
 
   reloadL10n() {
     this._l10n = this._createDOML10n();
   }
 
   async showAddressBarNotifier(recommendation, shouldExpand = false) {
     this.container.hidden = false;
 
-    this.label.value = await this.getStrings(
+    let notificationText = await this.getStrings(
       recommendation.content.notification_text
     );
-
+    this.label.value = notificationText;
     this.button.setAttribute(
       "tooltiptext",
-      await this.getStrings(recommendation.content.notification_text)
+      notificationText.attributes.tooltiptext
+    );
+    // For a11y, we want the more descriptive text.
+    this.container.setAttribute(
+      "aria-label",
+      notificationText.attributes.tooltiptext
     );
     this.button.setAttribute(
       "data-cfr-icon",
       CATEGORY_ICONS[recommendation.content.category]
     );
 
     // Wait for layout to flush to avoid a synchronous reflow then calculate the
     // label width. We can safely get the width even though the recommendation is
@@ -211,16 +216,21 @@ class PageAction {
     if (shouldExpand) {
       this._clearScheduledStateChanges();
 
       // After one second, expand
       this._expand(DELAY_BEFORE_EXPAND_MS);
 
       this.addImpression(recommendation);
     }
+
+    this.window.A11yUtils.announce({
+      raw: notificationText.attributes["a11y-announcement"],
+      source: this.container,
+    });
   }
 
   hideAddressBarNotifier() {
     this.container.hidden = true;
     this._clearScheduledStateChanges();
     this.urlbar.removeAttribute("cfr-recommendation-state");
     this.container.removeEventListener("click", this._showPopupOnClick);
     this.urlbar.removeEventListener("focus", this._collapse);
--- a/browser/components/newtab/lib/PanelTestProvider.jsm
+++ b/browser/components/newtab/lib/PanelTestProvider.jsm
@@ -138,17 +138,17 @@ const MESSAGES = () => [
     trigger: { id: "whatsNewPanelOpened" },
   },
   {
     id: "BOOKMARK_CFR",
     template: "cfr_doorhanger",
     content: {
       layout: "icon_and_message",
       category: "cfrFeatures",
-      notification_text: { string_id: "cfr-doorhanger-extension-notification" },
+      notification_text: { string_id: "cfr-doorhanger-feature-notification" },
       heading_text: { string_id: "cfr-doorhanger-sync-bookmarks-header" },
       info_icon: {
         label: { string_id: "cfr-doorhanger-extension-sumo-link" },
         sumo_path: "https://example.com",
       },
       text: { string_id: "cfr-doorhanger-sync-bookmarks-body" },
       icon: "chrome://branding/content/icon64.png",
       buttons: {
--- a/browser/components/newtab/test/browser/browser_asrouter_cfr.js
+++ b/browser/components/newtab/test/browser/browser_asrouter_cfr.js
@@ -10,75 +10,82 @@ const { ASRouter } = ChromeUtils.import(
 
 const createDummyRecommendation = ({
   action,
   category,
   heading_text,
   layout,
   skip_address_bar_notifier,
   template,
-}) => ({
-  template,
-  content: {
-    layout: layout || "addon_recommendation",
-    category,
-    anchor_id: "page-action-buttons",
-    skip_address_bar_notifier,
-    notification_text: "Mochitest",
-    heading_text: heading_text || "Mochitest",
-    info_icon: {
-      label: { attributes: { tooltiptext: "Why am I seeing this" } },
-      sumo_path: "extensionrecommendations",
-    },
-    icon: "foo",
-    icon_dark_theme: "bar",
-    learn_more: "extensionrecommendations",
-    addon: {
-      id: "addon-id",
-      title: "Addon name",
+}) => {
+  let recommendation = {
+    template,
+    content: {
+      layout: layout || "addon_recommendation",
+      category,
+      anchor_id: "page-action-buttons",
+      skip_address_bar_notifier,
+      heading_text: heading_text || "Mochitest",
+      info_icon: {
+        label: { attributes: { tooltiptext: "Why am I seeing this" } },
+        sumo_path: "extensionrecommendations",
+      },
       icon: "foo",
-      author: "Author name",
-      amo_url: "https://example.com",
-    },
-    descriptionDetails: { steps: [] },
-    text: "Mochitest",
-    buttons: {
-      primary: {
-        label: {
-          value: "OK",
-          attributes: { accesskey: "O" },
-        },
-        action: {
-          type: action.type,
-          data: {},
-        },
+      icon_dark_theme: "bar",
+      learn_more: "extensionrecommendations",
+      addon: {
+        id: "addon-id",
+        title: "Addon name",
+        icon: "foo",
+        author: "Author name",
+        amo_url: "https://example.com",
       },
-      secondary: [
-        {
+      descriptionDetails: { steps: [] },
+      text: "Mochitest",
+      buttons: {
+        primary: {
           label: {
-            value: "Cancel",
-            attributes: { accesskey: "C" },
+            value: "OK",
+            attributes: { accesskey: "O" },
+          },
+          action: {
+            type: action.type,
+            data: {},
           },
         },
-        {
-          label: {
-            value: "Cancel 1",
-            attributes: { accesskey: "A" },
+        secondary: [
+          {
+            label: {
+              value: "Cancel",
+              attributes: { accesskey: "C" },
+            },
+          },
+          {
+            label: {
+              value: "Cancel 1",
+              attributes: { accesskey: "A" },
+            },
           },
-        },
-        {
-          label: {
-            value: "Cancel 2",
-            attributes: { accesskey: "B" },
+          {
+            label: {
+              value: "Cancel 2",
+              attributes: { accesskey: "B" },
+            },
           },
-        },
-      ],
+        ],
+      },
     },
-  },
-});
+  };
+  recommendation.content.notification_text = new String("Mochitest"); // eslint-disable-line
+  recommendation.content.notification_text.attributes = {
+    tooltiptext: "Mochitest tooltip",
+    "a11y-announcement": "Mochitest announcement",
+  };
+  return recommendation;
+};
 
 function checkCFRFeaturesElements(notification) {
   Assert.ok(notification.hidden === false, "Panel should be visible");
   Assert.equal(
     notification.getAttribute("data-notification-category"),
     "message_and_animation",
     "Panel have correct data attribute"
   );
--- a/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
+++ b/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
@@ -2489,16 +2489,17 @@ describe("ASRouter", () => {
           sendAsyncMessage: sandbox.stub(),
           documentURI: { scheme: "https", host: "mozilla.com" },
         };
         target.ownerGlobal = {
           gBrowser: { selectedBrowser: target },
           document: { getElementById },
           promiseDocumentFlushed: sandbox.stub().resolves([{ width: 0 }]),
           setTimeout: sandbox.stub(),
+          A11yUtils: { announce: sandbox.stub() },
         };
         const firstMessage = { ...FAKE_RECOMMENDATION, id: "first_message" };
         const secondMessage = { ...FAKE_RECOMMENDATION, id: "second_message" };
         await Router.setState({ messages: [firstMessage, secondMessage] });
         global.DOMLocalization = class DOMLocalization {};
         sandbox.spy(CFRPageActions, "addRecommendation");
         sandbox.stub(Router, "addImpression").resolves();
 
--- a/browser/components/newtab/test/unit/asrouter/CFRPageActions.test.js
+++ b/browser/components/newtab/test/unit/asrouter/CFRPageActions.test.js
@@ -7,16 +7,17 @@ describe("CFRPageActions", () => {
   let clock;
   let fakeRecommendation;
   let fakeHost;
   let fakeBrowser;
   let dispatchStub;
   let globals;
   let containerElem;
   let elements;
+  let announceStub;
 
   const elementIDs = [
     "urlbar",
     "urlbar-input",
     "contextual-feature-recommendation",
     "cfr-button",
     "cfr-label",
     "contextual-feature-recommendation-notification",
@@ -36,16 +37,18 @@ describe("CFRPageActions", () => {
     "cfr-notification-footer-animation-label",
   ];
   const elementClassNames = ["popup-notification-body-container"];
 
   beforeEach(() => {
     sandbox = sinon.createSandbox();
     clock = sandbox.useFakeTimers();
 
+    announceStub = sandbox.stub();
+    const A11yUtils = { announce: announceStub };
     fakeRecommendation = { ...FAKE_RECOMMENDATION };
     fakeHost = "mozilla.org";
     fakeBrowser = {
       documentURI: {
         scheme: "https",
         host: fakeHost,
       },
       ownerGlobal: window,
@@ -59,16 +62,17 @@ describe("CFRPageActions", () => {
         .stub()
         .callsFake(fn => Promise.resolve(fn())),
       PopupNotifications: {
         show: sandbox.stub(),
         remove: sandbox.stub(),
       },
       PrivateBrowsingUtils: { isWindowPrivate: sandbox.stub().returns(false) },
       gBrowser: { selectedBrowser: fakeBrowser },
+      A11yUtils,
     });
     document.createXULElement = document.createElement;
 
     elements = {};
     const [body] = document.getElementsByTagName("body");
     containerElem = document.createElement("div");
     body.appendChild(containerElem);
     for (const id of elementIDs) {
@@ -89,32 +93,29 @@ describe("CFRPageActions", () => {
     CFRPageActions.clearRecommendations();
     containerElem.remove();
     sandbox.restore();
     globals.restore();
   });
 
   describe("PageAction", () => {
     let pageAction;
-    let getStringsStub;
 
     beforeEach(() => {
       pageAction = new PageAction(window, dispatchStub);
-      getStringsStub = sandbox.stub(pageAction, "getStrings").resolves("");
     });
 
     describe("#showAddressBarNotifier", () => {
       it("should un-hideAddressBarNotifier the element and set the right label value", async () => {
-        const FAKE_NOTIFICATION_TEXT = "FAKE_NOTIFICATION_TEXT";
-        getStringsStub
-          .withArgs(fakeRecommendation.content.notification_text)
-          .resolves(FAKE_NOTIFICATION_TEXT);
         await pageAction.showAddressBarNotifier(fakeRecommendation);
         assert.isFalse(pageAction.container.hidden);
-        assert.equal(pageAction.label.value, FAKE_NOTIFICATION_TEXT);
+        assert.equal(
+          pageAction.label.value,
+          fakeRecommendation.content.notification_text
+        );
       });
       it("should wait for the document layout to flush", async () => {
         sandbox.spy(pageAction.label, "getClientRects");
         await pageAction.showAddressBarNotifier(fakeRecommendation);
         assert.calledOnce(global.promiseDocumentFlushed);
         assert.callOrder(
           global.promiseDocumentFlushed,
           pageAction.label.getClientRects
@@ -351,17 +352,16 @@ describe("CFRPageActions", () => {
             { name: "first_attr", value: 42 },
             { name: "second_attr", value: "some string" },
             { name: "third_attr", value: [1, 2, 3] },
           ],
         },
       ];
 
       beforeEach(() => {
-        getStringsStub.restore();
         formatMessagesStub = sandbox
           .stub()
           .withArgs({ id: "hello_world" })
           .resolves(localeStrings);
         global.DOMLocalization.prototype.formatMessages = formatMessagesStub;
       });
 
       it("should return the argument if a string_id is not defined", async () => {
@@ -428,24 +428,26 @@ describe("CFRPageActions", () => {
         assert.calledOnce(stub);
         stub.restore();
       });
     });
 
     describe("#_showPopupOnClick", () => {
       let translateElementsStub;
       let setAttributesStub;
+      let getStringsStub;
       beforeEach(async () => {
         CFRPageActions.PageActionMap.set(fakeBrowser.ownerGlobal, pageAction);
         await CFRPageActions.addRecommendation(
           fakeBrowser,
           fakeHost,
           fakeRecommendation,
           dispatchStub
         );
+        getStringsStub = sandbox.stub(pageAction, "getStrings").resolves("");
         getStringsStub
           .callsFake(async a => a) // eslint-disable-line max-nested-callbacks
           .withArgs({ string_id: "primary_button_id" })
           .resolves({ value: "Primary Button", attributes: { accesskey: "p" } })
           .withArgs({ string_id: "secondary_button_id" })
           .resolves({
             value: "Secondary Button",
             attributes: { accesskey: "s" },
--- a/browser/components/newtab/test/unit/asrouter/constants.js
+++ b/browser/components/newtab/test/unit/asrouter/constants.js
@@ -76,23 +76,26 @@ export const FAKE_REMOTE_PROVIDER = {
 
 export const FAKE_REMOTE_SETTINGS_PROVIDER = {
   id: "remotey-settingsy",
   type: "remote-settings",
   bucket: "bucketname",
   enabled: true,
 };
 
+const notificationText = new String("Fake notification text"); // eslint-disable-line
+notificationText.attributes = { tooltiptext: "Fake tooltip text" };
+
 export const FAKE_RECOMMENDATION = {
   id: "fake_id",
   template: "cfr_doorhanger",
   content: {
     category: "cfrDummy",
     bucket_id: "fake_bucket_id",
-    notification_text: "Fake Notification Text",
+    notification_text: notificationText,
     info_icon: {
       label: "Fake Info Icon Label",
       sumo_path: "a_help_path_fragment",
     },
     heading_text: "Fake Heading Text",
     addon: {
       title: "Fake Addon Title",
       author: "Fake Addon Author",
--- a/browser/locales/en-US/browser/newtab/asrouter.ftl
+++ b/browser/locales/en-US/browser/newtab/asrouter.ftl
@@ -30,17 +30,25 @@ cfr-doorhanger-extension-learn-more-link
 
 # This string is used on a new line below the add-on name
 # Variables:
 #   $name (String) - Add-on author name
 cfr-doorhanger-extension-author = by { $name }
 
 # This is a notification displayed in the address bar.
 # When clicked it opens a panel with a message for the user.
-cfr-doorhanger-extension-notification = Recommendation
+cfr-doorhanger-extension-notification2 = Recommendation
+  .tooltiptext = Extension recommendation
+  .a11y-announcement = Extension recommendation available
+
+# This is a notification displayed in the address bar.
+# When clicked it opens a panel with a message for the user.
+cfr-doorhanger-feature-notification = Recommendation
+  .tooltiptext = Feature recommendation
+  .a11y-announcement = Feature recommendation available
 
 ## Add-on statistics
 ## These strings are used to display the total number of
 ## users and rating for an add-on. They are shown next to each other.
 
 # Variables:
 #   $total (Number) - The rating of the add-on from 1 to 5
 cfr-doorhanger-extension-rating =