Bug 1171746 - ensure tab specific panels close when you switch the tab r=jaws
☠☠ backed out by b4d5fa60f8e4 ☠ ☠
authorKatie Broida[:ktbee] <kbroida@gmail.com>
Thu, 11 Aug 2016 16:51:45 -0400
changeset 400404 33fcf83eefe43e02f560fdf9c182b88c87343cc1
parent 400403 316a84b6888a45ff70b5badf10a5899bdc09875e
child 400405 bb7b33ca26fa10d442577f62909c27890de8d752
push id26138
push usergszorc@mozilla.com
push dateSat, 13 Aug 2016 03:04:36 +0000
reviewersjaws
bugs1171746
milestone51.0a1
Bug 1171746 - ensure tab specific panels close when you switch the tab r=jaws 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 a certain <key> has been pressed. Adds tests for both closing specific tabs and checking keys. 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/test/tabPrompts/browser.ini
browser/base/content/test/tabPrompts/browser_closeTabSpecificPanels.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
@@ -1351,16 +1351,25 @@ 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", function() {
+      for (let panel of document.querySelectorAll("panel[tabspecific='true']")) {
+        if (panel.state == "open") {
+          panel.hidePopup();
+        }
+      }
+    });
+
     this.delayedStartupFinished = true;
 
     Services.obs.notifyObservers(window, "browser-delayed-startup-finished", "");
     TelemetryTimestamps.add("delayedStartupFinished");
   },
 
   // Returns the URI(s) to load at startup.
   _getUriToLoad: function () {
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -172,16 +172,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,59 @@ function runNextTest() {
     info("Running " + testFun.name);
     testFun()
   }
   else {
     finish();
   }
 }
 
+function test_eventMatchesKey() {
+  let eventMatchResult;
+  document.addEventListener("keypress", function(e) {
+      e.stopPropagation();
+      e.preventDefault();
+      eventMatchResult = eventMatchesKey(e, key);
+  });
+
+  let key = document.createElement("key");
+  let keyset = document.getElementById("mainKeyset");
+  key.setAttribute("key", "t");
+  key.setAttribute("modifiers", "accel");
+  keyset.appendChild(key);
+  EventUtils.synthesizeKey("t", {accelKey: true});
+  is(eventMatchResult, true, "eventMatchesKey: one modifier");
+  keyset.removeChild(key);
+
+  key = document.createElement("key");
+  key.setAttribute("key", "g");
+  key.setAttribute("modifiers", "accel,shift");
+  keyset.appendChild(key);
+  EventUtils.synthesizeKey("g", {accelKey: true, shiftKey: true});
+  is(eventMatchResult, true, "eventMatchesKey: combination modifiers");
+  keyset.removeChild(key);
+
+  key = document.createElement("key");
+  key.setAttribute("key", "w");
+  key.setAttribute("modifiers", "accel");
+  keyset.appendChild(key);
+  EventUtils.synthesizeKey("f", {accelKey: true});
+  is(eventMatchResult, false, "eventMatchesKey: mismatch keys");
+  keyset.removeChild(key);
+
+  key = document.createElement("key");
+  key.setAttribute("keycode", "VK_DELETE");
+  keyset.appendChild(key);
+  EventUtils.synthesizeKey("VK_DELETE", {accelKey: true});
+  is(eventMatchResult, false, "eventMatchesKey: mismatch modifiers");
+  keyset.removeChild(key);
+
+  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/test/tabPrompts/browser.ini
+++ b/browser/base/content/test/tabPrompts/browser.ini
@@ -1,3 +1,4 @@
+[browser_closeTabSpecificPanels.js]
 [browser_multiplePrompts.js]
 [browser_openPromptInBackgroundTab.js]
 support-files = openPromptOffTimeout.html
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_closeTabSpecificPanels.js
@@ -0,0 +1,41 @@
+"use strict";
+
+/*
+ * This test creates multiple panels, one that has been tagged as specific to its tab's content
+ * and one that isn't. When a tab loses focus, panel specific to that tab should close.
+ * The non-specific panel should remain open.
+ *
+ */
+
+add_task(function*() {
+  let tab1 = gBrowser.addTab("http://mochi.test:8888/#0");
+  let tab2 = gBrowser.addTab("http://mochi.test:8888/#1");
+  let specificPanel = document.createElement("panel");
+  specificPanel.setAttribute("tabspecific", "true");
+  let generalPanel = document.createElement("panel");
+  let anchor = document.getElementById(CustomizableUI.AREA_NAVBAR);
+
+  anchor.appendChild(specificPanel);
+  anchor.appendChild(generalPanel);
+  is(specificPanel.state, "closed", "specificPanel starts as closed");
+  is(generalPanel.state, "closed", "generalPanel starts as closed");
+
+  let specificPanelPromise = BrowserTestUtils.waitForEvent(specificPanel, "popupshown");
+  specificPanel.openPopupAtScreen(210, 210);
+  yield specificPanelPromise;
+  is(specificPanel.state, "open", "specificPanel has been opened");
+
+  let generalPanelPromise = BrowserTestUtils.waitForEvent(generalPanel, "popupshown");
+  generalPanel.openPopupAtScreen(510,510);
+  yield generalPanelPromise;
+  is(generalPanel.state, "open", "generalPanel has been opened");
+
+  gBrowser.tabContainer.advanceSelectedTab(-1, true);
+  is(specificPanel.state, "closed", "specificPanel panel is closed after its tab loses focus");
+  is(generalPanel.state, "open", "generalPanel is still open after tab switch");
+
+  specificPanel.remove();
+  generalPanel.remove();
+  gBrowser.removeTab(tab1);
+  gBrowser.removeTab(tab2);
+});
--- a/browser/base/content/utilityOverlay.js
+++ b/browser/base/content/utilityOverlay.js
@@ -473,16 +473,56 @@ 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"];
+
+  if (aEvent.key != keyPressed) {
+    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") {
+        keyModifiers[index] = AppConstants.platform == "macosx" ?  "Meta" : "Control";
+      } else {
+        keyModifiers[index] = modifier[0].toUpperCase() + modifier.slice(1);
+      }
+    });
+    return modifiers.every(modifier => keyModifiers.includes(modifier) == aEvent.getModifierState(modifier));
+  }
+  return true;
+}
+
 // 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
@@ -1353,16 +1353,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 +
@@ -2305,16 +2308,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;
@@ -2335,17 +2339,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() {