Bug 1171746 - ensure tab specific panels close when you switch the tab r?jaws draft
authorKatie Broida[:ktbee] <kbroida@gmail.com>
Thu, 11 Aug 2016 16:51:45 -0400
changeset 399673 34b2552aa6ee19d9bdc22d4fb781beb3d8954ac4
parent 399408 0502bd9e025edde29777ba1de4280f9b52af4663
child 528016 60174870e8999c4322612c8658f18315320388cc
push id25930
push userbmo:kbroida@gmail.com
push dateThu, 11 Aug 2016 20:57:07 +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() {