Bug 1320719 - The Esc key should deny permission requests for persistent prompts. r=florian draft
authorJohann Hofmann <jhofmann@mozilla.com>
Fri, 09 Dec 2016 15:04:01 -1000
changeset 456415 dd673fe21a890cfbb48447ecedfccefe1f54c09f
parent 455291 232424f9511d61179912ccaf5b169fb45f05599a
child 541212 7549e7e9ffacc1350431d9f3e63621083bb85b12
push id40481
push userbmo:jhofmann@mozilla.com
push dateThu, 05 Jan 2017 14:54:13 +0000
reviewersflorian
bugs1320719
milestone53.0a1
Bug 1320719 - The Esc key should deny permission requests for persistent prompts. r=florian MozReview-Commit-ID: 3qj0pub48q9
browser/base/content/test/popupNotifications/browser.ini
browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js
toolkit/modules/PopupNotifications.jsm
--- a/browser/base/content/test/popupNotifications/browser.ini
+++ b/browser/base/content/test/popupNotifications/browser.ini
@@ -11,12 +11,14 @@ skip-if = (os == "linux" && (debug || as
 [browser_popupNotification_3.js]
 skip-if = (os == "linux" && (debug || asan))
 [browser_popupNotification_4.js]
 skip-if = (os == "linux" && (debug || asan))
 [browser_popupNotification_5.js]
 skip-if = (os == "linux" && (debug || asan))
 [browser_popupNotification_checkbox.js]
 skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_keyboard.js]
+skip-if = (os == "linux" && (debug || asan))
 [browser_popupNotification_no_anchors.js]
 skip-if = (os == "linux" && (debug || asan))
 [browser_reshow_in_background.js]
 skip-if = (os == "linux" && (debug || asan))
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js
@@ -0,0 +1,53 @@
+/* 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/. */
+
+function test() {
+  waitForExplicitFinish();
+
+  ok(PopupNotifications, "PopupNotifications object exists");
+  ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+  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);
+      this.notifyObj.options.persistent = true;
+      showNotification(this.notifyObj);
+    },
+    onShown(popup) {
+      checkPopup(popup, this.notifyObj);
+      EventUtils.synthesizeKey("VK_ESCAPE", {});
+    },
+    onHidden(popup) {
+      ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked");
+      ok(this.notifyObj.secondaryActionClicked, "secondaryAction was clicked");
+      ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered");
+      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+    }
+  },
+  // Test that for non-persistent notifications, the escape key dismisses the notification.
+  { id: "Test#2",
+    *run() {
+      yield waitForWindowReadyForPopupNotifications(window);
+      this.notifyObj = new BasicNotification(this.id);
+      this.notification = showNotification(this.notifyObj);
+    },
+    onShown(popup) {
+      checkPopup(popup, this.notifyObj);
+      EventUtils.synthesizeKey("VK_ESCAPE", {});
+    },
+    onHidden(popup) {
+      ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked");
+      ok(!this.notifyObj.secondaryActionClicked, "secondaryAction was not clicked");
+      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
+      ok(!this.notifyObj.removedCallbackTriggered, "removed callback was not triggered");
+      this.notification.remove();
+    }
+  },
+];
--- a/toolkit/modules/PopupNotifications.jsm
+++ b/toolkit/modules/PopupNotifications.jsm
@@ -214,16 +214,43 @@ this.PopupNotifications = function Popup
   this.window = tabbrowser.ownerDocument.defaultView;
   this.panel = panel;
   this.tabbrowser = tabbrowser;
   this.iconBox = iconBox;
   this.buttonDelay = Services.prefs.getIntPref(PREF_SECURITY_DELAY);
 
   this.panel.addEventListener("popuphidden", this, true);
 
+  // This listener will be attached to the chrome window whenever a notification
+  // is showing, to allow the user to dismiss notifications using the escape key.
+  this._handleWindowKeyPress = aEvent => {
+    if (aEvent.keyCode != aEvent.DOM_VK_ESCAPE) {
+      return;
+    }
+
+    // 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;
+
+    // If the chrome window has a focused element, let it handle the ESC key instead.
+    if (!activeElement ||
+        activeElement == doc.body ||
+        activeElement == this.tabbrowser.selectedBrowser ||
+        // Ignore focused elements inside the notification.
+        getNotificationFromElement(activeElement) == notification ||
+        notification.contains(activeElement)) {
+      this._onButtonEvent(aEvent, "secondarybuttoncommand", notification);
+    }
+  };
+
   this.window.addEventListener("activate", this, true);
   if (this.tabbrowser.tabContainer)
     this.tabbrowser.tabContainer.addEventListener("TabSelect", this, true);
 }
 
 PopupNotifications.prototype = {
 
   window: null,
@@ -997,16 +1024,20 @@ PopupNotifications.prototype = {
       }
     }
 
     if (notificationsToShow.length > 0) {
       let anchorElement = anchors.values().next().value;
       if (anchorElement) {
         this._showPanel(notificationsToShow, anchorElement);
       }
+
+      // Setup a capturing event listener on the whole window to catch the
+      // escape key while persistent notifications are visible.
+      this.window.addEventListener("keypress", this._handleWindowKeyPress, true);
     } else {
       // Notify observers that we're not showing the popup (useful for testing)
       this._notify("updateNotShowing");
 
       // Close the panel if there are no notifications to show.
       // When called from PopupNotifications.show() we should never close the
       // panel, however. It may just be adding a dismissed notification, in
       // which case we want to continue showing any existing notifications.
@@ -1018,16 +1049,19 @@ PopupNotifications.prototype = {
       if (!haveNotifications) {
         if (useIconBox) {
           this.iconBox.hidden = true;
         } else if (anchors.size) {
           for (let anchorElement of anchors)
             anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING);
         }
       }
+
+      // Stop listening to keyboard events for notifications.
+      this.window.removeEventListener("keypress", this._handleWindowKeyPress, true);
     }
   },
 
   _updateAnchorIcons: function PopupNotifications_updateAnchorIcons(notifications,
                                                                     anchorElements) {
     for (let anchorElement of anchorElements) {
       anchorElement.setAttribute(ICON_ATTRIBUTE_SHOWING, "true");
       // Use the anchorID as a class along with the default icon class as a
@@ -1276,18 +1310,20 @@ PopupNotifications.prototype = {
         this._remove(notificationObj);
       } else {
         notificationObj.dismissed = true;
         this._fireCallback(notificationObj, NOTIFICATION_EVENT_DISMISSED);
       }
     }, this);
   },
 
-  _onButtonEvent(event, type) {
-    let notificationEl = getNotificationFromElement(event.originalTarget);
+  _onButtonEvent(event, type, notificationEl = null) {
+    if (!notificationEl) {
+      notificationEl = getNotificationFromElement(event.originalTarget);
+    }
 
     if (!notificationEl)
       throw "PopupNotifications._onButtonEvent: couldn't find notification element";
 
     if (!notificationEl.notification)
       throw "PopupNotifications._onButtonEvent: couldn't find notification";
 
     let notification = notificationEl.notification;