Bug 1519502 - Convert menu bindings to a Custom Element r=surkov
authorBrian Grinstead <bgrinstead@mozilla.com>
Thu, 18 Apr 2019 16:41:46 +0000
changeset 470131 223a8a0c4eed19547e6e843cfd265c728f7dc7c2
parent 470130 c9ffb38259ec6d3885c925a6733115ebb57822f8
child 470132 084d8d7331a72473e3bddcd537a98b5983087581
push id112843
push useraiakab@mozilla.com
push dateFri, 19 Apr 2019 09:50:22 +0000
treeherdermozilla-inbound@c06f27cbfe40 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssurkov
bugs1519502
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1519502 - Convert menu bindings to a Custom Element r=surkov Differential Revision: https://phabricator.services.mozilla.com/D19593
accessible/xul/XULMenuAccessible.cpp
browser/base/content/test/general/browser_contextmenu_childprocess.js
browser/components/extensions/test/browser/browser_ext_contextMenus.js
browser/components/extensions/test/browser/browser_ext_menus.js
browser/components/extensions/test/browser/browser_ext_menus_replace_menu.js
browser/components/extensions/test/browser/browser_ext_menus_replace_menu_context.js
browser/components/extensions/test/browser/head.js
devtools/client/scratchpad/scratchpad.js
dom/tests/mochitest/general/test_offsets.js
layout/xul/test/test_submenuClose.xul
toolkit/content/customElements.js
toolkit/content/tests/chrome/test_panelfrommenu.xul
toolkit/content/widgets/menu.js
toolkit/content/widgets/menu.xml
toolkit/content/xul.css
--- a/accessible/xul/XULMenuAccessible.cpp
+++ b/accessible/xul/XULMenuAccessible.cpp
@@ -255,29 +255,31 @@ void XULMenuitemAccessible::ActionNameAt
 
 uint8_t XULMenuitemAccessible::ActionCount() const { return 1; }
 
 ////////////////////////////////////////////////////////////////////////////////
 // XULMenuitemAccessible: Widgets
 
 bool XULMenuitemAccessible::IsActiveWidget() const {
   // Parent menu item is a widget, it's active when its popup is open.
-  nsIContent* menuPopupContent = mContent->GetFirstChild();
+  // Typically the <menupopup> is included in the document markup, and
+  // <menu> prepends content in front of it.
+  nsIContent* menuPopupContent = mContent->GetLastChild();
   if (menuPopupContent) {
     nsMenuPopupFrame* menuPopupFrame =
         do_QueryFrame(menuPopupContent->GetPrimaryFrame());
     return menuPopupFrame && menuPopupFrame->IsOpen();
   }
   return false;
 }
 
 bool XULMenuitemAccessible::AreItemsOperable() const {
   // Parent menu item is a widget, its items are operable when its popup is
   // open.
-  nsIContent* menuPopupContent = mContent->GetFirstChild();
+  nsIContent* menuPopupContent = mContent->GetLastChild();
   if (menuPopupContent) {
     nsMenuPopupFrame* menuPopupFrame =
         do_QueryFrame(menuPopupContent->GetPrimaryFrame());
     return menuPopupFrame && menuPopupFrame->IsOpen();
   }
   return false;
 }
 
--- a/browser/base/content/test/general/browser_contextmenu_childprocess.js
+++ b/browser/base/content/test/general/browser_contextmenu_childprocess.js
@@ -27,17 +27,17 @@ function checkItems(menuitem, arr) {
   for (let i = 0; i < arr.length; i += 2) {
     let str = arr[i];
     let details = arr[i + 1];
     if (str == "---") {
       is(menuitem.localName, "menuseparator", "menuseparator");
     } else if ("children" in details) {
       is(menuitem.localName, "menu", "submenu");
       is(menuitem.getAttribute("label"), str, str + " label");
-      checkItems(menuitem.firstElementChild.firstElementChild, details.children);
+      checkItems(menuitem.menupopup.firstElementChild, details.children);
     } else {
       is(menuitem.localName, "menuitem", str + " menuitem");
 
       is(menuitem.getAttribute("label"), str, str + " label");
       is(menuitem.getAttribute("type"), details.type, str + " type");
       is(menuitem.getAttribute("image"), details.icon ? gBaseURL + details.icon : "", str + " icon");
 
       if (details.checked)
--- a/browser/components/extensions/test/browser/browser_ext_contextMenus.js
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus.js
@@ -178,17 +178,17 @@ add_task(async function() {
   is(items.length, 0, "contextMenu item for selection was not found (context=image)");
 
   items = extensionMenuRoot.getElementsByAttribute("label", "parentToDel");
   is(items.length, 0, "contextMenu item for removed parent was not found (context=image)");
 
   items = extensionMenuRoot.getElementsByAttribute("label", "parent");
   is(items.length, 1, "contextMenu item for parent was found (context=image)");
 
-  is(items[0].childNodes[0].childNodes.length, 2, "child items for parent were found (context=image)");
+  is(items[0].menupopup.children.length, 2, "child items for parent were found (context=image)");
 
   // Click on ext-image item and check the click results
   await closeExtensionContextMenu(image);
 
   let result = await extension.awaitMessage("onclick");
   checkClickInfo(result);
   result = await extension.awaitMessage("browser.contextMenus.onClicked");
   checkClickInfo(result);
--- a/browser/components/extensions/test/browser/browser_ext_menus.js
+++ b/browser/components/extensions/test/browser/browser_ext_menus.js
@@ -82,17 +82,17 @@ add_task(async function test_actionConte
   for (const kind of ["page", "browser"]) {
     const menu = await openActionContextMenu(extension, kind);
     const [submenu, second, , , , last, separator] = menu.children;
 
     is(submenu.tagName, "menu", "Correct submenu type");
     is(submenu.label, "parent", "Correct submenu title");
 
     const popup = await openSubmenu(submenu);
-    is(popup, submenu.firstElementChild, "Correct submenu opened");
+    is(popup, submenu.menupopup, "Correct submenu opened");
     is(popup.children.length, 2, "Correct number of submenu items");
 
     let idPrefix = `${makeWidgetId(extension.id)}-menuitem-_`;
 
     is(second.tagName, "menuitem", "Second menu item type is correct");
     is(second.label, "click 1", "Second menu item title is correct");
     is(second.id, `${idPrefix}1`, "Second menu item id is correct");
 
@@ -246,17 +246,17 @@ add_task(async function test_tabContextM
   is(submenu.label, "alpha-beta parent", "Correct submenu title");
 
   isnot(gamma.label, "dummy", "`page` context menu item should not appear here");
 
   is(gamma.tagName, "menuitem", "Third menu item type is correct");
   is(gamma.label, "gamma", "Third menu item label is correct");
 
   const popup = await openSubmenu(submenu);
-  is(popup, submenu.firstElementChild, "Correct submenu opened");
+  is(popup, submenu.menupopup, "Correct submenu opened");
   is(popup.children.length, 2, "Correct number of submenu items");
 
   const [alpha, beta] = popup.children;
   is(alpha.tagName, "menuitem", "First menu item type is correct");
   is(alpha.label, "alpha", "First menu item label is correct");
   is(beta.tagName, "menuitem", "Second menu item type is correct");
   is(beta.label, "beta", "Second menu item label is correct");
 
@@ -391,17 +391,17 @@ add_task(async function test_tools_menu(
   const tabId = await second.awaitMessage("ready");
   const menu = await openToolsMenu();
 
   const [separator, submenu, gamma] = Array.from(menu.children).slice(-3);
   is(separator.tagName, "menuseparator", "Separator before first extension item");
 
   is(submenu.tagName, "menu", "Correct submenu type");
   is(submenu.getAttribute("label"), "Generated extension", "Correct submenu title");
-  is(submenu.firstElementChild.children.length, 2, "Correct number of submenu items");
+  is(submenu.menupopup.children.length, 2, "Correct number of submenu items");
 
   is(gamma.tagName, "menuitem", "Third menu item type is correct");
   is(gamma.getAttribute("label"), "gamma", "Third menu item label is correct");
 
   closeToolsMenu(gamma);
 
   const click = await second.awaitMessage("click");
   is(click.info.pageUrl, "http://example.com/", "Click info pageUrl is correct");
--- a/browser/components/extensions/test/browser/browser_ext_menus_replace_menu.js
+++ b/browser/components/extensions/test/browser/browser_ext_menus_replace_menu.js
@@ -177,17 +177,17 @@ add_task(async function overrideContext_
     checkIsDefaultMenuItemVisible(getVisibleChildrenIds(menu));
 
     let menuItems = menu.getElementsByAttribute("ext-type", "top-level-menu");
     is(menuItems.length, 1, "Expected top-level menu element for extension.");
     let topLevelExtensionMenuItem = menuItems[0];
     is(topLevelExtensionMenuItem.nextSibling, null, "Extension menu should be the last element.");
 
     const submenu = await openSubmenu(topLevelExtensionMenuItem);
-    is(submenu, topLevelExtensionMenuItem.firstElementChild, "Correct submenu opened");
+    is(submenu, topLevelExtensionMenuItem.menupopup, "Correct submenu opened");
 
     Assert.deepEqual(
       getVisibleChildrenIds(submenu),
       EXPECTED_EXTENSION_MENU_IDS,
       "Extension menu items should be in the submenu by default.");
 
     await closeContextMenu();
   }
--- a/browser/components/extensions/test/browser/browser_ext_menus_replace_menu_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_menus_replace_menu_context.js
@@ -195,17 +195,17 @@ add_task(async function overrideContext_
       `${makeWidgetId(extension.id)}-menuitem-_tab_context`,
       `${makeWidgetId(extension.id)}-menuitem-_tab_context_http`,
       `${makeWidgetId(extension.id)}-menuitem-_tab_context_viewType_moz`,
       `menuseparator`,
       topLevels[0].id,
     ], "Expected menu items after changing context to tab");
 
     let submenu = await openSubmenu(topLevels[0]);
-    is(submenu, topLevels[0].firstElementChild, "Correct submenu opened");
+    is(submenu, topLevels[0].menupopup, "Correct submenu opened");
 
     Assert.deepEqual(getVisibleChildrenIds(submenu), [
       `${makeWidgetId(otherExtension.id)}-menuitem-_tab_context`,
       `${makeWidgetId(otherExtension.id)}-menuitem-_tab_context_http`,
       `${makeWidgetId(otherExtension.id)}-menuitem-_tab_context_viewType_moz`,
     ], "Expected menu items in submenu after changing context to tab");
 
     extension.sendMessage("testTabAccess", tabId);
@@ -298,17 +298,17 @@ add_task(async function overrideContext_
       `${makeWidgetId(extension.id)}-menuitem-_bookmark_context_http`,
       `${makeWidgetId(extension.id)}-menuitem-_bookmark_context_moz`,
       `${makeWidgetId(extension.id)}-menuitem-_bookmark_context_viewType_moz`,
       `menuseparator`,
       topLevels[0].id,
     ], "Expected menu items after changing context to bookmark");
 
     let submenu = await openSubmenu(topLevels[0]);
-    is(submenu, topLevels[0].firstElementChild, "Correct submenu opened");
+    is(submenu, topLevels[0].menupopup, "Correct submenu opened");
 
     Assert.deepEqual(getVisibleChildrenIds(submenu), [
       `${makeWidgetId(otherExtension.id)}-menuitem-_bookmark_context`,
       `${makeWidgetId(otherExtension.id)}-menuitem-_bookmark_context_http`,
       `${makeWidgetId(otherExtension.id)}-menuitem-_bookmark_context_moz`,
       `${makeWidgetId(otherExtension.id)}-menuitem-_bookmark_context_viewType_moz`,
     ], "Expected menu items in submenu after changing context to bookmark");
     await closeContextMenu(menu);
--- a/browser/components/extensions/test/browser/head.js
+++ b/browser/components/extensions/test/browser/head.js
@@ -458,17 +458,17 @@ async function openChromeContextMenu(men
   const menu = win.document.getElementById(menuId);
   const shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
   EventUtils.synthesizeMouseAtCenter(node, {type: "contextmenu"}, win);
   await shown;
   return menu;
 }
 
 async function openSubmenu(submenuItem, win = window) {
-  const submenu = submenuItem.firstElementChild;
+  const submenu = submenuItem.menupopup;
   const shown = BrowserTestUtils.waitForEvent(submenu, "popupshown");
   EventUtils.synthesizeMouseAtCenter(submenuItem, {}, win);
   await shown;
   return submenu;
 }
 
 function closeChromeContextMenu(menuId, itemToSelect, win = window) {
   const menu = win.document.getElementById(menuId);
--- a/devtools/client/scratchpad/scratchpad.js
+++ b/devtools/client/scratchpad/scratchpad.js
@@ -1227,17 +1227,17 @@ var Scratchpad = {
     const maxRecent = Services.prefs.getIntPref(PREF_RECENT_FILES_MAX);
     const recentFilesMenu = document.getElementById("sp-open_recent-menu");
 
     if (maxRecent < 1) {
       recentFilesMenu.setAttribute("hidden", true);
       return;
     }
 
-    const recentFilesPopup = recentFilesMenu.firstChild;
+    const recentFilesPopup = recentFilesMenu.menupopup;
     const filePaths = this.getRecentFiles();
     const filename = this.getState().filename;
 
     recentFilesMenu.setAttribute("disabled", true);
     while (recentFilesPopup.hasChildNodes()) {
       recentFilesPopup.firstChild.remove();
     }
 
@@ -1300,17 +1300,17 @@ var Scratchpad = {
     const maxRecent = Services.prefs.getIntPref(PREF_RECENT_FILES_MAX);
     const menu = document.getElementById("sp-open_recent-menu");
 
     // Hide the menu if the 'PREF_RECENT_FILES_MAX'-pref is set to zero or less.
     if (maxRecent < 1) {
       menu.setAttribute("hidden", true);
     } else {
       if (menu.hasAttribute("hidden")) {
-        if (!menu.firstChild.hasChildNodes()) {
+        if (!menu.menupopup.hasChildNodes()) {
           this.populateRecentFilesMenu();
         }
 
         menu.removeAttribute("hidden");
       }
 
       const filePaths = this.getRecentFiles();
       if (maxRecent < filePaths.length) {
--- a/dom/tests/mochitest/general/test_offsets.js
+++ b/dom/tests/mochitest/general/test_offsets.js
@@ -2,16 +2,22 @@ var scrollbarWidth = 17, scrollbarHeight
 
 function testElements(baseid, callback)
 {
   scrollbarWidth = scrollbarHeight = gcs($("scrollbox-test"), "width");
 
   var elements = $(baseid).getElementsByTagName("*");
   for (var t = 0; t < elements.length; t++) {
     var element = elements[t];
+
+    // Ignore presentational content inside menus
+    if (element.closest("menu") && element.closest("[aria-hidden=true]")) {
+      continue;
+    }
+
     testElement(element);
   }
 
   var nonappended = document.createElement("div");
   nonappended.id = "nonappended";
   nonappended.setAttribute("_offsetParent", "null");
   testElement(nonappended);
 
--- a/layout/xul/test/test_submenuClose.xul
+++ b/layout/xul/test/test_submenuClose.xul
@@ -72,20 +72,20 @@ https://bugzilla.mozilla.org/show_bug.cg
   function handleCCloses() {
     menuCOpen = false;
   }
 
   function nextTest(e) {
     mainMenu = document.getElementById("menu");
     menuB = document.getElementById("b");
     menuC = document.getElementById("c");
-    menuB.firstChild.addEventListener("popupshown", handleBOpens, false);
-    menuB.firstChild.addEventListener("popuphidden", handleBCloses, false);
-    menuC.firstChild.addEventListener("popupshown", handleCOpens, false);
-    menuC.firstChild.addEventListener("popuphidden", handleCCloses, false);
+    menuB.menupopup.addEventListener("popupshown", handleBOpens, false);
+    menuB.menupopup.addEventListener("popuphidden", handleBCloses, false);
+    menuC.menupopup.addEventListener("popupshown", handleCOpens, false);
+    menuC.menupopup.addEventListener("popuphidden", handleCCloses, false);
     mainMenu.addEventListener("popupshown", ev => {
       synthesizeMouseAtCenter(menuB, {}, window);
     });
     mainMenu.open = true;
   }
   ]]>
   </script>
 </window>
--- a/toolkit/content/customElements.js
+++ b/toolkit/content/customElements.js
@@ -559,18 +559,18 @@ MozElements.BaseControlMixin = Base => {
       if (val) {
         this.setAttribute("tabindex", val);
       } else {
         this.removeAttribute("tabindex");
       }
     }
   }
 
-  Base.implementCustomInterface(BaseControl,
-                                [Ci.nsIDOMXULControlElement]);
+  MozXULElement.implementCustomInterface(BaseControl,
+                                         [Ci.nsIDOMXULControlElement]);
   return BaseControl;
 };
 MozElements.BaseControl = MozElements.BaseControlMixin(MozXULElement);
 
 const BaseTextMixin = Base => class BaseText extends MozElements.BaseControlMixin(Base) {
   set label(val) {
     this.setAttribute("label", val);
     return val;
@@ -617,16 +617,17 @@ const BaseTextMixin = Base => class Base
     }
     return val;
   }
 
   get accessKey() {
     return this.labelElement ? this.labelElement.accessKey : this.getAttribute("accesskey");
   }
 };
+MozElements.BaseTextMixin = BaseTextMixin;
 MozElements.BaseText = BaseTextMixin(MozXULElement);
 
 // Attach the base class to the window so other scripts can use it:
 window.MozXULElement = MozXULElement;
 
 customElements.setElementCreationCallback("browser", () => {
   Services.scriptloader.loadSubScript("chrome://global/content/elements/browser-custom-element.js", window);
 });
--- a/toolkit/content/tests/chrome/test_panelfrommenu.xul
+++ b/toolkit/content/tests/chrome/test_panelfrommenu.xul
@@ -69,23 +69,23 @@ function menuOpened()
   synthesizeKey("KEY_Enter");
 }
 
 function menuClosed()
 {
   // the panel will be open at this point, but the popupshown event
   // still needs to fire
   is($("panel").state, "showing", "panel is open after menu hide");
-  is($("menu").firstChild.state, "closed", "menu is closed after menu hide");
+  is($("menu").menupopup.state, "closed", "menu is closed after menu hide");
 }
 
 function panelOpened()
 {
   is($("panel").state, "open", "panel is open");
-  is($("menu").firstChild.state, "closed", "menu is closed");
+  is($("menu").menupopup.state, "closed", "menu is closed");
   $("panel").hidePopup();
 }
 
 function panelOnButtonOpened(panel)
 {
   is(panel.state, 'open', 'button panel is open');
   is(document.activeElement, document.documentElement, "focus blurred on panel from button open");
   synthesizeKey("KEY_ArrowDown");
--- a/toolkit/content/widgets/menu.js
+++ b/toolkit/content/widgets/menu.js
@@ -2,136 +2,287 @@
   * 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";
 
 // This is loaded into all XUL windows. Wrap in a block to prevent
 // leaking to window scope.
 {
-class MozMenuItemBase extends MozElements.BaseText {
-  // nsIDOMXULSelectControlItemElement
-  set value(val) {
-    this.setAttribute("value", val);
-  }
-  get value() {
-    return this.getAttribute("value");
-  }
+const MozMenuItemBaseMixin = Base => {
+  class MozMenuItemBase extends MozElements.BaseTextMixin(Base) {
+    // nsIDOMXULSelectControlItemElement
+    set value(val) {
+      this.setAttribute("value", val);
+    }
+    get value() {
+      return this.getAttribute("value");
+    }
 
-  // nsIDOMXULSelectControlItemElement
-  get selected() {
-    return this.getAttribute("selected") == "true";
-  }
+    // nsIDOMXULSelectControlItemElement
+    get selected() {
+      return this.getAttribute("selected") == "true";
+    }
 
-  // nsIDOMXULSelectControlItemElement
-  get control() {
-    var parent = this.parentNode;
-    // Return the parent if it is a menu or menulist.
-    if (parent && parent.parentNode instanceof XULMenuElement) {
-      return parent.parentNode;
+    // nsIDOMXULSelectControlItemElement
+    get control() {
+      var parent = this.parentNode;
+      // Return the parent if it is a menu or menulist.
+      if (parent && parent.parentNode instanceof XULMenuElement) {
+        return parent.parentNode;
+      }
+      return null;
     }
-    return null;
-  }
 
-  // nsIDOMXULContainerItemElement
-  get parentContainer() {
-    for (var parent = this.parentNode; parent; parent = parent.parentNode) {
-      if (parent instanceof XULMenuElement) {
-        return parent;
+    // nsIDOMXULContainerItemElement
+    get parentContainer() {
+      for (var parent = this.parentNode; parent; parent = parent.parentNode) {
+        if (parent instanceof XULMenuElement) {
+          return parent;
+        }
       }
+      return null;
     }
-    return null;
   }
-}
-
-MozXULElement.implementCustomInterface(MozMenuItemBase, [Ci.nsIDOMXULSelectControlItemElement, Ci.nsIDOMXULContainerItemElement]);
-
-class MozMenuBase extends MozMenuItemBase {
-  set open(val) {
-    this.openMenu(val);
-    return val;
-  }
-
-  get open() {
-    return this.hasAttribute("open");
-  }
+  MozXULElement.implementCustomInterface(MozMenuItemBase, [Ci.nsIDOMXULSelectControlItemElement, Ci.nsIDOMXULContainerItemElement]);
+  return MozMenuItemBase;
+};
 
-  get itemCount() {
-    var menupopup = this.menupopup;
-    return menupopup ? menupopup.children.length : 0;
-  }
-
-  get menupopup() {
-    const XUL_NS =
-      "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const MozMenuBaseMixin = Base => {
+  class MozMenuBase extends MozMenuItemBaseMixin(Base) {
+    set open(val) {
+      this.openMenu(val);
+      return val;
+    }
 
-    for (var child = this.firstElementChild; child; child = child.nextElementSibling) {
-      if (child.namespaceURI == XUL_NS && child.localName == "menupopup")
-        return child;
-    }
-    return null;
-  }
-
-  appendItem(aLabel, aValue) {
-    var menupopup = this.menupopup;
-    if (!menupopup) {
-      menupopup = this.ownerDocument.createXULElement("menupopup");
-      this.appendChild(menupopup);
+    get open() {
+      return this.hasAttribute("open");
     }
 
-    var menuitem = this.ownerDocument.createXULElement("menuitem");
-    menuitem.setAttribute("label", aLabel);
-    menuitem.setAttribute("value", aValue);
+    get itemCount() {
+      var menupopup = this.menupopup;
+      return menupopup ? menupopup.children.length : 0;
+    }
+
+    get menupopup() {
+      const XUL_NS =
+        "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
-    return menupopup.appendChild(menuitem);
-  }
+      for (var child = this.firstElementChild; child; child = child.nextElementSibling) {
+        if (child.namespaceURI == XUL_NS && child.localName == "menupopup")
+          return child;
+      }
+      return null;
+    }
+
+    appendItem(aLabel, aValue) {
+      var menupopup = this.menupopup;
+      if (!menupopup) {
+        menupopup = this.ownerDocument.createXULElement("menupopup");
+        this.appendChild(menupopup);
+      }
+
+      var menuitem = this.ownerDocument.createXULElement("menuitem");
+      menuitem.setAttribute("label", aLabel);
+      menuitem.setAttribute("value", aValue);
 
-  getIndexOfItem(aItem) {
-    var menupopup = this.menupopup;
-    if (menupopup) {
-      var items = menupopup.children;
-      var length = items.length;
-      for (var index = 0; index < length; ++index) {
-        if (items[index] == aItem)
-          return index;
+      return menupopup.appendChild(menuitem);
+    }
+
+    getIndexOfItem(aItem) {
+      var menupopup = this.menupopup;
+      if (menupopup) {
+        var items = menupopup.children;
+        var length = items.length;
+        for (var index = 0; index < length; ++index) {
+          if (items[index] == aItem)
+            return index;
+        }
       }
+      return -1;
     }
-    return -1;
+
+    getItemAtIndex(aIndex) {
+      var menupopup = this.menupopup;
+      if (!menupopup || aIndex < 0 || aIndex >= menupopup.children.length)
+        return null;
+
+      return menupopup.children[aIndex];
+    }
   }
-
-  getItemAtIndex(aIndex) {
-    var menupopup = this.menupopup;
-    if (!menupopup || aIndex < 0 || aIndex >= menupopup.children.length)
-      return null;
-
-    return menupopup.children[aIndex];
-  }
-}
-
-MozXULElement.implementCustomInterface(MozMenuBase, [Ci.nsIDOMXULContainerElement]);
+  MozXULElement.implementCustomInterface(MozMenuBase, [Ci.nsIDOMXULContainerElement]);
+  return MozMenuBase;
+};
 
 // The <menucaption> element is used for rendering <html:optgroup> inside of <html:select>,
 // See SelectParentHelper.jsm.
-class MozMenuCaption extends MozMenuBase {
+class MozMenuCaption extends MozMenuBaseMixin(MozXULElement) {
   static get inheritedAttributes() {
     return {
       ".menu-iconic-left": "selected,disabled,checked",
       ".menu-iconic-icon": "src=image,validate,src",
       ".menu-iconic-text": "value=label,crop,highlightable",
       ".menu-iconic-highlightable-text": "text=label,crop,highlightable",
     };
   }
 
   connectedCallback() {
     this.textContent = "";
     this.appendChild(MozXULElement.parseXULToFragment(`
-      <hbox class="menu-iconic-left" align="center" pack="center" role="none">
-        <image class="menu-iconic-icon" role="none"></image>
+      <hbox class="menu-iconic-left" align="center" pack="center" aria-hidden="true">
+        <image class="menu-iconic-icon" aria-hidden="true"></image>
       </hbox>
-      <label class="menu-iconic-text" flex="1" crop="right" role="none"></label>
-      <label class="menu-iconic-highlightable-text" crop="right" role="none"></label>
+      <label class="menu-iconic-text" flex="1" crop="right" aria-hidden="true"></label>
+      <label class="menu-iconic-highlightable-text" crop="right" aria-hidden="true"></label>
     `));
     this.initializeAttributeInheritance();
   }
 }
 
 customElements.define("menucaption", MozMenuCaption);
+
+// In general, wait to render menus inside menupopups until they are going to be visible:
+window.addEventListener("popupshowing", (e) => {
+  if (e.originalTarget.ownerDocument != document) {
+    return;
+  }
+  for (let menu of e.originalTarget.querySelectorAll("menu")) {
+    menu.render();
+  }
+}, { capture: true });
+
+const isHiddenWindow = document.documentURI == "chrome://browser/content/hiddenWindow.xul";
+
+class MozMenu extends MozMenuBaseMixin(MozElements.MozElementMixin(XULMenuElement)) {
+  static get inheritedAttributes() {
+    return {
+      ".menubar-text": "value=label,accesskey,crop",
+      ".menu-iconic-text": "value=label,accesskey,crop,highlightable",
+      ".menu-text": "value=label,accesskey,crop",
+      ".menu-iconic-highlightable-text": "text=label,crop,accesskey,highlightable",
+      ".menubar-left": "src=image",
+      ".menu-iconic-icon": "src=image,triggeringprincipal=iconloadingprincipal,validate",
+      ".menu-iconic-accel": "value=acceltext",
+      ".menu-right": "_moz-menuactive,disabled",
+      ".menu-accel": "value=acceltext",
+    };
+  }
+
+  get needsEagerRender() {
+    return this.isMenubarChild || this.isSizingPopup || !this.isInMenupopup;
+  }
+
+  get isMenubarChild() {
+    return this.matches("menubar > menu");
+  }
+
+  get isSizingPopup() {
+    return this.matches("[sizetopopup] menu") || this.matches("menulist menu");
+  }
+
+  get isInMenupopup() {
+    return this.matches("menupopup menu");
+  }
+
+  get isIconic() {
+    return this.classList.contains("menu-iconic");
+  }
+
+  get fragment() {
+    let {isMenubarChild, isIconic} = this;
+    let fragment = null;
+    // Add aria-hidden="true" on all DOM, since XULMenuAccessible handles accessibility here.
+    if (isMenubarChild && isIconic) {
+      if (!MozMenu.menubarIconicFrag) {
+        MozMenu.menubarIconicFrag = MozXULElement.parseXULToFragment(`
+          <image class="menubar-left" aria-hidden="true"/>
+          <label class="menubar-text" crop="right" aria-hidden="true"/>
+        `);
+      }
+      fragment = document.importNode(MozMenu.menubarIconicFrag, true);
+    }
+    if (isMenubarChild && !isIconic) {
+      if (!MozMenu.menubarFrag) {
+        MozMenu.menubarFrag = MozXULElement.parseXULToFragment(`
+          <label class="menubar-text" crop="right" aria-hidden="true"/>
+        `);
+      }
+      fragment = document.importNode(MozMenu.menubarFrag, true);
+    }
+    if (!isMenubarChild && isIconic) {
+      if (!MozMenu.normalIconicFrag) {
+        MozMenu.normalIconicFrag = MozXULElement.parseXULToFragment(`
+          <hbox class="menu-iconic-left" align="center" pack="center" aria-hidden="true">
+            <image class="menu-iconic-icon"/>
+          </hbox>
+          <label class="menu-iconic-text" flex="1" crop="right" aria-hidden="true"/>
+          <label class="menu-iconic-highlightable-text" crop="right" aria-hidden="true"/>
+          <hbox class="menu-accel-container" anonid="accel" aria-hidden="true">
+            <label class="menu-iconic-accel"/>
+          </hbox>
+          <hbox align="center" class="menu-right" aria-hidden="true">
+            <image/>
+          </hbox>
+       `);
+      }
+
+      fragment = document.importNode(MozMenu.normalIconicFrag, true);
+    }
+    if (!isMenubarChild && !isIconic) {
+      if (!MozMenu.normalFrag) {
+        MozMenu.normalFrag = MozXULElement.parseXULToFragment(`
+          <label class="menu-text" crop="right" aria-hidden="true"/>
+          <hbox class="menu-accel-container" anonid="accel" aria-hidden="true">
+            <label class="menu-accel"/>
+          </hbox>
+          <hbox align="center" class="menu-right" aria-hidden="true">
+            <image/>
+          </hbox>
+       `);
+      }
+
+      fragment = document.importNode(MozMenu.normalFrag, true);
+    }
+    return fragment;
+  }
+
+  render() {
+    // There are 2 main types of menus:
+    //  (1) direct descendant of a menubar
+    //  (2) all other menus
+    // There is also an "iconic" variation of (1) and (2) based on the class.
+    // To make this as simple as possible, we don't support menus being changed from one
+    // of these types to another after the initial DOM connection. It'd be possible to make
+    // this work by keeping track of the markup we prepend and then removing / re-prepending
+    // during a change, but it's not a feature we use anywhere currently.
+    if (this.renderedOnce) {
+      return;
+    }
+    this.renderedOnce = true;
+
+    // There will be a <menupopup /> already. Don't clear it out, just put our markup before it.
+    this.prepend(this.fragment);
+    this.initializeAttributeInheritance();
+  }
+
+  connectedCallback() {
+    // On OSX we will have a bunch of menus in the hidden window. They get converted
+    // into native menus based on the host attributes, so the inner DOM doesn't need
+    // to be created.
+    if (isHiddenWindow) {
+      return;
+    }
+
+    if (this.delayConnectedCallback()) {
+      return;
+    }
+
+    // Wait until we are going to be visible or required for sizing a popup.
+    if (!this.needsEagerRender) {
+      return;
+    }
+
+    this.render();
+  }
 }
+
+customElements.define("menu", MozMenu);
+}
--- a/toolkit/content/widgets/menu.xml
+++ b/toolkit/content/widgets/menu.xml
@@ -39,141 +39,25 @@
             }
           }
           return null;
         </getter>
       </property>
     </implementation>
   </binding>
 
-  <binding id="menu-base"
-           extends="chrome://global/content/bindings/menu.xml#menuitem-base">
-
-    <implementation implements="nsIDOMXULContainerElement">
-      <property name="open" onget="return this.hasAttribute('open');">
-        <setter><![CDATA[
-          this.openMenu(val);
-          return val;
-        ]]></setter>
-      </property>
-
-      <!-- nsIDOMXULContainerElement interface -->
-      <method name="appendItem">
-        <parameter name="aLabel"/>
-        <parameter name="aValue"/>
-        <body>
-          var menupopup = this.menupopup;
-          if (!menupopup) {
-            menupopup = this.ownerDocument.createXULElement("menupopup");
-            this.appendChild(menupopup);
-          }
-
-          var menuitem = this.ownerDocument.createXULElement("menuitem");
-          menuitem.setAttribute("label", aLabel);
-          menuitem.setAttribute("value", aValue);
-
-          return menupopup.appendChild(menuitem);
-        </body>
-      </method>
-
-      <property name="itemCount" readonly="true">
-        <getter>
-          var menupopup = this.menupopup;
-          return menupopup ? menupopup.children.length : 0;
-        </getter>
-      </property>
-
-      <method name="getIndexOfItem">
-        <parameter name="aItem"/>
-        <body>
-        <![CDATA[
-          var menupopup = this.menupopup;
-          if (menupopup) {
-            var items = menupopup.children;
-            var length = items.length;
-            for (var index = 0; index < length; ++index) {
-              if (items[index] == aItem)
-                return index;
-            }
-          }
-          return -1;
-        ]]>
-        </body>
-      </method>
-
-      <method name="getItemAtIndex">
-        <parameter name="aIndex"/>
-        <body>
-        <![CDATA[
-          var menupopup = this.menupopup;
-          if (!menupopup || aIndex < 0 || aIndex >= menupopup.children.length)
-            return null;
-
-          return menupopup.children[aIndex];
-        ]]>
-        </body>
-      </method>
-
-      <property name="menupopup" readonly="true">
-        <getter>
-        <![CDATA[
-          const XUL_NS =
-            "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-
-          for (var child = this.firstElementChild; child; child = child.nextElementSibling) {
-            if (child.namespaceURI == XUL_NS && child.localName == "menupopup")
-              return child;
-          }
-          return null;
-        ]]>
-        </getter>
-      </property>
-    </implementation>
-  </binding>
-
-  <binding id="menu"
-           extends="chrome://global/content/bindings/menu.xml#menu-base">
-    <content>
-      <xul:label class="menu-text" xbl:inherits="value=label,accesskey,crop" crop="right"/>
-      <xul:hbox class="menu-accel-container" anonid="accel">
-        <xul:label class="menu-accel" xbl:inherits="value=acceltext"/>
-      </xul:hbox>
-      <xul:hbox align="center" class="menu-right" xbl:inherits="_moz-menuactive,disabled">
-        <xul:image/>
-      </xul:hbox>
-      <children includes="menupopup"/>
-    </content>
-  </binding>
-
   <binding id="menuitem" extends="chrome://global/content/bindings/menu.xml#menuitem-base">
     <content>
       <xul:label class="menu-text" xbl:inherits="value=label,accesskey,crop,highlightable" crop="right"/>
       <xul:hbox class="menu-accel-container" anonid="accel">
         <xul:label class="menu-accel" xbl:inherits="value=acceltext"/>
       </xul:hbox>
     </content>
   </binding>
 
-  <binding id="menu-menubar"
-           extends="chrome://global/content/bindings/menu.xml#menu-base">
-    <content>
-      <xul:label class="menubar-text" xbl:inherits="value=label,accesskey,crop" crop="right"/>
-      <children includes="menupopup"/>
-    </content>
-  </binding>
-
-  <binding id="menu-menubar-iconic"
-           extends="chrome://global/content/bindings/menu.xml#menu-base">
-    <content>
-      <xul:image class="menubar-left" xbl:inherits="src=image"/>
-      <xul:label class="menubar-text" xbl:inherits="value=label,accesskey,crop" crop="right"/>
-      <children includes="menupopup"/>
-    </content>
-  </binding>
-
   <binding id="menuitem-iconic" extends="chrome://global/content/bindings/menu.xml#menuitem">
     <content>
       <xul:hbox class="menu-iconic-left" align="center" pack="center"
                 xbl:inherits="selected,_moz-menuactive,disabled,checked">
         <xul:image class="menu-iconic-icon" xbl:inherits="src=image,triggeringprincipal=iconloadingprincipal,validate"/>
       </xul:hbox>
       <xul:label class="menu-iconic-text" flex="1" xbl:inherits="value=label,accesskey,crop,highlightable" crop="right"/>
       <xul:label class="menu-iconic-highlightable-text" xbl:inherits="xbl:text=label,crop,accesskey,highlightable" crop="right"/>
@@ -189,26 +73,9 @@
                 xbl:inherits="selected,disabled,checked">
         <xul:image class="menu-iconic-icon" xbl:inherits="src=image,validate"/>
       </xul:hbox>
       <xul:label class="menu-iconic-text" flex="1" xbl:inherits="value=label,accesskey,crop,highlightable" crop="right"/>
       <xul:label class="menu-iconic-highlightable-text" xbl:inherits="xbl:text=label,crop,accesskey,highlightable" crop="right"/>
     </content>
   </binding>
 
-  <binding id="menu-iconic"
-           extends="chrome://global/content/bindings/menu.xml#menu-base">
-    <content>
-      <xul:hbox class="menu-iconic-left" align="center" pack="center">
-        <xul:image class="menu-iconic-icon" xbl:inherits="src=image"/>
-      </xul:hbox>
-      <xul:label class="menu-iconic-text" flex="1" xbl:inherits="value=label,accesskey,crop,highlightable" crop="right"/>
-      <xul:label class="menu-iconic-highlightable-text" xbl:inherits="xbl:text=label,crop,accesskey,highlightable" crop="right"/>
-      <xul:hbox class="menu-accel-container" anonid="accel">
-        <xul:label class="menu-iconic-accel" xbl:inherits="value=acceltext"/>
-      </xul:hbox>
-      <xul:hbox align="center" class="menu-right" xbl:inherits="_moz-menuactive,disabled">
-        <xul:image/>
-      </xul:hbox>
-      <children includes="menupopup|template"/>
-    </content>
-  </binding>
 </bindings>
--- a/toolkit/content/xul.css
+++ b/toolkit/content/xul.css
@@ -240,32 +240,16 @@ toolbar[type="menubar"] {
 %endif
 
 toolbarspring {
   -moz-box-flex: 1000;
 }
 
 /********* menu ***********/
 
-menubar > menu {
-  -moz-binding: url("chrome://global/content/bindings/menu.xml#menu-menubar");
-}
-
-menubar > menu.menu-iconic {
-  -moz-binding: url("chrome://global/content/bindings/menu.xml#menu-menubar-iconic");
-}
-
-menu {
-  -moz-binding: url("chrome://global/content/bindings/menu.xml#menu");
-}
-
-menu.menu-iconic {
-  -moz-binding: url("chrome://global/content/bindings/menu.xml#menu-iconic");
-}
-
 menubar > menu:empty {
   visibility: collapse;
 }
 
 /********* menuitem ***********/
 
 menuitem {
   -moz-binding: url("chrome://global/content/bindings/menu.xml#menuitem");