Bug 1518932 - Convert menulist to custom element r=paolo
authorTimothy Guan-tin Chien <timdream@gmail.com>
Mon, 28 Jan 2019 18:24:08 +0000
changeset 513567 c1032d34b5e0e525820446f713e4ad6288ebcb56
parent 513566 b77df8435ab1786ec31aab807ca78b7af2fd76cd
child 513568 2ec1e0bc9843aaccafc9427afa07c5ef81f5ca92
push id10862
push userffxbld-merge
push dateMon, 11 Mar 2019 13:01:11 +0000
treeherdermozilla-beta@a2e7f5c935da [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspaolo
bugs1518932, 1514926
milestone66.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 1518932 - Convert menulist to custom element r=paolo This custom element replaces XBL <content> usage by directly prepend the two needed child nodes when the element is connected. This is doable because - There isn't any direct access of child nodes under <menulist>. Everyone seems to access via .menupopup, which is usually the only child. - We don't need to move the children under <menulist>. If we need to and if the child is a <xbl:children> (which could happen if <menulist> is inside an XBL <content> that just get cloned to the document), the layout will get very confused and crash (see finding in bug 1514926) Differential Revision: https://phabricator.services.mozilla.com/D16149
browser/components/places/tests/browser/browser_bookmarkProperties_remember_folders.js
browser/components/preferences/browserLanguages.js
browser/components/preferences/in-content/tests/browser_browser_languages_subdialog.js
browser/components/preferences/in-content/tests/browser_change_app_handler.js
browser/components/preferences/in-content/tests/browser_permissions_dialog.js
browser/components/preferences/sitePermissions.js
browser/modules/SelectionChangedMenulist.jsm
browser/modules/webrtcUI.jsm
toolkit/content/customElements.js
toolkit/content/jar.mn
toolkit/content/tests/chrome/test_menulist_keynav.xul
toolkit/content/tests/chrome/test_menulist_position.xul
toolkit/content/widgets/menulist.js
toolkit/content/widgets/menulist.xml
toolkit/content/xul.css
toolkit/mozapps/extensions/content/extensions.xml
toolkit/mozapps/preferences/fontbuilder.js
toolkit/themes/osx/reftests/nostretch-ref.xul
toolkit/themes/osx/reftests/nostretch.xul
--- a/browser/components/places/tests/browser/browser_bookmarkProperties_remember_folders.js
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_remember_folders.js
@@ -42,17 +42,17 @@ async function assertRecentFolders(expec
 
   Assert.deepEqual(diskGuids, expectedGuids, `Should match the disk GUIDS for ${msg}`);
 
   await clickBookmarkStar();
 
   let actualGuids = [];
   function getGuids() {
     actualGuids = [];
-    const folderMenuPopup = document.getElementById("editBMPanel_folderMenuList").children[0];
+    const folderMenuPopup = document.getElementById("editBMPanel_folderMenuList").menupopup;
 
     let separatorFound = false;
     // The list of folders goes from editBMPanel_foldersSeparator to the end.
     for (let child of folderMenuPopup.children) {
       if (separatorFound) {
         actualGuids.push(child.folderGuid);
       } else if (child.id == "editBMPanel_foldersSeparator") {
         separatorFound = true;
--- a/browser/components/preferences/browserLanguages.js
+++ b/browser/components/preferences/browserLanguages.js
@@ -176,17 +176,17 @@ class OrderedListBox {
 
     return listitem;
   }
 }
 
 class SortedItemSelectList {
   constructor({menulist, button, onSelect, onChange, compareFn}) {
     this.menulist = menulist;
-    this.popup = menulist.firstElementChild;
+    this.popup = menulist.menupopup;
     this.button = button;
     this.compareFn = compareFn;
     this.items = [];
 
     // This will register the "command" listener.
     new SelectionChangedMenulist(this.menulist, () => {
       button.disabled = !menulist.selectedItem;
       if (menulist.selectedItem) {
--- a/browser/components/preferences/in-content/tests/browser_browser_languages_subdialog.js
+++ b/browser/components/preferences/in-content/tests/browser_browser_languages_subdialog.js
@@ -136,17 +136,17 @@ async function createDictionaryBrowseRes
 function assertLocaleOrder(list, locales) {
   is(list.itemCount, locales.split(",").length,
      "The right number of locales are selected");
   is(Array.from(list.children).map(child => child.value).join(","),
      locales, "The selected locales are in order");
 }
 
 function assertAvailableLocales(list, locales) {
-  let items = Array.from(list.firstElementChild.children);
+  let items = Array.from(list.menupopup.children);
   let listLocales = items
     .filter(item => item.value && item.value != "search");
   is(listLocales.length, locales.length, "The right number of locales are available");
   is(listLocales.map(item => item.value).sort(),
      locales.sort().join(","), "The available locales match");
   is(items[0].getAttribute("class"), "label-item", "The first row is a label");
 }
 
@@ -166,27 +166,27 @@ function assertTelemetryRecorded(events)
     .filter(([timestamp, category]) => category == TELEMETRY_CATEGORY)
     .map(relatedEvent => relatedEvent.slice(2, 6));
 
   // Events are now an array of: method, object[, value[, extra]] as expected.
   Assert.deepEqual(relatedEvents, events, "The events are recorded correctly");
 }
 
 function selectLocale(localeCode, available, dialogDoc) {
-  let [locale] = Array.from(available.firstElementChild.children)
+  let [locale] = Array.from(available.menupopup.children)
     .filter(item => item.value == localeCode);
   available.selectedItem = locale;
   dialogDoc.getElementById("add").doCommand();
 }
 
 async function openDialog(doc, search = false) {
   let dialogLoaded = promiseLoadSubDialog(BROWSER_LANGUAGES_URL);
   if (search) {
     doc.getElementById("defaultBrowserLanguageSearch").doCommand();
-    doc.getElementById("defaultBrowserLanguage").firstElementChild.hidePopup();
+    doc.getElementById("defaultBrowserLanguage").menupopup.hidePopup();
   } else {
     doc.getElementById("manageBrowserLanguagesButton").doCommand();
   }
   let dialogWin = await dialogLoaded;
   let dialogDoc = dialogWin.document;
   return {
     dialog: dialogDoc.getElementById("BrowserLanguagesDialog"),
     dialogDoc,
@@ -237,23 +237,23 @@ add_task(async function testDisabledBrow
   is(pl.userDisabled, true, "pl is disabled");
   is(pl.version, "1.0", "pl is the old 1.0 version");
   assertLocaleOrder(selected, "en-US,he");
 
   // Only fr is enabled and not selected, so it's the only locale available.
   assertAvailableLocales(available, ["fr"]);
 
   // Search for more languages.
-  available.firstElementChild.lastElementChild.doCommand();
-  available.firstElementChild.hidePopup();
+  available.menupopup.lastElementChild.doCommand();
+  available.menupopup.hidePopup();
   await waitForMutation(
-    available.firstElementChild,
+    available.menupopup,
     {childList: true},
     target =>
-      Array.from(available.firstElementChild.children)
+      Array.from(available.menupopup.children)
         .some(locale => locale.value == "pl"));
 
   // pl is now available since it is available remotely.
   assertAvailableLocales(available, ["fr", "pl"]);
 
   // Add pl.
   selectLocale("pl", available, dialogDoc);
 
@@ -481,17 +481,17 @@ add_task(async function testInstallFromA
   let {dialog, dialogDoc, available, selected} = await openDialog(doc, true);
   let firstDialogId = getDialogId(dialogDoc);
 
   // Make sure the message bar is still hidden.
   is(messageBar.hidden, true, "The message bar is still hidden after searching");
 
   if (available.itemCount == 1) {
     await waitForMutation(
-      available.firstElementChild,
+      available.menupopup,
       {childList: true},
       target => available.itemCount > 1);
   }
 
   // The initial order is set by the pref.
   assertLocaleOrder(selected, "en-US");
   assertAvailableLocales(available, ["fr", "he", "pl"]);
   is(Services.locale.availableLocales.join(","),
@@ -549,17 +549,17 @@ add_task(async function testInstallFromA
   await langpack.disable();
 
   ({dialogDoc, available, selected} = await openDialog(doc, true));
   let secondDialogId = getDialogId(dialogDoc);
 
   // Wait for the available langpacks to load.
   if (available.itemCount == 1) {
     await waitForMutation(
-      available.firstElementChild,
+      available.menupopup,
       {childList: true},
       target => available.itemCount > 1);
   }
   assertLocaleOrder(selected, "en-US");
   assertAvailableLocales(available, ["fr", "he", "pl"]);
 
   // Uninstall the langpack and dictionary.
   let installs = await AddonManager.getAddonsByTypes(["locale", "dictionary"]);
@@ -594,20 +594,20 @@ add_task(async function testDownloadEnab
       ["intl.multilingual.downloadEnabled", true],
     ],
   });
 
   await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
   let doc = gBrowser.contentDocument;
 
   let defaultMenulist = doc.getElementById("defaultBrowserLanguage");
-  ok(hasSearchOption(defaultMenulist.firstChild), "There's a search option in the General pane");
+  ok(hasSearchOption(defaultMenulist.menupopup), "There's a search option in the General pane");
 
   let { available } = await openDialog(doc, false);
-  ok(hasSearchOption(available.firstChild), "There's a search option in the dialog");
+  ok(hasSearchOption(available.menupopup), "There's a search option in the dialog");
 
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
 
 
 add_task(async function testDownloadDisabled() {
   await SpecialPowers.pushPrefEnv({
     set: [
@@ -615,20 +615,20 @@ add_task(async function testDownloadDisa
       ["intl.multilingual.downloadEnabled", false],
     ],
   });
 
   await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
   let doc = gBrowser.contentDocument;
 
   let defaultMenulist = doc.getElementById("defaultBrowserLanguage");
-  ok(!hasSearchOption(defaultMenulist.firstChild), "There's no search option in the General pane");
+  ok(!hasSearchOption(defaultMenulist.menupopup), "There's no search option in the General pane");
 
   let { available } = await openDialog(doc, false);
-  ok(!hasSearchOption(available.firstChild), "There's no search option in the dialog");
+  ok(!hasSearchOption(available.menupopup), "There's no search option in the dialog");
 
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
 
 add_task(async function testReorderMainPane() {
   await SpecialPowers.pushPrefEnv({
     set: [
       ["intl.multilingual.enabled", true],
@@ -649,26 +649,26 @@ add_task(async function testReorderMainP
 
   await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
   let doc = gBrowser.contentDocument;
 
   let messageBar = doc.getElementById("confirmBrowserLanguage");
   is(messageBar.hidden, true, "The message bar is hidden at first");
 
   let available = doc.getElementById("defaultBrowserLanguage");
-  let availableLocales = Array.from(available.firstElementChild.children);
+  let availableLocales = Array.from(available.menupopup.children);
   let availableCodes = availableLocales.map(item => item.value).sort().join(",");
   is(availableCodes, "en-US,fr,he,pl",
      "All of the available locales are listed");
 
   is(available.selectedItem.value, "en-US", "English is selected");
 
   let hebrew = availableLocales[availableLocales.findIndex(item => item.value == "he")];
   hebrew.click();
-  available.firstElementChild.hidePopup();
+  available.menupopup.hidePopup();
 
   await BrowserTestUtils.waitForCondition(
     () => !messageBar.hidden, "Wait for message bar to show");
 
   is(messageBar.hidden, false, "The message bar is now shown");
   is(messageBar.querySelector("button").getAttribute("locales"), "he,en-US",
      "The locales are set on the message bar button");
 
--- a/browser/components/preferences/in-content/tests/browser_change_app_handler.js
+++ b/browser/components/preferences/in-content/tests/browser_change_app_handler.js
@@ -25,17 +25,17 @@ add_task(async function() {
   let ourItem = container.querySelector("richlistitem[type='text/x-test-handler']");
   ok(ourItem, "handlersView is present");
   ourItem.scrollIntoView();
   container.selectItem(ourItem);
   ok(ourItem.selected, "Should be able to select our item.");
 
   let list = ourItem.querySelector(".actionsMenu");
 
-  let chooseItem = list.firstElementChild.querySelector(".choose-app-item");
+  let chooseItem = list.menupopup.querySelector(".choose-app-item");
   let dialogLoadedPromise = promiseLoadSubDialog("chrome://global/content/appPicker.xul");
   let cmdEvent = win.document.createEvent("xulcommandevent");
   cmdEvent.initCommandEvent("command", true, true, win, 0, false, false, false, false, null, 0);
   chooseItem.dispatchEvent(cmdEvent);
 
   let dialog = await dialogLoadedPromise;
   info("Dialog loaded");
 
@@ -53,17 +53,17 @@ add_task(async function() {
   ok(list.selectedItem, "Should have a selected item");
   ok(mimeInfo.preferredApplicationHandler.equals(list.selectedItem.handlerApp),
      "App should be visible as preferred item.");
 
 
   // Now try to 'manage' this list:
   dialogLoadedPromise = promiseLoadSubDialog("chrome://browser/content/preferences/applicationManager.xul");
 
-  let manageItem = list.firstElementChild.querySelector(".manage-app-item");
+  let manageItem = list.menupopup.querySelector(".manage-app-item");
   cmdEvent = win.document.createEvent("xulcommandevent");
   cmdEvent.initCommandEvent("command", true, true, win, 0, false, false, false, false, null, 0);
   manageItem.dispatchEvent(cmdEvent);
 
   dialog = await dialogLoadedPromise;
   info("Dialog loaded the second time");
 
   dialogDoc = dialog.document;
--- a/browser/components/preferences/in-content/tests/browser_permissions_dialog.js
+++ b/browser/components/preferences/in-content/tests/browser_permissions_dialog.js
@@ -14,19 +14,17 @@ var sitePermissionsDialog;
 
 function checkPermissionItem(origin, state) {
   let doc = sitePermissionsDialog.document;
 
   let label = doc.getElementsByTagName("label")[2];
   Assert.equal(label.value, origin);
 
   let menulist = doc.getElementsByTagName("menulist")[0];
-  let selectedIndex = menulist.selectedIndex;
-  let selectedItem = menulist.querySelectorAll("menuitem")[selectedIndex];
-  Assert.equal(selectedItem.value, state);
+  Assert.equal(menulist.value, state);
 }
 
 async function openPermissionsDialog() {
   let dialogOpened = promiseLoadSubDialog(PERMISSIONS_URL);
 
   await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
     let doc = content.document;
     let settingsButton = doc.getElementById("notificationSettingsButton");
--- a/browser/components/preferences/sitePermissions.js
+++ b/browser/components/preferences/sitePermissions.js
@@ -259,41 +259,37 @@ var gSitePermissionsManager = {
     let website = document.createXULElement("label");
     website.setAttribute("value", permission.origin);
     website.setAttribute("width", "50");
     hbox.setAttribute("class", "website-name");
     hbox.setAttribute("flex", "3");
     hbox.appendChild(website);
 
     let menulist = document.createXULElement("menulist");
-    let menupopup = document.createXULElement("menupopup");
     menulist.setAttribute("flex", "1");
     menulist.setAttribute("width", "50");
     menulist.setAttribute("class", "website-status");
-    menulist.appendChild(menupopup);
     let states = SitePermissions.getAvailableStates(permission.type);
     for (let state of states) {
       // Work around the (rare) edge case when a user has changed their
       // default permission type back to UNKNOWN while still having a
       // PROMPT permission set for an origin.
       if (state == SitePermissions.UNKNOWN &&
           permission.capability == SitePermissions.PROMPT) {
         state = SitePermissions.PROMPT;
       } else if (state == SitePermissions.UNKNOWN) {
         continue;
       }
-      let m = document.createXULElement("menuitem");
+      let m = menulist.appendItem(undefined, state);
       document.l10n.setAttributes(m, this._getCapabilityString(state));
-      m.setAttribute("value", state);
-      menupopup.appendChild(m);
     }
     menulist.value = permission.capability;
 
     menulist.addEventListener("select", () => {
-      this.onPermissionChange(permission, Number(menulist.selectedItem.value));
+      this.onPermissionChange(permission, Number(menulist.value));
     });
 
     row.appendChild(hbox);
     row.appendChild(menulist);
     richlistitem.appendChild(row);
     return richlistitem;
   },
 
--- a/browser/modules/SelectionChangedMenulist.jsm
+++ b/browser/modules/SelectionChangedMenulist.jsm
@@ -7,17 +7,17 @@
 var EXPORTED_SYMBOLS = ["SelectionChangedMenulist"];
 
 class SelectionChangedMenulist {
   // A menulist wrapper that will open the popup when navigating with the
   // keyboard on Windows and trigger the provided handler when the popup
   // is hiding. This matches the behaviour of MacOS and Linux more closely.
 
   constructor(menulist, onCommand) {
-    let popup = menulist.firstElementChild;
+    let popup = menulist.menupopup;
     let lastEvent;
 
     menulist.addEventListener("command", event => {
       lastEvent = event;
       if (popup.state != "open" && popup.state != "showing") {
         popup.openPopup();
       }
     });
--- a/browser/modules/webrtcUI.jsm
+++ b/browser/modules/webrtcUI.jsm
@@ -601,18 +601,19 @@ function prompt(aBrowser, aRequest) {
       }
 
       function listDevices(menupopup, devices) {
         while (menupopup.lastChild)
           menupopup.removeChild(menupopup.lastChild);
         // Removing the child nodes of the menupopup doesn't clear the value
         // attribute of the menulist. This can have unfortunate side effects
         // when the list is rebuilt with a different content, so we remove
-        // the value attribute explicitly.
+        // the value attribute and unset the selectedItem explicitly.
         menupopup.parentNode.removeAttribute("value");
+        menupopup.parentNode.selectedItem = null;
 
         for (let device of devices)
           addDeviceToList(menupopup, device.name, device.deviceIndex);
       }
 
       function checkDisabledWindowMenuItem() {
         let list = doc.getElementById("webRTC-selectWindow-menulist");
         let item = list.selectedItem;
@@ -623,16 +624,24 @@ function prompt(aBrowser, aRequest) {
           notificationElement.removeAttribute("invalidselection");
         }
       }
 
       function listScreenShareDevices(menupopup, devices) {
         while (menupopup.lastChild) {
           menupopup.removeChild(menupopup.lastChild);
         }
+
+        // Removing the child nodes of the menupopup doesn't clear the value
+        // attribute of the menulist. This can have unfortunate side effects
+        // when the list is rebuilt with a different content, so we remove
+        // the value attribute and unset the selectedItem explicitly.
+        menupopup.parentNode.removeAttribute("value");
+        menupopup.parentNode.selectedItem = null;
+
         let label = doc.getElementById("webRTC-selectWindow-label");
         const gumStringId = "getUserMedia.selectWindowOrScreen";
         label.setAttribute("value",
                            stringBundle.getString(gumStringId + ".label"));
         label.setAttribute("accesskey",
                            stringBundle.getString(gumStringId + ".accesskey"));
 
         // "Select a Window or Screen" is the default because we can't and don't
--- a/toolkit/content/customElements.js
+++ b/toolkit/content/customElements.js
@@ -311,16 +311,17 @@ if (!isDummyDocument) {
     "chrome://global/content/elements/tabbox.js",
     "chrome://global/content/elements/tree.js",
   ]) {
     Services.scriptloader.loadSubScript(script, window);
   }
 
   for (let [tag, script] of [
     ["findbar", "chrome://global/content/elements/findbar.js"],
+    ["menulist", "chrome://global/content/elements/menulist.js"],
     ["richlistbox", "chrome://global/content/elements/richlistbox.js"],
     ["stringbundle", "chrome://global/content/elements/stringbundle.js"],
     ["printpreview-toolbar", "chrome://global/content/printPreviewToolbar.js"],
     ["editor", "chrome://global/content/elements/editor.js"],
   ]) {
     customElements.setElementCreationCallback(tag, () => {
       Services.scriptloader.loadSubScript(script, window);
     });
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -67,17 +67,16 @@ toolkit.jar:
    content/global/bindings/checkbox.xml        (widgets/checkbox.xml)
    content/global/bindings/datekeeper.js       (widgets/datekeeper.js)
    content/global/bindings/datepicker.js       (widgets/datepicker.js)
    content/global/bindings/datetimebox.xml     (widgets/datetimebox.xml)
    content/global/bindings/datetimebox.css     (widgets/datetimebox.css)
 *  content/global/bindings/dialog.xml          (widgets/dialog.xml)
    content/global/bindings/general.xml         (widgets/general.xml)
    content/global/bindings/menu.xml            (widgets/menu.xml)
-   content/global/bindings/menulist.xml        (widgets/menulist.xml)
    content/global/bindings/notification.xml    (widgets/notification.xml)
    content/global/bindings/popup.xml           (widgets/popup.xml)
    content/global/bindings/radio.xml           (widgets/radio.xml)
    content/global/bindings/richlistbox.xml     (widgets/richlistbox.xml)
    content/global/bindings/scrollbox.xml       (widgets/scrollbox.xml)
    content/global/bindings/spinner.js          (widgets/spinner.js)
    content/global/bindings/tabbox.xml          (widgets/tabbox.xml)
    content/global/bindings/text.xml            (widgets/text.xml)
@@ -94,16 +93,17 @@ toolkit.jar:
    content/global/elements/editor.js           (widgets/editor.js)
    content/global/elements/general.js          (widgets/general.js)
    content/global/elements/notificationbox.js  (widgets/notificationbox.js)
    content/global/elements/pluginProblem.js    (widgets/pluginProblem.js)
    content/global/elements/radio.js            (widgets/radio.js)
    content/global/elements/richlistbox.js      (widgets/richlistbox.js)
    content/global/elements/marquee.css         (widgets/marquee.css)
    content/global/elements/marquee.js          (widgets/marquee.js)
+   content/global/elements/menulist.js         (widgets/menulist.js)
    content/global/elements/stringbundle.js     (widgets/stringbundle.js)
    content/global/elements/tabbox.js           (widgets/tabbox.js)
    content/global/elements/textbox.js          (widgets/textbox.js)
    content/global/elements/videocontrols.js    (widgets/videocontrols.js)
    content/global/elements/tree.js             (widgets/tree.js)
 #ifdef XP_MACOSX
    content/global/macWindowMenu.js
 #endif
--- a/toolkit/content/tests/chrome/test_menulist_keynav.xul
+++ b/toolkit/content/tests/chrome/test_menulist_keynav.xul
@@ -1,17 +1,17 @@
 <?xml version="1.0"?>
 <?xml-stylesheet href="chrome://global/skin" type="text/css"?>
 <?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
 
 <window title="Menulist Key Navigation Tests"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
-  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>      
-  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>      
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
 
 <button id="button1" label="One"/>
 <menulist id="list">
   <menupopup id="popup" onpopupshowing="return gShowPopup;">
     <menuitem id="i1" label="One"/>
     <menuitem id="i2" label="Two"/>
     <menuitem id="i2b" disabled="true" label="Two and a Half"/>
     <menuitem id="i3" label="Three"/>
@@ -99,17 +99,17 @@ function pressLetter()
 }
 
 function pressedAgain()
 {
   keyCheck(list, "T", 3, 1, "letter pressed again");
   SpecialPowers.setIntPref("ui.menu.incremental_search.timeout", 0); // prevent to timeout
   keyCheck(list, "W", 2, 1, "second letter pressed");
   SpecialPowers.clearUserPref("ui.menu.incremental_search.timeout");
-  setTimeout(differentPressed, 1200); 
+  setTimeout(differentPressed, 1200);
 }
 
 function differentPressed()
 {
   keyCheck(list, "O", 1, 1, "different letter pressed");
 
   if (gOpenPhase) {
     list.open = false;
@@ -150,17 +150,17 @@ function tabAndScroll()
   }
 
   // now make sure that using a key scrolls the menu correctly
 
   for (let i = 0; i < 65; i++) {
     list.appendItem("Item" + i, "item" + i);
   }
   list.open = true;
-  is(list.getBoundingClientRect().width, list.firstChild.getBoundingClientRect().width,
+  is(list.getBoundingClientRect().width, list.menupopup.getBoundingClientRect().width,
      "menu and popup width match");
   var minScrollbarWidth = window.matchMedia("(-moz-overlay-scrollbars)").matches ? 0 : 3;
   ok(list.getBoundingClientRect().width >= list.getItemAtIndex(0).getBoundingClientRect().width + minScrollbarWidth,
      "menuitem width accounts for scrollbar");
   list.open = false;
 
   list.menupopup.maxHeight = 100;
   list.open = true;
--- a/toolkit/content/tests/chrome/test_menulist_position.xul
+++ b/toolkit/content/tests/chrome/test_menulist_position.xul
@@ -36,17 +36,17 @@ function popupShown()
   var popuprect = menulist.menupopup.getBoundingClientRect();
 
   let marginLeft = parseFloat(getComputedStyle(menulist.menupopup).marginLeft);
   ok(isWithinHalfPixel(menurect.left + marginLeft, popuprect.left), "left position");
   ok(isWithinHalfPixel(menurect.right + marginLeft, popuprect.right), "right position");
 
   let index = menulist.selectedIndex;
   if (menulist.selectedItem && navigator.platform.includes("Mac")) {
-    let menulistlabel = document.getAnonymousElementByAttribute(menulist, "class", "menulist-label");
+    let menulistlabel = menulist.querySelector(".menulist-label");
     let mitemlabel = document.getAnonymousElementByAttribute(menulist.selectedItem, "class", "menu-iconic-text");
 
     ok(isWithinHalfPixel(menulistlabel.getBoundingClientRect().left,
                          mitemlabel.getBoundingClientRect().left),
        "Labels horizontally aligned for index " + index);
     ok(isWithinHalfPixel(menulistlabel.getBoundingClientRect().top,
                          mitemlabel.getBoundingClientRect().top),
        "Labels vertically aligned for index " + index);
rename from toolkit/content/widgets/menulist.xml
rename to toolkit/content/widgets/menulist.js
--- a/toolkit/content/widgets/menulist.xml
+++ b/toolkit/content/widgets/menulist.js
@@ -1,341 +1,408 @@
-<?xml version="1.0"?>
-<!-- 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/. -->
+/* 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";
 
-<bindings id="menulistBindings"
-   xmlns="http://www.mozilla.org/xbl"
-   xmlns:html="http://www.w3.org/1999/xhtml"
-   xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-   xmlns:xbl="http://www.mozilla.org/xbl">
+// This is loaded into all XUL windows. Wrap in a block to prevent
+// leaking to window scope.
+{
+
+const MozXULMenuElement = MozElementMixin(XULMenuElement);
+const MenuBaseControl = BaseControlMixin(MozXULMenuElement);
+
+class MozMenuList extends MenuBaseControl {
+  constructor() {
+    super();
 
-  <binding id="menulist"
-           extends="chrome://global/content/bindings/general.xml#basecontrol">
-    <content>
-      <xul:hbox class="menulist-label-box" flex="1">
-        <xul:image class="menulist-icon" xbl:inherits="src=image"/>
-        <xul:label class="menulist-label" xbl:inherits="value=label,crop,accesskey,highlightable" crop="right" flex="1"/>
-        <xul:label class="menulist-highlightable-label" xbl:inherits="xbl:text=label,crop,accesskey,highlightable" crop="right" flex="1"/>
-      </xul:hbox>
-      <xul:dropmarker class="menulist-dropmarker" type="menu" xbl:inherits="disabled,open"/>
-      <children includes="menupopup"/>
-    </content>
+    this.addEventListener("command", (event) => {
+      if (event.target.parentNode.parentNode == this) {
+        this.selectedItem = event.target;
+      }
+    }, true);
 
-    <handlers>
-      <handler event="command" phase="capturing"
-        action="if (event.target.parentNode.parentNode == this) this.selectedItem = event.target;"/>
+    this.addEventListener("popupshowing", (event) => {
+      if (event.target.parentNode == this) {
+        this.activeChild = null;
+        if (this.selectedItem)
+          // Not ready for auto-setting the active child in hierarchies yet.
+          // For now, only do this when the outermost menupopup opens.
+          this.activeChild = this.mSelectedInternal;
+      }
+    });
 
-      <handler event="popupshowing">
-        <![CDATA[
-          if (event.target.parentNode == this) {
-            this.activeChild = null;
-            if (this.selectedItem)
-              // Not ready for auto-setting the active child in hierarchies yet.
-              // For now, only do this when the outermost menupopup opens.
-              this.activeChild = this.mSelectedInternal;
-          }
-        ]]>
-      </handler>
+    this.addEventListener("keypress", (event) => {
+      if (event.altKey ||
+          event.ctrlKey ||
+          event.metaKey) {
+        return;
+      }
 
-      <handler event="keypress" modifiers="shift any" group="system">
-        <![CDATA[
-          if (!event.defaultPrevented &&
-              (event.keyCode == KeyEvent.DOM_VK_UP ||
-               event.keyCode == KeyEvent.DOM_VK_DOWN ||
-               event.keyCode == KeyEvent.DOM_VK_PAGE_UP ||
-               event.keyCode == KeyEvent.DOM_VK_PAGE_DOWN ||
-               event.keyCode == KeyEvent.DOM_VK_HOME ||
-               event.keyCode == KeyEvent.DOM_VK_END ||
-               event.keyCode == KeyEvent.DOM_VK_BACK_SPACE ||
-               event.charCode > 0)) {
-            // Moving relative to an item: start from the currently selected item
-            this.activeChild = this.mSelectedInternal;
-            if (this.handleKeyPress(event)) {
-              this.activeChild.doCommand();
-              event.preventDefault();
-            }
-          }
-        ]]>
-      </handler>
-    </handlers>
+      if (!event.defaultPrevented &&
+        (event.keyCode == KeyEvent.DOM_VK_UP ||
+          event.keyCode == KeyEvent.DOM_VK_DOWN ||
+          event.keyCode == KeyEvent.DOM_VK_PAGE_UP ||
+          event.keyCode == KeyEvent.DOM_VK_PAGE_DOWN ||
+          event.keyCode == KeyEvent.DOM_VK_HOME ||
+          event.keyCode == KeyEvent.DOM_VK_END ||
+          event.keyCode == KeyEvent.DOM_VK_BACK_SPACE ||
+          event.charCode > 0)) {
+        // Moving relative to an item: start from the currently selected item
+        this.activeChild = this.mSelectedInternal;
+        if (this.handleKeyPress(event)) {
+          this.activeChild.doCommand();
+          event.preventDefault();
+        }
+      }
+    }, { mozSystemGroup: true });
+
+  }
+
+  connectedCallback() {
+    if (this.delayConnectedCallback()) {
+      return;
+    }
 
-    <implementation implements="nsIDOMXULMenuListElement">
-      <constructor>
-        this.mSelectedInternal = null;
-        this.mAttributeObserver = null;
-        this.setInitialSelection();
-      </constructor>
+    if (this.getAttribute("popuponly") != "true") {
+      this.prepend(MozMenuList.fragment.cloneNode(true));
+      this._labelBox = this.children[0];
+      this._dropmarker = this.children[1];
+    }
+
+    this._updateAttributes();
+
+    this.mSelectedInternal = null;
+    this.mAttributeObserver = null;
+    this.setInitialSelection();
+  }
 
-      <method name="setInitialSelection">
-        <body>
-          <![CDATA[
-            var popup = this.menupopup;
-            if (popup) {
-              var arr = popup.getElementsByAttribute("selected", "true");
+  static get fragment() {
+    // Accessibility information of these nodes will be
+    // presented on XULComboboxAccessible generated from <menulist>;
+    // hide these nodes from the accessibility tree.
+    let frag = document.importNode(MozXULElement.parseXULToFragment(`
+        <hbox class="menulist-label-box" flex="1" role="none">
+          <image class="menulist-icon" role="none"/>
+          <label class="menulist-label" crop="right" flex="1" role="none"/>
+          <label class="menulist-highlightable-label" crop="right" flex="1" role="none"/>
+        </hbox>
+        <dropmarker class="menulist-dropmarker" type="menu" role="none"/>
+      `), true);
 
-              var editable = this.editable;
-              var value = this.value;
-              if (!arr.item(0) && value)
-                arr = popup.getElementsByAttribute(editable ? "label" : "value", value);
+    Object.defineProperty(this, "fragment", { value: frag });
+    return frag;
+  }
+
+  // nsIDOMXULSelectControlElement
+  set value(val) {
+    // if the new value is null, we still need to remove the old value
+    if (val == null)
+      return this.selectedItem = val;
 
-              if (arr.item(0))
-                this.selectedItem = arr[0];
-              else if (!editable)
-                this.selectedIndex = 0;
-            }
-          ]]>
-        </body>
-      </method>
+    var arr = null;
+    var popup = this.menupopup;
+    if (popup)
+      arr = popup.getElementsByAttribute("value", val);
+
+    if (arr && arr.item(0))
+      this.selectedItem = arr[0];
+    else {
+      this.selectedItem = null;
+      this.setAttribute("value", val);
+    }
 
-      <property name="value" onget="return this.getAttribute('value');">
-        <setter>
-          <![CDATA[
-            // if the new value is null, we still need to remove the old value
-            if (val == null)
-              return this.selectedItem = val;
+    return val;
+  }
+
+  // nsIDOMXULSelectControlElement
+  get value() {
+    return this.getAttribute("value");
+  }
+
+  // nsIDOMXULMenuListElement
+  set crop(val) {
+    this.setAttribute("crop", val);
+  }
 
-            var arr = null;
-            var popup = this.menupopup;
-            if (popup)
-              arr = popup.getElementsByAttribute("value", val);
+  // nsIDOMXULMenuListElement
+  get crop() {
+    return this.getAttribute("crop");
+  }
 
-            if (arr && arr.item(0))
-              this.selectedItem = arr[0];
-            else {
-              this.selectedItem = null;
-              this.setAttribute("value", val);
-            }
+  // nsIDOMXULMenuListElement
+  set image(val) {
+    this.setAttribute("image", val);
+    return val;
+  }
 
-            return val;
-          ]]>
-        </setter>
-      </property>
+  // nsIDOMXULMenuListElement
+  get image() {
+    return this.getAttribute("image");
+  }
 
-      <property name="crop" onset="this.setAttribute('crop',val); return val;"
-                            onget="return this.getAttribute('crop');"/>
-      <property name="image"  onset="this.setAttribute('image',val); return val;"
-                              onget="return this.getAttribute('image');"/>
-      <property name="label" readonly="true" onget="return this.getAttribute('label');"/>
-      <property name="description" onset="this.setAttribute('description',val); return val;"
-                                   onget="return this.getAttribute('description');"/>
+  // nsIDOMXULMenuListElement
+  get label() {
+    return this.getAttribute("label");
+  }
+
+  set description(val) {
+    this.setAttribute("description", val);
+    return val;
+  }
 
-      <property name="open" onset="this.openMenu(val); return val;"
-                            onget="return this.hasAttribute('open');"/>
+  get description() {
+    return this.getAttribute("description");
+  }
 
-      <property name="itemCount" readonly="true"
-                onget="return this.menupopup ? this.menupopup.children.length : 0"/>
+  // nsIDOMXULMenuListElement
+  set open(val) {
+    this.openMenu(val);
+    return val;
+  }
 
-      <property name="menupopup" readonly="true">
-        <getter>
-          <![CDATA[
-            var popup = this.firstElementChild;
-            while (popup && popup.localName != "menupopup")
-              popup = popup.nextElementSibling;
-            return popup;
-          ]]>
-        </getter>
-      </property>
+  // nsIDOMXULMenuListElement
+  get open() {
+    return this.hasAttribute("open");
+  }
+
+  // nsIDOMXULSelectControlElement
+  get itemCount() {
+    return this.menupopup ? this.menupopup.children.length : 0;
+  }
 
-      <method name="contains">
-        <parameter name="item"/>
-        <body>
-          <![CDATA[
-            if (!item)
-              return false;
-
-            var parent = item.parentNode;
-            return (parent && parent.parentNode == this);
-          ]]>
-        </body>
-      </method>
+  get menupopup() {
+    var popup = this.firstElementChild;
+    while (popup && popup.localName != "menupopup")
+      popup = popup.nextElementSibling;
+    return popup;
+  }
 
-      <property name="selectedIndex">
-        <getter>
-          <![CDATA[
-            // Quick and dirty. We won't deal with hierarchical menulists yet.
-            if (!this.selectedItem ||
-                !this.mSelectedInternal.parentNode ||
-                this.mSelectedInternal.parentNode.parentNode != this)
-              return -1;
+  // nsIDOMXULSelectControlElement
+  set selectedIndex(val) {
+    var popup = this.menupopup;
+    if (popup && 0 <= val) {
+      if (val < popup.children.length)
+        this.selectedItem = popup.children[val];
+    } else
+      this.selectedItem = null;
+    return val;
+  }
 
-            var children = this.mSelectedInternal.parentNode.children;
-            var i = children.length;
-            while (i--)
-              if (children[i] == this.mSelectedInternal)
-                break;
+  // nsIDOMXULSelectControlElement
+  get selectedIndex() {
+    // Quick and dirty. We won't deal with hierarchical menulists yet.
+    if (!this.selectedItem ||
+      !this.mSelectedInternal.parentNode ||
+      this.mSelectedInternal.parentNode.parentNode != this)
+      return -1;
+
+    var children = this.mSelectedInternal.parentNode.children;
+    var i = children.length;
+    while (i--)
+      if (children[i] == this.mSelectedInternal)
+        break;
 
-            return i;
-          ]]>
-        </getter>
-        <setter>
-          <![CDATA[
-            var popup = this.menupopup;
-            if (popup && 0 <= val) {
-              if (val < popup.children.length)
-                this.selectedItem = popup.children[val];
-            } else
-              this.selectedItem = null;
-            return val;
-          ]]>
-        </setter>
-      </property>
+    return i;
+  }
+
+  // nsIDOMXULSelectControlElement
+  set selectedItem(val) {
+    var oldval = this.mSelectedInternal;
+    if (oldval == val)
+      return val;
+
+    if (val && !this.contains(val))
+      return val;
+
+    if (oldval) {
+      oldval.removeAttribute("selected");
+      this.mAttributeObserver.disconnect();
+    }
 
-      <property name="selectedItem">
-        <getter>
-          <![CDATA[
-            return this.mSelectedInternal;
-          ]]>
-        </getter>
-        <setter>
-          <![CDATA[
-            var oldval = this.mSelectedInternal;
-            if (oldval == val)
-              return val;
+    this.mSelectedInternal = val;
+    let attributeFilter = ["value", "label", "image", "description"];
+    if (val) {
+      val.setAttribute("selected", "true");
+      for (let attr of attributeFilter) {
+        if (val.hasAttribute(attr)) {
+          this.setAttribute(attr, val.getAttribute(attr));
+        } else {
+          this.removeAttribute(attr);
+        }
+      }
 
-            if (val && !this.contains(val))
-              return val;
+      this.mAttributeObserver = new MutationObserver(this.handleMutation.bind(this));
+      this.mAttributeObserver.observe(val, { attributeFilter });
+    } else {
+      for (let attr of attributeFilter) {
+        this.removeAttribute(attr);
+      }
+    }
+
+    var event = document.createEvent("Events");
+    event.initEvent("select", true, true);
+    this.dispatchEvent(event);
+
+    event = document.createEvent("Events");
+    event.initEvent("ValueChange", true, true);
+    this.dispatchEvent(event);
+
+    return val;
+  }
 
-            if (oldval) {
-              oldval.removeAttribute("selected");
-              this.mAttributeObserver.disconnect();
-            }
+  // nsIDOMXULSelectControlElement
+  get selectedItem() {
+    return this.mSelectedInternal;
+  }
+
+  setInitialSelection() {
+    var popup = this.menupopup;
+    if (popup) {
+      var arr = popup.getElementsByAttribute("selected", "true");
 
-            this.mSelectedInternal = val;
-            let attributeFilter = ["value", "label", "image", "description"];
-            if (val) {
-              val.setAttribute("selected", "true");
-              for (let attr of attributeFilter) {
-                if (val.hasAttribute(attr)) {
-                  this.setAttribute(attr, val.getAttribute(attr));
-                } else {
-                  this.removeAttribute(attr);
-                }
-              }
+      var editable = this.editable;
+      var value = this.value;
+      if (!arr.item(0) && value)
+        arr = popup.getElementsByAttribute(editable ? "label" : "value", value);
+
+      if (arr.item(0))
+        this.selectedItem = arr[0];
+      else if (!editable)
+        this.selectedIndex = 0;
+    }
+  }
 
-              this.mAttributeObserver = new MutationObserver(this.handleMutation.bind(this));
-              this.mAttributeObserver.observe(val, { attributeFilter });
-            } else {
-              for (let attr of attributeFilter) {
-                this.removeAttribute(attr);
-              }
-            }
+  contains(item) {
+    if (!item)
+      return false;
+
+    var parent = item.parentNode;
+    return (parent && parent.parentNode == this);
+  }
 
-            var event = document.createEvent("Events");
-            event.initEvent("select", true, true);
-            this.dispatchEvent(event);
-
-            event = document.createEvent("Events");
-            event.initEvent("ValueChange", true, true);
-            this.dispatchEvent(event);
-
-            return val;
-          ]]>
-        </setter>
-      </property>
+  handleMutation(aRecords) {
+    for (let record of aRecords) {
+      let t = record.target;
+      if (t == this.mSelectedInternal) {
+        let attrName = record.attributeName;
+        switch (attrName) {
+          case "value":
+          case "label":
+          case "image":
+          case "description":
+            if (t.hasAttribute(attrName)) {
+              this.setAttribute(attrName, t.getAttribute(attrName));
+            } else {
+              this.removeAttribute(attrName);
+            }
+        }
+      }
+    }
+  }
 
-      <method name="handleMutation">
-        <parameter name="aRecords"/>
-        <body>
-          <![CDATA[
-            for (let record of aRecords) {
-              let t = record.target;
-              if (t == this.mSelectedInternal) {
-                let attrName = record.attributeName;
-                switch (attrName) {
-                  case "value":
-                  case "label":
-                  case "image":
-                  case "description":
-                    if (t.hasAttribute(attrName)) {
-                      this.setAttribute(attrName, t.getAttribute(attrName));
-                    } else {
-                      this.removeAttribute(attrName);
-                    }
-                }
-              }
-            }
-          ]]>
-        </body>
-      </method>
+  // nsIDOMXULSelectControlElement
+  getIndexOfItem(item) {
+    var popup = this.menupopup;
+    if (popup) {
+      var children = popup.children;
+      var i = children.length;
+      while (i--)
+        if (children[i] == item)
+          return i;
+    }
+    return -1;
+  }
+
+  // nsIDOMXULSelectControlElement
+  getItemAtIndex(index) {
+    var popup = this.menupopup;
+    if (popup) {
+      var children = popup.children;
+      if (index >= 0 && index < children.length)
+        return children[index];
+    }
+    return null;
+  }
 
-      <method name="getIndexOfItem">
-        <parameter name="item"/>
-        <body>
-        <![CDATA[
-          var popup = this.menupopup;
-          if (popup) {
-            var children = popup.children;
-            var i = children.length;
-            while (i--)
-              if (children[i] == item)
-                return i;
-          }
-          return -1;
-        ]]>
-        </body>
-      </method>
+  appendItem(label, value, description) {
+    if (!this.menupopup) {
+      this.appendChild(MozXULElement.parseXULToFragment(`<menupopup />`));
+    }
+
+    var popup = this.menupopup;
+    popup.appendChild(MozXULElement.parseXULToFragment(`<menuitem />`));
+
+    var item = popup.lastElementChild;
+    if (label !== undefined) {
+      item.setAttribute("label", label);
+    }
+    item.setAttribute("value", value);
+    if (description) {
+      item.setAttribute("description", description);
+    }
+
+    return item;
+  }
+
+  removeAllItems() {
+    this.selectedItem = null;
+    var popup = this.menupopup;
+    if (popup)
+      this.removeChild(popup);
+  }
 
-      <method name="getItemAtIndex">
-        <parameter name="index"/>
-        <body>
-        <![CDATA[
-          var popup = this.menupopup;
-          if (popup) {
-            var children = popup.children;
-            if (index >= 0 && index < children.length)
-              return children[index];
-          }
-          return null;
-        ]]>
-        </body>
-      </method>
+  disconnectedCallback() {
+    if (this.mAttributeObserver) {
+      this.mAttributeObserver.disconnect();
+    }
 
-      <method name="appendItem">
-        <parameter name="label"/>
-        <parameter name="value"/>
-        <parameter name="description"/>
-        <body>
-        <![CDATA[
-          var popup = this.menupopup ||
-                      this.appendChild(document.createXULElement("menupopup"));
-          var item = document.createXULElement("menuitem");
-          item.setAttribute("label", label);
-          item.setAttribute("value", value);
-          if (description)
-            item.setAttribute("description", description);
+    if (this._labelBox) {
+      this._labelBox.remove();
+      this._dropmarker.remove();
+      this._labelBox = null;
+      this._dropmarker = null;
+    }
+  }
+
+  static get observedAttributes() {
+    return ["label", "crop", "accesskey", "highlightable", "image", "disabled",
+            "open"];
+  }
+
+  attributeChangedCallback() {
+    if (this.isConnectedAndReady) {
+      this._updateAttributes();
+    }
+  }
 
-          popup.appendChild(item);
-          return item;
-        ]]>
-        </body>
-      </method>
+  _updateAttributes() {
+    if (!this._labelBox) {
+      return;
+    }
 
-      <method name="removeAllItems">
-        <body>
-        <![CDATA[
-          this.selectedItem = null;
-          var popup = this.menupopup;
-          if (popup)
-            this.removeChild(popup);
-        ]]>
-        </body>
-      </method>
+    let icon = this._labelBox.querySelector(".menulist-icon");
+    this.inheritAttribute(icon, "src=image");
+
+    let label = this._labelBox.querySelector(".menulist-label");
+    this.inheritAttribute(label, "value=label");
+    this.inheritAttribute(label, "crop");
+    this.inheritAttribute(label, "accesskey");
+    this.inheritAttribute(label, "highlightable");
 
-      <destructor>
-        <![CDATA[
-          if (this.mAttributeObserver) {
-            this.mAttributeObserver.disconnect();
-          }
-        ]]>
-      </destructor>
-    </implementation>
-  </binding>
+    let highlightableLabel = this._labelBox.querySelector(".menulist-highlightable-label");
+    this.inheritAttribute(highlightableLabel, "text=label");
+    this.inheritAttribute(highlightableLabel, "crop");
+    this.inheritAttribute(highlightableLabel, "accesskey");
+    this.inheritAttribute(highlightableLabel, "highlightable");
 
-  <binding id="menulist-popuponly"
-           extends="chrome://global/content/bindings/menulist.xml#menulist">
-    <content>
-      <children includes="menupopup"/>
-    </content>
-  </binding>
-</bindings>
+    this.inheritAttribute(this._dropmarker, "disabled");
+    this.inheritAttribute(this._dropmarker, "open");
+  }
+}
+
+MenuBaseControl.implementCustomInterface(MozMenuList, [Ci.nsIDOMXULMenuListElement,
+                                                       Ci.nsIDOMXULSelectControlElement]);
+
+customElements.define("menulist", MozMenuList);
+
+}
--- a/toolkit/content/xul.css
+++ b/toolkit/content/xul.css
@@ -598,22 +598,17 @@ panel[type="autocomplete-richlistbox"] {
   max-width: 0px;
   width: 0 !important;
   min-width: 0%;
   min-height: 0%;
 }
 
 /********** menulist **********/
 
-menulist {
-  -moz-binding: url("chrome://global/content/bindings/menulist.xml#menulist");
-}
-
 menulist[popuponly="true"] {
-  -moz-binding: url("chrome://global/content/bindings/menulist.xml#menulist-popuponly");
   -moz-appearance: none !important;
   margin: 0 !important;
   height: 0 !important;
   min-height: 0 !important;
   border: 0 !important;
 }
 
 menulist > menupopup > menuitem {
--- a/toolkit/mozapps/extensions/content/extensions.xml
+++ b/toolkit/mozapps/extensions/content/extensions.xml
@@ -685,16 +685,18 @@
             </xul:menulist>
           </xul:hbox>
         </xul:vbox>
       </xul:hbox>
     </content>
 
     <implementation>
       <constructor><![CDATA[
+        window.customElements.upgrade(this._stateMenulist);
+
         this._installStatus = document.getAnonymousElementByAttribute(this, "anonid", "install-status");
         this._installStatus.mControl = this;
 
         this.setAttribute("contextmenu", "addonitem-popup");
 
         this._showStatus("none");
 
         this._initWithAddon(this.mAddon);
--- a/toolkit/mozapps/preferences/fontbuilder.js
+++ b/toolkit/mozapps/preferences/fontbuilder.js
@@ -14,19 +14,20 @@ var FontBuilder = {
                            .createInstance(Ci.nsIFontEnumerator);
     }
     return this._enumerator;
   },
 
   _allFonts: null,
   _langGroupSupported: false,
   async buildFontList(aLanguage, aFontType, aMenuList) {
-    // Reset the list
-    while (aMenuList.hasChildNodes())
-      aMenuList.firstChild.remove();
+    // Remove the original <menupopup>
+    if (aMenuList.menupopup) {
+      aMenuList.menupopup.remove();
+    }
 
     let defaultFont = null;
     // Load Font Lists
     let fonts = await this.enumerator.EnumerateFontsAsync(aLanguage, aFontType);
     if (fonts.length > 0)
       defaultFont = this.enumerator.getDefaultFont(aLanguage, aFontType);
     else {
       fonts = await this.enumerator.EnumerateFontsAsync(aLanguage, "");
--- a/toolkit/themes/osx/reftests/nostretch-ref.xul
+++ b/toolkit/themes/osx/reftests/nostretch-ref.xul
@@ -61,16 +61,26 @@ function createButton(v) {
 }
 function createTextField(v) {
   return elWithValue("textbox", v);
 }
 function createMenulist(v) {
   let [list, popup, item] = [cE("menulist"), cE("menupopup"), elWithValue("menuitem", v)];
   item.setAttribute("selected", "true");
   popup.appendChild(item);
+  // XXX Generate "anonymous" nodes until we could run
+  // custom elements in Reftest, see bug 1465457.
+  list.innerHTML = `
+    <hbox class="menulist-label-box" flex="1">
+      <image class="menulist-icon" />
+      <label class="menulist-label" crop="right" flex="1" value="${v}"/>
+      <label class="menulist-highlightable-label" crop="right" flex="1"/>
+    </hbox>
+    <dropmarker class="menulist-dropmarker" type="menu"/>
+  `;
   list.appendChild(popup);
   return list;
 }
 function loaded() {
   let template = document.getElementById("template");
   ["regular", "small"].forEach(function(size) {
     let wrapper = document.querySelectorAll("#wrapper > ." + size)[0];
     allPairs([
--- a/toolkit/themes/osx/reftests/nostretch.xul
+++ b/toolkit/themes/osx/reftests/nostretch.xul
@@ -74,16 +74,26 @@ function createButton(v) {
 }
 function createTextField(v) {
   return elWithValue("textbox", v);
 }
 function createMenulist(v) {
   let [list, popup, item] = [cE("menulist"), cE("menupopup"), elWithValue("menuitem", v)];
   item.setAttribute("selected", "true");
   popup.appendChild(item);
+  // XXX Generate "anonymous" nodes until we could run
+  // custom elements in Reftest, see bug 1465457.
+  list.innerHTML = `
+    <hbox class="menulist-label-box" flex="1">
+      <image class="menulist-icon" />
+      <label class="menulist-label" crop="right" flex="1" value="${v}"/>
+      <label class="menulist-highlightable-label" crop="right" flex="1"/>
+    </hbox>
+    <dropmarker class="menulist-dropmarker" type="menu"/>
+  `;
   list.appendChild(popup);
   return list;
 }
 function loaded() {
   let template = document.getElementById("template");
   ["regular", "small"].forEach(function(size) {
     let wrapper = document.querySelectorAll("#wrapper > ." + size)[0];
     allPairs([