Bug 1519577 Convert toolbarbutton to a custom element r=surkov
authorAndrew Swan <aswan@mozilla.com>
Mon, 20 May 2019 10:01:02 -0700
changeset 474978 c8e9b6a81194dff2d37b4f67d23a419fd4587e49
parent 474977 595c3065e9ac64ed1d4ada38c3a83c6ba2a0ff91
child 474979 c0a4aecf98b026eb7f88e5d105f85c3572ed522a
push id113185
push useraswan@mozilla.com
push dateWed, 22 May 2019 23:25:35 +0000
treeherdermozilla-inbound@c8e9b6a81194 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssurkov
bugs1519577
milestone69.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 1519577 Convert toolbarbutton to a custom element r=surkov Differential Revision: https://phabricator.services.mozilla.com/D31941
browser/base/content/browser-pageActions.js
browser/base/content/browser-places.js
browser/base/content/browser.js
browser/base/content/browser.xul
browser/base/content/tabbrowser.xml
browser/base/content/test/tabs/browser_audioTabIcon.js
browser/components/customizableui/CustomizableUI.jsm
browser/components/customizableui/PanelMultiView.jsm
browser/components/customizableui/content/panelUI.js
browser/components/customizableui/test/head.js
browser/components/downloads/content/indicator.js
browser/components/downloads/test/browser/browser_overflow_anchor.js
browser/components/extensions/ExtensionControlledPopup.jsm
browser/components/extensions/parent/ext-browserAction.js
browser/components/extensions/test/browser/browser_ext_browserAction_context.js
browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js
browser/components/places/content/browserPlacesViews.js
browser/components/places/tests/browser/browser_views_liveupdate.js
browser/components/search/test/browser/browser_tooManyEnginesOffered.js
browser/components/uitour/UITour.jsm
layout/xul/test/test_bug987230.xul
toolkit/components/extensions/test/browser/browser_ext_themes_toolbarbutton_colors.js
toolkit/content/customElements.js
toolkit/content/jar.mn
toolkit/content/widgets/button.js
toolkit/content/widgets/button.xml
toolkit/content/widgets/toolbarbutton.js
toolkit/content/widgets/toolbarbutton.xml
toolkit/content/xul.css
--- a/browser/base/content/browser-pageActions.js
+++ b/browser/base/content/browser-pageActions.js
@@ -235,17 +235,17 @@ var BrowserPageActions = {
     }
     let buttonNode = document.createXULElement("toolbarbutton");
     buttonNode.classList.add(
       "subviewbutton",
       "subviewbutton-iconic",
       "pageAction-panel-button"
     );
     if (action.isBadged) {
-      buttonNode.classList.add("badged-button");
+      buttonNode.setAttribute("badged", "true");
     }
     buttonNode.setAttribute("actionid", action.id);
     buttonNode.addEventListener("command", event => {
       this.doCommandForAction(action, event, buttonNode);
     });
     return buttonNode;
   },
 
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -1131,18 +1131,17 @@ var LibraryUI = {
         !libraryButton.closest("#nav-bar") ||
         !window.toolbar.visible ||
         !this.COSMETIC_ANIMATIONS_ENABLED) {
       return false;
     }
 
     let animatableBox = document.getElementById("library-animatable-box");
     let navBar = document.getElementById("nav-bar");
-    let libraryIcon = document.getAnonymousElementByAttribute(libraryButton, "class", "toolbarbutton-icon");
-    let iconBounds = window.windowUtils.getBoundsWithoutFlushing(libraryIcon);
+    let iconBounds = window.windowUtils.getBoundsWithoutFlushing(libraryButton.icon);
     let libraryBounds = window.windowUtils.getBoundsWithoutFlushing(libraryButton);
 
     animatableBox.style.setProperty("--library-button-height", libraryBounds.height + "px");
     animatableBox.style.setProperty("--library-icon-x", iconBounds.x + "px");
     if (navBar.hasAttribute("brighttext")) {
       animatableBox.setAttribute("brighttext", "true");
     } else {
       animatableBox.removeAttribute("brighttext");
@@ -1192,18 +1191,17 @@ var LibraryUI = {
       if (!libraryButton ||
           libraryButton.getAttribute("cui-areatype") == "menu-panel" ||
           libraryButton.getAttribute("overflowedItem") == "true" ||
           !libraryButton.closest("#nav-bar")) {
         return;
       }
 
       let animatableBox = document.getElementById("library-animatable-box");
-      let libraryIcon = document.getAnonymousElementByAttribute(libraryButton, "class", "toolbarbutton-icon");
-      let iconBounds = window.windowUtils.getBoundsWithoutFlushing(libraryIcon);
+      let iconBounds = window.windowUtils.getBoundsWithoutFlushing(libraryButton.icon);
 
       // Resizing the window will only have the ability to change the X offset of the
       // library button.
       animatableBox.style.setProperty("--library-icon-x", iconBounds.x + "px");
 
       LibraryUI._windowResizeRunning = false;
     });
   },
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -563,49 +563,49 @@ function UpdateBackForwardCommands(aWebN
       forwardCommand.removeAttribute("disabled");
     else
       forwardCommand.setAttribute("disabled", true);
   }
 }
 
 /**
  * Click-and-Hold implementation for the Back and Forward buttons
- * XXXmano: should this live in toolbarbutton.xml?
+ * XXXmano: should this live in toolbarbutton.js?
  */
 function SetClickAndHoldHandlers() {
   // Bug 414797: Clone the back/forward buttons' context menu into both buttons.
   let popup = document.getElementById("backForwardMenu").cloneNode(true);
   popup.removeAttribute("id");
   // Prevent the back/forward buttons' context attributes from being inherited.
   popup.setAttribute("context", "");
 
   let backButton = document.getElementById("back-button");
   backButton.setAttribute("type", "menu");
-  backButton.appendChild(popup);
+  backButton.prepend(popup);
   gClickAndHoldListenersOnElement.add(backButton);
 
   let forwardButton = document.getElementById("forward-button");
   popup = popup.cloneNode(true);
   forwardButton.setAttribute("type", "menu");
-  forwardButton.appendChild(popup);
+  forwardButton.prepend(popup);
   gClickAndHoldListenersOnElement.add(forwardButton);
 }
 
 
 const gClickAndHoldListenersOnElement = {
   _timers: new Map(),
 
   _mousedownHandler(aEvent) {
     if (aEvent.button != 0 ||
         aEvent.currentTarget.open ||
         aEvent.currentTarget.disabled)
       return;
 
     // Prevent the menupopup from opening immediately
-    aEvent.currentTarget.firstElementChild.hidden = true;
+    aEvent.currentTarget.menupopup.hidden = true;
 
     aEvent.currentTarget.addEventListener("mouseout", this);
     aEvent.currentTarget.addEventListener("mouseup", this);
     this._timers.set(aEvent.currentTarget, setTimeout((b) => this._openMenu(b), 500, aEvent.currentTarget));
   },
 
   _clickHandler(aEvent) {
     if (aEvent.button == 0 &&
@@ -8386,18 +8386,17 @@ var PanicButtonNotifier = {
       let removeListeners = () => {
         popup.removeEventListener("mouseover", onUserInteractsWithPopup);
         window.removeEventListener("keydown", onUserInteractsWithPopup);
         popup.removeEventListener("popuphidden", removeListeners);
       };
       popup.addEventListener("popuphidden", removeListeners);
 
       let widget = CustomizableUI.getWidget("panic-button").forWindow(window);
-      let anchor = widget.anchor;
-      anchor = document.getAnonymousElementByAttribute(anchor, "class", "toolbarbutton-icon");
+      let anchor = widget.anchor.icon;
       popup.openPopup(anchor, popup.getAttribute("position"));
     } catch (ex) {
       Cu.reportError(ex);
     }
   },
   close() {
     let popup = document.getElementById("panic-button-success-notification");
     popup.hidePopup();
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -740,17 +740,18 @@
                            ondrop="newTabButtonObserver.onDrop(event)"
                            ondragover="newTabButtonObserver.onDragOver(event)"
                            ondragenter="newTabButtonObserver.onDragOver(event)"
                            ondragexit="newTabButtonObserver.onDragExit(event)"
                            cui-areatype="toolbar"
                            removable="true"/>
 
             <toolbarbutton id="alltabs-button"
-                           class="toolbarbutton-1 chromeclass-toolbar-additional tabs-alltabs-button badged-button"
+                           class="toolbarbutton-1 chromeclass-toolbar-additional tabs-alltabs-button"
+                           badged="true"
                            oncommand="gTabsPanel.showAllTabsPanel();"
                            label="&listAllTabs.label;"
                            tooltiptext="&listAllTabs.label;"
                            removable="false"/>
           </hbox>
         </hbox>
 
         <hbox class="titlebar-spacer" type="post-tabs"/>
@@ -1016,17 +1017,18 @@
         </toolbaritem>
 
         <toolbarspring cui-areatype="toolbar" class="chromeclass-toolbar-additional"/>
 
         <!-- This is a placeholder for the Downloads Indicator.  It is visible
              during the customization of the toolbar, in the palette, and before
              the Downloads Indicator overlay is loaded. -->
         <toolbarbutton id="downloads-button"
-                       class="toolbarbutton-1 chromeclass-toolbar-additional badged-button"
+                       class="toolbarbutton-1 chromeclass-toolbar-additional"
+                       badged="true"
                        key="key_openDownloads"
                        onmousedown="DownloadsIndicatorView.onCommand(event);"
                        onkeypress="DownloadsIndicatorView.onCommand(event);"
                        ondrop="DownloadsIndicatorView.onDrop(event);"
                        ondragover="DownloadsIndicatorView.onDragOver(event);"
                        ondragenter="DownloadsIndicatorView.onDragOver(event);"
                        label="&downloads.label;"
                        removable="true"
@@ -1052,17 +1054,18 @@
                        removable="true"
                        onmousedown="PanelUI.showSubView('appMenu-libraryView', this, event);"
                        onkeypress="PanelUI.showSubView('appMenu-libraryView', this, event);"
                        closemenu="none"
                        cui-areatype="toolbar"
                        tooltiptext="&libraryButton.tooltip;"
                        label="&places.library.title;"/>
 
-        <toolbarbutton id="fxa-toolbar-menu-button" class="toolbarbutton-1 badged-button chromeclass-toolbar-additional subviewbutton-nav"
+        <toolbarbutton id="fxa-toolbar-menu-button" class="toolbarbutton-1 chromeclass-toolbar-additional subviewbutton-nav"
+                       badged="true"
                        onmousedown="gSync.toggleAccountPanel('PanelUI-fxa', event)"
                        onkeypress="gSync.toggleAccountPanel('PanelUI-fxa', event)"
                        consumeanchor="fxa-toolbar-menu-button"
                        closemenu="none"
                        label="&fxa.menu.firefoxAccount;"
                        tooltiptext="&fxa.menu.firefoxAccount;"
                        cui-areatype="toolbar"
                        removable="true">
@@ -1079,17 +1082,18 @@
         <box class="toolbarbutton-animatable-box">
           <image class="toolbarbutton-animatable-image"/>
         </box>
       </toolbarbutton>
 
       <toolbaritem id="PanelUI-button"
                    removable="false">
         <toolbarbutton id="PanelUI-menu-button"
-                       class="toolbarbutton-1 badged-button"
+                       class="toolbarbutton-1"
+                       badged="true"
                        consumeanchor="PanelUI-button"
                        label="&brandShortName;"
                        tooltiptext="&appmenu.tooltip;"/>
       </toolbaritem>
 
       <hbox id="window-controls" hidden="true" pack="end" skipintoolbarset="true"
             ordinal="1000">
         <toolbarbutton id="minimize-button"
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -205,27 +205,30 @@
               let longPressBehavior = Services.prefs.getIntPref("privacy.userContext.longPressBehavior");
 
               // If longPressBehavior pref is set to 0 (or any invalid value)
               // long press menu is disabled.
               if (containersEnabled && (longPressBehavior <= 0 || longPressBehavior > 2)) {
                 containersEnabled = false;
               }
 
+              // There are separate "new tab" buttons for when the tab strip
+              // is overflowed and when it is not.  Attach the long click
+              // popup to both of them.
               const newTab = document.getElementById("new-tab-button");
               const newTab2 = document.getAnonymousElementByAttribute(this, "anonid", "tabs-newtab-button");
 
               for (let parent of [newTab, newTab2]) {
                 if (!parent)
                   continue;
 
                 gClickAndHoldListenersOnElement.remove(parent);
                 parent.removeAttribute("type");
-                if (parent.firstElementChild) {
-                  parent.firstElementChild.remove();
+                if (parent.menupopup) {
+                  parent.menupopup.remove();
                 }
 
                 if (containersEnabled) {
                   let popup = document.createElementNS(
                                 "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
                                 "menupopup");
                   if (parent.id) {
                     popup.id = "newtab-popup";
@@ -235,17 +238,17 @@
                   popup.className = "new-tab-popup";
                   popup.setAttribute("position", "after_end");
                   popup.addEventListener("popupshowing", event => {
                     createUserContextMenu(event, {
                       useAccessKeys: false,
                       showDefaultTab: Services.prefs.getIntPref("privacy.userContext.longPressBehavior") == 1,
                     });
                   });
-                  parent.appendChild(popup);
+                  parent.prepend(popup);
 
                   // longPressBehavior == 2 means that the menu is shown after X
                   // millisecs. Otherwise, with 1, the menu is open immediatelly.
                   if (longPressBehavior == 2) {
                     gClickAndHoldListenersOnElement.add(parent);
                   }
 
                   parent.setAttribute("type", "menu");
--- a/browser/base/content/test/tabs/browser_audioTabIcon.js
+++ b/browser/base/content/test/tabs/browser_audioTabIcon.js
@@ -162,18 +162,17 @@ async function test_playing_icon_on_tab(
 async function test_playing_icon_on_hidden_tab(tab) {
   let oldSelectedTab = gBrowser.selectedTab;
   let otherTabs = [
     await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE, true, true),
     await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE, true, true),
   ];
   let tabContainer = tab.parentNode;
   let alltabsButton = document.getElementById("alltabs-button");
-  let alltabsBadge = document.getAnonymousElementByAttribute(
-    alltabsButton, "class", "toolbarbutton-badge");
+  let alltabsBadge = alltabsButton.badgeLabel;
 
   function assertIconShowing() {
     is(getComputedStyle(alltabsBadge).backgroundImage,
       'url("chrome://browser/skin/tabbrowser/badge-audio-playing.svg")',
       "The audio playing icon is shown");
     is(tabContainer.getAttribute("hiddensoundplaying"), "true", "There are hidden audio tabs");
   }
 
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -4433,17 +4433,17 @@ OverflowableToolbar.prototype = {
     return new Promise(resolve => {
       let doc = this._panel.ownerDocument;
       this._panel.hidden = false;
       let multiview = this._panel.querySelector("panelmultiview");
       let mainViewId = multiview.getAttribute("mainViewId");
       let mainView = doc.getElementById(mainViewId);
       let contextMenu = doc.getElementById(mainView.getAttribute("context"));
       gELS.addSystemEventListener(contextMenu, "command", this, true);
-      let anchor = doc.getAnonymousElementByAttribute(this._chevron, "class", "toolbarbutton-icon");
+      let anchor = this._chevron.icon;
       // Ensure we update the gEditUIVisible flag when opening the popup, in
       // case the edit controls are in it.
       this._panel.addEventListener("popupshowing", () => doc.defaultView.updateEditUIVisibility(), {once: true});
       PanelMultiView.openPopup(this._panel, anchor || this._chevron, {
         triggerEvent: aEvent,
       }).catch(Cu.reportError);
       this._chevron.open = true;
 
--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -1343,16 +1343,21 @@ var PanelView = class extends Associated
         "toolbarbutton[wrap]:not([hidden])",
       ].join(",");
       for (let element of this.node.querySelectorAll(selector)) {
         // Ignore items in hidden containers.
         if (element.closest("[hidden]")) {
           continue;
         }
 
+        // Ignore content inside a <toolbarbutton>
+        if (element.tagName != "toolbarbutton" && element.closest("toolbarbutton")) {
+          continue;
+        }
+
         // Take the label for toolbarbuttons; it only exists on those elements.
         element = element.multilineLabel || element;
 
         let bounds = element.getBoundingClientRect();
         let previous = gMultiLineElementsMap.get(element);
         // We don't need to (re-)apply the workaround for invisible elements or
         // on elements we've seen before and haven't changed in the meantime.
         if (!bounds.width || !bounds.height ||
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -861,21 +861,17 @@ const PanelUI = {
     AppMenuNotifications.callMainAction(window, target.notification, false);
   },
 
   _getPopupId(notification) { return "appMenu-" + notification.id + "-notification"; },
 
   _getBadgeStatus(notification) { return notification.id; },
 
   _getPanelAnchor(candidate) {
-    let iconAnchor =
-      document.getAnonymousElementByAttribute(candidate, "class",
-                                              "toolbarbutton-badge-stack") ||
-      document.getAnonymousElementByAttribute(candidate, "class",
-                                              "toolbarbutton-icon");
+    let iconAnchor = candidate.badgeStack || candidate.icon;
     return iconAnchor || candidate;
   },
 
   _addedShortcuts: false,
   _ensureShortcutsShown(view = this.mainView) {
     if (view.hasAttribute("added-shortcuts")) {
       return;
     }
--- a/browser/components/customizableui/test/head.js
+++ b/browser/components/customizableui/test/head.js
@@ -25,17 +25,17 @@ registerCleanupFunction(() => Services.p
 
 var {synthesizeDragStart, synthesizeDrop, synthesizeMouseAtCenter} = EventUtils;
 
 const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 const kForceOverflowWidthPx = 300;
 
 function createDummyXULButton(id, label, win = window) {
-  let btn = document.createElementNS(kNSXUL, "toolbarbutton");
+  let btn = win.document.createElementNS(kNSXUL, "toolbarbutton");
   btn.id = id;
   btn.setAttribute("label", label || id);
   btn.className = "toolbarbutton-1 chromeclass-toolbar-additional";
   win.gNavToolbox.palette.appendChild(btn);
   return btn;
 }
 
 var gAddedToolbars = new Set();
@@ -440,18 +440,17 @@ function checkContextMenu(aContextMenu, 
     } catch (e) {
       ok(false, "Exception when checking context menu: " + e);
     }
   }
 }
 
 function waitForOverflowButtonShown(win = window) {
   let ov = win.document.getElementById("nav-bar-overflow-button");
-  let icon = win.document.getAnonymousElementByAttribute(ov, "class", "toolbarbutton-icon");
-  return waitForElementShown(icon);
+  return waitForElementShown(ov.icon);
 }
 function waitForElementShown(element) {
   let win = element.ownerGlobal;
   let dwu = win.windowUtils;
   return BrowserTestUtils.waitForCondition(() => {
     info("Waiting for overflow button to have non-0 size");
     let bounds = dwu.getBoundsWithoutFlushing(element);
     return bounds.width > 0 && bounds.height > 0;
--- a/browser/components/downloads/content/indicator.js
+++ b/browser/components/downloads/content/indicator.js
@@ -608,21 +608,20 @@ const DownloadsIndicatorView = {
 
     return this._indicator = indicator;
   },
 
   get indicatorAnchor() {
     let widgetGroup = CustomizableUI.getWidget("downloads-button");
     if (widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
       let overflowIcon = widgetGroup.forWindow(window).anchor;
-      return document.getAnonymousElementByAttribute(overflowIcon, "class", "toolbarbutton-icon");
+      return overflowIcon.icon;
     }
 
-    return document.getAnonymousElementByAttribute(this.indicator, "class",
-                                                   "toolbarbutton-badge-stack");
+    return this.indicator.badgeStack;
   },
 
   get _progressIcon() {
     return this.__progressIcon ||
       (this.__progressIcon = document.getElementById("downloads-indicator-progress-inner"));
   },
 
   get notifier() {
--- a/browser/components/downloads/test/browser/browser_overflow_anchor.js
+++ b/browser/components/downloads/test/browser/browser_overflow_anchor.js
@@ -29,28 +29,26 @@ add_task(async function test_overflow_an
 
   let promise = promisePanelOpened();
   EventUtils.sendMouseEvent({ type: "mousedown", button: 0 }, button.node);
   info("waiting for panel to open");
   await promise;
 
   let panel = DownloadsPanel.panel;
   let chevron = document.getElementById("nav-bar-overflow-button");
-  let chevronIcon = document.getAnonymousElementByAttribute(chevron,
-                                                            "class", "toolbarbutton-icon");
-  is(panel.anchorNode, chevronIcon, "Panel should be anchored to the chevron`s icon.");
+
+  is(panel.anchorNode, chevron.icon, "Panel should be anchored to the chevron`s icon.");
 
   DownloadsPanel.hidePanel();
 
   gCustomizeMode.addToToolbar(button.node);
 
   // Now try opening the panel again.
   promise = promisePanelOpened();
   EventUtils.sendMouseEvent({ type: "mousedown", button: 0 }, button.node);
   await promise;
 
-  let downloadsAnchor = document.getAnonymousElementByAttribute(button.node, "class",
-                                                               "toolbarbutton-badge-stack");
+  let downloadsAnchor = button.node.badgeStack;
   is(panel.anchorNode, downloadsAnchor);
 
   DownloadsPanel.hidePanel();
 });
 
--- a/browser/components/extensions/ExtensionControlledPopup.jsm
+++ b/browser/components/extensions/ExtensionControlledPopup.jsm
@@ -270,18 +270,17 @@ class ExtensionControlledPopup {
         `${makeWidgetId(extensionId)}-browser-action`);
       if (action) {
         action = action.areaType == "toolbar" && action.forWindow(win).node;
       }
 
       // Anchor to a toolbar browserAction if found, otherwise use the menu button.
       anchorButton = action || doc.getElementById("PanelUI-menu-button");
     }
-    let anchor = doc.getAnonymousElementByAttribute(
-      anchorButton, "class", "toolbarbutton-icon");
+    let anchor = anchorButton.icon;
     panel.hidden = false;
     popupnotification.show();
     panel.openPopup(anchor);
   }
 
   getAddonDetails(doc, addon) {
     const defaultIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
 
--- a/browser/components/extensions/parent/ext-browserAction.js
+++ b/browser/components/extensions/parent/ext-browserAction.js
@@ -160,18 +160,18 @@ this.browserAction = class extends Exten
         if (view) {
           this.clearPopup();
           CustomizableUI.hidePanelForNode(view);
           view.remove();
         }
       },
 
       onCreated: node => {
-        node.classList.add("badged-button");
         node.classList.add("webextension-browser-action");
+        node.setAttribute("badged", "true");
         node.setAttribute("constrain-size", "true");
         node.setAttribute("data-extensionid", this.extension.id);
 
         node.onmousedown = event => this.handleEvent(event);
         node.onmouseover = event => this.handleEvent(event);
         node.onmouseout = event => this.handleEvent(event);
 
         this.updateButton(node, this.globals, true);
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
@@ -124,18 +124,17 @@ async function runTests(options) {
 
     is(getListStyleImage(button), details.icon, "icon URL is correct");
     is(button.getAttribute("tooltiptext"), title, "image title is correct");
     is(button.getAttribute("label"), title, "image label is correct");
     is(button.getAttribute("badge"), details.badge, "badge text is correct");
     is(button.getAttribute("disabled") == "true", !details.enabled, "disabled state is correct");
 
     if (details.badge) {
-      let badge = button.ownerDocument.getAnonymousElementByAttribute(
-        button, "class", "toolbarbutton-badge");
+      let badge = button.badgeLabel;
       let style = window.getComputedStyle(badge);
       let expected = {
         backgroundColor: serializeColor(details.badgeBackgroundColor),
         color: serializeColor(details.badgeTextColor),
       };
       for (let [prop, value] of Object.entries(expected)) {
         is(style[prop], value, `${prop} is correct`);
       }
@@ -421,17 +420,17 @@ add_task(async function testBadgeColorPe
     manifest: {
       browser_action: {},
     },
   });
   await extension.startup();
 
   function getBadgeForWindow(win) {
     const widget = getBrowserActionWidget(extension).forWindow(win).node;
-    return document.getAnonymousElementByAttribute(widget, "class", "toolbarbutton-badge");
+    return widget.badgeLabel;
   }
 
   let badge = getBadgeForWindow(window);
   const badgeChanged = new Promise((resolve) => {
     const observer = new MutationObserver(() => resolve());
     observer.observe(badge, {attributes: true, attributeFilter: ["style"]});
   });
 
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js
@@ -242,17 +242,17 @@ add_task(async function browseraction_co
     toolbarCtxMenu.hidePopup();
 
     info("Pin the browserAction and another button to the overflow menu");
     CustomizableUI.addWidgetToArea(buttonId, CustomizableUI.AREA_FIXED_OVERFLOW_PANEL);
     CustomizableUI.addWidgetToArea(otherButtonId, CustomizableUI.AREA_FIXED_OVERFLOW_PANEL);
 
     info("Wait until the overflow menu is ready");
     let overflowButton = win.document.getElementById("nav-bar-overflow-button");
-    let icon = win.document.getAnonymousElementByAttribute(overflowButton, "class", "toolbarbutton-icon");
+    let icon = overflowButton.icon;
     await waitForElementShown(icon);
 
     if (!customizing) {
       info("Open overflow menu");
       let menu = win.document.getElementById("widget-overflow");
       let shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
       overflowButton.click();
       await shown;
@@ -321,17 +321,17 @@ async function runTestContextMenu({
   info("Test toolbar context menu in browserAction");
   await testContextMenu("toolbar-context-menu", customizing);
 
   info("Pin the browserAction and another button to the overflow menu");
   CustomizableUI.addWidgetToArea(buttonId, CustomizableUI.AREA_FIXED_OVERFLOW_PANEL);
 
   info("Wait until the overflow menu is ready");
   let overflowButton = win.document.getElementById("nav-bar-overflow-button");
-  let icon = win.document.getAnonymousElementByAttribute(overflowButton, "class", "toolbarbutton-icon");
+  let icon = overflowButton.icon;
   await waitForElementShown(icon);
 
   if (!customizing) {
     info("Open overflow menu");
     let menu = win.document.getElementById("widget-overflow");
     let shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
     overflowButton.click();
     await shown;
--- a/browser/components/places/content/browserPlacesViews.js
+++ b/browser/components/places/content/browserPlacesViews.js
@@ -1514,17 +1514,17 @@ PlacesToolbar.prototype = {
         aEvent.preventDefault();
         // Open the menu.
         draggedElt.open = true;
         return;
       }
 
       // If the menu is open, close it.
       if (draggedElt.open) {
-        draggedElt.lastElementChild.hidePopup();
+        draggedElt.menupopup.hidePopup();
         draggedElt.open = false;
       }
     }
 
     // Activate the view and cache the dragged element.
     this._draggedElt = draggedElt._placesNode;
     this._rootElt.focus();
 
--- a/browser/components/places/tests/browser/browser_views_liveupdate.js
+++ b/browser/components/places/tests/browser/browser_views_liveupdate.js
@@ -284,17 +284,17 @@ function getNodeForToolbarItem(itemGuid,
       if (child._placesNode.bookmarkGuid == itemGuid) {
         let valid = validator ? validator(child) : true;
         return [child._placesNode, i - staticNodes, valid];
       }
 
       // Don't search in queries, they could contain our item in a
       // different position.  Search only folders
       if (PlacesUtils.nodeIsFolder(child._placesNode)) {
-        var popup = child.lastElementChild;
+        var popup = child.menupopup;
         popup.openPopup();
         var foundNode = findNode(popup);
         popup.hidePopup();
         if (foundNode[0] != null)
           return foundNode;
       }
     }
     return [null, null];
--- a/browser/components/search/test/browser/browser_tooManyEnginesOffered.js
+++ b/browser/components/search/test/browser/browser_tooManyEnginesOffered.js
@@ -32,17 +32,17 @@ add_task(async function test() {
 
   // Make sure it has only one add-engine menu button item.
   let items = getOpenSearchItems();
   Assert.equal(items.length, 1, "A single button");
   let menuButton = items[0];
   Assert.equal(menuButton.type, "menu", "A menu button");
 
   // Mouse over the menu button to open it.
-  let buttonPopup = menuButton.firstElementChild;
+  let buttonPopup = menuButton.menupopup;
   promise = promiseEvent(buttonPopup, "popupshown");
   EventUtils.synthesizeMouse(menuButton, 5, 5, { type: "mousemove" });
   await promise;
 
   Assert.ok(menuButton.open, "Submenu should be open");
 
   // Check the engines inside the submenu.
   Assert.equal(buttonPopup.children.length, 6, "Expected number of engines");
--- a/browser/components/uitour/UITour.jsm
+++ b/browser/components/uitour/UITour.jsm
@@ -82,19 +82,17 @@ var UITour = {
         // If the user is logged in, use the avatar element.
         let fxAFooter = aDocument.getElementById("appMenu-fxa-container");
         if (fxAFooter.getAttribute("fxastatus")) {
           return aDocument.getElementById("appMenu-fxa-avatar");
         }
 
         // Otherwise use the sync setup icon.
         let statusButton = aDocument.getElementById("appMenu-fxa-label");
-        return aDocument.getAnonymousElementByAttribute(statusButton,
-                                                        "class",
-                                                        "toolbarbutton-icon");
+        return statusButton.icon;
       },
       // This is a fake widgetName starting with the "appMenu-" prefix so we know
       // to automatically open the appMenu when annotating this target.
       widgetName: "appMenu-fxa-label",
     }],
     ["addons",      {query: "#appMenu-addons-button"}],
     ["appMenu",     {
       addTargetListener: (aDocument, aCallback) => {
--- a/layout/xul/test/test_bug987230.xul
+++ b/layout/xul/test/test_bug987230.xul
@@ -1,17 +1,17 @@
 <?xml version="1.0"?>
 <?xml-stylesheet type="text/css" href="chrome://global/skin"?>
 <?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
 <!--
 https://bugzilla.mozilla.org/show_bug.cgi?id=987230
 -->
 <window title="Mozilla Bug 987230"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-        onload="SimpleTest.waitForFocus(nextTest, window)">
+        onload="SimpleTest.waitForFocus(startTest, window)">
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
   <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
 
 
   <!-- test results are displayed in the html:body -->
   <body xmlns="http://www.w3.org/1999/xhtml">
   <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=987230"
      target="_blank">Mozilla Bug 987230</a>
@@ -103,19 +103,24 @@ https://bugzilla.mozilla.org/show_bug.cg
   }
 
   function onAnchorClick(e) {
     info("click: " + e.target.id);
     ok(!popupHasShown, "Popup should only be shown once");
     popup.openPopup(anchor, "bottomcenter topright");
   }
 
-  let popup = document.getElementById("mypopup");
-  let outerAnchor = document.getElementById("toolbarbutton-anchor");
-  let anchor = document.getAnonymousElementByAttribute(outerAnchor, "class", "toolbarbutton-icon");
+  let popup, outerAnchor, anchor;
+
+  function startTest() {
+    popup = document.getElementById("mypopup");
+    outerAnchor = document.getElementById("toolbarbutton-anchor");
+    anchor = outerAnchor.icon;
+    nextTest();
+  }
 
   function nextTest(e) {
     synthesizeMouse(outerAnchor, 5, 5, {});
   }
 
   ]]>
   </script>
 </window>
--- a/toolkit/components/extensions/test/browser/browser_ext_themes_toolbarbutton_colors.js
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbarbutton_colors.js
@@ -27,17 +27,17 @@ add_task(async function test_button_back
       "image1.png": BACKGROUND,
     },
 
   });
 
   await extension.startup();
 
   let toolbarButton = document.querySelector("#home-button");
-  let toolbarButtonIcon = document.getAnonymousElementByAttribute(toolbarButton, "class", "toolbarbutton-icon");
+  let toolbarButtonIcon = toolbarButton.icon;
   let toolbarButtonIconCS = window.getComputedStyle(toolbarButtonIcon);
 
   InspectorUtils.addPseudoClassLock(toolbarButton, ":hover");
 
   Assert.equal(
     toolbarButtonIconCS.getPropertyValue("background-color"),
     `rgb(${hexToRGB(BUTTON_BACKGROUND_HOVER).join(", ")})`,
     "Toolbar button hover background is set."
--- a/toolkit/content/customElements.js
+++ b/toolkit/content/customElements.js
@@ -654,16 +654,17 @@ if (!isDummyDocument) {
     "chrome://global/content/elements/popupnotification.js",
     "chrome://global/content/elements/radio.js",
     "chrome://global/content/elements/richlistbox.js",
     "chrome://global/content/elements/autocomplete-popup.js",
     "chrome://global/content/elements/autocomplete-richlistitem.js",
     "chrome://global/content/elements/textbox.js",
     "chrome://global/content/elements/tabbox.js",
     "chrome://global/content/elements/text.js",
+    "chrome://global/content/elements/toolbarbutton.js",
     "chrome://global/content/elements/tree.js",
     "chrome://global/content/elements/wizard.js",
   ]) {
     Services.scriptloader.loadSubScript(script, window);
   }
 
   for (let [tag, script] of [
     ["findbar", "chrome://global/content/elements/findbar.js"],
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -57,32 +57,30 @@ toolkit.jar:
    content/global/TopLevelVideoDocument.js
    content/global/timepicker.xhtml
    content/global/treeUtils.js
 #ifndef MOZ_FENNEC
    content/global/viewZoomOverlay.js
 #endif
    content/global/widgets.css
    content/global/bindings/autocomplete.xml    (widgets/autocomplete.xml)
-   content/global/bindings/button.xml          (widgets/button.xml)
    content/global/bindings/calendar.js         (widgets/calendar.js)
    content/global/bindings/datekeeper.js       (widgets/datekeeper.js)
    content/global/bindings/datepicker.js       (widgets/datepicker.js)
    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/popup.xml           (widgets/popup.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/textbox.xml         (widgets/textbox.xml)
    content/global/bindings/timekeeper.js       (widgets/timekeeper.js)
    content/global/bindings/timepicker.js       (widgets/timepicker.js)
-   content/global/bindings/toolbarbutton.xml   (widgets/toolbarbutton.xml)
    content/global/bindings/wizard.xml          (widgets/wizard.xml)
    content/global/elements/autocomplete-popup.js              (widgets/autocomplete-popup.js)
    content/global/elements/autocomplete-richlistitem.js       (widgets/autocomplete-richlistitem.js)
    content/global/elements/browser-custom-element.js          (widgets/browser-custom-element.js)
    content/global/elements/button.js           (widgets/button.js)
    content/global/elements/checkbox.js         (widgets/checkbox.js)
    content/global/elements/datetimebox.js      (widgets/datetimebox.js)
    content/global/elements/findbar.js          (widgets/findbar.js)
@@ -98,16 +96,17 @@ toolkit.jar:
    content/global/elements/marquee.js          (widgets/marquee.js)
    content/global/elements/menulist.js         (widgets/menulist.js)
    content/global/elements/popupnotification.js  (widgets/popupnotification.js)
    content/global/elements/search-textbox.js     (widgets/search-textbox.js)
    content/global/elements/stringbundle.js     (widgets/stringbundle.js)
    content/global/elements/tabbox.js           (widgets/tabbox.js)
    content/global/elements/text.js             (widgets/text.js)
    content/global/elements/textbox.js          (widgets/textbox.js)
+   content/global/elements/toolbarbutton.js    (widgets/toolbarbutton.js)
    content/global/elements/videocontrols.js    (widgets/videocontrols.js)
    content/global/elements/tree.js             (widgets/tree.js)
    content/global/elements/wizard.js           (widgets/wizard.js)
 #ifdef XP_MACOSX
    content/global/macWindowMenu.js
 #endif
    content/global/gmp-sources/openh264.json    (gmp-sources/openh264.json)
    content/global/gmp-sources/widevinecdm.json (gmp-sources/widevinecdm.json)
--- a/toolkit/content/widgets/button.js
+++ b/toolkit/content/widgets/button.js
@@ -209,16 +209,18 @@
           this.checked = true;
         }
       }
     }
   }
 
   MozXULElement.implementCustomInterface(MozButtonBase, [Ci.nsIDOMXULButtonElement]);
 
+  MozElements.ButtonBase = MozButtonBase;
+
   class MozButton extends MozButtonBase {
     static get inheritedAttributes() {
       return {
         ".box-inherit": "align,dir,pack,orient",
         ".button-icon": "src=image",
         ".button-text": "value=label,accesskey,crop",
         ".button-menu-dropmarker": "open,disabled,label",
       };
deleted file mode 100644
--- a/toolkit/content/widgets/button.xml
+++ /dev/null
@@ -1,192 +0,0 @@
-<?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/. -->
-
-
-<bindings id="buttonBindings"
-   xmlns="http://www.mozilla.org/xbl"
-   xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-   xmlns:xbl="http://www.mozilla.org/xbl">
-
-  <binding id="button-base" extends="chrome://global/content/bindings/general.xml#basetext">
-    <implementation implements="nsIDOMXULButtonElement">
-      <property name="type"
-                onget="return this.getAttribute('type');"
-                onset="this.setAttribute('type', val); return val;"/>
-
-      <property name="dlgType"
-                onget="return this.getAttribute('dlgtype');"
-                onset="this.setAttribute('dlgtype', val); return val;"/>
-
-      <property name="group"
-                onget="return this.getAttribute('group');"
-                onset="this.setAttribute('group', val); return val;"/>
-
-      <property name="open" onget="return this.hasAttribute('open');">
-        <setter><![CDATA[
-          if (this.hasMenu()) {
-            this.openMenu(val);
-          } else if (val) {
-            // Fall back to just setting the attribute
-            this.setAttribute("open", "true");
-          } else {
-            this.removeAttribute("open");
-          }
-          return val;
-        ]]></setter>
-      </property>
-
-      <property name="checked" onget="return this.hasAttribute('checked');">
-        <setter><![CDATA[
-          if (this.type == "radio" && val) {
-            var sibs = this.parentNode.getElementsByAttribute("group", this.group);
-            for (var i = 0; i < sibs.length; ++i)
-              sibs[i].removeAttribute("checked");
-          }
-
-          if (val)
-            this.setAttribute("checked", "true");
-          else
-            this.removeAttribute("checked");
-
-          return val;
-        ]]></setter>
-      </property>
-
-      <method name ="filterButtons">
-        <parameter name="node"/>
-        <body>
-        <![CDATA[
-          // if the node isn't visible, don't descend into it.
-          var cs = node.ownerGlobal.getComputedStyle(node);
-          if (cs.visibility != "visible" || cs.display == "none") {
-            return NodeFilter.FILTER_REJECT;
-          }
-          // but it may be a popup element, in which case we look at "state"...
-          if (cs.display == "-moz-popup" && node.state != "open") {
-            return NodeFilter.FILTER_REJECT;
-          }
-          // OK - the node seems visible, so it is a candidate.
-          if (node.localName == "button" && node.accessKey && !node.disabled)
-            return NodeFilter.FILTER_ACCEPT;
-          return NodeFilter.FILTER_SKIP;
-        ]]>
-        </body>
-      </method>
-
-      <method name="fireAccessKeyButton">
-        <parameter name="aSubtree"/>
-        <parameter name="aAccessKeyLower"/>
-        <body>
-        <![CDATA[
-          var iterator = aSubtree.ownerDocument.createTreeWalker(aSubtree,
-                                                                 NodeFilter.SHOW_ELEMENT,
-                                                                 this.filterButtons);
-          while (iterator.nextNode()) {
-            var test = iterator.currentNode;
-            if (test.accessKey.toLowerCase() == aAccessKeyLower &&
-                !test.disabled && !test.collapsed && !test.hidden) {
-              test.focus();
-              test.click();
-              return true;
-            }
-          }
-          return false;
-        ]]>
-        </body>
-      </method>
-
-      <method name="_handleClick">
-        <body>
-        <![CDATA[
-          if (!this.disabled) {
-            if (this.type == "checkbox") {
-              this.checked = !this.checked;
-            } else if (this.type == "radio") {
-              this.checked = true;
-            }
-          }
-        ]]>
-        </body>
-      </method>
-    </implementation>
-
-    <handlers>
-      <!-- While it would seem we could do this by handling oncommand, we can't
-           because any external oncommand handlers might get called before ours,
-           and then they would see the incorrect value of checked. Additionally
-           a command attribute would redirect the command events anyway.-->
-      <handler event="click" button="0" action="this._handleClick();"/>
-      <handler event="keypress" key=" ">
-      <![CDATA[
-        this._handleClick();
-        // Prevent page from scrolling on the space key.
-        event.preventDefault();
-      ]]>
-      </handler>
-
-      <handler event="keypress">
-      <![CDATA[
-        if (this.hasMenu()) {
-          if (this.open)
-            return;
-        } else {
-          if (event.keyCode == KeyEvent.DOM_VK_UP ||
-              (event.keyCode == KeyEvent.DOM_VK_LEFT &&
-                document.defaultView.getComputedStyle(this.parentNode)
-                        .direction == "ltr") ||
-              (event.keyCode == KeyEvent.DOM_VK_RIGHT &&
-                document.defaultView.getComputedStyle(this.parentNode)
-                        .direction == "rtl")) {
-            event.preventDefault();
-            window.document.commandDispatcher.rewindFocus();
-            return;
-          }
-
-          if (event.keyCode == KeyEvent.DOM_VK_DOWN ||
-              (event.keyCode == KeyEvent.DOM_VK_RIGHT &&
-                document.defaultView.getComputedStyle(this.parentNode)
-                        .direction == "ltr") ||
-              (event.keyCode == KeyEvent.DOM_VK_LEFT &&
-                document.defaultView.getComputedStyle(this.parentNode)
-                        .direction == "rtl")) {
-            event.preventDefault();
-            window.document.commandDispatcher.advanceFocus();
-            return;
-          }
-        }
-
-        if (event.keyCode || event.charCode <= 32 || event.altKey ||
-            event.ctrlKey || event.metaKey)
-          return; // No printable char pressed, not a potential accesskey
-
-        // Possible accesskey pressed
-        var charPressedLower = String.fromCharCode(event.charCode).toLowerCase();
-
-        // If the accesskey of the current button is pressed, just activate it
-        if (this.accessKey.toLowerCase() == charPressedLower) {
-          this.click();
-          return;
-        }
-
-        // Search for accesskey in the list of buttons for this doc and each subdoc
-        // Get the buttons for the main document and all sub-frames
-        for (var frameCount = -1; frameCount < window.top.frames.length; frameCount++) {
-          var doc = (frameCount == -1) ? window.top.document :
-            window.top.frames[frameCount].document;
-          if (this.fireAccessKeyButton(doc.documentElement, charPressedLower))
-            return;
-        }
-
-        // Test anonymous buttons
-        var dlg = window.top.document;
-        var buttonBox = dlg.getAnonymousElementByAttribute(dlg.documentElement,
-                                                         "anonid", "buttons");
-        if (buttonBox)
-          this.fireAccessKeyButton(buttonBox, charPressedLower);
-      ]]>
-      </handler>
-    </handlers>
-  </binding>
-</bindings>
new file mode 100644
--- /dev/null
+++ b/toolkit/content/widgets/toolbarbutton.js
@@ -0,0 +1,153 @@
+/* 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";
+
+// This is loaded into all XUL windows. Wrap in a block to prevent
+// leaking to window scope.
+{
+  const KEEP_CHILDREN = new Set(["observes", "template", "menupopup", "panel", "tooltip"]);
+
+  window.addEventListener("popupshowing", (e) => {
+    if (e.originalTarget.ownerDocument != document) {
+      return;
+    }
+
+    e.originalTarget.setAttribute("hasbeenopened", "true");
+    for (let el of e.originalTarget.querySelectorAll("toolbarbutton")) {
+      el.render();
+    }
+  }, {capture: true});
+
+  class MozToolbarbutton extends MozElements.ButtonBase {
+    static get inheritedAttributes() {
+      return {
+        ".toolbarbutton-icon": "validate,src=image,label,type,consumeanchor,triggeringprincipal=iconloadingprincipal",
+        ".toolbarbutton-text": "value=label,accesskey,crop,dragover-top,wrap",
+        ".toolbarbutton-multiline-text": "text=label,accesskey,wrap",
+        ".toolbarbutton-menu-dropmarker": "disabled,label",
+
+        ".toolbarbutton-badge": "value=badge,style=badgeStyle",
+      };
+    }
+
+    static get fragment() {
+      let frag = document.importNode(MozXULElement.parseXULToFragment(`
+        <image class="toolbarbutton-icon"></image>
+        <label class="toolbarbutton-text" crop="right" flex="1"></label>
+        <label class="toolbarbutton-multiline-text" flex="1"></label>
+        <dropmarker type="menu" class="toolbarbutton-menu-dropmarker"></dropmarker>`), true);
+      Object.defineProperty(this, "fragment", {value: frag});
+      return frag;
+    }
+
+    static get badgedFragment() {
+      let frag = document.importNode(MozXULElement.parseXULToFragment(`
+        <stack class="toolbarbutton-badge-stack">
+          <image class="toolbarbutton-icon"/>
+          <label class="toolbarbutton-badge" top="0" end="0" crop="none"/>
+        </stack>
+        <label class="toolbarbutton-text" crop="right" flex="1"/>
+        <label class="toolbarbutton-multiline-text" flex="1"/>
+        <dropmarker anonid="dropmarker" type="menu"
+                    class="toolbarbutton-menu-dropmarker"/>`), true);
+      Object.defineProperty(this, "badgedFragment", {value: frag});
+      return frag;
+    }
+
+    get _hasRendered() {
+      return (this.querySelector(":scope > .toolbarbutton-text") != null);
+    }
+
+    connectedCallback() {
+      if (this.delayConnectedCallback()) {
+        return;
+      }
+
+      // Defer creating DOM elements for content inside popups.
+      // These will be added in the popupshown handler above.
+      let panel = this.closest("panel");
+      if (panel && !panel.hasAttribute("hasbeenopened")) {
+        return;
+      }
+
+      this.render();
+    }
+
+    render() {
+      if (this._hasRendered) {
+        return;
+      }
+
+      let badged = (this.getAttribute("badged") == "true");
+
+      if (badged) {
+        let moveChildren = [];
+        for (let child of this.children) {
+          if (!KEEP_CHILDREN.has(child.tagName)) {
+            moveChildren.push(child);
+          }
+        }
+
+        this.appendChild(this.constructor.badgedFragment.cloneNode(true));
+
+        if (moveChildren.length > 0) {
+          let {badgeStack, icon} = this;
+          for (let child of moveChildren) {
+            badgeStack.insertBefore(child, icon);
+          }
+        }
+      } else {
+        let moveChildren = [];
+        for (let child of this.children) {
+          if (!KEEP_CHILDREN.has(child.tagName) && child.tagName != "box") {
+            // XBL toolbarbutton doesn't insert any anonymous content
+            // if it has a child of any other type
+            return;
+          }
+
+          if (child.tagName == "box") {
+            moveChildren.push(child);
+          }
+        }
+
+        this.appendChild(this.constructor.fragment.cloneNode(true));
+
+        // XBL toolbarbutton explicitly places any <box> children
+        // right before the menu marker.
+        for (let child of moveChildren) {
+          this.insertBefore(child, this.lastChild);
+        }
+      }
+
+      this.initializeAttributeInheritance();
+    }
+
+    get icon() {
+      return this.querySelector(".toolbarbutton-icon");
+    }
+
+    get badgeLabel() {
+      return this.querySelector(".toolbarbutton-badge");
+    }
+
+    get badgeStack() {
+      return this.querySelector(".toolbarbutton-badge-stack");
+    }
+
+    get multilineLabel() {
+      return this.querySelector(".toolbarbutton-multiline-text");
+    }
+
+    get dropmarker() {
+      return this.querySelector(".toolbarbutton-menu-dropmarker");
+    }
+
+    get menupopup() {
+      return this.querySelector("menupopup");
+    }
+  }
+
+  customElements.define("toolbarbutton", MozToolbarbutton);
+}
deleted file mode 100644
--- a/toolkit/content/widgets/toolbarbutton.xml
+++ /dev/null
@@ -1,48 +0,0 @@
-<?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/. -->
-
-
-<bindings id="toolbarbuttonBindings"
-   xmlns="http://www.mozilla.org/xbl"
-   xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-   xmlns:xbl="http://www.mozilla.org/xbl">
-
-  <binding id="toolbarbutton"
-           extends="chrome://global/content/bindings/button.xml#button-base">
-    <implementation>
-      <property name="multilineLabel"
-        onget="return document.getAnonymousElementByAttribute(this, 'class', 'toolbarbutton-multiline-text');" />
-    </implementation>
-    <content>
-      <children includes="observes|template|menupopup|panel|tooltip"/>
-      <xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label,type,consumeanchor,triggeringprincipal=iconloadingprincipal"/>
-      <xul:label class="toolbarbutton-text" crop="right" flex="1"
-                 xbl:inherits="value=label,accesskey,crop,dragover-top,wrap"/>
-      <xul:label class="toolbarbutton-multiline-text" flex="1"
-                 xbl:inherits="xbl:text=label,accesskey,wrap"/>
-      <children includes="box"/>
-      <xul:dropmarker anonid="dropmarker" type="menu"
-                      class="toolbarbutton-menu-dropmarker" xbl:inherits="disabled,label"/>
-    </content>
-  </binding>
-
-  <binding id="toolbarbutton-badged"
-           extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton">
-    <content>
-      <children includes="observes|template|menupopup|panel|tooltip"/>
-      <xul:stack class="toolbarbutton-badge-stack">
-        <children/>
-        <xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label,consumeanchor"/>
-        <xul:label class="toolbarbutton-badge" xbl:inherits="value=badge,style=badgeStyle" top="0" end="0" crop="none"/>
-      </xul:stack>
-      <xul:label class="toolbarbutton-text" crop="right" flex="1"
-                 xbl:inherits="value=label,accesskey,crop,wrap"/>
-      <xul:label class="toolbarbutton-multiline-text" flex="1"
-                 xbl:inherits="xbl:text=label,accesskey,wrap"/>
-      <xul:dropmarker anonid="dropmarker" type="menu"
-                      class="toolbarbutton-menu-dropmarker" xbl:inherits="disabled,label"/>
-    </content>
-  </binding>
-</bindings>
--- a/toolkit/content/xul.css
+++ b/toolkit/content/xul.css
@@ -102,25 +102,16 @@ label.text-link, label[onclick] {
 }
 
 label html|span.accesskey {
   text-decoration: underline;
 }
 
 /********** toolbarbutton **********/
 
-toolbarbutton {
-  -moz-binding: url("chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton");
-}
-
-toolbarbutton.badged-button > toolbarbutton,
-toolbarbutton.badged-button {
-  -moz-binding: url("chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton-badged");
-}
-
 .toolbarbutton-badge:not([value]),
 .toolbarbutton-badge[value=""] {
   display: none;
 }
 
 toolbarbutton:not([type="menu"]) > .toolbarbutton-menu-dropmarker,
 toolbar[mode="icons"] .toolbarbutton-text,
 toolbar[mode="icons"] .toolbarbutton-multiline-text,