Bug 1334496 - Part 1 - Add an autofocus option to PopupNotifications.show. r=Paolo draft
authorJohann Hofmann <jhofmann@mozilla.com>
Fri, 17 Feb 2017 14:56:23 +0100
changeset 556363 75f395dcadd1134fe7ca6169533ffaa4c65a1485
parent 556295 720b9177c6856c1c4339d0fac1bf5149c0d53950
child 556364 ccae9db2d090fd9ab967159bb5a136a9c54ad02b
push id52525
push userbmo:jhofmann@mozilla.com
push dateWed, 05 Apr 2017 18:56:26 +0000
reviewersPaolo
bugs1334496
milestone55.0a1
Bug 1334496 - Part 1 - Add an autofocus option to PopupNotifications.show. r=Paolo MozReview-Commit-ID: DrJOjUWJJOD
browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js
toolkit/modules/PopupNotifications.jsm
--- a/browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js
@@ -3,17 +3,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 function test() {
   waitForExplicitFinish();
 
   ok(PopupNotifications, "PopupNotifications object exists");
   ok(PopupNotifications.panel, "PopupNotifications panel exists");
 
-  setup();
+  // Force tabfocus for all elements on OSX.
+  SpecialPowers.pushPrefEnv({"set": [["accessibility.tabfocus", 7]]}).then(setup);
 }
 
 var tests = [
   // Test that for persistent notifications,
   // the secondary action is triggered by pressing the escape key.
   { id: "Test#1",
     run() {
       this.notifyObj = new BasicNotification(this.id);
@@ -48,17 +49,16 @@ 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,
       });
       this.notification = showNotification(this.notifyObj);
     },
     *onShown(popup) {
@@ -71,18 +71,16 @@ var tests = [
       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",
         },
@@ -127,9 +125,51 @@ var tests = [
 
       is(document.activeElement, popup.childNodes[0].closebutton);
 
       notification1.remove();
       notification2.remove();
       goNext();
     },
   },
+  // Test that passing the autofocus option will focus an opened notification.
+  { id: "Test#5",
+    *run() {
+      this.notifyObj = new BasicNotification(this.id);
+      this.notifyObj.anchorID = "geo-notification-icon";
+      this.notifyObj.addOptions({
+        autofocus: true,
+      });
+      this.notification = showNotification(this.notifyObj);
+    },
+    *onShown(popup) {
+      checkPopup(popup, this.notifyObj);
+
+      // Initial focus on open is null because a panel itself
+      // can not be focused, next tab focus will be inside the panel.
+      is(Services.focus.focusedElement, null);
+
+      EventUtils.synthesizeKey("VK_TAB", {});
+      is(Services.focus.focusedElement, popup.childNodes[0].closebutton);
+      dismissNotification(popup);
+    },
+    *onHidden() {
+      // Focus the urlbar to check that it stays focused.
+      gURLBar.focus();
+
+      // Show another notification and make sure it's not autofocused.
+      let notifyObj = new BasicNotification(this.id);
+      notifyObj.id += "_2";
+      notifyObj.anchorID = "default-notification-icon";
+
+      let opened = waitForNotificationPanel();
+      let notification = showNotification(notifyObj);
+      let popup = yield opened;
+      checkPopup(popup, notifyObj);
+
+      // Check that the urlbar is still focused.
+      is(Services.focus.focusedElement, gURLBar.inputField);
+
+      this.notification.remove();
+      notification.remove();
+    }
+  },
 ];
--- a/toolkit/modules/PopupNotifications.jsm
+++ b/toolkit/modules/PopupNotifications.jsm
@@ -241,25 +241,25 @@ this.PopupNotifications = function Popup
 
     // Esc key cancels the topmost notification, if there is one.
     let notification = this.panel.firstChild;
     if (!notification) {
       return;
     }
 
     let doc = this.window.document;
-    let activeElement = doc.activeElement;
+    let focusedElement = Services.focus.focusedElement;
 
     // If the chrome window has a focused element, let it handle the ESC key instead.
-    if (!activeElement ||
-        activeElement == doc.body ||
-        activeElement == this.tabbrowser.selectedBrowser ||
+    if (!focusedElement ||
+        focusedElement == doc.body ||
+        focusedElement == this.tabbrowser.selectedBrowser ||
         // Ignore focused elements inside the notification.
-        getNotificationFromElement(activeElement) == notification ||
-        notification.contains(activeElement)) {
+        getNotificationFromElement(focusedElement) == notification ||
+        notification.contains(focusedElement)) {
       this._onButtonEvent(aEvent, "secondarybuttoncommand", notification);
     }
   };
 
   this.window.addEventListener("activate", this, true);
   if (this.tabbrowser.tabContainer)
     this.tabbrowser.tabContainer.addEventListener("TabSelect", this, true);
 }
@@ -349,16 +349,18 @@ PopupNotifications.prototype = {
    *        persistent:  A boolean. If true, the notification will always
    *                     persist even across tab and app changes (but not across
    *                     location changes), until the user accepts or rejects
    *                     the request. The notification will never be implicitly
    *                     dismissed.
    *        dismissed:   Whether the notification should be added as a dismissed
    *                     notification. Dismissed notifications can be activated
    *                     by clicking on their anchorElement.
+   *        autofocus:   Whether the notification should be autofocused on
+   *                     showing, stealing focus from any other focused element.
    *        eventCallback:
    *                     Callback to be invoked when the notification changes
    *                     state. The callback's first argument is a string
    *                     identifying the state change:
    *                     "dismissed": notification has been dismissed by the
    *                                  user (e.g. by clicking away or switching
    *                                  tabs)
    *                     "removed": notification has been removed (due to
@@ -460,16 +462,24 @@ PopupNotifications.prototype = {
     notifications.push(notification);
 
     let isActiveBrowser = this._isActiveBrowser(browser);
     let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
     let isActiveWindow = fm.activeWindow == this.window;
 
     if (isActiveBrowser) {
       if (isActiveWindow) {
+
+        // Autofocus if the notification requests focus.
+        if (options && !options.dismissed && options.autofocus) {
+          this.panel.removeAttribute("noautofocus");
+        } else {
+          this.panel.setAttribute("noautofocus", "true");
+        }
+
         // show panel now
         this._update(notifications, new Set([notification.anchorElement]), true);
       } else {
         // indicate attention and update the icon if necessary
         if (!notification.dismissed) {
           this.window.getAttention();
         }
         this._updateAnchorIcons(notifications, this._getAnchorsForNotifications(
@@ -1313,16 +1323,22 @@ 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 the notification specified it wants to autofocus on first show.
+    // When the panel is closed, we have to restore the attribute to its default
+    // value, so we don't autofocus it if it's 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();