Bug 1340112 - Move focus into popupnotification panels when selected via keyboard. r=florian draft
authorJohann Hofmann <jhofmann@mozilla.com>
Thu, 16 Feb 2017 15:39:07 +0100
changeset 485811 02eb026c8d6bf40e231fda37f364f893dbcd9e8b
parent 485098 a9ec72f82299250e6023988e238931bbca0ef7fa
child 486050 8fee95e4d2873038047f52045ffb142d7053f170
push id45860
push userbmo:jhofmann@mozilla.com
push dateFri, 17 Feb 2017 08:18:57 +0000
reviewersflorian
bugs1340112
milestone54.0a1
Bug 1340112 - Move focus into popupnotification panels when selected via keyboard. r=florian MozReview-Commit-ID: 3DSugKa3cQ3
browser/base/content/test/popupNotifications/browser_popupNotification_5.js
browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js
toolkit/modules/PopupNotifications.jsm
--- a/browser/base/content/test/popupNotifications/browser_popupNotification_5.js
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_5.js
@@ -334,16 +334,18 @@ var tests = [
   // Test clicking the anchor icon.
   // Clicking the anchor of an already visible persistent notification should
   // focus the main action button, but not cause additional showing/shown event
   // callback calls.
   // Clicking the anchor of a dismissed notification should show it, even when
   // the currently displayed notification is a persistent one.
   { id: "Test#11",
     *run() {
+      yield SpecialPowers.pushPrefEnv({"set": [["accessibility.tabfocus", 7]]});
+
       function clickAnchor(notifyObj) {
         let anchor = document.getElementById(notifyObj.anchorID);
         EventUtils.synthesizeMouseAtCenter(anchor, {});
       }
 
       let popup = PopupNotifications.panel;
 
       let notifyObj1 = new BasicNotification(this.id);
@@ -354,20 +356,21 @@ var tests = [
       let notification1 = showNotification(notifyObj1);
       yield shown;
       checkPopup(popup, notifyObj1);
       ok(!notifyObj1.dismissalCallbackTriggered,
          "Should not have dismissed the notification");
       notifyObj1.shownCallbackTriggered = false;
       notifyObj1.showingCallbackTriggered = false;
 
-      // Click the anchor. This should focus the primary button, but
-      // not call event callbacks on the notification object.
+      // Click the anchor. This should focus the closebutton
+      // (because it's the first focusable element), but not
+      // call event callbacks on the notification object.
       clickAnchor(notifyObj1);
-      is(document.activeElement, popup.childNodes[0].button);
+      is(document.activeElement, popup.childNodes[0].closebutton);
       ok(!notifyObj1.dismissalCallbackTriggered,
          "Should not have dismissed the notification");
       ok(!notifyObj1.shownCallbackTriggered,
          "Should have triggered the shown event again");
       ok(!notifyObj1.showingCallbackTriggered,
          "Should have triggered the showing event again");
 
       // Add another notification.
--- a/browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js
@@ -48,27 +48,88 @@ var tests = [
       ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
       ok(!this.notifyObj.removedCallbackTriggered, "removed callback was not triggered");
       this.notification.remove();
     }
   },
   // Test that the space key on an anchor element focuses an active notification
   { id: "Test#3",
     *run() {
+      yield SpecialPowers.pushPrefEnv({"set": [["accessibility.tabfocus", 7]]});
       this.notifyObj = new BasicNotification(this.id);
       this.notifyObj.anchorID = "geo-notification-icon";
       this.notifyObj.addOptions({
-        persistent: true
+        persistent: true,
       });
       this.notification = showNotification(this.notifyObj);
     },
     *onShown(popup) {
       checkPopup(popup, this.notifyObj);
       let anchor = document.getElementById(this.notifyObj.anchorID);
       anchor.focus();
       is(document.activeElement, anchor);
       EventUtils.synthesizeKey(" ", {});
-      is(document.activeElement, popup.childNodes[0].button);
+      is(document.activeElement, popup.childNodes[0].closebutton);
       this.notification.remove();
     },
     onHidden(popup) { }
   },
+  // Test that you can switch between active notifications with the space key
+  // and that the notification is focused on selection.
+  { id: "Test#4",
+    *run() {
+      yield SpecialPowers.pushPrefEnv({"set": [["accessibility.tabfocus", 7]]});
+
+      let notifyObj1 = new BasicNotification(this.id);
+      notifyObj1.id += "_1";
+      notifyObj1.anchorID = "default-notification-icon";
+      notifyObj1.addOptions({
+        hideClose: true,
+        checkbox: {
+          label: "Test that elements inside the panel can be focused",
+        },
+        persistent: true,
+      });
+      let opened = waitForNotificationPanel();
+      let notification1 = showNotification(notifyObj1);
+      yield opened;
+
+      let notifyObj2 = new BasicNotification(this.id);
+      notifyObj2.id += "_2";
+      notifyObj2.anchorID = "geo-notification-icon";
+      notifyObj2.addOptions({
+        persistent: true,
+      });
+      opened = waitForNotificationPanel();
+      let notification2 = showNotification(notifyObj2);
+      let popup = yield opened;
+
+      // Make sure notification 2 is visible
+      checkPopup(popup, notifyObj2);
+
+      // Activate the anchor for notification 1 and wait until it's shown.
+      let anchor = document.getElementById(notifyObj1.anchorID);
+      anchor.focus();
+      is(document.activeElement, anchor);
+      opened = waitForNotificationPanel();
+      EventUtils.synthesizeKey(" ", {});
+      popup = yield opened;
+      checkPopup(popup, notifyObj1);
+
+      is(document.activeElement, popup.childNodes[0].checkbox);
+
+      // Activate the anchor for notification 2 and wait until it's shown.
+      anchor = document.getElementById(notifyObj2.anchorID);
+      anchor.focus();
+      is(document.activeElement, anchor);
+      opened = waitForNotificationPanel();
+      EventUtils.synthesizeKey(" ", {});
+      popup = yield opened;
+      checkPopup(popup, notifyObj2);
+
+      is(document.activeElement, popup.childNodes[0].closebutton);
+
+      notification1.remove();
+      notification2.remove();
+      goNext();
+    },
+  },
 ];
--- a/toolkit/modules/PopupNotifications.jsm
+++ b/toolkit/modules/PopupNotifications.jsm
@@ -1219,27 +1219,27 @@ PopupNotifications.prototype = {
     }
 
     // If the panel is not closed, and the anchor is different, immediately mark all
     // active notifications for the previous anchor as dismissed
     if (this.panel.state != "closed" && anchor != this._currentAnchorElement) {
       this._dismissOrRemoveCurrentNotifications();
     }
 
-    // Ensure we move focus into the panel because it's opened through user interaction:
-    this.panel.removeAttribute("noautofocus");
-
     // Avoid reshowing notifications that are already shown and have not been dismissed.
     if (this.panel.state == "closed" || anchor != this._currentAnchorElement) {
-      this._reshowNotifications(anchor);
-    }
+      // As soon as the panel is shown, focus the first element in the selected notification.
+      this.panel.addEventListener("popupshown",
+        () => this.window.document.commandDispatcher.advanceFocusIntoSubtree(this.panel),
+        {once: true});
 
-    // If the user re-selects the current notification, focus it.
-    if (anchor == this._currentAnchorElement && this.panel.firstChild) {
-      this.panel.firstChild.button.focus();
+      this._reshowNotifications(anchor);
+    } else {
+      // Focus the first element in the selected notification.
+      this.window.document.commandDispatcher.advanceFocusIntoSubtree(this.panel);
     }
   },
 
   _reshowNotifications: function PopupNotifications_reshowNotifications(anchor, browser) {
     // Mark notifications anchored to this anchor as un-dismissed
     browser = browser || this.tabbrowser.selectedBrowser;
     let notifications = this._getNotificationsForBrowser(browser);
     notifications.forEach(function(n) {
@@ -1309,22 +1309,16 @@ PopupNotifications.prototype = {
     return undefined;
   },
 
   _onPopupHidden: function PopupNotifications_onPopupHidden(event) {
     if (event.target != this.panel) {
       return;
     }
 
-    // We may have removed the "noautofocus" attribute before showing the panel
-    // if it was opened with user interaction. When the panel is closed, we have
-    // to restore the attribute to its default value, so we don't autofocus it
-    // if it is subsequently opened from a different code path.
-    this.panel.setAttribute("noautofocus", "true");
-
     // Handle the case where the panel was closed programmatically.
     if (this._ignoreDismissal) {
       this._ignoreDismissal.resolve();
       this._ignoreDismissal = null;
       return;
     }
 
     this._dismissOrRemoveCurrentNotifications();