Bug 1558565 - Fix accounts and folders submenus in the new appmenu. r=mkmelin
authorPaul Morris <paul@paulwmorris.com>
Fri, 21 Jun 2019 09:20:17 -0400
changeset 35973 71b2391d6c438c32cdd1f9598b96b7f7efe6cde7
parent 35972 50955fe1cb9823bed5efef3d7b5e5f076e87f0a1
child 35974 ca1b8ae86f9bd685de4c9c2cc019c38637996977
push id392
push userclokep@gmail.com
push dateMon, 02 Sep 2019 20:17:19 +0000
reviewersmkmelin
bugs1558565
Bug 1558565 - Fix accounts and folders submenus in the new appmenu. r=mkmelin
mail/base/content/mainCommandSet.inc.xul
mail/components/customizableui/content/panelUI.inc.xul
mail/test/mozmill/folder-widget/test-message-filters.js
mailnews/base/content/folder-menupopup.js
--- a/mail/base/content/mainCommandSet.inc.xul
+++ b/mail/base/content/mainCommandSet.inc.xul
@@ -166,17 +166,17 @@
     <command id="cmd_nextUnreadThread" oncommand="goDoCommand('cmd_nextUnreadThread')" disabled="true"/>
     <command id="cmd_previousMsg" oncommand="goDoCommand('cmd_previousMsg')" disabled="true"/>
     <command id="cmd_previousUnreadMsg" oncommand="goDoCommand('cmd_previousUnreadMsg')" disabled="true"/>
     <command id="cmd_previousFlaggedMsg" oncommand="goDoCommand('cmd_previousFlaggedMsg')" disabled="true"/>
     <command id="cmd_goStartPage" oncommand="goDoCommand('cmd_goStartPage');"/>
     <command id="cmd_undoCloseTab" oncommand="goDoCommand('cmd_undoCloseTab');"/>
     <command id="cmd_goForward" oncommand="goDoCommand('cmd_goForward')" disabled="true"/>
     <command id="cmd_goBack" oncommand="goDoCommand('cmd_goBack')" disabled="true"/>
-    <command id="cmd_goFolder" oncommand="gFolderTreeView.selectFolder(event.target._folder, true);" disabled="true"/>
+    <command id="cmd_goFolder" oncommand="gFolderTreeView.selectFolder(event.explicitOriginalTarget._folder, true);" disabled="true"/>
     <command id="cmd_chat" oncommand="goDoCommand('cmd_chat')" disabled="true"/>
   </commandset>
 
   <commandset id="mailMessageMenuItems"
               commandupdater="true"
               events="create-menu-message"
               oncommandupdate="goUpdateMailMenuItems(this)">
     <command id="cmd_archive" oncommand="goDoCommand('cmd_archive')"/>
--- a/mail/components/customizableui/content/panelUI.inc.xul
+++ b/mail/components/customizableui/content/panelUI.inc.xul
@@ -673,52 +673,35 @@
                        class="subviewbutton subviewbutton-nav"
                        label="&offlineMenu.label;"
                        closemenu="none"
                        oncommand="PanelUI.showSubView('appMenu-fileOfflineView', this)"/>
       </vbox>
     </panelview>
 
     <!-- File / Get New Message For -->
-    <panelview id="appMenu-fileGetNewMsgForView"
+    <panelview is="folder-panelview" id="appMenu-fileGetNewMsgForView"
                title="&getNewMsgForCmd.label;"
-               class="PanelUI-subView">
+               class="PanelUI-subView"
+               mode="getMail"
+               expandFolders="false"
+               oncommand="MsgGetMessagesForAccount(event.target._folder); event.stopPropagation();">
       <vbox class="panel-subview-body">
-        <!-- TODO appmenu dynamically populate account items here
-             Once dynamic folders subviews work, use these commented
-             toolbarbuttons in place of the <menu> below. -->
-        <!-- <toolbarbutton id="appmenu_getnewmsgs_all_accounts"
+        <toolbarbutton id="appmenu_getnewmsgs_all_accounts"
                        class="subviewbutton subviewbutton-iconic"
                        label="&getAllNewMsgCmdPopupMenu.label;"
                        key="key_getAllNewMessages"
                        command="cmd_getMsgsForAuthAccounts"/>
         <toolbarbutton id="appmenu_getnewmsgs_current_account"
                        class="subviewbutton subviewbutton-iconic"
                        label="&getNewMsgCurrentAccountCmdPopupMenu.label;"
                        key="key_getNewMessages"
                        command="cmd_getNewMessages"/>
-        <toolbarseparator/> -->
-        <menu id="appmenu_getNewMsgFor"
-              label="&getNewMsgForCmd.label;"
-              oncommand="MsgGetMessagesForAccount();">
-          <menupopup is="folder-menupopup" id="appmenu_getAllNewMsgPopup"
-                     mode="getMail"
-                     expandFolders="false"
-                     oncommand="MsgGetMessagesForAccount(event.target._folder); event.stopPropagation();">
-            <menuitem id="appmenu_getnewmsgs_all_accounts"
-                      label="&getAllNewMsgCmdPopupMenu.label;"
-                      key="key_getAllNewMessages"
-                      command="cmd_getMsgsForAuthAccounts"/>
-            <menuitem id="appmenu_getnewmsgs_current_account"
-                      label="&getNewMsgCurrentAccountCmdPopupMenu.label;"
-                      key="key_getNewMessages"
-                      command="cmd_getNewMessages"/>
-            <menuseparator/>
-          </menupopup>
-        </menu>
+        <toolbarseparator/>
+        <!-- toolbarbuttons are dynamically added here by folder-panelview custom element. -->
       </vbox>
     </panelview>
 
     <!-- File / Offline -->
     <panelview id="appMenu-fileOfflineView"
                title="&offlineMenu.label;"
                class="PanelUI-subView">
       <vbox class="panel-subview-body">
@@ -1379,31 +1362,24 @@
         <toolbarbutton id="appmenu_prevFlaggedMsg"
                        class="subviewbutton subviewbutton-iconic"
                        label="&prevStarredMsgCmd.label;"
                        command="cmd_previousFlaggedMsg"/>
       </vbox>
     </panelview>
 
     <!-- Go / Folder -->
-    <panelview id="appMenu-goFolderView"
+    <!-- toolbarbuttons are dynamically added by folder-panelview custom element. -->
+    <panelview is="folder-panelview" id="appMenu-goFolderView"
                title="&folderMenu.label;"
-               class="PanelUI-subView">
-      <vbox class="panel-subview-body">
-        <!-- TODO appmenu dynamically populate -->
-        <menu id="appmenu_goFolderMenu"
-              label="&folderMenu.label;"
-              command="cmd_goFolder">
-          <menupopup is="folder-menupopup" id="appmenu_GoFolderPopup"
-                     showFileHereLabel="true"
-                     showRecent="true"
-                     recentLabel="&contextMoveCopyMsgRecentMenu.label;"/>
-        </menu>
-      </vbox>
-    </panelview>
+               class="PanelUI-subView"
+               showFileHereLabel="true"
+               showRecent="true"
+               command="cmd_goFolder"
+               recentLabel="&contextMoveCopyMsgRecentMenu.label;"/>
 
     <!-- Go / Recently Closed Tabs -->
     <!-- Dynamically populated when shown. -->
     <!-- TODO appmenu - what about this 'observes' bit?
         <menu id="appmenu_goRecentlyClosedTabs"
                   label="&goRecentlyClosedTabs.label;"
                   observes="cmd_undoCloseTab">
     -->
@@ -1673,56 +1649,42 @@
         <toolbarbutton id="appmenu_recalculateJunkScore"
                        class="subviewbutton subviewbutton-iconic"
                        label="&recalculateJunkScoreCmd.label;"
                        command="cmd_recalculateJunkScore"/>
       </vbox>
     </panelview>
 
     <!-- Message / Move To -->
-    <panelview id="appMenu-messageMoveToView"
+    <!-- toolbarbuttons are dynamically added by folder-panelview custom element. -->
+    <panelview is="folder-panelview" id="appMenu-messageMoveToView"
                title="&moveMsgToMenu.label;"
-               class="PanelUI-subView">
-      <vbox class="panel-subview-body">
-        <!-- TODO appmenu dynamic population -->
-        <menu id="appmenu_moveMenu"
-              label="&moveMsgToMenu.label;"
-              oncommand="MsgMoveMessage(event.target._folder)">
-          <menupopup is="folder-menupopup"
-                     mode="filing"
-                     showFileHereLabel="true"
-                     showRecent="true"
-                     recentLabel="&moveCopyMsgRecentMenu.label;"
-                     showFavorites="true"
-                     favoritesLabel="&contextMoveCopyMsgFavoritesMenu.label;"
-                     favoritesAccessKey="&contextMoveCopyMsgFavoritesMenu.accesskey;"/>
-        </menu>
-      </vbox>
-    </panelview>
+               class="PanelUI-subView"
+               oncommand="MsgMoveMessage(event.target._folder)"
+               mode="filing"
+               showFileHereLabel="true"
+               showRecent="true"
+               recentLabel="&moveCopyMsgRecentMenu.label;"
+               showFavorites="true"
+               favoritesLabel="&contextMoveCopyMsgFavoritesMenu.label;"
+               favoritesAccessKey="&contextMoveCopyMsgFavoritesMenu.accesskey;"/>
 
     <!-- Message / Copy To -->
-    <panelview id="appMenu-messageCopyToView"
+    <!-- toolbarbuttons are dynamically added by folder-panelview custom element. -->
+    <panelview is="folder-panelview" id="appMenu-messageCopyToView"
                title="&copyMsgToMenu.label;"
-               class="PanelUI-subView">
-      <vbox class="panel-subview-body">
-        <!-- TODO appmenu dynamic population -->
-        <menu id="appmenu_copyMenu"
-              label="&copyMsgToMenu.label;"
-              oncommand="MsgCopyMessage(event.target._folder)">
-          <menupopup is="folder-menupopup"
-                      mode="filing"
-                      showFileHereLabel="true"
-                      showRecent="true"
-                      recentLabel="&moveCopyMsgRecentMenu.label;"
-                      showFavorites="true"
-                      favoritesLabel="&contextMoveCopyMsgFavoritesMenu.label;"
-                      favoritesAccessKey="&contextMoveCopyMsgFavoritesMenu.accesskey;"/>
-        </menu>
-      </vbox>
-    </panelview>
+               class="PanelUI-subView"
+               oncommand="MsgCopyMessage(event.target._folder)"
+               mode="filing"
+               showFileHereLabel="true"
+               showRecent="true"
+               recentLabel="&moveCopyMsgRecentMenu.label;"
+               showFavorites="true"
+               favoritesLabel="&contextMoveCopyMsgFavoritesMenu.label;"
+               favoritesAccessKey="&contextMoveCopyMsgFavoritesMenu.accesskey;"/>
 
     <!-- Tools -->
     <panelview id="appMenu-toolsView"
                title="&tasksMenu.label;"
                class="PanelUI-subView">
       <vbox class="panel-subview-body">
         <toolbarbutton hidden="true"
                        id="appmenu_tasksMenuMail"
--- a/mail/test/mozmill/folder-widget/test-message-filters.js
+++ b/mail/test/mozmill/folder-widget/test-message-filters.js
@@ -93,18 +93,18 @@ function test_customize_toolbar_doesnt_d
     wait_for_window_focused(mc.window);
 
     const subview = mc.click_through_appmenu(
       [{id: "appmenu_File"}, {id: "appmenu_getNewMsgFor"}]);
 
     assert_equals(subview.children.length, 5,
                   "Incorrect number of items for GetNewMessages before customization");
 
-    // TODO appmenu - Now click somewhere that causes the appmenu to close.
-    // (Once this test is no longer skipped, see below.)
+    // Close the appmenu.
+    mc.click(mc.eid("button-appmenu"));
   }
 
   check_getAllNewMsgMenu();
 
   plan_for_new_window("mailnews:customizeToolbar");
   // Open the customization dialog.
   mc.rightClick(mc.eid("mail-bar3"));
   mc.click(mc.eid("CustomizeMailToolbar"));
@@ -114,20 +114,16 @@ function test_customize_toolbar_doesnt_d
   wait_for_window_focused(customc.window);
   plan_for_window_close(customc);
   customc.click(customc.eid("donebutton"));
   wait_for_window_close();
 
   check_getAllNewMsgMenu();
 }
 test_customize_toolbar_doesnt_double_get_mail_menu.EXCLUDED_PLATFORMS = ["darwin"];
-// TODO appmenu - Skipped because it depends on the folder-menupopup code being
-// adapted for use in the appmenu.  Namely the call to click_through_appmenu
-// won't work because the UI it expects will not be there yet.
-test_customize_toolbar_doesnt_double_get_mail_menu.__force_skip__ = true;
 
 /* A helper function that opens up the new filter dialog (assuming that the
  * main filters dialog is already open), creates a simple filter, and then
  * closes the dialog.
  */
 function create_simple_filter() {
   // Open the "Tools » Message Filters…" window,
   // a.k.a. "tasksMenu » filtersCmd".
--- a/mailnews/base/content/folder-menupopup.js
+++ b/mailnews/base/content/folder-menupopup.js
@@ -1,48 +1,101 @@
 /* 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/. */
 
 "use strict";
 
-/* globals MozElements */
+/* globals MozElements MozXULElement PanelUI */
+
+// This file implements both `folder-menupopup` custom elements used in
+// traditional menus and `folder-panelview` custom elements used in the appmenu.
 
 // Wrap in a block to prevent leaking to window scope.
 {
   const { FeedUtils } = ChromeUtils.import("resource:///modules/FeedUtils.jsm");
   const { allAccountsSorted, folderNameCompare, getSpecialFolderString, getMostRecentFolders } =
     ChromeUtils.import("resource:///modules/folderUtils.jsm");
   const { fixIterator, toArray } = ChromeUtils.import("resource:///modules/iteratorUtils.jsm");
   const { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
   const { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
   const { StringBundle } = ChromeUtils.import("resource:///modules/StringBundle.js");
 
   /**
-   * The MozFolderMenupopup widget is used as a menupopup for selecting
-   * a folder from the list of all the folders from every account. It is also
-   * used for selecting the account from the list of all the accounts. The each
-   * menuitem gets displayed with the folder or account name and icon.
+   * Creates an element, sets attributes on it, including always setting the
+   * "generated" attribute to "true", and returns the element. The "generated"
+   * attribute is used to determine which elements to remove when clearing
+   * the menu.
+   *
+   * @param {string} tagName    The tag name of the element to generate.
+   * @param {Object} [attributes]  Optional attributes to set on the element.
+   * @param {Object} [isObject]  The optional "is" object to use when creating
+   *                             the element, typically `{is: "folder-menupopup"}`
+   *                             or `{is: "folder-panelview"}`.
+   */
+  function generateElement(tagName, attributes, isObject) {
+    const element = document.createXULElement(tagName, isObject);
+    element.setAttribute("generated", "true");
+
+    if (attributes) {
+      Object.entries(attributes).forEach(([key, value]) => {
+        element.setAttribute(key, value);
+      });
+    }
+    return element;
+  }
+
+  /**
+   * Each time this function is called it generates a unique ID attribute,
+   * e.g. for a `panelview` element. It keeps track of a counter (via a
+   * closure) that increments each time it is called, guaranteeing the IDs it
+   * returns are unique.
    *
-   * @extends {MozElements.MozMenuPopup}
+   * @param {string} prefix  Prefix that is combined with a number to make the ID.
+   * @return {string}        The unique ID.
    */
-  class MozFolderMenupopup extends MozElements.MozMenuPopup {
+  const getUniquePanelViewId = (() => {
+    let counter = 0;
+    return (prefix) => {
+      counter += 1;
+      return prefix + counter;
+    };
+  })();
+
+  /**
+   * A "mixin" function to add shared code to the classes for the
+   * `folder-menupopup` and `folder-panelview` custom elements. Takes a "Base"
+   * class, and returns a class that extends the "Base" class.
+   *
+   * We use this mixin approach because the `folder-menupopup` class needs to
+   * extend `MozMenuPopup` while the `folder-panelview` class should just extend
+   * `MozXULElement`.
+   *
+   * The shared code in this mixin class works with both `menupopup` and
+   * `panelview` custom elements by using functions and properties from the
+   * "Base" `folder-menupopup` or `folder-panelview` class. Generally the
+   * mixin class determines *what* needs to be built and then defers to the
+   * "Base" custom element class to handle *how* to build it for that menu type.
+   *
+   * Note how this double duty raises some naming challenges. For example,
+   * variables that could be bound to either a `menupopup` or a `panelview`
+   * might be named "submenu". See also `menu`/`menuitem` vs `toolbarbutton`.
+   * (`panelview` and `toolbarbutton` are used in the appmenu.)
+   *
+   * @param {Class} Base  A class to be extended with shared functionality.
+   * @return {Class}      A class that extends the first class.
+   */
+  let FolderMenuMixin = (Base) => class extends Base {
     constructor() {
       super();
 
-      // In order to improve performance, we're not going to build any of the
-      // menu until we're shown.
-      // note: _ensureInitialized can be called repeatedly without issue, so
-      //       don't worry about it here.
-      this.addEventListener("popupshowing", (event) => {
-        this._ensureInitialized();
-      }, true);
-
       window.addEventListener("unload", () => {
+        // Clean up when being destroyed.
         this._removeListener();
+        this._teardown();
       }, { once: true });
 
       // If non-null, the subFolders of this nsIMsgFolder will be used to
       // populate this menu.  If this is null, the menu will be populated
       // using the root-folders for all accounts.
       this._parentFolder = null;
 
       this._stringBundle = null;
@@ -204,26 +257,30 @@
         },
         OnItemBoolPropertyChanged(item, property, old, newItem) {
           this._setCssSelectorsForItem(item);
         },
         OnItemUnicharPropertyChanged(item, property, old, newItem) {
           this._setCssSelectorsForItem(item);
         },
         OnItemPropertyFlagChanged(item, property, old, newItem) { },
+
         OnItemEvent(folder, eventName) {
           if (eventName == "MRMTimeChanged") {
             if (this._menu.getAttribute("showRecent") != "true" ||
                 !this._menu._initializedSpecials.has("recent") ||
-                !this._menu.firstChild || !this._menu.firstChild.firstChild) {
+                !this._menu.childWrapper.firstChild) {
               return;
             }
+
+            const recentMenuItem = this._menu.childWrapper.firstChild;
+            const recentSubMenu = this._menu._getSubMenuForMenuItem(recentMenuItem);
+
             // If this folder is already in the recent menu, return.
-            if (this._getChildForItem(folder,
-                  this._menu.firstChild.firstChild)) {
+            if (!recentSubMenu || this._getChildForItem(folder, recentSubMenu)) {
               return;
             }
           } else if (eventName == "RenameCompleted") {
             // Special casing folder renames here, since they require more work
             // since sort-order may have changed.
             if (!this._getChildForItem(folder)) {
               return;
             }
@@ -238,28 +295,25 @@
          * Helper function to check and see whether we have a menuitem for this
          * particular nsIMsgFolder.
          *
          * @param {nsIMsgFolder} item  The folder to check.
          * @param {Element} [menu]     Optional menu to look in, defaults to this._menu.
          * @returns {Element|null}     The menuitem for that folder, or null if no
          *                             child for that folder exists.
          */
-        _getChildForItem(item, menu) {
-          let _menu = menu || this._menu;
-          if (!_menu || !_menu.hasChildNodes()) {
+        _getChildForItem(item, menu = this._menu) {
+          if (!menu ||
+              !menu.childWrapper.hasChildNodes() ||
+              !(item instanceof Ci.nsIMsgFolder)) {
             return null;
           }
-          if (!(item instanceof Ci.nsIMsgFolder)) {
-            return null;
-          }
-          for (let i = 0; i < _menu.childNodes.length; i++) {
-            let folder = _menu.childNodes[i]._folder;
-            if (folder && folder.URI == item.URI) {
-              return _menu.childNodes[i];
+          for (let child of menu.childWrapper.childNodes) {
+            if (child._folder && child._folder.URI == item.URI) {
+              return child;
             }
           }
           return null;
         },
       };
 
       // True if we have already built our menu items and are now just
       // listening for changes.
@@ -272,68 +326,25 @@
       // The format for displaying names of folders.
       this._displayformat = null;
     }
 
     connectedCallback() {
       if (this.delayConnectedCallback()) {
         return;
       }
-
-      this.setAttribute("is", "folder-menupopup");
+      // Call the connectedCallback of the "base" class this mixin class is extending.
+      super.connectedCallback();
 
       this._stringBundle = new StringBundle("chrome://messenger/locale/folderWidgets.properties");
 
       // Get the displayformat if set.
       if (this.parentNode && this.parentNode.localName == "menulist") {
         this._displayformat = this.parentNode.getAttribute("displayformat");
       }
-
-      // Find out if we are in a wrapper (customize toolbars mode is active).
-      let inWrapper = false;
-      let node = this;
-      while (node instanceof XULElement) {
-        if (node.id.startsWith("wrapper-")) {
-          inWrapper = true;
-          break;
-        }
-        node = node.parentNode;
-      }
-
-      if (!inWrapper) {
-        if (this.hasAttribute("original-width")) {
-           // If we were in a wrapper before and have a width stored, restore it now.
-          if (this.getAttribute("original-width") == "none") {
-            this.removeAttribute("width");
-          } else {
-            this.setAttribute("width", this.getAttribute("original-width"));
-          }
-
-          this.removeAttribute("original-width");
-        }
-
-        // If we are a child of a menulist, and we aren't in a wrapper, we
-        // need to build our content right away, otherwise the menulist
-        // won't have proper sizing.
-        if (this.parentNode && this.parentNode.localName == "menulist") {
-          this._ensureInitialized();
-        }
-      } else {
-        // But if we're in a wrapper, remove our children, because we're
-        // getting re-created when the toolbar customization closes.
-        this._teardown();
-
-        // Store our current width and set a safe small width when we show
-        // in a wrapper.
-        if (!this.hasAttribute("original-width")) {
-          this.setAttribute("original-width", this.hasAttribute("width") ?
-            this.getAttribute("width") : "none");
-          this.setAttribute("width", "100");
-        }
-      }
     }
 
     set parentFolder(val) {
       return this._parentFolder = val;
     }
 
     get parentFolder() {
       return this._parentFolder;
@@ -462,67 +473,43 @@
     /**
      * Add menu items that only appear at top level, like "Recent".
      */
     _addTopLevelMenuItems() {
       const showRecent = this.getAttribute("showRecent") == "true";
       const showFavorites = this.getAttribute("showFavorites") == "true";
 
       if (showRecent) {
-        this.appendChild(this._buildSpecialMenu("recent"));
+        this.childWrapper.appendChild(this._buildSpecialMenu({
+          "special": "recent",
+          "label": this.getAttribute("recentLabel"),
+          "accessKey": this.getAttribute("recentAccessKey"),
+        }));
       }
       if (showFavorites) {
-        this.appendChild(this._buildSpecialMenu("favorites"));
+        this.childWrapper.appendChild(this._buildSpecialMenu({
+          "special": "favorites",
+          "label": this.getAttribute("favoritesLabel"),
+          "accessKey": this.getAttribute("favoritesAccessKey"),
+        }));
       }
       if (showRecent || showFavorites) {
-        // If we added Recent and/or Favorites, separate them from the rest of the items.
-        const sep = document.createXULElement("menuseparator");
-        sep.setAttribute("generated", "true");
-        this.appendChild(sep);
+        this.childWrapper.appendChild(this._buildSeparator());
       }
     }
 
     /**
-     * This only creates the menu item (or toolbarbutton) in the top-level
-     * menulist. The submenu is created once the popup is really shown,
-     * via the _buildSpecialSubmenu method.
+     * Populate a "recent" or "favorites" special submenu with either the
+     * recently used or favorite folders, to allow for easy access.
      *
-     * @param {string} type  Type of special menu (e.g. "recent", "favorites").
-     * @return {Element}     The `menu` or `toolbarbutton` element.
+     * @param {Element} menu  The menu or toolbarbutton element for which one
+     *                        wants to populate the special sub menu.
+     * @param {Element} submenu  The submenu element, typically a menupopup or panelview.
      */
-    _buildSpecialMenu(type) {
-      // Now create the Recent folder menu and its children.
-      let menu = document.createXULElement("menu");
-      menu.setAttribute("special", type);
-
-      menu.setAttribute("label", this.getAttribute((type == "recent") ?
-        "recentLabel" : "favoritesLabel"));
-
-      menu.setAttribute("accesskey", this.getAttribute((type == "recent") ?
-        "recentAccessKey" : "favoritesAccessKey"));
-
-      menu.setAttribute("generated", "true");
-
-      let popup = document.createXULElement("menupopup");
-      popup.setAttribute("class", this.getAttribute("class"));
-      popup.addEventListener("popupshowing", (event) => {
-        this._buildSpecialSubmenu(menu);
-      }, { once: true });
-
-      menu.appendChild(popup);
-      return menu;
-    }
-
-    /**
-     * Builds a submenu with all of the recently used folders in it, to
-     * allow for easy access.
-     *
-     * @param {Element} menu  The menu element for which one wants to build the special sub menu.
-     */
-    _buildSpecialSubmenu(menu) {
+    _populateSpecialSubmenu(menu, submenu) {
       let specialType = menu.getAttribute("special");
       if (this._initializedSpecials.has(specialType)) {
         return;
       }
 
       // Iterate through all folders in all accounts matching the current filter.
       let specialFolders = toArray(
         fixIterator(MailServices.accounts.allFolders, Ci.nsIMsgFolder));
@@ -578,25 +565,23 @@
         folderItem.label = label;
       }
 
       // Make sure the entries are sorted alphabetically.
       specialFoldersMap.sort((a, b) => folderNameCompare(a.label, b.label));
 
       // Create entries for each of the recent folders.
       for (let folderItem of specialFoldersMap) {
-        let node = document.createXULElement("menuitem");
-
-        node.setAttribute("label", folderItem.label);
-        node._folder = folderItem.folder;
+        let attributes = {
+          label: folderItem.label,
+          ...this._getCssSelectorAttributes(folderItem.folder),
+        };
 
-        node.setAttribute("class", "folderMenuItem menuitem-iconic");
-        this._setCssSelectors(folderItem.folder, node);
-        node.setAttribute("generated", "true");
-        menu.menupopup.appendChild(node);
+        submenu.childWrapper.appendChild(
+          this._buildMenuItem(attributes, folderItem.folder));
       }
 
       if (specialFoldersMap.length == 0) {
         menu.setAttribute("disabled", "true");
       }
 
       this._initializedSpecials.add(specialType);
     }
@@ -613,43 +598,39 @@
      *     mode is not equal to newFolder.
      *  The menu item will have the value of the fileHereLabel attribute as
      *  label or if the attribute does not exist the name of the parent
      *  folder instead.
      *
      * @param {string} mode  The mode attribute.
      */
     _maybeAddParentFolderMenuItem(mode) {
-      let parent = this._parentFolder;
-      if (parent && (this.getAttribute("showFileHereLabel") == "true" || !mode)) {
+      let folder = this._parentFolder;
+      if (folder && (this.getAttribute("showFileHereLabel") == "true" || !mode)) {
         let showAccountsFileHere = this.getAttribute("showAccountsFileHere");
-        if ((!parent.isServer || showAccountsFileHere != "false") &&
-             (!mode || mode == "newFolder" || parent.noSelect ||
-               parent.canFileMessages || showAccountsFileHere == "true")) {
-          let menuitem = document.createXULElement("menuitem");
 
-          menuitem._folder = parent;
-          menuitem.setAttribute("generated", "true");
+        if ((!folder.isServer || showAccountsFileHere != "false") &&
+             (!mode || mode == "newFolder" || folder.noSelect ||
+               folder.canFileMessages || showAccountsFileHere == "true")) {
+          let attributes = {};
+
           if (this.hasAttribute("fileHereLabel")) {
-            menuitem.setAttribute("label", this.getAttribute("fileHereLabel"));
-            menuitem.setAttribute("accesskey", this.getAttribute("fileHereAccessKey"));
+            attributes.label = this.getAttribute("fileHereLabel");
+            attributes.accesskey = this.getAttribute("fileHereAccessKey");
           } else {
-            menuitem.setAttribute("label", parent.prettyName);
-            menuitem.setAttribute("class", "folderMenuItem menuitem-iconic");
-            this._setCssSelectors(parent, menuitem);
-          }
-          this.appendChild(menuitem);
-
-          if (parent.noSelect) {
-            menuitem.setAttribute("disabled", "true");
+            attributes.label = folder.prettyName;
+            Object.assign(attributes, this._getCssSelectorAttributes(folder));
           }
 
-          let sep = document.createXULElement("menuseparator");
-          sep.setAttribute("generated", "true");
-          this.appendChild(sep);
+          if (folder.noSelect) {
+            attributes.disabled = "true";
+          }
+
+          this.childWrapper.appendChild(this._buildMenuItem(attributes, folder));
+          this.childWrapper.appendChild(this._buildSeparator());
         }
       }
     }
 
     /**
      * Add menu items, one for each folder.
      *
      * @param {nsIMsgFolder[]} folders          Array of folder objects.
@@ -664,78 +645,80 @@
       // We need to call this, or hasSubFolders will always return false.
       // Remove this workaround when Bug 502900 is fixed.
       MailUtils.discoverFolders();
       this._serversOnly = true;
 
       let [shouldExpand, labels] = this._getShouldExpandAndLabels();
 
       for (let folder of folders) {
-        let node;
         if (!folder.isServer) {
           this._serversOnly = false;
         }
 
-        // If we're going to add subFolders, we need to make menus, not
-        // menuitems.
+        let attributes = {
+          label: this._getFolderLabel(mode, globalInboxFolder, folder),
+          ...this._getCssSelectorAttributes(folder),
+        };
+
+        if (disableServers.includes(folder.server.key)) {
+          attributes.disabled = "true";
+        }
+
         if (!folder.hasSubFolders || !shouldExpand(folder.server.type)) {
-          node = document.createXULElement("menuitem");
-          node.setAttribute("class", "folderMenuItem menuitem-iconic");
-          node.setAttribute("generated", "true");
-          this.appendChild(node);
+          // There are no subfolders, create a simple menu item.
+          this.childWrapper.appendChild(this._buildMenuItem(attributes, folder));
         } else {
-          this._serversOnly = false;
+          // There are subfolders, create a menu item with a submenu.
           // xxx this is slightly problematic in that we haven't confirmed
           //     whether any of the subfolders will pass the filter.
-          node = document.createXULElement("menu");
-          node.setAttribute("class", "folderMenuItem menu-iconic");
-          node.setAttribute("generated", "true");
-          this.appendChild(node);
 
-          // Create the submenu.
-          let popup = document.createXULElement("menupopup", { "is": "folder-menupopup" });
-          popup._parentFolder = folder;
+          this._serversOnly = false;
+
+          let submenuAttributes = {};
 
           ["class", "type", "fileHereLabel", "showFileHereLabel", "oncommand",
             "mode", "disableServers", "position"].forEach(attribute => {
             if (this.hasAttribute(attribute)) {
-              popup.setAttribute(attribute, this.getAttribute(attribute));
+              submenuAttributes[attribute] = this.getAttribute(attribute);
             }
           });
 
-          // If there are labels, add the labels now.
+          const [menuItem, submenu] = this._buildMenuItemWithSubmenu(attributes,
+            true, folder, submenuAttributes);
+
+          // If there are labels, we add an item and separator to the submenu.
           if (labels) {
-            let serverNode = document.createXULElement("menuitem");
-            serverNode.setAttribute("label", labels[folder.server.type]);
-            serverNode._folder = folder;
-            serverNode.setAttribute("generated", "true");
-            popup.appendChild(serverNode);
-            let sep = document.createXULElement("menuseparator");
-            sep.setAttribute("generated", "true");
-            popup.appendChild(sep);
+            const serverAttributes = { label: labels[folder.server.type] };
+
+            submenu.childWrapper.appendChild(
+              this._buildMenuItem(serverAttributes, folder, this));
+
+            submenu.childWrapper.appendChild(this._buildSeparator());
           }
-          popup.setAttribute("generated", "true");
-          node.appendChild(popup);
-        }
 
-        if (disableServers.includes(folder.server.key)) {
-          node.setAttribute("disabled", "true");
+          this.childWrapper.appendChild(menuItem);
         }
+      }
+    }
 
-        node._folder = folder;
-        let label = "";
-        if (mode == "deferred" && folder.isServer &&
-            folder.server.rootFolder == globalInboxFolder) {
-          label = this._stringBundle.get("globalInbox", [folder.prettyName]);
-        } else {
-          label = folder.prettyName;
-        }
-        node.setAttribute("label", label);
-        this._setCssSelectors(folder, node);
+    /**
+     * Return the label to use for a folder.
+     *
+     * @param {string} mode  The mode, e.g. "deferred".
+     * @param {nsIMsgFolder} globalInboxFolder  The root/global inbox folder.
+     * @param {nsIMsgFolder} folder  The folder for which we are getting a label.
+     * @return {string}  The label to use for the folder.
+     */
+    _getFolderLabel(mode, globalInboxFolder, folder) {
+      if (mode == "deferred" && folder.isServer &&
+          folder.server.rootFolder == globalInboxFolder) {
+        return this._stringBundle.get("globalInbox", [folder.prettyName]);
       }
+      return folder.prettyName;
     }
 
     /**
      * Let the user have a list of subfolders for all account types, none of
      * them, or only some of them.  Returns an array containing a function that
      * determines whether to show subfolders for a given account type, and an
      * object mapping account types to label names (may be null).
      *
@@ -769,39 +752,57 @@
           }
         }
         shouldExpand = (e) => types.includes(e);
       }
       return [shouldExpand, labels];
     }
 
     /**
-     * This function adds attributes on menu/menuitems to make it easier for
-     * css to style them.
+     * Set attributes on a menu, menuitem, or toolbarbutton element to allow
+     * for CSS styling.
      *
      * @param {nsIMsgFolder} folder  The folder that corresponds to the menu/menuitem.
      * @param {Element} menuNode     The actual DOM node to set attributes on.
      */
     _setCssSelectors(folder, menuNode) {
-      // First set the SpecialFolder attribute.
-      menuNode.setAttribute("SpecialFolder", getSpecialFolderString(folder));
+        const cssAttributes = this._getCssSelectorAttributes(folder);
+
+        Object.entries(cssAttributes).forEach(([key, value]) =>
+          menuNode.setAttribute(key, value));
+    }
 
-      // Now set the biffState.
+    /**
+     * Returns attributes to be set on a menu, menuitem, or toolbarbutton
+     * element to allow for CSS styling.
+     *
+     * @param {nsIMsgFolder} folder  The folder that corresponds to the menu item.
+     * @return {Object}              Contains the CSS selector attributes.
+     */
+    _getCssSelectorAttributes(folder) {
+      let attributes = {};
+
+      // First the SpecialFolder attribute.
+      attributes.SpecialFolder = getSpecialFolderString(folder);
+
+      // Now the biffState.
       let biffStates = ["NewMail", "NoMail", "UnknownMail"];
       for (let state of biffStates) {
         if (folder.biffState == Ci.nsIMsgFolder["nsMsgBiffState_" + state]) {
-          menuNode.setAttribute("BiffState", state);
+          attributes.BiffState = state;
           break;
         }
       }
 
-      menuNode.setAttribute("IsServer", folder.isServer);
-      menuNode.setAttribute("IsSecure", folder.server.isSecure);
-      menuNode.setAttribute("ServerType", folder.server.type);
-      menuNode.setAttribute("IsFeedFolder", !!FeedUtils.getFeedUrlsInFolder(folder));
+      attributes.IsServer = folder.isServer;
+      attributes.IsSecure = folder.server.isSecure;
+      attributes.ServerType = folder.server.type;
+      attributes.IsFeedFolder = !!FeedUtils.getFeedUrlsInFolder(folder);
+
+      return attributes;
     }
 
     /**
      * This function returns a formatted display name for a menulist
      * selected folder. The desired format is set as the 'displayformat'
      * attribute of the folderpicker's <menulist>, one of:
      * 'name' (default) - Folder
      * 'verbose'        - Folder on Account
@@ -824,16 +825,18 @@
         return FeedUtils.getFolderPrettyPath(folder) || folder.name;
       }
 
       return folder.name;
     }
 
     /**
      * Makes a given folder selected.
+     * TODO: This function does not work yet for the appmenu. However, as of
+     * June 2019, this functionality is not used in the appmenu.
      *
      * @param {nsIMsgFolder} inputFolder  The folder to select (if none, then Choose Folder).
      * @return {boolean}                  Is true if any usable folder was found, otherwise false.
      * @note  If inputFolder is not in this popup, but is instead a descendant of
      *        a member of the popup, that ancestor will be selected.
      */
     selectFolder(inputFolder) {
       // Set the label of the menulist element as if folder had been selected.
@@ -894,42 +897,397 @@
         noFolders = false;
       }
 
       setupParent(folder, this.parentNode, noFolders);
       return !!folder;
     }
 
     /**
-     * Removes all menu items for this popup, resets all fields, and
-     * removes the listener. This function is invoked when a change
-     * that affects this menu is detected by our listener.
+     * Removes all menu items from this menu, removes their submenus (needed for
+     * the appmenu where the `panelview` submenus are not children of the
+     * `toolbarbutton` menu items), resets all fields, and removes the listener.
+     * This function is called when a change that affects this menu is detected
+     * by the listener.
      */
     _teardown() {
       if (!this._initialized) {
         return;
       }
-
-      for (let i = this.childNodes.length - 1; i >= 0; i--) {
-        let child = this.childNodes[i];
-        if (child.getAttribute("generated") != "true") {
+      const children = this.childWrapper.childNodes;
+      // We iterate in reverse order because childNodes is live so it changes
+      // as we remove child nodes.
+      for (let i = children.length - 1; i >= 0; i--) {
+        const item = children[i];
+        if (item.getAttribute("generated") != "true") {
           continue;
         }
-        if ("_teardown" in child) {
-          child._teardown();
+        const submenu = this._getSubMenuForMenuItem(item);
+
+        if (submenu && "_teardown" in submenu) {
+          submenu._teardown();
+          submenu.remove();
         }
-        child.remove();
+        item.remove();
       }
 
       this._removeListener();
 
       this._initialized = false;
       this._initializedSpecials.clear();
     }
-    disconnectedCallback() {
-      // Clean up when being destroyed.
-      this._removeListener();
-      this._teardown();
+  };
+
+  /**
+   * The MozFolderMenupopup widget is used as a menupopup that contains menu
+   * items and submenus for all folders from every account (or some subset of
+   * folders and accounts). It is also used to provide a menu with a menuitem
+   * for each account. Each menu item gets displayed with the folder or
+   * account name and icon. It uses code that is also used by MozFolderPanelView
+   * via the FolderMenuMixin function.
+   *
+   * @extends {MozElements.MozMenuPopup}
+   */
+  let MozFolderMenuPopup = FolderMenuMixin(class extends MozElements.MozMenuPopup {
+    constructor() {
+      super();
+
+      // To improve performance, only build the menu when it is shown.
+      this.addEventListener("popupshowing", (event) => {
+        this._ensureInitialized();
+      }, true);
+
+      // Because the menu items in a panelview go inside a child vbox but are
+      // direct children of a menupopup, we set up a consistent way to append
+      // and access menu items for both cases.
+      this.childWrapper = this;
+    }
+
+    connectedCallback() {
+      if (this.delayConnectedCallback()) {
+        return;
+      }
+
+      this.setAttribute("is", "folder-menupopup");
+
+      // Find out if we are in a wrapper (customize toolbars mode is active).
+      let inWrapper = false;
+      let node = this;
+      while (node instanceof XULElement) {
+        if (node.id.startsWith("wrapper-")) {
+          inWrapper = true;
+          break;
+        }
+        node = node.parentNode;
+      }
+
+      if (!inWrapper) {
+        if (this.hasAttribute("original-width")) {
+          // If we were in a wrapper before and have a width stored, restore it now.
+          if (this.getAttribute("original-width") == "none") {
+            this.removeAttribute("width");
+          } else {
+            this.setAttribute("width", this.getAttribute("original-width"));
+          }
+
+          this.removeAttribute("original-width");
+        }
+
+        // If we are a child of a menulist, and we aren't in a wrapper, we
+        // need to build our content right away, otherwise the menulist
+        // won't have proper sizing.
+        if (this.parentNode && this.parentNode.localName == "menulist") {
+          this._ensureInitialized();
+        }
+      } else {
+        // But if we're in a wrapper, remove our children, because we're
+        // getting re-created when the toolbar customization closes.
+        this._teardown();
+
+        // Store our current width and set a safe small width when we show
+        // in a wrapper.
+        if (!this.hasAttribute("original-width")) {
+          this.setAttribute("original-width", this.hasAttribute("width") ?
+            this.getAttribute("width") : "none");
+          this.setAttribute("width", "100");
+        }
+      }
+    }
+
+    /**
+     * Given a menu item, return the menupopup that it opens.
+     *
+     * @param {Element} menu   The menu item, typically a `menu` element.
+     * @return {Element|null}  The `menupopup` element or null if none found.
+     */
+    _getSubMenuForMenuItem(menu) {
+      return menu.querySelector("menupopup");
+    }
+
+    /**
+     * Returns a `menuseparator` element for use in a `menupopup`.
+     */
+    _buildSeparator() {
+      return generateElement("menuseparator");
+    }
+
+    /**
+     * Builds a menu item (`menuitem`) element that does not open a submenu
+     * (i.e. not a `menu` element).
+     *
+     * @param {Object} [attributes]  Attributes to set on the element.
+     * @param {nsIMsgFolder} folder  The folder associated with the menu item.
+     * @returns {Element}            A `menuitem`.
+     */
+    _buildMenuItem(attributes, folder) {
+        const menuitem = generateElement("menuitem", attributes);
+        menuitem.classList.add("folderMenuItem", "menuitem-iconic");
+        menuitem._folder = folder;
+        return menuitem;
+    }
+
+    /**
+     * Builds a menu item (`menu`) element and an associated submenu
+     * (`menupopup`) element.
+     *
+     * @param {Object} attributes         Attributes to set on the `menu` element.
+     * @param {boolean} folderSubmenu     Whether the submenu is to be a
+     *                                    `folder-menupopup` element.
+     * @param {nsIMsgFolder} [folder]     The folder associated with the menu item.
+     * @param {Object} submenuAttributes  Attributes to set on the `menupopup` element.
+     * @return {Element[]}       Array containing the `menu` and
+     *                                    `menupopup` elements.
+     */
+    _buildMenuItemWithSubmenu(attributes, folderSubmenu, folder, submenuAttributes) {
+      const menu = generateElement("menu", attributes);
+      menu.classList.add("folderMenuItem", "menu-iconic");
+
+      const isObject = folderSubmenu ? { "is": "folder-menupopup" } : null;
+
+      const menupopup = generateElement("menupopup", submenuAttributes, isObject);
+
+      if (folder) {
+        menu._folder = folder;
+        menupopup._parentFolder = folder;
+      }
+
+      if (!menupopup.childWrapper) {
+        menupopup.childWrapper = menupopup;
+      }
+
+      menu.appendChild(menupopup);
+
+      return [menu, menupopup];
+    }
+
+    /**
+     * Build a special menu item (`menu`) and an empty submenu (`menupopup`)
+     * for it. The submenu is populated just before it is shown by
+     * `_populateSpecialSubmenu`.
+     *
+     * The submenu (`menupopup`) is just a standard element, not a custom
+     * element (`folder-menupopup`).
+     *
+     * @param {Object} [attributes]  Attributes to set on the menu item element.
+     * @return {Element}             The menu item (`menu`) element.
+     */
+    _buildSpecialMenu(attributes) {
+      const [menu, menupopup] = this._buildMenuItemWithSubmenu(attributes);
+
+      menupopup.addEventListener("popupshowing", (event) => {
+        this._populateSpecialSubmenu(menu, menupopup);
+      }, { once: true });
+
+      return menu;
     }
-  }
+  });
+
+  customElements.define("folder-menupopup", MozFolderMenuPopup, { extends: "menupopup" });
+
+  /**
+   * Used as a panelview in the appmenu/hamburger menu. It contains
+   * menu items and submenus for all folders from every account (or some subset
+   * of folders and accounts). It is also used to provide a menu with a menuitem
+   * for each account. Each menu item gets displayed with the folder or account
+   * name and icon. It uses code that is also used by MozFolderMenupopup via
+   * the FolderMenuMixin function.
+   *
+   * @extends {MozXULElement}
+   */
+  let MozFolderPanelView = FolderMenuMixin(class extends MozXULElement {
+    constructor() {
+      super();
+
+      // To improve performance, only build the menu when it is shown.
+      this.addEventListener("ViewShowing", (event) => {
+        this._ensureInitialized();
+      }, true);
+   }
+
+    connectedCallback() {
+      // In the appmenu the panelview elements may move around, so we only want
+      // connectedCallback to run once.
+      if (this.delayConnectedCallback() || this.hasConnected) {
+        return;
+      }
+      this.hasConnected = true;
+      this.setAttribute("is", "folder-panelview");
+      this._setUpPanelView(this);
+    }
+
+    /**
+     * Set up a `folder-panelview` or a plain `panelview` element. If the
+     * panelview was statically defined in a XUL file then it may already have
+     * a child <vbox> element, if it was dynamically generated it may not yet.
+     *
+     * @param {Element} panelview  The panelview to set up.
+     */
+    _setUpPanelView(panelview) {
+      let subviewBody = panelview.querySelector(".panel-subview-body");
+
+      if (!subviewBody) {
+        subviewBody = document.createXULElement("vbox");
+        subviewBody.classList.add("panel-subview-body");
+        panelview.appendChild(subviewBody);
+      }
+      // Because the menu items in a panelview go inside a child vbox but are
+      // direct children of a menupopup, we set up a consistent way to append
+      // and access menu items for both cases.
+      panelview.childWrapper = subviewBody;
+      panelview.classList.add("PanelUI-subView");
+
+      // Prevent the back button from firing the command that is set on the
+      // panelview by stopping propagation of the event. The back button does
+      // not exist until the panelview is shown for the first time (when the
+      // header is added to it).
+      panelview.addEventListener("ViewShown", () => {
+        const backButton = panelview.querySelector(".panel-header > .subviewbutton-back");
+        if (backButton) {
+          backButton.addEventListener("command", event => event.stopPropagation());
+        }
+      }, { once: true });
+    }
+
+    /**
+     * Given a menu item, return the submenu that it opens.
+     *
+     * @param {Element} item   The menu item, typically a `toolbarbutton`.
+     * @return {Element|null}  The submenu (or null if none found), typically a
+     *                         `panelview` element.
+     */
+    _getSubMenuForMenuItem(item) {
+      const panelviewId = item.getAttribute("panelviewId");
+      if (panelviewId) {
+        return document.getElementById(panelviewId);
+      }
+      return null;
+    }
+
+    /**
+     * Returns a `toolbarseparator` element for use in a `panelview`.
+     */
+    _buildSeparator() {
+      return generateElement("toolbarseparator");
+    }
 
-  customElements.define("folder-menupopup", MozFolderMenupopup, { extends: "menupopup" });
+    /**
+     * Builds a menu item (`toolbarbutton`) element that does not open a submenu.
+     *
+     * @param {Object} [attributes]  Attributes to set on the element.
+     * @param {nsIMsgFolder} folder  The folder associated with the menu item.
+     * @returns {Element}            A `toolbarbutton`.
+     */
+    _buildMenuItem(attributes, folder) {
+        const button = generateElement("toolbarbutton", attributes);
+        button._folder = folder;
+
+        button.classList.add("folderMenuItem", "subviewbutton",
+          "subviewbutton-iconic");
+        return button;
+    }
+
+    /**
+     * Builds a menu item (`toolbarbutton`) element and an associated submenu
+     * (`panelview`) element.
+     *
+     * @param {Object} attributes         Attributes to set on the
+     *                                    `toolbarbutton` element.
+     * @param {boolean} folderSubmenu     Whether the submenu is to be a
+     *                                    `folder-panelview` element.
+     * @param {nsIMsgFolder} [folder]     The folder associated with the menu item.
+     * @param {Object} submenuAttributes  Attributes to set on the `panelview`
+     *                                    element.
+     * @return {Element[]}                Array containing the `toolbarbutton`
+     *                                    and `panelview` elements.
+     */
+    _buildMenuItemWithSubmenu(attributes, folderSubmenu, folder, submenuAttributes) {
+      const button = generateElement("toolbarbutton", attributes);
+
+      button.classList.add("folderMenuItem", "subviewbutton",
+        "subviewbutton-iconic", "subviewbutton-nav");
+
+      const isObject = folderSubmenu ? { "is": "folder-panelview" } : null;
+
+      const panelview = generateElement("panelview", submenuAttributes, isObject);
+
+      if (!folderSubmenu) {
+        this._setUpPanelView(panelview);
+      }
+
+      if (folder) {
+        panelview._parentFolder = folder;
+        panelview._folder = folder;
+      }
+
+      const panelviewId = getUniquePanelViewId("folderPanelView");
+      panelview.setAttribute("id", panelviewId);
+
+      // Pass these attributes down from panelview to panelview.
+      ["command", "oncommand"].forEach(attribute => {
+        if (this.hasAttribute(attribute)) {
+          panelview.setAttribute(attribute, this.getAttribute(attribute));
+        }
+      });
+
+      if (!submenuAttributes || (submenuAttributes && !submenuAttributes.label)) {
+        panelview.setAttribute("label", attributes.label);
+      }
+
+      button.addEventListener("command", (event) => {
+        // Stop event propagation so the command that is set on the panelview
+        // is not fired when we are just navigating to a submenu.
+        event.stopPropagation();
+        PanelUI.showSubView(panelviewId, panelview);
+      });
+
+      // Save the panelviewId on the menu item so we have a way to access the
+      // panelview from the menu item that opens it.
+      button.setAttribute("panelviewId", panelviewId);
+      button.setAttribute("closemenu", "none");
+      document.querySelector("#appMenu-multiView").appendChild(panelview);
+
+      return [button, panelview];
+    }
+
+    /**
+     * Build a special menu item (`toolbarbutton`) and an empty submenu
+     * (`panelview`) for it. The submenu is populated just before it is shown
+     * by `_populateSpecialSubmenu`.
+     *
+     * The submenu (`panelview`) is just a standard element, not a custom
+     * element (`folder-panelview`).
+     *
+     * @param {Object} [attributes]  Attributes to set on the menu item element.
+     * @return {Element}             The menu item (`toolbarbutton`) element.
+     */
+    _buildSpecialMenu(attributes) {
+      const [button, panelview] = this._buildMenuItemWithSubmenu(attributes);
+
+      panelview.addEventListener("ViewShowing", (event) => {
+        this._populateSpecialSubmenu(button, panelview);
+      }, { once: true });
+
+      return button;
+    }
+  });
+
+  customElements.define("folder-panelview", MozFolderPanelView, { extends: "panelview" });
 }