Bug 1487065 - Implement popup-notification as a Custom Element r=MattN
authorBrian Grinstead <bgrinstead@mozilla.com>
Thu, 07 Feb 2019 22:16:26 +0000
changeset 457653 04b57ffa53ce4c248bc3869247fed266b37eab2f
parent 457652 de71a4462179bb6c014c5e11782aad68db7648e0
child 457654 1fb53779a65c7b12f10b61c288e0bd20776443b8
push id35516
push userrmaries@mozilla.com
push dateFri, 08 Feb 2019 04:23:26 +0000
treeherdermozilla-central@d599d1a73a3a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1487065
milestone67.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 1487065 - Implement popup-notification as a Custom Element r=MattN Differential Revision: https://phabricator.services.mozilla.com/D17699
browser/base/content/browser-addons.js
browser/base/content/browser.css
browser/base/content/browser.js
browser/base/content/popup-notifications.inc
browser/base/content/test/plugins/browser_private_clicktoplay.js
browser/base/content/test/popupNotifications/browser_displayURI.js
browser/base/content/test/popupNotifications/browser_popupNotification_checkbox.js
browser/base/content/test/popupNotifications/head.js
browser/base/content/test/webrtc/head.js
browser/base/content/urlbarBindings.xml
browser/components/customizableui/content/panelUI.js
browser/components/customizableui/test/browser_panelUINotifications.js
browser/components/customizableui/test/browser_panelUINotifications_fullscreen.js
browser/components/customizableui/test/browser_panelUINotifications_fullscreen_noAutoHideToolbar.js
browser/components/customizableui/test/browser_panelUINotifications_multiWindow.js
browser/components/extensions/ExtensionControlledPopup.jsm
browser/components/extensions/test/browser/browser_ExtensionControlledPopup.js
browser/components/extensions/test/browser/browser_ext_chrome_settings_overrides_home.js
browser/components/extensions/test/browser/browser_ext_tabs_hide.js
browser/components/extensions/test/browser/browser_ext_url_overrides_newtab.js
browser/extensions/formautofill/FormAutofillDoorhanger.jsm
browser/extensions/formautofill/content/formautofill.css
browser/themes/shared/addons/extension-controlled.inc.css
dom/notification/test/browser/browser_permission_dismiss.js
testing/marionette/harness/marionette_harness/tests/unit/test_click.py
testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/notifications.py
toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js
toolkit/components/passwordmgr/test/browser/head.js
toolkit/content/customElements.js
toolkit/content/jar.mn
toolkit/content/widgets/notification.xml
toolkit/content/widgets/popupnotification.js
toolkit/content/xul.css
toolkit/modules/PopupNotifications.jsm
toolkit/mozapps/extensions/test/xpinstall/head.js
toolkit/mozapps/update/tests/browser/browser_TelemetryUpdatePing.js
toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadOptIn.js
toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadOptIn_bgWin.js
toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadOptIn_staging.js
toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloaded.js
toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloaded_staged.js
toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_patch_partialBadSize_complete.js
toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_partialApplyFailure_complete.js
toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_partialApplyFailure_complete_staging.js
toolkit/mozapps/update/tests/browser/head.js
--- a/browser/base/content/browser-addons.js
+++ b/browser/base/content/browser-addons.js
@@ -1,16 +1,148 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
  * 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 file is loaded into the browser window scope.
 /* eslint-env mozilla/browser-window */
 
+customElements.define("addon-progress-notification", class MozAddonProgressNotification extends customElements.get("popupnotification") {
+  show() {
+    super.show();
+    this.progressmeter = document.getElementById("addon-progress-notification-progressmeter");
+
+    this.progresstext = document.getElementById("addon-progress-notification-progresstext");
+
+    if (!this.notification)
+      return;
+
+    this.notification.options.installs.forEach(function(aInstall) {
+      aInstall.addListener(this);
+    }, this);
+
+    // Calling updateProgress can sometimes cause this notification to be
+    // removed in the middle of refreshing the notification panel which
+    // makes the panel get refreshed again. Just initialise to the
+    // undetermined state and then schedule a proper check at the next
+    // opportunity
+    this.setProgress(0, -1);
+    this._updateProgressTimeout = setTimeout(this.updateProgress.bind(this), 0);
+  }
+
+  disconnectedCallback() {
+    this.destroy();
+  }
+
+  destroy() {
+    if (!this.notification)
+      return;
+    this.notification.options.installs.forEach(function(aInstall) {
+      aInstall.removeListener(this);
+    }, this);
+
+    clearTimeout(this._updateProgressTimeout);
+  }
+
+  setProgress(aProgress, aMaxProgress) {
+    if (aMaxProgress == -1) {
+      this.progressmeter.removeAttribute("value");
+    } else {
+      this.progressmeter.setAttribute("value", (aProgress * 100) / aMaxProgress);
+    }
+
+    let now = Date.now();
+
+    if (!this.notification.lastUpdate) {
+      this.notification.lastUpdate = now;
+      this.notification.lastProgress = aProgress;
+      return;
+    }
+
+    let delta = now - this.notification.lastUpdate;
+    if ((delta < 400) && (aProgress < aMaxProgress))
+      return;
+
+    delta /= 1000;
+
+    // This algorithm is the same used by the downloads code.
+    let speed = (aProgress - this.notification.lastProgress) / delta;
+    if (this.notification.speed)
+      speed = speed * 0.9 + this.notification.speed * 0.1;
+
+    this.notification.lastUpdate = now;
+    this.notification.lastProgress = aProgress;
+    this.notification.speed = speed;
+
+    let status = null;
+    [status, this.notification.last] = DownloadUtils.getDownloadStatus(aProgress, aMaxProgress, speed, this.notification.last);
+    this.progresstext.setAttribute("value", status);
+    this.progresstext.setAttribute("tooltiptext", status);
+  }
+
+  cancel() {
+    let installs = this.notification.options.installs;
+    installs.forEach(function(aInstall) {
+      try {
+        aInstall.cancel();
+      } catch (e) {
+        // Cancel will throw if the download has already failed
+      }
+    }, this);
+
+    PopupNotifications.remove(this.notification);
+  }
+
+  updateProgress() {
+    if (!this.notification)
+      return;
+
+    let downloadingCount = 0;
+    let progress = 0;
+    let maxProgress = 0;
+
+    this.notification.options.installs.forEach(function(aInstall) {
+      if (aInstall.maxProgress == -1)
+        maxProgress = -1;
+      progress += aInstall.progress;
+      if (maxProgress >= 0)
+        maxProgress += aInstall.maxProgress;
+      if (aInstall.state < AddonManager.STATE_DOWNLOADED)
+        downloadingCount++;
+    });
+
+    if (downloadingCount == 0) {
+      this.destroy();
+      this.progressmeter.removeAttribute("value");
+      let status = gNavigatorBundle.getString("addonDownloadVerifying");
+      this.progresstext.setAttribute("value", status);
+      this.progresstext.setAttribute("tooltiptext", status);
+    } else {
+      this.setProgress(progress, maxProgress);
+    }
+  }
+
+  onDownloadProgress() {
+    this.updateProgress();
+  }
+
+  onDownloadFailed() {
+    this.updateProgress();
+  }
+
+  onDownloadCancelled() {
+    this.updateProgress();
+  }
+
+  onDownloadEnded() {
+    this.updateProgress();
+  }
+});
+
 // Removes a doorhanger notification if all of the installs it was notifying
 // about have ended in some way.
 function removeNotificationOnEnd(notification, installs) {
   let count = installs.length;
 
   function maybeRemove(install) {
     install.removeListener(this);
 
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -1002,20 +1002,16 @@ html|*#fullscreen-exit-button {
 
 .popup-anchor {
   /* should occupy space but not be visible */
   opacity: 0;
   pointer-events: none;
   -moz-stack-sizing: ignore;
 }
 
-#addon-progress-notification {
-  -moz-binding: url("chrome://browser/content/urlbarBindings.xml#addon-progress-notification");
-}
-
 browser[tabmodalPromptShowing] {
   -moz-user-focus: none !important;
 }
 
 /* Status panel */
 
 #statuspanel {
   position: fixed;
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -18,16 +18,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
   CFRPageActions: "resource://activity-stream/lib/CFRPageActions.jsm",
   CharsetMenu: "resource://gre/modules/CharsetMenu.jsm",
   Color: "resource://gre/modules/Color.jsm",
   ContentSearch: "resource:///modules/ContentSearch.jsm",
   ContextualIdentityService: "resource://gre/modules/ContextualIdentityService.jsm",
   CustomizableUI: "resource:///modules/CustomizableUI.jsm",
   Deprecated: "resource://gre/modules/Deprecated.jsm",
   DownloadsCommon: "resource:///modules/DownloadsCommon.jsm",
+  DownloadUtils: "resource://gre/modules/DownloadUtils.jsm",
   E10SUtils: "resource://gre/modules/E10SUtils.jsm",
   ExtensionsUI: "resource:///modules/ExtensionsUI.jsm",
   FormValidationHandler: "resource:///modules/FormValidationHandler.jsm",
   LanguagePrompt: "resource://gre/modules/LanguagePrompt.jsm",
   HomePage: "resource:///modules/HomePage.jsm",
   LightweightThemeConsumer: "resource://gre/modules/LightweightThemeConsumer.jsm",
   LightweightThemeManager: "resource://gre/modules/LightweightThemeManager.jsm",
   Log: "resource://gre/modules/Log.jsm",
--- a/browser/base/content/popup-notifications.inc
+++ b/browser/base/content/popup-notifications.inc
@@ -56,17 +56,17 @@
       <popupnotificationcontent orient="vertical">
         <textbox id="password-notification-username"/>
         <textbox id="password-notification-password" type="password" show-content=""/>
         <checkbox id="password-notification-visibilityToggle" hidden="true"/>
       </popupnotificationcontent>
     </popupnotification>
 
 
-    <popupnotification id="addon-progress-notification" hidden="true">
+    <popupnotification id="addon-progress-notification" is="addon-progress-notification" hidden="true">
       <popupnotificationcontent orient="vertical">
         <html:progress id="addon-progress-notification-progressmeter" max="100"/>
         <label id="addon-progress-notification-progresstext" crop="end"/>
       </popupnotificationcontent>
     </popupnotification>
 
     <popupnotification id="addon-install-confirmation-notification" hidden="true">
       <popupnotificationcontent id="addon-install-confirmation-content" orient="vertical"/>
--- a/browser/base/content/test/plugins/browser_private_clicktoplay.js
+++ b/browser/base/content/test/plugins/browser_private_clicktoplay.js
@@ -182,44 +182,11 @@ add_task(async function test3c() {
   await promiseShown;
   is(gPrivateWindow.PopupNotifications.panel.firstElementChild.secondaryButton.hidden, true,
      "Test 2c, Activated plugin in a private window should not have visible 'Block' button.");
   is(gPrivateWindow.PopupNotifications.panel.firstElementChild.checkbox.hidden, true,
      "Test 2c, Activated plugin in a private window should not have visible 'Remember' checkbox.");
 
   BrowserTestUtils.loadURI(gPrivateBrowser, gHttpTestRoot + "plugin_two_types.html");
   await BrowserTestUtils.browserLoaded(gPrivateBrowser);
-});
-
-add_task(async function test3d() {
-  let popupNotification = gPrivateWindow.PopupNotifications.getNotification("click-to-play-plugins", gPrivateBrowser);
-  ok(popupNotification, "Test 3d, Should have a click-to-play notification");
-
-  // Check the list item status
-  let promiseShown = BrowserTestUtils.waitForEvent(gPrivateWindow.PopupNotifications.panel,
-                                                   "Shown");
-  popupNotification.reshow();
-  await promiseShown;
-  for (let item of gPrivateWindow.PopupNotifications.panel.firstElementChild.children) {
-    let allowalways = item.openOrClosedShadowRoot.getElementById("allowalways");
-    ok(allowalways, "Test 3d, should have list item for allow always");
-    let allownow = item.openOrClosedShadowRoot.getElementById("allownow");
-    ok(allownow, "Test 3d, should have list item for allow now");
-    let block = item.openOrClosedShadowRoot.getElementById("block");
-    ok(block, "Test 3d, should have list item for block");
-
-    if (item.action.pluginName === "Test") {
-      is(item.value, "allowalways", "Test 3d, Plugin should bet set to 'allow always'");
-      ok(!allowalways.hidden, "Test 3d, Plugin set to 'always allow' should have a visible 'always allow' action.");
-      ok(allownow.hidden, "Test 3d, Plugin set to 'always allow' should have an invisible 'allow now' action.");
-      ok(block.hidden, "Test 3d, Plugin set to 'always allow' should have an invisible 'block' action.");
-    } else if (item.action.pluginName === "Second Test") {
-      is(item.value, "block", "Test 3d, Second plugin should bet set to 'block'");
-      ok(allowalways.hidden, "Test 3d, Plugin set to 'block' should have a visible 'always allow' action.");
-      ok(!allownow.hidden, "Test 3d, Plugin set to 'block' should have a visible 'allow now' action.");
-      ok(!block.hidden, "Test 3d, Plugin set to 'block' should have a visible 'block' action.");
-    } else {
-      ok(false, "Test 3d, Unexpected plugin '" + item.action.pluginName + "'");
-    }
-  }
 
   finishTest();
 });
--- a/browser/base/content/test/popupNotifications/browser_displayURI.js
+++ b/browser/base/content/test/popupNotifications/browser_displayURI.js
@@ -3,36 +3,32 @@
  */
 
 async function check(contentTask, options = {}) {
   await BrowserTestUtils.withNewTab("https://test1.example.com/", async function(browser) {
     let popupShownPromise = waitForNotificationPanel();
     await ContentTask.spawn(browser, null, contentTask);
     let panel = await popupShownPromise;
     let notification = panel.children[0];
-    let body = document.getAnonymousElementByAttribute(notification,
-                                                       "class",
-                                                       "popup-notification-body");
+    let body = notification.querySelector(".popup-notification-body");
     ok(body.innerHTML.includes("example.com"), "Check that at least the eTLD+1 is present in the markup");
   });
 
   let channel = NetUtil.newChannel({
     uri: getRootDirectory(gTestPath),
     loadUsingSystemPrincipal: true,
   });
   channel = channel.QueryInterface(Ci.nsIFileChannel);
 
   await BrowserTestUtils.withNewTab(channel.file.path, async function(browser) {
     let popupShownPromise = waitForNotificationPanel();
     await ContentTask.spawn(browser, null, contentTask);
     let panel = await popupShownPromise;
     let notification = panel.children[0];
-    let body = document.getAnonymousElementByAttribute(notification,
-                                                       "class",
-                                                       "popup-notification-body");
+    let body = notification.querySelector(".popup-notification-body");
     if (notification.id == "geolocation-notification") {
       ok(body.innerHTML.includes("local file"), `file:// URIs should be displayed as local file.`);
     } else {
       ok(body.innerHTML.includes("Unknown origin"), "file:// URIs should be displayed as unknown origin.");
     }
   });
 
   if (!options.skipOnExtension) {
@@ -55,19 +51,17 @@ async function check(contentTask, option
     await extension.startup();
     let extensionURI = await extension.awaitMessage("extension-tab-url");
 
     await BrowserTestUtils.withNewTab(extensionURI, async function(browser) {
       let popupShownPromise = waitForNotificationPanel();
       await ContentTask.spawn(browser, null, contentTask);
       let panel = await popupShownPromise;
       let notification = panel.children[0];
-      let body = document.getAnonymousElementByAttribute(notification,
-                                                         "class",
-                                                         "popup-notification-body");
+      let body = notification.querySelector(".popup-notification-body");
       ok(body.innerHTML.includes("Test Extension Name"),
          "Check the the extension name is present in the markup");
     });
 
     await extension.unload();
   }
 }
 
--- a/browser/base/content/test/popupNotifications/browser_popupNotification_checkbox.js
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_checkbox.js
@@ -14,17 +14,17 @@ function test() {
 function checkCheckbox(checkbox, label, checked = false, hidden = false) {
   is(checkbox.label, label, "Checkbox should have the correct label");
   is(checkbox.hidden, hidden, "Checkbox should be shown");
   is(checkbox.checked, checked, "Checkbox should be checked by default");
 }
 
 function checkMainAction(notification, disabled = false) {
   let mainAction = notification.button;
-  let warningLabel = document.getAnonymousElementByAttribute(notification, "class", "popup-notification-warning");
+  let warningLabel = notification.querySelector(".popup-notification-warning");
   is(warningLabel.hidden, !disabled, "Warning label should be shown");
   is(mainAction.disabled, disabled, "MainAction should be disabled");
 }
 
 function promiseElementVisible(element) {
   // HTMLElement.offsetParent is null when the element is not visisble
   // (or if the element has |position: fixed|). See:
   // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
--- a/browser/base/content/test/popupNotifications/head.js
+++ b/browser/base/content/test/popupNotifications/head.js
@@ -178,18 +178,17 @@ function checkPopup(popup, notifyObj) {
   ok(notifyObj.showingCallbackTriggered, "showing callback was triggered");
   ok(notifyObj.shownCallbackTriggered, "shown callback was triggered");
 
   let notifications = popup.childNodes;
   is(notifications.length, 1, "one notification displayed");
   let notification = notifications[0];
   if (!notification)
     return;
-  let icon = document.getAnonymousElementByAttribute(notification, "class",
-                                                     "popup-notification-icon");
+  let icon = notification.querySelector(".popup-notification-icon");
   if (notifyObj.id == "geolocation") {
     isnot(icon.boxObject.width, 0, "icon for geo displayed");
     ok(popup.anchorNode.classList.contains("notification-anchor-icon"),
        "notification anchored to icon");
   }
 
   let description = notifyObj.message.split("<>");
   let text = {};
@@ -213,17 +212,17 @@ function checkPopup(popup, notifyObj) {
     let secondaryAction = notifyObj.secondaryActions[0];
     is(notification.getAttribute("secondarybuttonlabel"), secondaryAction.label,
        "secondary action label matches");
     is(notification.getAttribute("secondarybuttonaccesskey"),
        secondaryAction.accessKey, "secondary action accesskey matches");
   }
   // Additional secondary actions appear as menu items.
   let actualExtraSecondaryActions =
-    Array.filter(notification.childNodes, child => child.nodeName == "menuitem");
+    Array.filter(notification.menupopup.childNodes, child => child.nodeName == "menuitem");
   let extraSecondaryActions = notifyObj.secondaryActions ? notifyObj.secondaryActions.slice(1) : [];
   is(actualExtraSecondaryActions.length, extraSecondaryActions.length,
      "number of extra secondary actions matches");
   extraSecondaryActions.forEach(function(a, i) {
     is(actualExtraSecondaryActions[i].getAttribute("label"), a.label,
        "label for extra secondary action " + i + " matches");
     is(actualExtraSecondaryActions[i].getAttribute("accesskey"), a.accessKey,
        "accessKey for extra secondary action " + i + " matches");
--- a/browser/base/content/test/webrtc/head.js
+++ b/browser/base/content/test/webrtc/head.js
@@ -322,22 +322,27 @@ function promiseNoPopupNotification(aNam
 const kActionAlways = 1;
 const kActionDeny = 2;
 const kActionNever = 3;
 
 function activateSecondaryAction(aAction) {
   let notification = PopupNotifications.panel.firstElementChild;
   switch (aAction) {
     case kActionNever:
-      notification.checkbox.setAttribute("checked", true); // fallthrough
+      if (!notification.checkbox.checked) {
+        notification.checkbox.click();
+      }
+      // fallthrough
     case kActionDeny:
       notification.secondaryButton.click();
       break;
     case kActionAlways:
-      notification.checkbox.setAttribute("checked", true);
+      if (!notification.checkbox.checked) {
+        notification.checkbox.click();
+      }
       notification.button.click();
       break;
   }
 }
 
 function getMediaCaptureState() {
   return new Promise(resolve => {
     let mm = _mm();
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -2717,176 +2717,9 @@ file, You can obtain one at http://mozil
           if (action && action.params.url) {
             UrlbarUtils.setupSpeculativeConnection(action.params.url, window);
           }
         }
       ]]></handler>
 
     </handlers>
   </binding>
-
-  <binding id="addon-progress-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification">
-    <implementation>
-      <constructor><![CDATA[
-        if (!this.notification)
-          return;
-
-        this.notification.options.installs.forEach(function(aInstall) {
-          aInstall.addListener(this);
-        }, this);
-
-        // Calling updateProgress can sometimes cause this notification to be
-        // removed in the middle of refreshing the notification panel which
-        // makes the panel get refreshed again. Just initialise to the
-        // undetermined state and then schedule a proper check at the next
-        // opportunity
-        this.setProgress(0, -1);
-        this._updateProgressTimeout = setTimeout(this.updateProgress.bind(this), 0);
-      ]]></constructor>
-
-      <destructor><![CDATA[
-        this.destroy();
-      ]]></destructor>
-
-      <field name="progressmeter" readonly="true">
-        document.getElementById("addon-progress-notification-progressmeter");
-      </field>
-      <field name="progresstext" readonly="true">
-        document.getElementById("addon-progress-notification-progresstext");
-      </field>
-      <property name="DownloadUtils" readonly="true">
-        <getter><![CDATA[
-          let module = {};
-          ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", module);
-          Object.defineProperty(this, "DownloadUtils", {
-            configurable: true,
-            enumerable: true,
-            writable: true,
-            value: module.DownloadUtils,
-          });
-          return module.DownloadUtils;
-        ]]></getter>
-      </property>
-
-      <method name="destroy">
-        <body><![CDATA[
-          if (!this.notification)
-            return;
-
-          this.notification.options.installs.forEach(function(aInstall) {
-            aInstall.removeListener(this);
-          }, this);
-          clearTimeout(this._updateProgressTimeout);
-        ]]></body>
-      </method>
-
-      <method name="setProgress">
-        <parameter name="aProgress"/>
-        <parameter name="aMaxProgress"/>
-        <body><![CDATA[
-          if (aMaxProgress == -1) {
-            this.progressmeter.removeAttribute("value");
-          } else {
-            this.progressmeter.setAttribute("value", (aProgress * 100) / aMaxProgress);
-          }
-
-          let now = Date.now();
-
-          if (!this.notification.lastUpdate) {
-            this.notification.lastUpdate = now;
-            this.notification.lastProgress = aProgress;
-            return;
-          }
-
-          let delta = now - this.notification.lastUpdate;
-          if ((delta < 400) && (aProgress < aMaxProgress))
-            return;
-
-          delta /= 1000;
-
-          // This algorithm is the same used by the downloads code.
-          let speed = (aProgress - this.notification.lastProgress) / delta;
-          if (this.notification.speed)
-            speed = speed * 0.9 + this.notification.speed * 0.1;
-
-          this.notification.lastUpdate = now;
-          this.notification.lastProgress = aProgress;
-          this.notification.speed = speed;
-
-          let status = null;
-          [status, this.notification.last] = this.DownloadUtils.getDownloadStatus(aProgress, aMaxProgress, speed, this.notification.last);
-          this.progresstext.setAttribute("value", status);
-          this.progresstext.setAttribute("tooltiptext", status);
-        ]]></body>
-      </method>
-
-      <method name="cancel">
-        <body><![CDATA[
-          let installs = this.notification.options.installs;
-          installs.forEach(function(aInstall) {
-            try {
-              aInstall.cancel();
-            } catch (e) {
-              // Cancel will throw if the download has already failed
-            }
-          }, this);
-
-          PopupNotifications.remove(this.notification);
-        ]]></body>
-      </method>
-
-      <method name="updateProgress">
-        <body><![CDATA[
-          if (!this.notification)
-            return;
-
-          let downloadingCount = 0;
-          let progress = 0;
-          let maxProgress = 0;
-
-          this.notification.options.installs.forEach(function(aInstall) {
-            if (aInstall.maxProgress == -1)
-              maxProgress = -1;
-            progress += aInstall.progress;
-            if (maxProgress >= 0)
-              maxProgress += aInstall.maxProgress;
-            if (aInstall.state < AddonManager.STATE_DOWNLOADED)
-              downloadingCount++;
-          });
-
-          if (downloadingCount == 0) {
-            this.destroy();
-            this.progressmeter.removeAttribute("value");
-            let status = gNavigatorBundle.getString("addonDownloadVerifying");
-            this.progresstext.setAttribute("value", status);
-            this.progresstext.setAttribute("tooltiptext", status);
-          } else {
-            this.setProgress(progress, maxProgress);
-          }
-        ]]></body>
-      </method>
-
-      <method name="onDownloadProgress">
-        <body><![CDATA[
-          this.updateProgress();
-        ]]></body>
-      </method>
-
-      <method name="onDownloadFailed">
-        <body><![CDATA[
-          this.updateProgress();
-        ]]></body>
-      </method>
-
-      <method name="onDownloadCancelled">
-        <body><![CDATA[
-          this.updateProgress();
-        ]]></body>
-      </method>
-
-      <method name="onDownloadEnded">
-        <body><![CDATA[
-          this.updateProgress();
-        ]]></body>
-      </method>
-    </implementation>
-  </binding>
 </bindings>
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -791,17 +791,17 @@ const PanelUI = {
       popupnotification.setAttribute("name", desc.name);
       popupnotification.setAttribute("endlabel", desc.end);
     }
     if (notification.options.popupIconURL) {
       popupnotification.setAttribute("icon", notification.options.popupIconURL);
     }
 
     popupnotification.notification = notification;
-    popupnotification.hidden = false;
+    popupnotification.show();
   },
 
   _showBadge(notification) {
     let badgeStatus = this._getBadgeStatus(notification);
     this.menuButton.setAttribute("badge-status", badgeStatus);
   },
 
   // "Banner item" here refers to an item in the hamburger panel menu. They will
@@ -894,20 +894,14 @@ XPCOMUtils.defineConstant(this, "PanelUI
 /**
  * Gets the currently selected locale for display.
  * @return  the selected locale
  */
 function getLocale() {
   return Services.locale.appLocaleAsLangTag;
 }
 
+/**
+ * Given a DOM node inside a <popupnotification>, return the parent <popupnotification>.
+ */
 function getNotificationFromElement(aElement) {
-  // Need to find the associated notification object, which is a bit tricky
-  // since it isn't associated with the element directly - this is kind of
-  // gross and very dependent on the structure of the popupnotification
-  // binding's content.
-  let notificationEl;
-  let parent = aElement;
-  while (parent && (parent = aElement.ownerDocument.getBindingParent(parent))) {
-    notificationEl = parent;
-  }
-  return notificationEl;
+  return aElement.closest("popupnotification");
 }
--- a/browser/components/customizableui/test/browser_panelUINotifications.js
+++ b/browser/components/customizableui/test/browser_panelUINotifications.js
@@ -8,32 +8,30 @@ const {AppMenuNotifications} = ChromeUti
  */
 add_task(async function testMainActionCalled() {
   let options = {
     gBrowser: window.gBrowser,
     url: "about:blank",
   };
 
   await BrowserTestUtils.withNewTab(options, function(browser) {
-    let doc = browser.ownerDocument;
-
     is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
     let mainActionCalled = false;
     let mainAction = {
       callback: () => { mainActionCalled = true; },
     };
     AppMenuNotifications.showNotification("update-manual", mainAction);
 
     isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
     let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
     is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
     let doorhanger = notifications[0];
     is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
 
-    let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
+    let button = doorhanger.button;
     button.click();
 
     ok(mainActionCalled, "Main action callback was called");
     is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
     is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
   });
 });
 
@@ -45,33 +43,31 @@ add_task(async function testMainActionCa
  */
 add_task(async function testSecondaryActionWorkflow() {
   let options = {
     gBrowser: window.gBrowser,
     url: "about:blank",
   };
 
   await BrowserTestUtils.withNewTab(options, async function(browser) {
-    let doc = browser.ownerDocument;
-
     is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
 
     let mainActionCalled = false;
     let mainAction = {
       callback: () => { mainActionCalled = true; },
     };
     AppMenuNotifications.showNotification("update-manual", mainAction);
 
     isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
     let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
     is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
     let doorhanger = notifications[0];
     is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
 
-    let secondaryActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
+    let secondaryActionButton = doorhanger.secondaryButton;
     secondaryActionButton.click();
 
     is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
 
     is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is displaying on PanelUI button.");
 
     await gCUITestUtils.openMainMenu();
     isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is hidden on PanelUI button.");
@@ -93,18 +89,16 @@ add_task(async function testSecondaryAct
 /**
  * We want to ensure a few things with this:
  * - Adding a doorhanger will make a badge disappear
  * - once the notification for the doorhanger is resolved (removed, not just dismissed),
  *   then we display any other badges that are remaining.
  */
 add_task(async function testInteractionWithBadges() {
   await BrowserTestUtils.withNewTab("about:blank", async function(browser) {
-    let doc = browser.ownerDocument;
-
     AppMenuNotifications.showBadgeOnlyNotification("fxa-needs-authentication");
     is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
     is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
 
     let mainActionCalled = false;
     let mainAction = {
       callback: () => { mainActionCalled = true; },
     };
@@ -112,17 +106,17 @@ add_task(async function testInteractionW
 
     isnot(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is hidden on PanelUI button.");
     isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
     let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
     is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
     let doorhanger = notifications[0];
     is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
 
-    let secondaryActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
+    let secondaryActionButton = doorhanger.secondaryButton;
     secondaryActionButton.click();
 
     is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
 
     is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is displaying on PanelUI button.");
 
     await gCUITestUtils.openMainMenu();
     isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is hidden on PanelUI button.");
@@ -139,34 +133,32 @@ add_task(async function testInteractionW
   });
 });
 
 /**
  * This tests that adding a badge will not dismiss any existing doorhangers.
  */
 add_task(async function testAddingBadgeWhileDoorhangerIsShowing() {
   await BrowserTestUtils.withNewTab("about:blank", function(browser) {
-    let doc = browser.ownerDocument;
-
     is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
     let mainActionCalled = false;
     let mainAction = {
       callback: () => { mainActionCalled = true; },
     };
     AppMenuNotifications.showNotification("update-manual", mainAction);
     AppMenuNotifications.showBadgeOnlyNotification("fxa-needs-authentication");
 
     isnot(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is hidden on PanelUI button.");
     isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
     let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
     is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
     let doorhanger = notifications[0];
     is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
 
-    let mainActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
+    let mainActionButton = doorhanger.button;
     mainActionButton.click();
 
     ok(mainActionCalled, "Main action callback was called");
     is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
     is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
     AppMenuNotifications.removeNotification(/.*/);
     is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
   });
@@ -209,18 +201,16 @@ add_task(async function testMultipleBadg
   });
 });
 
 /**
  * Tests that non-badges also operate like a stack.
  */
 add_task(async function testMultipleNonBadges() {
   await BrowserTestUtils.withNewTab("about:blank", async function(browser) {
-    let doc = browser.ownerDocument;
-
     is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
 
     let updateManualAction = {
         called: false,
         callback: () => { updateManualAction.called = true; },
     };
     let updateRestartAction = {
         called: false,
@@ -241,17 +231,17 @@ add_task(async function testMultipleNonB
     AppMenuNotifications.showNotification("update-restart", updateRestartAction);
 
     isnot(PanelUI.notificationPanel.state, "closed", "Doorhanger is showing.");
     notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
     is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
     doorhanger = notifications[0];
     is(doorhanger.id, "appMenu-update-restart-notification", "PanelUI is displaying the update-restart notification.");
 
-    let secondaryActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
+    let secondaryActionButton = doorhanger.secondaryButton;
     secondaryActionButton.click();
 
     is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
     is(PanelUI.menuButton.getAttribute("badge-status"), "update-restart", "update-restart badge is displaying on PanelUI button.");
 
     await gCUITestUtils.openMainMenu();
     isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-restart", "update-restart badge is hidden on PanelUI button.");
     let menuItem = PanelUI.mainView.querySelector(".panel-banner-item");
--- a/browser/components/customizableui/test/browser_panelUINotifications_fullscreen.js
+++ b/browser/components/customizableui/test/browser_panelUINotifications_fullscreen.js
@@ -1,15 +1,13 @@
 "use strict";
 
 const {AppMenuNotifications} = ChromeUtils.import("resource://gre/modules/AppMenuNotifications.jsm");
 
 add_task(async function testFullscreen() {
-  let doc = document;
-
   is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
   let mainActionCalled = false;
   let mainAction = {
     callback: () => { mainActionCalled = true; },
   };
   AppMenuNotifications.showNotification("update-manual", mainAction);
 
   isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
@@ -30,14 +28,13 @@ add_task(async function testFullscreen()
 
   let popupshownPromise = BrowserTestUtils.waitForEvent(PanelUI.notificationPanel, "popupshown");
   EventUtils.synthesizeKey("KEY_F11");
   await popupshownPromise;
   await new Promise(executeSoon);
   isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
   isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is not displaying on PanelUI button.");
 
-  let mainActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
-  mainActionButton.click();
+  doorhanger.button.click();
   ok(mainActionCalled, "Main action callback was called");
   is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
   is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
 });
--- a/browser/components/customizableui/test/browser_panelUINotifications_fullscreen_noAutoHideToolbar.js
+++ b/browser/components/customizableui/test/browser_panelUINotifications_fullscreen_noAutoHideToolbar.js
@@ -72,18 +72,17 @@ add_task(async function testFullscreen()
   await ContentTask.spawn(gBrowser.selectedBrowser, {}, async () => {
     content.document.exitFullscreen();
   });
   await popupshownPromise;
   await new Promise(executeSoon);
   isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is shown after exiting DOM fullscreen.");
   isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is not displaying on PanelUI button.");
 
-  let mainActionButton = document.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
-  mainActionButton.click();
+  doorhanger.button.click();
   ok(mainActionCalled, "Main action callback was called");
   is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
   is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
 
   fullscreenPromise = BrowserTestUtils.waitForEvent(window, "fullscreen");
   EventUtils.synthesizeKey("KEY_F11");
   await fullscreenPromise;
 });
--- a/browser/components/customizableui/test/browser_panelUINotifications_multiWindow.js
+++ b/browser/components/customizableui/test/browser_panelUINotifications_multiWindow.js
@@ -9,18 +9,16 @@ const {AppMenuNotifications} = ChromeUti
  */
 add_task(async function testDoesNotShowDoorhangerForBackgroundWindow() {
   let options = {
     gBrowser: window.gBrowser,
     url: "about:blank",
   };
 
   await BrowserTestUtils.withNewTab(options, async function(browser) {
-    let doc = browser.ownerDocument;
-
     let win = await BrowserTestUtils.openNewBrowserWindow();
     await SimpleTest.promiseFocus(win);
     let mainActionCalled = false;
     let mainAction = {
       callback: () => { mainActionCalled = true; },
     };
     AppMenuNotifications.showNotification("update-manual", mainAction);
     is(PanelUI.notificationPanel.state, "closed", "The background window's doorhanger is closed.");
@@ -29,17 +27,17 @@ add_task(async function testDoesNotShowD
     await BrowserTestUtils.closeWindow(win);
     await SimpleTest.promiseFocus(window);
     isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
     let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
     is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
     let doorhanger = notifications[0];
     is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
 
-    let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
+    let button = doorhanger.button;
     button.click();
 
     ok(mainActionCalled, "Main action callback was called");
     is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
     is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
   });
 });
 
@@ -53,22 +51,20 @@ add_task(async function testBackgroundWi
     gBrowser: window.gBrowser,
     url: "about:blank",
   };
 
   await BrowserTestUtils.withNewTab(options, async function(browser) {
     let win = await BrowserTestUtils.openNewBrowserWindow();
     await SimpleTest.promiseFocus(win);
     AppMenuNotifications.showNotification("update-manual", {callback() {}});
-    let doc = win.gBrowser.ownerDocument;
     let notifications = [...win.PanelUI.notificationPanel.children].filter(n => !n.hidden);
     is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
     let doorhanger = notifications[0];
-    let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
-    button.click();
+    doorhanger.button.click();
 
     await BrowserTestUtils.closeWindow(win);
     await SimpleTest.promiseFocus(window);
 
     is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
     is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
   });
 });
@@ -84,21 +80,20 @@ add_task(async function testBackgroundWi
     gBrowser: window.gBrowser,
     url: "about:blank",
   };
 
   await BrowserTestUtils.withNewTab(options, async function(browser) {
     let win = await BrowserTestUtils.openNewBrowserWindow();
     await SimpleTest.promiseFocus(win);
     AppMenuNotifications.showNotification("update-manual", {callback() {}});
-    let doc = win.gBrowser.ownerDocument;
     let notifications = [...win.PanelUI.notificationPanel.children].filter(n => !n.hidden);
     is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
     let doorhanger = notifications[0];
-    let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
+    let button = doorhanger.secondaryButton;
     button.click();
 
     await BrowserTestUtils.closeWindow(win);
     await SimpleTest.promiseFocus(window);
 
     is(PanelUI.notificationPanel.state, "closed", "The background window's doorhanger is closed.");
     is(PanelUI.menuButton.hasAttribute("badge-status"), true,
        "The dismissed notification should still have a badge status");
@@ -111,21 +106,20 @@ add_task(async function testBackgroundWi
  * Tests that when we open a new window while a notification is showing, the
  * notification also shows on the new window.
  */
 add_task(async function testOpenWindowAfterShowingNotification() {
   AppMenuNotifications.showNotification("update-manual", {callback() {}});
 
   let win = await BrowserTestUtils.openNewBrowserWindow();
   await SimpleTest.promiseFocus(win);
-  let doc = win.gBrowser.ownerDocument;
   let notifications = [...win.PanelUI.notificationPanel.children].filter(n => !n.hidden);
   is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
   let doorhanger = notifications[0];
-  let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
+  let button = doorhanger.secondaryButton;
   button.click();
 
   await BrowserTestUtils.closeWindow(win);
   await SimpleTest.promiseFocus(window);
 
   is(PanelUI.notificationPanel.state, "closed", "The background window's doorhanger is closed.");
   is(PanelUI.menuButton.hasAttribute("badge-status"), true,
      "The dismissed notification should still have a badge status");
--- a/browser/components/extensions/ExtensionControlledPopup.jsm
+++ b/browser/components/extensions/ExtensionControlledPopup.jsm
@@ -223,17 +223,17 @@ class ExtensionControlledPopup {
     }
 
     let addon = await AddonManager.getAddonByID(extensionId);
     this.populateDescription(doc, addon);
 
     // Setup the command handler.
     let handleCommand = async (event) => {
       panel.hidePopup();
-      if (event.originalTarget.getAttribute("anonid") == "button") {
+      if (event.originalTarget == popupnotification.button) {
         // Main action is to keep changes.
         await this.setConfirmation(extensionId);
       } else {
         // Secondary action is to restore settings.
         if (this.beforeDisableAddon) {
           await this.beforeDisableAddon(this, win);
         }
         await addon.disable();
@@ -266,17 +266,17 @@ class ExtensionControlledPopup {
       }
 
       // 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");
     panel.hidden = false;
-    popupnotification.hidden = false;
+    popupnotification.show();
     panel.openPopup(anchor);
   }
 
   getAddonDetails(doc, addon) {
     const defaultIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
 
     let image = doc.createXULElement("image");
     image.setAttribute("src", addon.iconURL || defaultIcon);
--- a/browser/components/extensions/test/browser/browser_ExtensionControlledPopup.js
+++ b/browser/components/extensions/test/browser/browser_ExtensionControlledPopup.js
@@ -99,26 +99,24 @@ add_task(async function testExtensionCon
     Services.obs.notifyObservers(null, observerTopic);
     return popupShown;
   }
 
   function closePopupWithAction(action, extensionId) {
     let done;
     if (action == "ignore") {
       panel.hidePopup();
-    } else {
-      if (action == "button") {
-        done = TestUtils.waitForCondition(() => {
-          return ExtensionSettingsStore.getSetting(confirmedType, id, id).value;
-        });
-      } else if (action == "secondarybutton") {
-        done = awaitEvent("shutdown", id);
-      }
-      doc.getAnonymousElementByAttribute(
-        popupnotification, "anonid", action).click();
+    } else if (action == "button") {
+      done = TestUtils.waitForCondition(() => {
+        return ExtensionSettingsStore.getSetting(confirmedType, id, id).value;
+      });
+      popupnotification.button.click();
+    } else if (action == "secondarybutton") {
+      done = awaitEvent("shutdown", id);
+      popupnotification.secondaryButton.click();
     }
     return done;
   }
 
   // No callbacks are initially called.
   ok(!onObserverAdded.called, "No observer has been added");
   ok(!onObserverRemoved.called, "No observer has been removed");
   ok(!beforeDisableAddon.called, "Settings have not been restored");
--- a/browser/components/extensions/test/browser/browser_ext_chrome_settings_overrides_home.js
+++ b/browser/components/extensions/test/browser/browser_ext_chrome_settings_overrides_home.js
@@ -321,30 +321,29 @@ add_task(async function test_doorhanger_
   await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
   await popupShown;
 
   ok(gURLBar.value.endsWith("ext2.html"), "ext2 is in control");
 
   // Click Restore Settings.
   let popupHidden = promisePopupHidden(panel);
   let prefPromise = promisePrefChangeObserved(HOMEPAGE_URL_PREF);
-  document.getAnonymousElementByAttribute(
-    popupnotification, "anonid", "secondarybutton").click();
+  popupnotification.secondaryButton.click();
   await prefPromise;
   await popupHidden;
 
   // Expect a new doorhanger for the next extension.
   await promisePopupShown(panel);
 
   ok(gURLBar.value.endsWith("ext1.html"), "ext1 is in control");
 
   // Click Restore Settings again.
   popupHidden = promisePopupHidden(panel);
   prefPromise = promisePrefChangeObserved(HOMEPAGE_URL_PREF);
-  document.getAnonymousElementByAttribute(popupnotification, "anonid", "secondarybutton").click();
+  popupnotification.secondaryButton.click();
   await popupHidden;
   await prefPromise;
 
   is(getHomePageURL(), defaultHomePage, "The homepage is set back to default");
 
   await ext1.unload();
   await ext2.unload();
 });
@@ -394,30 +393,30 @@ add_task(async function test_doorhanger_
   is(description.textContent,
      "An extension,  Ext2, changed what you see when you open your homepage and new windows.Learn more",
      "The extension name is in the popup");
 
   // Click Restore Settings.
   let popupHidden = promisePopupHidden(panel);
   let prefPromise = promisePrefChangeObserved(HOMEPAGE_URL_PREF);
   let popupnotification = doc.getElementById("extension-homepage-notification");
-  doc.getAnonymousElementByAttribute(popupnotification, "anonid", "secondarybutton").click();
+  popupnotification.secondaryButton.click();
   await prefPromise;
   await popupHidden;
 
   // Expect a new doorhanger for the next extension.
   await promisePopupShown(panel);
 
   ok(win.gURLBar.value.endsWith("ext1.html"), "ext1 is in control");
   is(description.textContent,
      "An extension,  Ext1, changed what you see when you open your homepage and new windows.Learn more",
      "The extension name is in the popup");
 
   // Click Keep Changes.
-  doc.getAnonymousElementByAttribute(popupnotification, "anonid", "button").click();
+  popupnotification.button.click();
   await TestUtils.waitForCondition(() => isConfirmed(ext1Id));
 
   ok(getHomePageURL().endsWith("ext1.html"), "The homepage is still the set");
 
   await BrowserTestUtils.closeWindow(win);
   await ext1.unload();
   await ext2.unload();
 
--- a/browser/components/extensions/test/browser/browser_ext_tabs_hide.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_hide.js
@@ -60,18 +60,17 @@ add_task(function test_doorhanger_keep()
 
     is(gBrowser.visibleTabs.length, 2, "There are 2 visible tabs now");
     is(panel.anchorNode.closest("toolbarbutton").id,
        "alltabs-button", "The doorhanger is anchored to the all tabs button");
 
     // Click the Keep Tabs Hidden button.
     let popupnotification = document.getElementById("extension-tab-hide-notification");
     let popupHidden = promisePopupHidden(panel);
-    document.getAnonymousElementByAttribute(
-      popupnotification, "anonid", "button").click();
+    popupnotification.button.click();
     await popupHidden;
 
     // Hide another tab and ensure the popup didn't open.
     extension.sendMessage("hide", {url: "*://*/?two"});
     await extension.awaitMessage("done");
     is(panel.state, "closed", "The popup is still closed");
     is(gBrowser.visibleTabs.length, 1, "There's one visible tab now");
 
@@ -92,31 +91,30 @@ add_task(function test_doorhanger_disabl
     await popupShown;
 
     is(gBrowser.visibleTabs.length, 2, "There are 2 visible tabs now");
     is(panel.anchorNode.closest("toolbarbutton").id,
        "alltabs-button", "The doorhanger is anchored to the all tabs button");
 
     // verify the contents of the description.
     let popupnotification = document.getElementById("extension-tab-hide-notification");
-    let description = popupnotification.querySelector("description");
+    let description = popupnotification.querySelector("#extension-tab-hide-notification-description");
     let addon = await AddonManager.getAddonByID(extension.id);
     ok(description.textContent.includes(addon.name),
        "The extension name is in the description");
     let images = Array.from(description.querySelectorAll("image"));
     is(images.length, 2, "There are two images");
     ok(images.some(img => img.src.includes("addon-icon.png")),
        "There's an icon for the extension");
     ok(images.some(img => getComputedStyle(img).backgroundImage.includes("arrow-dropdown-16.svg")),
        "There's an icon for the all tabs menu");
 
     // Click the Disable Extension button.
     let popupHidden = promisePopupHidden(panel);
-    document.getAnonymousElementByAttribute(
-      popupnotification, "anonid", "secondarybutton").click();
+    popupnotification.secondaryButton.click();
     await popupHidden;
     await new Promise(executeSoon);
 
     is(gBrowser.visibleTabs.length, 3, "There are 3 visible tabs again");
     is(addon.userDisabled, true, "The extension is now disabled");
   });
 });
 
--- a/browser/components/extensions/test/browser/browser_ext_url_overrides_newtab.js
+++ b/browser/components/extensions/test/browser/browser_ext_url_overrides_newtab.js
@@ -12,25 +12,21 @@ function getNotificationSetting(extensio
   return ExtensionSettingsStore.getSetting("newTabNotification", extensionId);
 }
 
 function getNewTabDoorhanger() {
   return document.getElementById("extension-new-tab-notification");
 }
 
 function clickKeepChanges(notification) {
-  let button = document.getAnonymousElementByAttribute(
-    notification, "anonid", "button");
-  button.click();
+  notification.button.click();
 }
 
 function clickRestoreSettings(notification) {
-  let button = document.getAnonymousElementByAttribute(
-    notification, "anonid", "secondarybutton");
-  button.click();
+  notification.secondaryButton.click();
 }
 
 function waitForNewTab() {
   let eventName = "browser-open-newtab-start";
   return new Promise(resolve => {
     function observer() {
       Services.obs.removeObserver(observer, eventName);
       resolve();
@@ -212,17 +208,17 @@ add_task(async function test_new_tab_kee
 
   // Ensure the panel is open and the setting isn't saved yet.
   is(panel.getAttribute("panelopen"), "true",
      "The notification panel is open after opening New Tab");
   is(getNotificationSetting(extensionId), null,
      "The New Tab notification is not set for this extension");
   is(panel.anchorNode.closest("toolbarbutton").id, "PanelUI-menu-button",
      "The doorhanger is anchored to the menu icon");
-  is(panel.querySelector("description").textContent,
+  is(panel.querySelector("#extension-new-tab-notification-description").textContent,
      "An extension,  New Tab Add-on, changed the page you see when you open a new tab.Learn more",
      "The description includes the add-on name");
 
   // Click the Keep Changes button.
   let confirmationSaved = TestUtils.waitForCondition(() => {
     return ExtensionSettingsStore.getSetting(
       "newTabNotification", extensionId, extensionId).value;
   });
--- a/browser/extensions/formautofill/FormAutofillDoorhanger.jsm
+++ b/browser/extensions/formautofill/FormAutofillDoorhanger.jsm
@@ -383,17 +383,17 @@ let FormAutofillDoorhanger = {
         const notificationElementId = notificationId + "-notification";
         const notification = chromeDoc.getElementById(notificationElementId);
         const notificationContent = notification.querySelector("popupnotificationcontent") ||
                                     chromeDoc.createXULElement("popupnotificationcontent");
         if (!notification.contains(notificationContent)) {
           notificationContent.setAttribute("orient", "vertical");
           this._appendDescription(notificationContent, descriptionLabel, descriptionIcon);
           this._appendPrivacyPanelLink(notificationContent, linkMessage, spotlightURL);
-          notification.append(notificationContent);
+          notification.appendNotificationContent(notificationContent);
         }
         this._updateDescription(notificationContent, description);
       };
       this._setAnchor(browser, anchor);
       chromeWin.PopupNotifications.show(
         browser,
         notificationId,
         message,
--- a/browser/extensions/formautofill/content/formautofill.css
+++ b/browser/extensions/formautofill/content/formautofill.css
@@ -41,22 +41,22 @@
   min-width: 150px !important;
 }
 
 #PopupAutoComplete[firstresultstyle="autofill-insecureWarning"] {
   min-width: 200px !important;
 }
 
 /* Form Autofill Doorhanger */
-#autofill-address-notification > popupnotificationcontent > .desc-message-box,
-#autofill-credit-card-notification > popupnotificationcontent > .desc-message-box {
+#autofill-address-notification popupnotificationcontent > .desc-message-box,
+#autofill-credit-card-notification popupnotificationcontent > .desc-message-box {
   margin-block-end: 12px;
 }
-#autofill-credit-card-notification > popupnotificationcontent > .desc-message-box > image {
+#autofill-credit-card-notification popupnotificationcontent > .desc-message-box > image {
   margin-inline-start: 6px;
   width: 16px;
   height: 16px;
   list-style-image: url(chrome://formautofill/content/icon-credit-card-generic.svg);
 }
-#autofill-address-notification > popupnotificationcontent > .desc-message-box > description,
-#autofill-credit-card-notification > popupnotificationcontent > .desc-message-box > description {
+#autofill-address-notification popupnotificationcontent > .desc-message-box > description,
+#autofill-credit-card-notification popupnotificationcontent > .desc-message-box > description {
   font-style: italic;
 }
--- a/browser/themes/shared/addons/extension-controlled.inc.css
+++ b/browser/themes/shared/addons/extension-controlled.inc.css
@@ -10,17 +10,17 @@
   font-size: 1.3em;
   font-weight: lighter;
 }
 
 .extension-controlled-notification {
   margin-bottom: 0;
 }
 
-.extension-controlled-notification > popupnotificationcontent > description > .extension-controlled-icon {
+.extension-controlled-notification popupnotificationcontent > description > .extension-controlled-icon {
   height: 16px;
   width: 16px;
   vertical-align: bottom;
 }
 
 .extension-controlled-icon.alltabs-icon {
   background: url("chrome://global/skin/icons/arrow-dropdown-16.svg");
   /* This icon has a lot of extra space to the sides, reduce that a little. */
--- a/dom/notification/test/browser/browser_permission_dismiss.js
+++ b/dom/notification/test/browser/browser_permission_dismiss.js
@@ -21,20 +21,17 @@ function clickDoorhangerButton(aButtonIn
   ok(true, notifications.length + " notification(s)");
   let notification = notifications[0];
 
   if (aButtonIndex == PROMPT_ALLOW_BUTTON) {
     ok(true, "Triggering main action (allow the permission)");
     notification.button.doCommand();
   } else if (aButtonIndex == PROMPT_NEVER_BUTTON) {
     ok(true, "Triggering secondary action (deny the permission permanently)");
-    // The menuitems in the dropdown are accessible as direct children of the panel,
-    // because they are injected into a <children> node in the XBL binding.
-    // The "never" button is the first menuitem in the dropdown.
-    notification.querySelector("menuitem").doCommand();
+    notification.menupopup.querySelector("menuitem").doCommand();
   } else {
     ok(true, "Triggering secondary action (deny the permission temporarily)");
     notification.secondaryButton.doCommand();
   }
 }
 
 /**
  * Opens a tab which calls `Notification.requestPermission()` with a callback
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_click.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_click.py
@@ -365,20 +365,18 @@ class TestClickNavigation(MarionetteTest
         super(TestClickNavigation, self).setUp()
 
         self.test_page = self.marionette.absolute_url("clicks.html")
         self.marionette.navigate(self.test_page)
 
     def close_notification(self):
         try:
             with self.marionette.using_context("chrome"):
-                popup = self.marionette.find_element(
-                    By.CSS_SELECTOR, "#notification-popup popupnotification")
-                popup.find_element(By.ANON_ATTRIBUTE,
-                                   {"anonid": "closebutton"}).click()
+                self.marionette.find_element(By.CSS_SELECTOR,
+                    "#notification-popup popupnotification .popup-notification-closebutton").click()
         except errors.NoSuchElementException:
             pass
 
     def test_click_link_page_load(self):
         self.marionette.find_element(By.LINK_TEXT, "333333").click()
         self.assertNotEqual(self.marionette.get_url(), self.test_page)
         self.assertEqual(self.marionette.title, "Marionette Test")
 
--- a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/notifications.py
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/notifications.py
@@ -17,18 +17,17 @@ class BaseNotification(UIBaseLib):
     __metaclass__ = ABCMeta
 
     @property
     def close_button(self):
         """Provide access to the close button.
 
         :returns: The close button.
         """
-        return self.element.find_element(By.ANON_ATTRIBUTE,
-                                         {'anonid': 'closebutton'})
+        return self.element.find_element(By.CSS_SELECTOR, ".popup-notification-closebutton")
 
     @property
     def label(self):
         """Provide access to the notification label.
 
         :returns: The notification label.
         """
         return self.element.get_attribute('label')
--- a/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js
+++ b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js
@@ -45,17 +45,17 @@ add_task(async function test_clickNever(
     is(fieldValues.username, "notifyu1", "Checking submitted username");
     is(fieldValues.password, "notifyp1", "Checking submitted password");
     let notif = getCaptureDoorhanger("password-save");
     ok(notif, "got notification popup");
     is(true, Services.logins.getLoginSavingEnabled("http://example.com"),
        "Checking for login saving enabled");
 
     await checkDoorhangerUsernamePassword("notifyu1", "notifyp1");
-    clickDoorhangerButton(notif, NEVER_BUTTON);
+    clickDoorhangerButton(notif, NEVER_MENUITEM);
   });
 
   is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
 
   info("Make sure Never took effect");
   await testSubmittingLoginForm("subtst_notifications_1.html", function(fieldValues) {
     is(fieldValues.username, "notifyu1", "Checking submitted username");
     is(fieldValues.password, "notifyp1", "Checking submitted password");
--- a/toolkit/components/passwordmgr/test/browser/head.js
+++ b/toolkit/components/passwordmgr/test/browser/head.js
@@ -68,21 +68,21 @@ function checkOnlyLoginWasUsedTwice({ ju
     is(logins[0].timeLastUsed, logins[0].timePasswordChanged, "timeLastUsed == timePasswordChanged");
   } else {
     is(logins[0].timeCreated, logins[0].timePasswordChanged, "timeChanged not updated");
   }
 }
 
 // Begin popup notification (doorhanger) functions //
 
-const REMEMBER_BUTTON = 0;
-const NEVER_BUTTON = 2;
+const REMEMBER_BUTTON = "button";
+const NEVER_MENUITEM = 0;
 
-const CHANGE_BUTTON = 0;
-const DONT_CHANGE_BUTTON = 1;
+const CHANGE_BUTTON = "button";
+const DONT_CHANGE_BUTTON = "secondaryButton";
 
 /**
  * Checks if we have a password capture popup notification
  * of the right type and with the right label.
  *
  * @param {String} aKind The desired `passwordNotificationType`
  * @param {Object} [popupNotifications = PopupNotifications]
  * @param {Object} [browser = null] Optional browser whose notifications should be searched.
@@ -112,25 +112,25 @@ function getCaptureDoorhanger(aKind, pop
 function clickDoorhangerButton(aPopup, aButtonIndex) {
   ok(true, "Looking for action at index " + aButtonIndex);
 
   let notifications = aPopup.owner.panel.children;
   ok(notifications.length > 0, "at least one notification displayed");
   ok(true, notifications.length + " notification(s)");
   let notification = notifications[0];
 
-  if (aButtonIndex == 0) {
+  if (aButtonIndex == "button") {
     ok(true, "Triggering main action");
     notification.button.doCommand();
-  } else if (aButtonIndex == 1) {
+  } else if (aButtonIndex == "secondaryButton") {
     ok(true, "Triggering secondary action");
     notification.secondaryButton.doCommand();
-  } else if (aButtonIndex <= aPopup.secondaryActions.length) {
-    ok(true, "Triggering secondary action " + aButtonIndex);
-    notification.children[aButtonIndex - 1].doCommand();
+  } else {
+    ok(true, "Triggering menuitem # " + aButtonIndex);
+    notification.menupopup.querySelectorAll("menuitem")[aButtonIndex].doCommand();
   }
 }
 
 /**
  * Checks the doorhanger's username and password.
  *
  * @param {String} username The username.
  * @param {String} password The password.
--- a/toolkit/content/customElements.js
+++ b/toolkit/content/customElements.js
@@ -379,16 +379,17 @@ customElements.setElementCreationCallbac
 
 // For now, don't load any elements in the extension dummy document.
 // We will want to load <browser> when that's migrated (bug 1441935).
 const isDummyDocument = document.documentURI == "chrome://extensions/content/dummy.xul";
 if (!isDummyDocument) {
   for (let script of [
     "chrome://global/content/elements/general.js",
     "chrome://global/content/elements/notificationbox.js",
+    "chrome://global/content/elements/popupnotification.js",
     "chrome://global/content/elements/radio.js",
     "chrome://global/content/elements/richlistbox.js",
     "chrome://global/content/elements/autocomplete-richlistitem.js",
     "chrome://global/content/elements/textbox.js",
     "chrome://global/content/elements/tabbox.js",
     "chrome://global/content/elements/tree.js",
   ]) {
     Services.scriptloader.loadSubScript(script, window);
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -66,17 +66,16 @@ toolkit.jar:
    content/global/bindings/calendar.js         (widgets/calendar.js)
    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.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/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)
 *  content/global/bindings/textbox.xml         (widgets/textbox.xml)
@@ -93,16 +92,17 @@ toolkit.jar:
    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/popupnotification.js  (widgets/popupnotification.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
rename from toolkit/content/widgets/notification.xml
rename to toolkit/content/widgets/popupnotification.js
--- a/toolkit/content/widgets/notification.xml
+++ b/toolkit/content/widgets/popupnotification.js
@@ -1,103 +1,153 @@
-<?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";
 
-
-<!DOCTYPE bindings [
-<!ENTITY % notificationDTD SYSTEM "chrome://global/locale/notification.dtd">
-%notificationDTD;
-]>
+// This is loaded into all XUL windows. Wrap in a block to prevent
+// leaking to window scope.
+{
+class MozPopupNotification extends MozXULElement {
+  static get observedAttributes() {
+    return [
+      "buttonaccesskey",
+      "buttoncommand",
+      "buttonhighlight",
+      "buttonlabel",
+      "closebuttoncommand",
+      "closebuttonhidden",
+      "dropmarkerhidden",
+      "dropmarkerpopupshown",
+      "endlabel",
+      "icon",
+      "iconclass",
+      "label",
+      "learnmoreclick",
+      "learnmoreurl",
+      "mainactiondisabled",
+      "menucommand",
+      "name",
+      "origin",
+      "origin",
+      "popupid",
+      "secondarybuttonaccesskey",
+      "secondarybuttoncommand",
+      "secondarybuttonhidden",
+      "secondarybuttonlabel",
+      "secondendlabel",
+      "secondname",
+      "warninghidden",
+      "warninglabel",
+    ];
+  }
 
-<bindings id="notificationBindings"
-          xmlns="http://www.mozilla.org/xbl"
-          xmlns:xbl="http://www.mozilla.org/xbl"
-          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-          xmlns:html = "http://www.w3.org/1999/xhtml">
+  _updateAttributes() {
+    for (let [ el, attrs ] of this._inheritedAttributeMap.entries()) {
+      for (let attr of attrs) {
+        this.inheritAttribute(el, attr);
+      }
+    }
+  }
+
+  get _inheritedAttributeMap() {
+    if (!this.__inheritedAttributeMap) {
+      this.__inheritedAttributeMap = new Map();
+      for (let el of this.querySelectorAll("[inherits]")) {
+        this.__inheritedAttributeMap.set(el, el.getAttribute("inherits").split(","));
+      }
+    }
+    return this.__inheritedAttributeMap;
+  }
+
+  attributeChangedCallback(name, oldValue, newValue) {
+    if (!this._hasSlotted || oldValue === newValue) {
+      return;
+    }
+
+    this._updateAttributes();
+  }
 
-  <binding id="popup-notification">
-    <content orient="vertical">
-      <xul:hbox class="popup-notification-header-container">
-        <children includes="popupnotificationheader"/>
-      </xul:hbox>
-      <xul:hbox align="start" class="popup-notification-body-container">
-        <xul:image class="popup-notification-icon"
-                   xbl:inherits="popupid,src=icon,class=iconclass"/>
-        <xul:vbox flex="1" pack="start"
-                  class="popup-notification-body" xbl:inherits="popupid">
-          <xul:hbox align="start">
-            <xul:vbox flex="1">
-              <xul:label class="popup-notification-origin header"
-                         xbl:inherits="value=origin,tooltiptext=origin"
-                         crop="center"/>
+  show() {
+    this.slotContents();
+
+    if (this.checkboxState) {
+      this.checkbox.checked = this.checkboxState.checked;
+      this.checkbox.setAttribute("label", this.checkboxState.label);
+      this.checkbox.hidden = false;
+    } else {
+      this.checkbox.hidden = true;
+    }
+
+    this.hidden = false;
+  }
+
+  slotContents() {
+    if (this._hasSlotted) {
+      return;
+    }
+    this._hasSlotted = true;
+    this.appendChild(MozXULElement.parseXULToFragment(`
+      <hbox class="popup-notification-header-container"></hbox>
+      <hbox align="start" class="popup-notification-body-container">
+        <image class="popup-notification-icon"
+               inherits="popupid,src=icon,class=iconclass"/>
+        <vbox flex="1" pack="start" class="popup-notification-body">
+          <hbox align="start">
+            <vbox flex="1">
+              <label class="popup-notification-origin header" inherits="value=origin,tooltiptext=origin" crop="center"></label>
               <!-- These need to be on the same line to avoid creating
-                   whitespace between them (whitespace is added in the
-                   localization file, if necessary). -->
-              <xul:description class="popup-notification-description" xbl:inherits="popupid"><html:span
-                xbl:inherits="xbl:text=label,popupid"/><html:b xbl:inherits="xbl:text=name,popupid"/><html:span
-              xbl:inherits="xbl:text=endlabel,popupid"/><html:b xbl:inherits="xbl:text=secondname,popupid"/><html:span
-              xbl:inherits="xbl:text=secondendlabel,popupid"/></xul:description>
-            </xul:vbox>
-            <xul:toolbarbutton anonid="closebutton"
-                               class="messageCloseButton close-icon popup-notification-closebutton tabbable"
-                               xbl:inherits="oncommand=closebuttoncommand,hidden=closebuttonhidden"
-                               tooltiptext="&closeNotification.tooltip;"/>
-          </xul:hbox>
-          <children includes="popupnotificationcontent"/>
-          <xul:label class="text-link popup-notification-learnmore-link"
-                     xbl:inherits="onclick=learnmoreclick,href=learnmoreurl">&learnMore;</xul:label>
-          <xul:checkbox anonid="checkbox"
-                        xbl:inherits="hidden=checkboxhidden,checked=checkboxchecked,label=checkboxlabel,oncommand=checkboxcommand" />
-          <xul:description class="popup-notification-warning" xbl:inherits="hidden=warninghidden,xbl:text=warninglabel"/>
-        </xul:vbox>
-      </xul:hbox>
-      <xul:hbox class="popup-notification-footer-container">
-        <children includes="popupnotificationfooter"/>
-      </xul:hbox>
-      <xul:hbox class="popup-notification-button-container panel-footer">
-        <children includes="button"/>
-        <xul:button anonid="secondarybutton"
-                    class="popup-notification-button popup-notification-secondary-button"
-                    xbl:inherits="oncommand=secondarybuttoncommand,label=secondarybuttonlabel,accesskey=secondarybuttonaccesskey,hidden=secondarybuttonhidden"/>
-        <xul:toolbarseparator xbl:inherits="hidden=dropmarkerhidden"/>
-        <xul:button anonid="menubutton"
-                    type="menu"
-                    class="popup-notification-button popup-notification-dropmarker"
-                    aria-label="&moreActionsButton.accessibleLabel;"
-                    xbl:inherits="onpopupshown=dropmarkerpopupshown,hidden=dropmarkerhidden">
-          <xul:menupopup anonid="menupopup"
-                         position="after_end"
-                         aria-label="&moreActionsButton.accessibleLabel;"
-                         xbl:inherits="oncommand=menucommand">
-            <children/>
-          </xul:menupopup>
-        </xul:button>
-        <xul:button anonid="button"
-                    class="popup-notification-button popup-notification-primary-button"
-                    label="&defaultButton.label;"
-                    accesskey="&defaultButton.accesskey;"
-                    xbl:inherits="oncommand=buttoncommand,label=buttonlabel,accesskey=buttonaccesskey,default=buttonhighlight,disabled=mainactiondisabled"/>
-      </xul:hbox>
-    </content>
-    <implementation>
-      <field name="checkbox" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "checkbox");
-      </field>
-      <field name="closebutton" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "closebutton");
-      </field>
-      <field name="button" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "button");
-      </field>
-      <field name="secondaryButton" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "secondarybutton");
-      </field>
-      <field name="menubutton" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "menubutton");
-      </field>
-      <field name="menupopup" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "menupopup");
-      </field>
-    </implementation>
-  </binding>
-</bindings>
+                  whitespace between them (whitespace is added in the
+                  localization file, if necessary). -->
+              <description class="popup-notification-description" inherits="popupid"><html:span inherits="text=label,popupid"></html:span><html:b inherits="text=name,popupid"></html:b><html:span inherits="text=endlabel,popupid"></html:span><html:b inherits="text=secondname,popupid"></html:b><html:span inherits="text=secondendlabel,popupid"></html:span></description>
+            </vbox>
+            <toolbarbutton class="messageCloseButton close-icon popup-notification-closebutton tabbable" inherits="oncommand=closebuttoncommand,hidden=closebuttonhidden" tooltiptext="&closeNotification.tooltip;"></toolbarbutton>
+          </hbox>
+          <label class="text-link popup-notification-learnmore-link" inherits="onclick=learnmoreclick,href=learnmoreurl">&learnMore;</label>
+          <checkbox class="popup-notification-checkbox" oncommand="PopupNotifications._onCheckboxCommand(event)"></checkbox>
+          <description class="popup-notification-warning" inherits="hidden=warninghidden,text=warninglabel"></description>
+        </vbox>
+      </hbox>
+      <hbox class="popup-notification-footer-container"></hbox>
+      <hbox class="popup-notification-button-container panel-footer">
+        <button class="popup-notification-button popup-notification-secondary-button" inherits="oncommand=secondarybuttoncommand,label=secondarybuttonlabel,accesskey=secondarybuttonaccesskey,hidden=secondarybuttonhidden"></button>
+        <toolbarseparator inherits="hidden=dropmarkerhidden"></toolbarseparator>
+        <button type="menu" class="popup-notification-button popup-notification-dropmarker" aria-label="&moreActionsButton.accessibleLabel;" inherits="onpopupshown=dropmarkerpopupshown,hidden=dropmarkerhidden">
+          <menupopup position="after_end" aria-label="&moreActionsButton.accessibleLabel;" inherits="oncommand=menucommand">
+          </menupopup>
+        </button>
+        <button class="popup-notification-button popup-notification-primary-button" label="&defaultButton.label;" accesskey="&defaultButton.accesskey;" inherits="oncommand=buttoncommand,label=buttonlabel,accesskey=buttonaccesskey,default=buttonhighlight,disabled=mainactiondisabled"></button>
+      </hbox>
+    `, ["chrome://global/locale/notification.dtd"]));
+
+    this.button = this.querySelector(".popup-notification-primary-button");
+    this.secondaryButton =  this.querySelector(".popup-notification-secondary-button");
+    this.checkbox = this.querySelector(".popup-notification-checkbox");
+    this.closebutton = this.querySelector(".popup-notification-closebutton");
+    this.menubutton = this.querySelector(".popup-notification-dropmarker");
+    this.menupopup = this.menubutton.querySelector("menupopup");
+
+    let popupnotificationfooter = this.querySelector("popupnotificationfooter");
+    if (popupnotificationfooter) {
+      this.querySelector(".popup-notification-footer-container").append(popupnotificationfooter);
+    }
+
+    let popupnotificationheader = this.querySelector("popupnotificationheader");
+    if (popupnotificationheader) {
+      this.querySelector(".popup-notification-header-container").append(popupnotificationheader);
+    }
+
+    for (let popupnotificationcontent of this.querySelectorAll("popupnotificationcontent")) {
+      this.appendNotificationContent(popupnotificationcontent);
+    }
+
+    this._updateAttributes();
+  }
+
+  appendNotificationContent(el) {
+    let nextSibling = this.querySelector(".popup-notification-body > .popup-notification-learnmore-link");
+    nextSibling.before(el);
+  }
+}
+
+customElements.define("popupnotification", MozPopupNotification);
+}
--- a/toolkit/content/xul.css
+++ b/toolkit/content/xul.css
@@ -169,17 +169,17 @@ iframe {
   editor,
   iframe {
     display: block;
   }
 }
 
 /*********** popup notification ************/
 popupnotification {
-  -moz-binding: url("chrome://global/content/bindings/notification.xml#popup-notification");
+  -moz-box-orient: vertical;
 }
 
 .popup-notification-menubutton:not([label]) {
   display: none;
 }
 
 /********** checkbox **********/
 
--- a/toolkit/modules/PopupNotifications.jsm
+++ b/toolkit/modules/PopupNotifications.jsm
@@ -47,26 +47,21 @@ function getAnchorFromBrowser(aBrowser, 
     if (ChromeUtils.getClassName(anchor) == "XULElement") {
       return anchor;
     }
     return aBrowser.ownerDocument.getElementById(anchor);
   }
   return null;
 }
 
+/**
+ * Given a DOM node inside a <popupnotification>, return the parent <popupnotification>.
+ */
 function getNotificationFromElement(aElement) {
-  // Need to find the associated notification object, which is a bit tricky
-  // since it isn't associated with the element directly - this is kind of
-  // gross and very dependent on the structure of the popupnotification
-  // binding's content.
-  let notificationEl;
-  let parent = aElement;
-  while (parent && (parent = aElement.ownerDocument.getBindingParent(parent)))
-    notificationEl = parent;
-  return notificationEl;
+  return aElement.closest("popupnotification");
 }
 
 /**
  * Notification object describes a single popup notification.
  *
  * @see PopupNotifications.show()
  */
 function Notification(id, message, anchorID, mainAction, secondaryActions,
@@ -246,17 +241,16 @@ function PopupNotifications(tabbrowser, 
     let doc = this.window.document;
     let focusedElement = Services.focus.focusedElement;
 
     // If the chrome window has a focused element, let it handle the ESC key instead.
     if (!focusedElement ||
         focusedElement == doc.body ||
         focusedElement == this.tabbrowser.selectedBrowser ||
         // Ignore focused elements inside the notification.
-        getNotificationFromElement(focusedElement) == notification ||
         notification.contains(focusedElement)) {
       let escAction = notification.notification.options.escAction;
       this._onButtonEvent(aEvent, escAction, "esc-press", notification);
     }
   };
 
   let documentElement = this.window.document.documentElement;
   let locationBarHidden = documentElement.getAttribute("chromehidden").includes("location");
@@ -752,27 +746,16 @@ PopupNotifications.prototype = {
       this.panel.removeChild(popupnotification);
 
       // If this notification was provided by the chrome document rather than
       // created ad hoc, move it back to where we got it from.
       let originalParent = gNotificationParents.get(popupnotification);
       if (originalParent) {
         popupnotification.notification = null;
 
-        // Remove nodes dynamically added to the notification's menu button
-        // in _refreshPanel.
-        let contentNode = popupnotification.lastElementChild;
-        while (contentNode) {
-          let previousSibling = contentNode.previousElementSibling;
-          if (contentNode.nodeName == "menuitem" ||
-              contentNode.nodeName == "menuseparator")
-            popupnotification.removeChild(contentNode);
-          contentNode = previousSibling;
-        }
-
         // Re-hide the notification such that it isn't rendered in the chrome
         // document. _refreshPanel will unhide it again when needed.
         popupnotification.hidden = true;
 
         originalParent.appendChild(popupnotification);
       }
     }
   },
@@ -898,16 +881,17 @@ PopupNotifications.prototype = {
         }
       } else
         popupnotification.removeAttribute("origin");
 
       if (n.options.hideClose)
         popupnotification.setAttribute("closebuttonhidden", "true");
 
       popupnotification.notification = n;
+      let menuitems = [];
 
       if (n.mainAction && n.secondaryActions && n.secondaryActions.length > 0) {
         let telemetryStatId = TELEMETRY_STAT_ACTION_2;
 
         let secondaryAction = n.secondaryActions[0];
         popupnotification.setAttribute("secondarybuttonlabel", secondaryAction.label);
         popupnotification.setAttribute("secondarybuttonaccesskey", secondaryAction.accessKey);
         popupnotification.setAttribute("secondarybuttoncommand", "PopupNotifications._onButtonEvent(event, 'secondarybuttoncommand');");
@@ -915,17 +899,17 @@ PopupNotifications.prototype = {
         for (let i = 1; i < n.secondaryActions.length; i++) {
           let action = n.secondaryActions[i];
           let item = doc.createXULElement("menuitem");
           item.setAttribute("label", action.label);
           item.setAttribute("accesskey", action.accessKey);
           item.notification = n;
           item.action = action;
 
-          popupnotification.appendChild(item);
+          menuitems.push(item);
 
           // We can only record a limited number of actions in telemetry. If
           // there are more, the latest are all recorded in the last bucket.
           item.action.telemetryStatId = telemetryStatId;
           if (telemetryStatId < TELEMETRY_STAT_ACTION_LAST) {
             telemetryStatId++;
           }
         }
@@ -936,38 +920,39 @@ PopupNotifications.prototype = {
       } else {
         popupnotification.setAttribute("secondarybuttonhidden", "true");
         popupnotification.setAttribute("dropmarkerhidden", "true");
       }
 
       let checkbox = n.options.checkbox;
       if (checkbox && checkbox.label) {
         let checked = n._checkboxChecked != null ? n._checkboxChecked : !!checkbox.checked;
-
-        popupnotification.setAttribute("checkboxhidden", "false");
-        popupnotification.setAttribute("checkboxchecked", checked);
-        popupnotification.setAttribute("checkboxlabel", checkbox.label);
-
-        popupnotification.setAttribute("checkboxcommand", "PopupNotifications._onCheckboxCommand(event);");
+        popupnotification.checkboxState = {
+          checked,
+          label: checkbox.label,
+        };
 
         if (checked) {
           this._setNotificationUIState(popupnotification, checkbox.checkedState);
         } else {
           this._setNotificationUIState(popupnotification, checkbox.uncheckedState);
         }
       } else {
-        popupnotification.setAttribute("checkboxhidden", "true");
+        popupnotification.checkboxState = null;
         popupnotification.setAttribute("warninghidden", "true");
       }
 
       this.panel.appendChild(popupnotification);
 
       // The popupnotification may be hidden if we got it from the chrome
       // document rather than creating it ad hoc.
-      popupnotification.hidden = false;
+      popupnotification.show();
+
+      popupnotification.menupopup.textContent = "";
+      popupnotification.menupopup.append(...menuitems);
     }, this);
   },
 
   _setNotificationUIState(notification, state = {}) {
     if (state.disableMainAction ||
         notification.hasAttribute("invalidselection")) {
       notification.setAttribute("mainactiondisabled", "true");
     } else {
@@ -1050,19 +1035,16 @@ PopupNotifications.prototype = {
       this._currentAnchorElement = anchorElement;
 
       if (notificationsToShow.some(n => n.options.persistent)) {
         this.panel.setAttribute("noautohide", "true");
       } else {
         this.panel.removeAttribute("noautohide");
       }
 
-      // On OS X and Linux we need a different panel arrow color for
-      // click-to-play plugins, so copy the popupid and use css.
-      this.panel.setAttribute("popupid", this.panel.firstElementChild.getAttribute("popupid"));
       notificationsToShow.forEach(function(n) {
         // Record that the notification was actually displayed on screen.
         // Notifications that were opened a second time or that were originally
         // shown with "options.dismissed" will be recorded in a separate bucket.
         n._recordTelemetryStat(TELEMETRY_STAT_OFFERED);
         // Remember the time the notification was shown for the security delay.
         n.timeShown = this.window.performance.now();
       }, this);
@@ -1494,25 +1476,17 @@ PopupNotifications.prototype = {
     event.stopPropagation();
   },
 
   _onCommand(event) {
     // Ignore events from buttons as they are submitting and so don't need checks
     if (event.originalTarget.localName == "button") {
       return;
     }
-    let notificationEl = event.target;
-    // Find notification like getNotificationFromElement but some nodes are non-anon
-    while (notificationEl) {
-      if (notificationEl.localName == "popupnotification") {
-        break;
-      }
-      notificationEl =
-        notificationEl.ownerDocument.getBindingParent(notificationEl) || notificationEl.parentNode;
-    }
+    let notificationEl = getNotificationFromElement(event.target);
 
     let notification = notificationEl.notification;
     if (notification.options.checkbox) {
       if (notificationEl.checkbox.checked) {
         this._setNotificationUIState(notificationEl, notification.options.checkbox.checkedState);
       } else {
         this._setNotificationUIState(notificationEl, notification.options.checkbox.uncheckedState);
       }
@@ -1600,17 +1574,17 @@ PopupNotifications.prototype = {
     this._update();
   },
 
   _onMenuCommand: function PopupNotifications_onMenuCommand(event) {
     let target = event.originalTarget;
     if (!target.action || !target.notification)
       throw "menucommand target has no associated action/notification";
 
-    let notificationEl = target.parentElement;
+    let notificationEl = getNotificationFromElement(target);
     event.stopPropagation();
 
     target.notification._recordTelemetryStat(target.action.telemetryStatId);
 
     try {
       target.action.callback.call(undefined, {
         checkboxChecked: notificationEl.checkbox.checked,
         source: "menucommand",
--- a/toolkit/mozapps/extensions/test/xpinstall/head.js
+++ b/toolkit/mozapps/extensions/test/xpinstall/head.js
@@ -241,17 +241,17 @@ var Harness = {
     }
   },
 
   handleEvent(event) {
     if (event.type === "popupshown") {
       if (event.target == PanelUI.notificationPanel) {
         PanelUI.notificationPanel.hidePopup();
       } else if (event.target.firstElementChild) {
-        let popupId = event.target.getAttribute("popupid");
+        let popupId = event.target.firstElementChild.getAttribute("popupid");
         if (popupId === "addon-webext-permissions") {
           this.popupReady(event.target.firstElementChild);
         } else if (popupId === "addon-install-failed") {
           event.target.firstElementChild.button.click();
         }
       }
     }
   },
--- a/toolkit/mozapps/update/tests/browser/browser_TelemetryUpdatePing.js
+++ b/toolkit/mozapps/update/tests/browser/browser_TelemetryUpdatePing.js
@@ -29,17 +29,17 @@ add_task(async function testUpdatePingRe
       notificationId: "update-available",
       button: "button",
       beforeClick() {
         checkWhatsNewLink(window, "update-available-whats-new");
       },
     },
     {
       notificationId: "update-restart",
-      button: "secondarybutton",
+      button: "secondaryButton",
       cleanup() {
         AppMenuNotifications.removeNotification(/.*/);
       },
     },
   ]);
 
   // We cannot control when the ping will be generated/archived after we trigger
   // an update, so let's make sure to have one before moving on with validation.
--- a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadOptIn.js
+++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadOptIn.js
@@ -11,15 +11,15 @@ add_task(async function testBasicPromptN
       notificationId: "update-available",
       button: "button",
       beforeClick() {
         checkWhatsNewLink(window, "update-available-whats-new");
       },
     },
     {
       notificationId: "update-restart",
-      button: "secondarybutton",
+      button: "secondaryButton",
       cleanup() {
         AppMenuNotifications.removeNotification(/.*/);
       },
     },
   ]);
 });
--- a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadOptIn_bgWin.js
+++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadOptIn_bgWin.js
@@ -22,16 +22,16 @@ add_task(async function testUpdatesBackg
       await popupShownPromise;
 
       checkWhatsNewLink(window, "update-available-whats-new");
       let buttonEl = getNotificationButton(window, "update-available", "button");
       buttonEl.click();
     },
     {
       notificationId: "update-restart",
-      button: "secondarybutton",
+      button: "secondaryButton",
       cleanup() {
         AppMenuNotifications.removeNotification(/.*/);
       },
     },
   ]);
 });
 
--- a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadOptIn_staging.js
+++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadOptIn_staging.js
@@ -11,15 +11,15 @@ add_task(async function testBasicPrompt(
       notificationId: "update-available",
       button: "button",
       beforeClick() {
         checkWhatsNewLink(window, "update-available-whats-new");
       },
     },
     {
       notificationId: "update-restart",
-      button: "secondarybutton",
+      button: "secondaryButton",
       cleanup() {
         AppMenuNotifications.removeNotification(/.*/);
       },
     },
   ]);
 });
--- a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloaded.js
+++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloaded.js
@@ -1,13 +1,13 @@
 add_task(async function testCompleteAndPartialPatchesWithBadCompleteSize() {
   let updateParams = "invalidCompleteSize=1&promptWaitTime=0";
 
   await runUpdateTest(updateParams, 1, [
     {
       notificationId: "update-restart",
-      button: "secondarybutton",
+      button: "secondaryButton",
       cleanup() {
         AppMenuNotifications.removeNotification(/.*/);
       },
     },
   ]);
 });
--- a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloaded_staged.js
+++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloaded_staged.js
@@ -3,15 +3,15 @@ add_task(async function testCompleteAndP
     [PREF_APP_UPDATE_STAGING_ENABLED, true],
   ]});
 
   let updateParams = "invalidCompleteSize=1&promptWaitTime=0";
 
   await runUpdateTest(updateParams, 1, [
     {
       notificationId: "update-restart",
-      button: "secondarybutton",
+      button: "secondaryButton",
       cleanup() {
         AppMenuNotifications.removeNotification(/.*/);
       },
     },
   ]);
 });
--- a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_patch_partialBadSize_complete.js
+++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_patch_partialBadSize_complete.js
@@ -1,13 +1,13 @@
 add_task(async function testCompleteAndPartialPatchesWithBadPartialSize() {
   let updateParams = "invalidPartialSize=1&promptWaitTime=0";
 
   await runUpdateTest(updateParams, 1, [
     {
       notificationId: "update-restart",
-      button: "secondarybutton",
+      button: "secondaryButton",
       cleanup() {
         AppMenuNotifications.removeNotification(/.*/);
       },
     },
   ]);
 });
--- a/toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_partialApplyFailure_complete.js
+++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_partialApplyFailure_complete.js
@@ -6,15 +6,15 @@ add_task(async function testPartialPatch
   patches += getLocalPatchString(patchProps);
   let updateProps = {isCompleteUpdate: "false",
                      promptWaitTime: "0"};
   let updates = getLocalUpdateString(updateProps, patches);
 
   await runUpdateProcessingTest(updates, [
     {
       notificationId: "update-restart",
-      button: "secondarybutton",
+      button: "secondaryButton",
       cleanup() {
         AppMenuNotifications.removeNotification(/.*/);
       },
     },
   ]);
 });
--- a/toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_partialApplyFailure_complete_staging.js
+++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_partialApplyFailure_complete_staging.js
@@ -10,15 +10,15 @@ add_task(async function testPartialPatch
   patches += getLocalPatchString(patchProps);
   let updateProps = {isCompleteUpdate: "false",
                      promptWaitTime: "0"};
   let updates = getLocalUpdateString(updateProps, patches);
 
   await runUpdateProcessingTest(updates, [
     {
       notificationId: "update-restart",
-      button: "secondarybutton",
+      button: "secondaryButton",
       cleanup() {
         AppMenuNotifications.removeNotification(/.*/);
       },
     },
   ]);
 });
--- a/toolkit/mozapps/update/tests/browser/head.js
+++ b/toolkit/mozapps/update/tests/browser/head.js
@@ -367,17 +367,17 @@ function waitForEvent(topic, status = nu
  *         The ID of the notification to get the button for.
  * @param  button
  *         The anonid of the button to get.
  * @return The button element.
  */
 function getNotificationButton(win, notificationId, button) {
   let notification = win.document.getElementById(`appMenu-${notificationId}-notification`);
   is(notification.hidden, false, `${notificationId} notification is showing`);
-  return win.document.getAnonymousElementByAttribute(notification, "anonid", button);
+  return notification[button];
 }
 
 /**
  * Ensures that the "What's new" link with the provided ID is displayed and
  * matches the url parameter provided. If no URL is provided, it will instead
  * ensure that the link matches the default link URL.
  *
  * @param  win