Bug 1171746 - ensure tab specific panels close when you switch the tab r?gijs draft
authorKatie Broida[:ktbee] <kbroida@gmail.com>
Fri, 22 Jul 2016 12:38:20 -0400
changeset 391386 922a6174a830f8fa342543d33f14800710f2cda3
parent 390747 4b83d342c9f79da4ec067392a1d5ed97e0edd683
child 526207 79fde7fac445fb2e513eb8cdf5d957433be1aed1
push id23892
push userbmo:kbroida@gmail.com
push dateFri, 22 Jul 2016 16:43:18 +0000
reviewersgijs
bugs1171746
milestone50.0a1
Bug 1171746 - ensure tab specific panels close when you switch the tab r?gijs Adds a tabspecific attribute to the edit bookmarks panel and the Pocket subview panel to signal that these popups should close when the user navigates away from the tab. It also specifies that the localized keyboard short cut for closing a window should close the edit bookmarks panel and the tab by adding a general function to check whether "key_close" has been pressed. MozReview-Commit-ID: AxW5uQgDQQB
browser/base/content/browser-places.js
browser/base/content/browser.js
browser/base/content/browser.xul
browser/base/content/test/general/browser_utilityOverlay.js
browser/base/content/utilityOverlay.js
browser/components/customizableui/CustomizableUI.jsm
browser/components/customizableui/content/panelUI.js
browser/extensions/pocket/bootstrap.js
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -123,16 +123,23 @@ var StarUI = {
                 aEvent.target.classList.contains("expander-down") ||
                 aEvent.target.id == "editBMPanel_newFolderButton")  {
               //XXX Why is this necessary? The defaultPrevented check should
               //    be enough.
               break;
             }
             this.panel.hidePopup();
             break;
+          // This case is for catching character-generating keypresses
+          case 0:
+            let accessKey = document.getElementById("key_close");
+            if (eventMatchesKey(aEvent, accessKey)) {
+                this.panel.hidePopup();
+            }
+            break;
         }
         break;
       case "mouseout":
         // Explicit fall-through
       case "popupshown":
         // Don't handle events for descendent elements.
         if (aEvent.target != aEvent.currentTarget) {
           break;
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1380,16 +1380,17 @@ var gBrowserInit = {
                         slot.status != Ci.nsIPKCS11Slot.SLOT_READY;
         if (mpEnabled) {
           Services.telemetry.getHistogramById("MASTER_PASSWORD_ENABLED").add(mpEnabled);
         }
       }, 5000);
 
       PanicButtonNotifier.init();
     });
+    gBrowser.tabContainer.addEventListener("TabSelect", this.closeTabSpecificPanels);
     this.delayedStartupFinished = true;
 
     Services.obs.notifyObservers(window, "browser-delayed-startup-finished", "");
     TelemetryTimestamps.add("delayedStartupFinished");
   },
 
   // Returns the URI(s) to load at startup.
   _getUriToLoad: function () {
@@ -4236,16 +4237,24 @@ var XULBrowserWindow = {
     elt.openPopupAtScreen(anchor.boxObject.screenX + x, anchor.boxObject.screenY + y, false, null);
   },
 
   hideTooltip: function () {
     let elt = document.getElementById("remoteBrowserTooltip");
     elt.hidePopup();
   },
 
+  closeTabSpecificPanels: function() {
+    for (let panel of document.querySelectorAll("panel[tabspecific='true']")) {
+      if (panel.state == "open") {
+        panel.hidePopup();
+      }
+    }
+  },
+
   getTabCount: function () {
     return gBrowser.tabs.length;
   },
 
   updateStatusField: function () {
     var text, type, types = ["overLink"];
     if (this._busyUI)
       types.push("status");
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -179,16 +179,17 @@
       <description/>
     </panel>
 
     <panel id="editBookmarkPanel"
            type="arrow"
            orient="vertical"
            ignorekeys="true"
            hidden="true"
+           tabspecific="true"
            onpopupshown="StarUI.panelShown(event);"
            aria-labelledby="editBookmarkPanelTitle">
       <row id="editBookmarkPanelHeader" align="center" hidden="true">
         <vbox align="center">
           <image id="editBookmarkPanelStarIcon"/>
         </vbox>
         <vbox>
           <label id="editBookmarkPanelTitle"/>
--- a/browser/base/content/test/general/browser_utilityOverlay.js
+++ b/browser/base/content/test/general/browser_utilityOverlay.js
@@ -1,13 +1,14 @@
 /* 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/. */
 
 const gTests = [
+  test_eventMatchesKey,
   test_getTopWin,
   test_getBoolPref,
   test_openNewTabWith,
   test_openUILink
 ];
 
 function test () {
   waitForExplicitFinish();
@@ -20,16 +21,41 @@ function runNextTest() {
     info("Running " + testFun.name);
     testFun()
   }
   else {
     finish();
   }
 }
 
+function test_eventMatchesKey() {
+  let eventMatchResult;
+  document.addEventListener("keypress", function(e) {
+      eventMatchResult = eventMatchesKey(e, key);
+  });
+
+  let key = document.getElementById("key_find");
+  EventUtils.synthesizeKey("f", {accelKey: true});
+  is(eventMatchResult, true, "eventMatchesKey: one modifier");
+
+  key = document.getElementById("key_findPrevious");
+  EventUtils.synthesizeKey("g", {accelKey: true, shiftKey: true});
+  is(eventMatchResult, true, "eventMatchesKey: combination modifiers");
+
+  key = document.getElementById("key_close");
+  EventUtils.synthesizeKey("f", {accelKey: true});
+  is(eventMatchResult, false, "eventMatchesKey: mismatch keys");
+
+  key = document.getElementById("key_delete");
+  EventUtils.synthesizeKey("VK_DELETE", {accelKey: true});
+  is(eventMatchResult, false, "eventMatchesKey: mismatch modifiers");
+
+  runNextTest();
+}
+
 function test_getTopWin() {
   is(getTopWin(), window, "got top window");
   runNextTest();
 }
 
 
 function test_getBoolPref() {
   is(getBoolPref("browser.search.openintab", false), false, "getBoolPref");
--- a/browser/base/content/utilityOverlay.js
+++ b/browser/base/content/utilityOverlay.js
@@ -471,16 +471,59 @@ function closeMenus(node)
     if (node.namespaceURI == "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
     && (node.tagName == "menupopup" || node.tagName == "popup"))
       node.hidePopup();
 
     closeMenus(node.parentNode);
   }
 }
 
+/** This function takes in a key element and compares it to the keys pressed during an event.
+ *
+ * @param aEvent
+ *        The KeyboardEvent event you want to compare against your key.
+ *
+ * @param aKey
+ *        The <key> element checked to see if it was called in aEvent.
+ *        For example, aKey can be a variable set to document.getElementById("key_close")
+ *        to check if the close command key was pressed in aEvent.
+*/
+function eventMatchesKey(aEvent, aKey)
+{
+  let keyPressed = aKey.getAttribute("key").toLowerCase();
+  let keyModifiers = aKey.getAttribute("modifiers");
+  let modifiers = ["Alt", "Control", "Meta", "Shift"];
+  let keysMatch;
+
+  if (aEvent.key == keyPressed) {
+    keysMatch = true;
+  } else {
+    return false;
+  }
+  let eventModifiers = modifiers.filter(modifier => aEvent.getModifierState(modifier));
+  // Check if aEvent has a modifier and aKey doesn't
+  if (eventModifiers.length > 0 && keyModifiers.length == 0) {
+     return false;
+  }
+  // Check whether aKey's modifiers match aEvent's modifiers
+  if (keyModifiers) {
+    keyModifiers = keyModifiers.split(/[\s,]+/);
+    // Capitalize first letter of aKey's modifers to compare to aEvent's modifier
+    keyModifiers.forEach(function(modifier, index) {
+      if (modifier == "accel") {
+        AppConstants.platform == "macosx" ? keyModifiers[index] = "Meta" : keyModifiers[index] = "Control";
+      } else {
+        keyModifiers[index] = modifier[0].toUpperCase() + modifier.slice(1);
+      }
+    });
+    return modifiers.every(modifier => keyModifiers.includes(modifier) == aEvent.getModifierState(modifier));
+  }
+  return keysMatch;
+}
+
 // Gather all descendent text under given document node.
 function gatherTextUnder ( root )
 {
   var text = "";
   var node = root.firstChild;
   var depth = 1;
   while ( node && depth > 0 ) {
     // See if this node is text.
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -1355,16 +1355,19 @@ var CustomizableUIInternal = {
       node.setAttribute("id", aWidget.id);
       node.setAttribute("widget-id", aWidget.id);
       node.setAttribute("widget-type", aWidget.type);
       if (aWidget.disabled) {
         node.setAttribute("disabled", true);
       }
       node.setAttribute("removable", aWidget.removable);
       node.setAttribute("overflows", aWidget.overflows);
+      if (aWidget.tabSpecific) {
+        node.setAttribute("tabspecific", aWidget.tabSpecific);
+      }
       node.setAttribute("label", this.getLocalizedProperty(aWidget, "label"));
       let additionalTooltipArguments = [];
       if (aWidget.shortcutId) {
         let keyEl = aDocument.getElementById(aWidget.shortcutId);
         if (keyEl) {
           additionalTooltipArguments.push(ShortcutUtils.prettifyShortcut(keyEl));
         } else {
           log.error("Key element with id '" + aWidget.shortcutId + "' for widget '" + aWidget.id +
@@ -2307,16 +2310,17 @@ var CustomizableUIInternal = {
       implementation: aData,
       source: aSource || CustomizableUI.SOURCE_EXTERNAL,
       instances: new Map(),
       currentArea: null,
       removable: true,
       overflows: true,
       defaultArea: null,
       shortcutId: null,
+      tabspecific: false,
       tooltiptext: null,
       showInPrivateBrowsing: true,
       _introducedInVersion: -1,
     };
 
     if (typeof aData.id != "string" || !/^[a-z0-9-_]{1,}$/i.test(aData.id)) {
       log.error("Given an illegal id in normalizeWidget: " + aData.id);
       return null;
@@ -2337,17 +2341,17 @@ var CustomizableUIInternal = {
 
     const kOptStringProps = ["label", "tooltiptext", "shortcutId"];
     for (let prop of kOptStringProps) {
       if (typeof aData[prop] == "string") {
         widget[prop] = aData[prop];
       }
     }
 
-    const kOptBoolProps = ["removable", "showInPrivateBrowsing", "overflows"];
+    const kOptBoolProps = ["removable", "showInPrivateBrowsing", "overflows", "tabSpecific"];
     for (let prop of kOptBoolProps) {
       if (typeof aData[prop] == "boolean") {
         widget[prop] = aData[prop];
       }
     }
 
     // When we normalize builtin widgets, areas have not yet been registered:
     if (aData.defaultArea &&
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -330,16 +330,19 @@ const PanelUI = {
         return;
       }
 
       let tempPanel = document.createElement("panel");
       tempPanel.setAttribute("type", "arrow");
       tempPanel.setAttribute("id", "customizationui-widget-panel");
       tempPanel.setAttribute("class", "cui-widget-panel");
       tempPanel.setAttribute("viewId", aViewId);
+      if (aAnchor.getAttribute("tabspecific")) {
+        tempPanel.setAttribute("tabspecific", true);
+      }
       if (this._disableAnimations) {
         tempPanel.setAttribute("animate", "false");
       }
       tempPanel.setAttribute("context", "");
       document.getElementById(CustomizableUI.AREA_NAVBAR).appendChild(tempPanel);
       // If the view has a footer, set a convenience class on the panel.
       tempPanel.classList.toggle("cui-widget-panelWithFooter",
                                  viewNode.querySelector(".panel-subview-footer"));
--- a/browser/extensions/pocket/bootstrap.js
+++ b/browser/extensions/pocket/bootstrap.js
@@ -135,16 +135,17 @@ function CreatePocketWidget(reason) {
   // if upgrading from builtin version and the button was placed in ui,
   // seenWidget will not be null
   let seenWidget = CustomizableUI.getPlacementOfWidget("pocket-button", false, true);
   let pocketButton = {
     id: "pocket-button",
     defaultArea: CustomizableUI.AREA_NAVBAR,
     introducedInVersion: "pref",
     type: "view",
+    tabSpecific: true,
     viewId: "PanelUI-pocketView",
     label: gPocketBundle.GetStringFromName("pocket-button.label"),
     tooltiptext: gPocketBundle.GetStringFromName("pocket-button.tooltiptext"),
     // Use forwarding functions here to avoid loading Pocket.jsm on startup:
     onViewShowing: function() {
       return Pocket.onPanelViewShowing.apply(this, arguments);
     },
     onViewHiding: function() {