Bug 1382579 - Part 2: UITour should support showMenu, showInfo, showHighlight on the Page Action Panel, r=gijs draft
authorFischer.json <fischer.json@gmail.com>
Tue, 08 Aug 2017 11:19:09 +0800
changeset 642382 bc774e747c84e9a26f73ab5c0bfaddade47b956d
parent 641504 be1bd805a80de4c078782f962315cda969ba8856
child 642383 cc41e38fe53300e988570b9187d104bf5c9fa76d
push id72719
push userfliu@mozilla.com
push dateTue, 08 Aug 2017 03:33:37 +0000
reviewersgijs
bugs1382579, 1382700, 1386201
milestone57.0a1
Bug 1382579 - Part 2: UITour should support showMenu, showInfo, showHighlight on the Page Action Panel, r=gijs This commit - makes UITour support showMenu, showInfo, showHighlight on the Page Action Panel - makes UITour support showInfo, showHighlight on the Page Action buttons and on the urlbar's bookmark #star-button button btw - fixes Bug 1382700 - "UITour lacks the `hideMenu` api support for the single search bar (urlbar) dropdown menu" together - fixes Bug 1386201 - "UITour wouldn't close the appMenu if running the tracking-protection's ui tour" together MozReview-Commit-ID: Fou1sD4gAs4
browser/components/uitour/UITour-lib.js
browser/components/uitour/UITour.jsm
--- a/browser/components/uitour/UITour-lib.js
+++ b/browser/components/uitour/UITour-lib.js
@@ -98,24 +98,30 @@ if (typeof Mozilla == "undefined") {
    * @see Mozilla.UITour.showInfo
    *
    * @description Valid values:<ul>
    * <li>accountStatus
    * <li>addons
    * <li>appMenu
    * <li>backForward
    * <li>bookmarks
+   * <li>bookmark-star-button
    * <li>controlCenter-trackingUnblock
    * <li>controlCenter-trackingBlock
    * <li>customize
    * <li>devtools
    * <li>forget
    * <li>help
    * <li>home
    * <li>library
+   * <li>pageActionButton
+   * <li>pageAction-panel-bookmark
+   * <li>pageAction-panel-copyURL
+   * <li>pageAction-panel-emailLink
+   * <li>pageAction-panel-sendToDevice
    * <li>pocket
    * <li>privateWindow
    * <li>quit
    * <li>readerMode-urlBar
    * <li>search
    * <li>searchIcon
    * <li>searchPrefsLink
    * <li>selectedTabIcon
--- a/browser/components/uitour/UITour.jsm
+++ b/browser/components/uitour/UITour.jsm
@@ -79,17 +79,18 @@ this.UITour = {
   url: null,
   seenPageIDs: null,
   // This map is not persisted and is used for
   // building the content source of a potential tour.
   pageIDsForSession: new Map(),
   pageIDSourceBrowsers: new WeakMap(),
   /* Map from browser chrome windows to a Set of <browser>s in which a tour is open (both visible and hidden) */
   tourBrowsersByWindow: new WeakMap(),
-  appMenuOpenForAnnotation: new Set(),
+  // Menus opened by api users explictly through `Mozilla.UITour.showMenu` call
+  noautohideMenus: new Set(),
   availableTargetsCache: new WeakMap(),
   clearAvailableTargetsCache() {
     this.availableTargetsCache = new WeakMap();
   },
 
   _annotationPanelMutationObservers: new WeakMap(),
 
   highlightEffects: ["random", "wobble", "zoom", "color"],
@@ -199,16 +200,34 @@ this.UITour = {
     }],
     ["trackingProtection", {
       query: "#tracking-protection-icon",
     }],
     ["urlbar",      {
       query: "#urlbar",
       widgetName: "urlbar-container",
     }],
+    ["pageActionButton", {
+      query: "#pageActionButton"
+    }],
+    ["pageAction-panel-bookmark", {
+      query: "#pageAction-panel-bookmark"
+    }],
+    ["pageAction-panel-copyURL", {
+      query: "#pageAction-panel-copyURL"
+    }],
+    ["pageAction-panel-emailLink", {
+      query: "#pageAction-panel-emailLink"
+    }],
+    ["pageAction-panel-sendToDevice", {
+      query: "#pageAction-panel-sendToDevice"
+    }],
+    ["bookmark-star-button", {
+      query: "#star-button"
+    }]
   ]),
 
   init() {
     log.debug("Initializing UITour");
     // Lazy getter is initialized here so it can be replicated any time
     // in a test.
     delete this.seenPageIDs;
     Object.defineProperty(this, "seenPageIDs", {
@@ -449,24 +468,26 @@ this.UITour = {
       }
 
       case "resetTheme": {
         this.resetTheme();
         break;
       }
 
       case "showMenu": {
+        this.noautohideMenus.add(data.name);
         this.showMenu(window, data.name, () => {
           if (typeof data.showCallbackID == "string")
             this.sendPageCallback(messageManager, data.showCallbackID);
         });
         break;
       }
 
       case "hideMenu": {
+        this.noautohideMenus.delete(data.name);
         this.hideMenu(window, data.name);
         break;
       }
 
       case "showNewTab": {
         this.showNewTab(window, browser);
         break;
       }
@@ -787,43 +808,74 @@ this.UITour = {
     return {
       seenPageIDs: [...this.seenPageIDs.keys()],
     };
   },
 
   /**
    * Tear down a tour from a tab e.g. upon switching/closing tabs.
    */
-  teardownTourForBrowser(aWindow, aBrowser, aTourPageClosing = false) {
+  async teardownTourForBrowser(aWindow, aBrowser, aTourPageClosing = false) {
     log.debug("teardownTourForBrowser: aBrowser = ", aBrowser, aTourPageClosing);
 
     if (this.pageIDSourceBrowsers.has(aBrowser)) {
       let pageID = this.pageIDSourceBrowsers.get(aBrowser);
       this.setExpiringTelemetryBucket(pageID, aTourPageClosing ? "closed" : "inactive");
     }
 
     let openTourBrowsers = this.tourBrowsersByWindow.get(aWindow);
     if (aTourPageClosing && openTourBrowsers) {
       openTourBrowsers.delete(aBrowser);
     }
 
     this.hideHighlight(aWindow);
     this.hideInfo(aWindow);
-    // Ensure the menu panel is hidden before calling recreatePopup so popup events occur.
-    this.hideMenu(aWindow, "appMenu");
-    this.hideMenu(aWindow, "controlCenter");
 
-    // Clean up panel listeners after calling hideMenu above.
-    aWindow.PanelUI.panel.removeEventListener("popuphiding", this.hideAppMenuAnnotations);
-    aWindow.PanelUI.panel.removeEventListener("ViewShowing", this.hideAppMenuAnnotations);
-    aWindow.PanelUI.panel.removeEventListener("popuphidden", this.onPanelHidden);
-    let controlCenterPanel = aWindow.gIdentityHandler._identityPopup;
-    controlCenterPanel.removeEventListener("popuphidden", this.onPanelHidden);
-    controlCenterPanel.removeEventListener("popuphiding", this.hideControlCenterAnnotations);
+    let panels = [
+      {
+        name: "appMenu",
+        node: aWindow.PanelUI.panel,
+        events: [
+          [ "popuphidden", this.onPanelHidden ],
+          [ "popuphiding", this.hideAppMenuAnnotations ],
+          [ "ViewShowing", this.hideAppMenuAnnotations ]
+        ]
+      },
+      {
+        name: "pageActionPanel",
+        node: aWindow.BrowserPageActions.panelNode,
+        events: [
+          [ "popuphidden", this.onPanelHidden ],
+          [ "popuphiding", this.hidePageActionPanelAnnotations ],
+          [ "ViewShowing", this.hidePageActionPanelAnnotations ]
+        ]
+      },
+      {
+        name: "controlCenter",
+        node: aWindow.gIdentityHandler._identityPopup,
+        events: [
+          [ "popuphidden", this.onPanelHidden ],
+          [ "popuphiding", this.hideControlCenterAnnotations ]
+        ]
+      },
+    ];
+    for (let panel of panels) {
+      // Ensure the menu panel is hidden and clean up panel listeners after calling hideMenu.
+      if (panel.node.state != "closed") {
+        await new Promise(resolve => {
+          panel.node.addEventListener("popuphidden", resolve, { once: true });
+          this.hideMenu(aWindow, panel.name);
+        });
+      }
+      for (let [ name, listener ] of panel.events) {
+        panel.node.removeEventListener(name, listener);
+      }
+    }
 
+    this.noautohideMenus.clear();
     this.resetTheme();
 
     // If there are no more tour tabs left in the window, teardown the tour for the whole window.
     if (!openTourBrowsers || openTourBrowsers.size == 0) {
       this.teardownTourForWindow(aWindow);
     }
   },
 
@@ -936,68 +988,103 @@ this.UITour = {
     // Use the widget for filtering if it exists since the target may be the icon inside.
     if (aTarget.widgetName) {
       targetElement = aTarget.node.ownerDocument.getElementById(aTarget.widgetName);
     }
 
     return targetElement.id.startsWith("appMenu-");
   },
 
+  targetIsInPageActionPanel(aTarget) {
+    return aTarget.node.id.startsWith("pageAction-panel-");
+  },
+
   /**
-   * Called before opening or after closing a highlight or info panel to see if
-   * we need to open or close the appMenu to see the annotation's anchor.
+   * Called before opening or after closing a highlight or an info tooltip to see if
+   * we need to open or close the menu to see the annotation's anchor.
+   *
+   * @param {ChromeWindow} aWindow the chrome window
+   * @param {bool} aShouldOpen true means we should open the menu, otherwise false
+   * @param {String} aMenuName "appMenu" or "pageActionPanel"
    */
-  _setAppMenuStateForAnnotation(aWindow, aAnnotationType, aShouldOpenForHighlight, aTarget = null,
-                                aCallback = null) {
-    log.debug("_setAppMenuStateForAnnotation:", aAnnotationType);
-    log.debug("_setAppMenuStateForAnnotation: Menu is expected to be:", aShouldOpenForHighlight ? "open" : "closed");
+  _setMenuStateForAnnotation(aWindow, aShouldOpen, aMenuName) {
+    log.debug("_setMenuStateForAnnotation: Menu is ", aMenuName);
+    log.debug("_setMenuStateForAnnotation: Menu is expected to be:", aShouldOpen ? "open" : "closed");
+    let menu = aMenuName == "appMenu" ? aWindow.PanelUI.panel : aWindow.BrowserPageActions.panelNode;
 
     // If the panel is in the desired state, we're done.
-    let panelIsOpen = aWindow.PanelUI.panel.state != "closed";
-    if (aShouldOpenForHighlight == panelIsOpen) {
-      log.debug("_setAppMenuStateForAnnotation: Panel already in expected state");
-      if (aCallback)
-        aCallback();
-      return;
-    }
-
-    // Don't close the menu if it wasn't opened by us (e.g. via showmenu instead).
-    if (!aShouldOpenForHighlight && !this.appMenuOpenForAnnotation.has(aAnnotationType)) {
-      log.debug("_setAppMenuStateForAnnotation: Menu not opened by us, not closing");
-      if (aCallback)
-        aCallback();
-      return;
-    }
-
-    if (aShouldOpenForHighlight) {
-      this.appMenuOpenForAnnotation.add(aAnnotationType);
-    } else {
-      this.appMenuOpenForAnnotation.delete(aAnnotationType);
+    let panelIsOpen = menu.state != "closed";
+    if (aShouldOpen == panelIsOpen) {
+      log.debug("_setMenuStateForAnnotation: Menu already in expected state");
+      return Promise.resolve();
     }
 
     // Actually show or hide the menu
-    if (this.appMenuOpenForAnnotation.size) {
-      log.debug("_setAppMenuStateForAnnotation: Opening the menu");
-      this.showMenu(aWindow, "appMenu", async () => {
-        // PanelMultiView's like the AppMenu might shuffle the DOM, which might result
-        // in our target being invalidated if it was anonymous content (since the XBL
-        // binding it belonged to got destroyed). We work around this by re-querying for
-        // the node and stuffing it into the old target structure.
-        log.debug("_setAppMenuStateForAnnotation: Refreshing target");
-        let refreshedTarget = await this.getTarget(aWindow, aTarget.targetName);
-        aTarget.node = refreshedTarget.node;
-        aCallback();
+    let promise = null;
+    if (aShouldOpen) {
+      log.debug("_setMenuStateForAnnotation: Opening the menu");
+      promise = new Promise(resolve => {
+        this.showMenu(aWindow, aMenuName, resolve);
+      });
+    } else if (!this.noautohideMenus.has(aMenuName)) {
+      // If the menu was opened explictly by api user through `Mozilla.UITour.showMenu`,
+      // it should be closed explictly by api user through `Mozilla.UITour.hideMenu`.
+      // So we shouldn't get to here to close it for the highlight/info annotation.
+      log.debug("_setMenuStateForAnnotation: Closing the menu");
+      promise = new Promise(resolve => {
+        menu.addEventListener("popuphidden", resolve, { once: true });
+        this.hideMenu(aWindow, aMenuName);
       });
-    } else {
-      log.debug("_setAppMenuStateForAnnotation: Closing the menu");
-      this.hideMenu(aWindow, "appMenu");
-      if (aCallback)
-        aCallback();
+    }
+    return promise;
+  },
+
+  /**
+   * Ensure the target's visibility and the open/close states of menus for the target.
+   *
+   * @param {ChromeWindow} aChromeWindow The chrome window
+   * @param {Object} aTarget The target on which we show highlight or show info.
+   */
+  async _ensureTarget(aChromeWindow, aTarget) {
+    let shouldOpenAppMenu = false;
+    let shouldOpenPageActionPanel = false;
+    if (this.targetIsInAppMenu(aTarget)) {
+      shouldOpenAppMenu = true;
+    } else if (this.targetIsInPageActionPanel(aTarget)) {
+      shouldOpenPageActionPanel = true;
+      // Ensure the panel visibility so as to ensure the visibility of
+      // the target element inside the panel otherwise
+      // we would be rejected in the below `isElementVisible` checking.
+      aChromeWindow.BrowserPageActions.panelNode.hidden = false;
     }
 
+    // Prevent showing a panel at an undefined position.
+    if (!this.isElementVisible(aTarget.node)) {
+      return Promise.reject(`_ensureTarget: Reject the ${aTarget.name} target since it isn't visible.`);
+    }
+
+    let menuToOpen = null;
+    let menuClosePromises = [];
+    if (shouldOpenAppMenu) {
+      menuToOpen = "appMenu";
+      menuClosePromises.push(this._setMenuStateForAnnotation(aChromeWindow, false, "pageActionPanel"));
+    } else if (shouldOpenPageActionPanel) {
+      menuToOpen = "pageActionPanel";
+      menuClosePromises.push(this._setMenuStateForAnnotation(aChromeWindow, false, "appMenu"));
+    } else {
+      menuClosePromises.push(this._setMenuStateForAnnotation(aChromeWindow, false, "appMenu"));
+      menuClosePromises.push(this._setMenuStateForAnnotation(aChromeWindow, false, "pageActionPanel"));
+    }
+
+    let promise = Promise.all(menuClosePromises);
+    await promise;
+    if (menuToOpen) {
+      promise = this._setMenuStateForAnnotation(aChromeWindow, true, menuToOpen);
+    }
+    return promise;
   },
 
   previewTheme(aTheme) {
     let origin = Services.prefs.getCharPref("browser.uitour.themeOrigin");
     let data = LightweightThemeManager.parseTheme(aTheme, origin);
     if (data)
       LightweightThemeManager.previewTheme(data);
   },
@@ -1006,41 +1093,44 @@ this.UITour = {
     LightweightThemeManager.resetPreview();
   },
 
   /**
    * The node to which a highlight or notification(-popup) is anchored is sometimes
    * obscured because it may be inside an overflow menu. This function should figure
    * that out and offer the overflow chevron as an alternative.
    *
-   * @param {Node} aAnchor The element that's supposed to be the anchor
+   * @param {ChromeWindow} aChromeWindow The chrome window
+   * @param {Object} aTarget The target object whose node is supposed to be the anchor
    * @type {Node}
    */
-  _correctAnchor(aAnchor) {
+  async _correctAnchor(aChromeWindow, aTarget) {
+    // PanelMultiView's like the AppMenu might shuffle the DOM, which might result
+    // in our anchor being invalidated if it was anonymous content (since the XBL
+    // binding it belonged to got destroyed). We work around this by re-querying for
+    // the node and stuffing it into the old anchor structure.
+    let refreshedTarget = await this.getTarget(aChromeWindow, aTarget.targetName);
+    let node = aTarget.node = refreshedTarget.node;
     // If the target is in the overflow panel, just return the overflow button.
-    if (aAnchor.getAttribute("overflowedItem")) {
-      let doc = aAnchor.ownerDocument;
-      let placement = CustomizableUI.getPlacementOfWidget(aAnchor.id);
-      let areaNode = doc.getElementById(placement.area);
-      return areaNode.overflowable._chevron;
+    if (node.closest("#widget-overflow-scroller")) {
+      return CustomizableUI.getWidget(node.id).forWindow(aChromeWindow).anchor;
     }
-
-    return aAnchor;
+    return node;
   },
 
   /**
    * @param aChromeWindow The chrome window that the highlight is in. Necessary since some targets
    *                      are in a sub-frame so the defaultView is not the same as the chrome
    *                      window.
    * @param aTarget    The element to highlight.
    * @param aEffect    (optional) The effect to use from UITour.highlightEffects or "none".
    * @see UITour.highlightEffects
    */
-  showHighlight(aChromeWindow, aTarget, aEffect = "none") {
-    function showHighlightPanel() {
+  async showHighlight(aChromeWindow, aTarget, aEffect = "none") {
+    let showHighlightPanel = (aAnchorEl) => {
       let highlighter = aChromeWindow.document.getElementById("UITourHighlight");
 
       let effect = aEffect;
       if (effect == "random") {
         // Exclude "random" from the randomly selected effects.
         let randomEffect = 1 + Math.floor(Math.random() * (this.highlightEffects.length - 1));
         if (randomEffect == this.highlightEffects.length)
           randomEffect--; // On the order of 1 in 2^62 chance of this happening.
@@ -1048,17 +1138,17 @@ this.UITour = {
       }
       // Toggle the effect attribute to "none" and flush layout before setting it so the effect plays.
       highlighter.setAttribute("active", "none");
       aChromeWindow.getComputedStyle(highlighter).animationName;
       highlighter.setAttribute("active", effect);
       highlighter.parentElement.setAttribute("targetName", aTarget.targetName);
       highlighter.parentElement.hidden = false;
 
-      let highlightAnchor = this._correctAnchor(aTarget.node);
+      let highlightAnchor = aAnchorEl;
       let targetRect = highlightAnchor.getBoundingClientRect();
       let highlightHeight = targetRect.height;
       let highlightWidth = targetRect.width;
       let minDimension = Math.min(highlightHeight, highlightWidth);
       let maxDimension = Math.max(highlightHeight, highlightWidth);
 
       // If the dimensions are within 200% of each other (to include the bookmarks button),
       // make the highlight a circle with the largest dimension as the diameter.
@@ -1082,55 +1172,52 @@ this.UITour = {
       let highlightWindow = aChromeWindow;
       let highlightStyle = highlightWindow.getComputedStyle(highlighter);
       let highlightHeightWithMin = Math.max(highlightHeight, parseFloat(highlightStyle.minHeight));
       let highlightWidthWithMin = Math.max(highlightWidth, parseFloat(highlightStyle.minWidth));
       let offsetX = -(Math.max(0, highlightWidthWithMin - targetRect.width) / 2);
       let offsetY = -(Math.max(0, highlightHeightWithMin - targetRect.height) / 2);
       this._addAnnotationPanelMutationObserver(highlighter.parentElement);
       highlighter.parentElement.openPopup(highlightAnchor, "overlap", offsetX, offsetY);
-    }
+    };
 
-    // Prevent showing a panel at an undefined position.
-    if (!this.isElementVisible(aTarget.node)) {
-      log.warn("showHighlight: Not showing a highlight since the target isn't visible", aTarget);
-      return;
+    try {
+      await this._ensureTarget(aChromeWindow, aTarget);
+      let anchorEl = await this._correctAnchor(aChromeWindow, aTarget);
+      showHighlightPanel(anchorEl);
+    } catch (e) {
+      log.warn(e);
     }
-
-    this._setAppMenuStateForAnnotation(aChromeWindow, "highlight",
-                                       this.targetIsInAppMenu(aTarget),
-                                       aTarget,
-                                       showHighlightPanel.bind(this));
   },
 
   hideHighlight(aWindow) {
     let highlighter = aWindow.document.getElementById("UITourHighlight");
     this._removeAnnotationPanelMutationObserver(highlighter.parentElement);
     highlighter.parentElement.hidePopup();
     highlighter.removeAttribute("active");
-
-    this._setAppMenuStateForAnnotation(aWindow, "highlight", false);
+    this._setMenuStateForAnnotation(aWindow, false, "appMenu");
+    this._setMenuStateForAnnotation(aWindow, false, "pageActionPanel");
   },
 
   /**
    * Show an info panel.
    *
    * @param {ChromeWindow} aChromeWindow
    * @param {Node}     aAnchor
    * @param {String}   [aTitle=""]
    * @param {String}   [aDescription=""]
    * @param {String}   [aIconURL=""]
    * @param {Object[]} [aButtons=[]]
    * @param {Object}   [aOptions={}]
    * @param {String}   [aOptions.closeButtonCallback]
    * @param {String}   [aOptions.targetCallback]
    */
-  showInfo(aChromeWindow, aAnchor, aTitle = "", aDescription = "",
+  async showInfo(aChromeWindow, aAnchor, aTitle = "", aDescription = "",
            aIconURL = "", aButtons = [], aOptions = {}) {
-    function showInfoPanel(aAnchorEl) {
+    let showInfoPanel = (aAnchorEl) => {
       aAnchorEl.focus();
 
       let document = aChromeWindow.document;
       let tooltip = document.getElementById("UITourTooltip");
       let tooltipTitle = document.getElementById("UITourTooltipTitle");
       let tooltipDesc = document.getElementById("UITourTooltipDescription");
       let tooltipIcon = document.getElementById("UITourTooltipIcon");
       let tooltipButtons = document.getElementById("UITourTooltipButtons");
@@ -1214,112 +1301,111 @@ this.UITour = {
 
       this._addAnnotationPanelMutationObserver(tooltip);
       tooltip.openPopup(aAnchorEl, alignment, xOffset || 0, yOffset || 0);
       if (tooltip.state == "closed") {
         document.defaultView.addEventListener("endmodalstate", function() {
           tooltip.openPopup(aAnchorEl, alignment);
         }, {once: true});
       }
-    }
-
-    // Prevent showing a panel at an undefined position.
-    if (!this.isElementVisible(aAnchor.node)) {
-      log.warn("showInfo: Not showing since the target isn't visible", aAnchor);
-      return;
-    }
-
-    // We need to bind the anchor argument to the showInfoPanel function call
-    // after _setAppMenuStateForAnnotation has finished, since
-    // _setAppMenuStateForAnnotation might have refreshed the anchor node.
-    let callShowInfoPanel = () => {
-      showInfoPanel.call(this, this._correctAnchor(aAnchor.node));
     };
 
-    this._setAppMenuStateForAnnotation(aChromeWindow, "info",
-                                       this.targetIsInAppMenu(aAnchor),
-                                       aAnchor,
-                                       callShowInfoPanel);
+    try {
+      await this._ensureTarget(aChromeWindow, aAnchor);
+      let anchorEl = await this._correctAnchor(aChromeWindow, aAnchor);
+      showInfoPanel(anchorEl);
+    } catch (e) {
+      log.warn(e);
+    }
   },
 
   isInfoOnTarget(aChromeWindow, aTargetName) {
     let document = aChromeWindow.document;
     let tooltip = document.getElementById("UITourTooltip");
     return tooltip.getAttribute("targetName") == aTargetName && tooltip.state != "closed";
   },
 
   hideInfo(aWindow) {
     let document = aWindow.document;
-
     let tooltip = document.getElementById("UITourTooltip");
     this._removeAnnotationPanelMutationObserver(tooltip);
     tooltip.hidePopup();
-    this._setAppMenuStateForAnnotation(aWindow, "info", false);
+    this._setMenuStateForAnnotation(aWindow, false, "appMenu");
+    this._setMenuStateForAnnotation(aWindow, false, "pageActionPanel");
 
     let tooltipButtons = document.getElementById("UITourTooltipButtons");
     while (tooltipButtons.firstChild)
       tooltipButtons.firstChild.remove();
   },
 
   showMenu(aWindow, aMenuName, aOpenCallback = null) {
     log.debug("showMenu:", aMenuName);
     function openMenuButton(aMenuBtn) {
       if (!aMenuBtn || !aMenuBtn.boxObject || aMenuBtn.open) {
         if (aOpenCallback)
           aOpenCallback();
         return;
       }
       if (aOpenCallback)
-        aMenuBtn.addEventListener("popupshown", onPopupShown);
+        aMenuBtn.addEventListener("popupshown", aOpenCallback, { once: true });
       aMenuBtn.boxObject.openMenu(true);
     }
-    function onPopupShown(event) {
-      this.removeEventListener("popupshown", onPopupShown);
-      aOpenCallback(event);
-    }
 
-    if (aMenuName == "appMenu") {
-      aWindow.PanelUI.panel.setAttribute("noautohide", "true");
-      // If the popup is already opened, don't recreate the widget as it may cause a flicker.
-      if (aWindow.PanelUI.panel.state != "open") {
-        this.recreatePopup(aWindow.PanelUI.panel);
+    if (aMenuName == "appMenu" || aMenuName == "pageActionPanel") {
+      let menu = {
+        onPanelHidden: this.onPanelHidden
+      };
+      if (aMenuName == "appMenu") {
+        menu.node = aWindow.PanelUI.panel;
+        menu.hideMenuAnnotations = this.hideAppMenuAnnotations;
+        menu.show = () => aWindow.PanelUI.show();
+      } else {
+        menu.node = aWindow.BrowserPageActions.panelNode;
+        menu.hideMenuAnnotations = this.hidePageActionPanelAnnotations;
+        menu.show = () => aWindow.BrowserPageActions.showPanel();
       }
-      aWindow.PanelUI.panel.addEventListener("popuphiding", this.hideAppMenuAnnotations);
-      aWindow.PanelUI.panel.addEventListener("ViewShowing", this.hideAppMenuAnnotations);
-      aWindow.PanelUI.panel.addEventListener("popuphidden", this.onPanelHidden);
+
+      menu.node.setAttribute("noautohide", "true");
+      // If the popup is already opened, don't recreate the widget as it may cause a flicker.
+      if (menu.node.state != "open") {
+        this.recreatePopup(menu.node);
+      }
       if (aOpenCallback) {
-        aWindow.PanelUI.panel.addEventListener("popupshown", onPopupShown);
+        menu.node.addEventListener("popupshown", aOpenCallback, { once: true });
       }
-      aWindow.PanelUI.show();
+      menu.node.addEventListener("popuphidden", menu.onPanelHidden);
+      menu.node.addEventListener("popuphiding", menu.hideMenuAnnotations);
+      menu.node.addEventListener("ViewShowing", menu.hideMenuAnnotations);
+      menu.show();
     } else if (aMenuName == "bookmarks") {
       let menuBtn = aWindow.document.getElementById("bookmarks-menu-button");
       openMenuButton(menuBtn);
     } else if (aMenuName == "controlCenter") {
       let popup = aWindow.gIdentityHandler._identityPopup;
 
       // Add the listener even if the panel is already open since it will still
       // only get registered once even if it was UITour that opened it.
       popup.addEventListener("popuphiding", this.hideControlCenterAnnotations);
       popup.addEventListener("popuphidden", this.onPanelHidden);
 
-      popup.setAttribute("noautohide", true);
+      popup.setAttribute("noautohide", "true");
       this.clearAvailableTargetsCache();
 
       if (popup.state == "open") {
         if (aOpenCallback) {
           aOpenCallback();
         }
         return;
       }
 
       this.recreatePopup(popup);
 
       // Open the control center
       if (aOpenCallback) {
-        popup.addEventListener("popupshown", onPopupShown);
+        popup.addEventListener("popupshown", aOpenCallback, { once: true });
       }
       aWindow.document.getElementById("identity-box").click();
     } else if (aMenuName == "pocket") {
       this.getTarget(aWindow, "pocket").then(async function onPocketTarget(target) {
         let widgetGroupWrapper = CustomizableUI.getWidget(target.widgetName);
         if (widgetGroupWrapper.type != "view" || !widgetGroupWrapper.viewId) {
           log.error("Can't open the pocket menu without a view");
           return;
@@ -1354,19 +1440,19 @@ this.UITour = {
         let widgetWrapper = widgetGroupWrapper.forWindow(aWindow);
         aWindow.PanelUI.showSubView(widgetGroupWrapper.viewId,
                                     widgetWrapper.anchor,
                                     placement.area);
       }).catch(log.error);
     } else if (aMenuName == "urlbar") {
       this.getTarget(aWindow, "urlbar").then(target => {
         let urlbar = target.node;
-        urlbar.popup.addEventListener("popupshown", evt => {
-          aOpenCallback && aOpenCallback(evt);
-        }, {once: true});
+        if (aOpenCallback) {
+          urlbar.popup.addEventListener("popupshown", aOpenCallback, { once: true });
+        }
         urlbar.focus();
         // To demonstrate the ability of searching, we type "Firefox" in advance
         // for URLBar's dropdown. To limit the search results on browser-related
         // items, we use "Firefox" hard-coded rather than l10n brandShortName
         // entity to avoid unrelated or unpredicted results for, like, Nightly
         // or translated entites.
         const SEARCH_STRING = "Firefox";
         urlbar.value = SEARCH_STRING;
@@ -1386,24 +1472,28 @@ this.UITour = {
     if (aMenuName == "appMenu") {
       aWindow.PanelUI.hide();
     } else if (aMenuName == "bookmarks") {
       let menuBtn = aWindow.document.getElementById("bookmarks-menu-button");
       closeMenuButton(menuBtn);
     } else if (aMenuName == "controlCenter") {
       let panel = aWindow.gIdentityHandler._identityPopup;
       panel.hidePopup();
+    } else if (aMenuName == "urlbar") {
+      aWindow.gURLBar.closePopup();
+    } else if (aMenuName == "pageActionPanel") {
+      aWindow.BrowserPageActions.panelNode.hidePopup();
     }
   },
 
   showNewTab(aWindow, aBrowser) {
     aWindow.openLinkIn("about:newtab", "current", {targetBrowser: aBrowser});
   },
 
-  hideAnnotationsForPanel(aEvent, aTargetPositionCallback) {
+  _hideAnnotationsForPanel(aEvent, aTargetPositionCallback) {
     let win = aEvent.target.ownerGlobal;
     let annotationElements = new Map([
       // [annotationElement (panel), method to hide the annotation]
       [win.document.getElementById("UITourHighlightContainer"), UITour.hideHighlight.bind(UITour)],
       [win.document.getElementById("UITourTooltip"), UITour.hideInfo.bind(UITour)],
     ]);
     annotationElements.forEach((hideMethod, annotationElement) => {
       if (annotationElement.state != "closed") {
@@ -1415,25 +1505,28 @@ this.UITour = {
               annotationElement.state == "closed" ||
               !aTargetPositionCallback(aTarget)) {
             return;
           }
           hideMethod(win);
         }).catch(log.error);
       }
     });
-    UITour.appMenuOpenForAnnotation.clear();
   },
 
   hideAppMenuAnnotations(aEvent) {
-    UITour.hideAnnotationsForPanel(aEvent, UITour.targetIsInAppMenu);
+    UITour._hideAnnotationsForPanel(aEvent, UITour.targetIsInAppMenu);
+  },
+
+  hidePageActionPanelAnnotations(aEvent) {
+    UITour._hideAnnotationsForPanel(aEvent, UITour.targetIsInPageActionPanel);
   },
 
   hideControlCenterAnnotations(aEvent) {
-    UITour.hideAnnotationsForPanel(aEvent, (aTarget) => {
+    UITour._hideAnnotationsForPanel(aEvent, (aTarget) => {
       return aTarget.targetName.startsWith("controlCenter-");
     });
   },
 
   onPanelHidden(aEvent) {
     aEvent.target.removeAttribute("noautohide");
     UITour.recreatePopup(aEvent.target);
     UITour.clearAvailableTargetsCache();