Bug 1471403 - Part 4 - Convert "notificationbox" to a custom class. r=bgrins
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Fri, 09 Nov 2018 14:58:18 +0000
changeset 445387 b6676334369f8034686e9c50ceb941a27bfa6674
parent 445386 5fc40fe6b994e684f9db2c67b0c4d7e8be5540df
child 445388 abe6431b995081ec03f8b30a785f075553ec4095
push id109739
push userpaolo.mozmail@amadzone.org
push dateFri, 09 Nov 2018 17:25:47 +0000
treeherdermozilla-inbound@b6676334369f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1471403
milestone65.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 1471403 - Part 4 - Convert "notificationbox" to a custom class. r=bgrins As part of the conversion, support for notificationsHidden and children that are not notifications is also removed. Differential Revision: https://phabricator.services.mozilla.com/D10894
browser/base/content/browser.js
browser/base/content/tabbrowser.js
browser/base/content/test/general/browser_datachoices_notification.js
browser/base/content/test/general/head.js
browser/base/content/test/urlbar/browser_urlbarSearchSingleWordNotification.js
browser/modules/test/browser/browser_ProcessHangNotifications.js
browser/modules/test/browser/browser_UnsubmittedCrashHandler.js
devtools/client/scratchpad/index.xul
devtools/client/scratchpad/scratchpad.js
devtools/client/scratchpad/test/browser_scratchpad_edit_ui_updates.js
devtools/client/scratchpad/test/browser_scratchpad_open.js
devtools/client/webide/content/webide.js
devtools/client/webide/content/webide.xul
devtools/client/webide/test/test_basic.html
devtools/client/webide/test/test_fullscreenToolbox.html
testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
toolkit/components/normandy/test/browser/browser_Heartbeat.js
toolkit/content/customElements.js
toolkit/content/jar.mn
toolkit/content/tests/chrome/test_bug457632.xul
toolkit/content/tests/chrome/test_bug509732.xul
toolkit/content/tests/chrome/test_notificationbox.xul
toolkit/content/widgets/notification.xml
toolkit/content/widgets/notificationbox.js
toolkit/content/xul.css
toolkit/themes/shared/notification.inc.css
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -296,36 +296,38 @@ Object.defineProperty(this, "gNavToolbox
 
 // High priority notification bars shown at the top of the window.
 Object.defineProperty(this, "gHighPriorityNotificationBox", {
   configurable: true,
   enumerable: true,
   get() {
     delete this.gHighPriorityNotificationBox;
 
-    let notificationbox = document.createXULElement("notificationbox");
-    notificationbox.className = "global-notificationbox";
-    notificationbox.setAttribute("notificationside", "top");
-    document.getElementById("appcontent").prepend(notificationbox);
+    let notificationbox = new MozElements.NotificationBox(element => {
+      element.classList.add("global-notificationbox");
+      element.setAttribute("notificationside", "top");
+      document.getElementById("appcontent").append(element);
+    });
 
     return this.gHighPriorityNotificationBox = notificationbox;
   },
 });
 
 // Regular notification bars shown at the bottom of the window.
 Object.defineProperty(this, "gNotificationBox", {
   configurable: true,
   enumerable: true,
   get() {
     delete this.gNotificationBox;
 
-    let notificationbox = document.createXULElement("notificationbox");
-    notificationbox.className = "global-notificationbox";
-    notificationbox.setAttribute("notificationside", "bottom");
-    document.getElementById("browser-bottombox").appendChild(notificationbox);
+    let notificationbox = new MozElements.NotificationBox(element => {
+      element.classList.add("global-notificationbox");
+      element.setAttribute("notificationside", "bottom");
+      document.getElementById("browser-bottombox").appendChild(element);
+    });
 
     return this.gNotificationBox = notificationbox;
   },
 });
 
 // Smart getter for the findbar.  If you don't wish to force the creation of
 // the findbar, check gFindBarInitialized first.
 
@@ -3529,22 +3531,22 @@ var PrintPreviewListener = {
     this._chromeState.sidebarOpen = SidebarUI.isOpen;
     this._sidebarCommand = SidebarUI.currentID;
     SidebarUI.hide();
 
     this._chromeState.findOpen = gFindBarInitialized && !gFindBar.hidden;
     if (gFindBarInitialized)
       gFindBar.close();
 
-    gBrowser.getNotificationBox().hidden = true;
-    gNotificationBox.hidden = true;
+    gBrowser.getNotificationBox().stack.hidden = true;
+    gNotificationBox.stack.hidden = true;
   },
   _showChrome() {
-    gNotificationBox.hidden = false;
-    gBrowser.getNotificationBox().hidden = false;
+    gNotificationBox.stack.hidden = false;
+    gBrowser.getNotificationBox().stack.hidden = false;
 
     if (this._chromeState.findOpen) {
       gLazyFindCommand("open");
     }
 
     if (this._chromeState.sidebarOpen) {
       SidebarUI.show(this._sidebarCommand);
     }
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -649,24 +649,24 @@ window._gBrowser = {
     return this.getBrowserContainer(aBrowser).parentNode;
   },
 
   getBrowserContainer(aBrowser) {
     return (aBrowser || this.selectedBrowser).parentNode.parentNode;
   },
 
   getNotificationBox(aBrowser) {
-    let container = this.getBrowserContainer(aBrowser);
-    let notificationbox = container.firstElementChild;
-    if (notificationbox.localName != "notificationbox") {
-      notificationbox = document.createXULElement("notificationbox");
-      notificationbox.setAttribute("notificationside", "top");
-      container.prepend(notificationbox);
-    }
-    return notificationbox;
+    let browser = aBrowser || this.selectedBrowser;
+    if (!browser._notificationBox) {
+      browser._notificationBox = new MozElements.NotificationBox(element => {
+        element.setAttribute("notificationside", "top");
+        this.getBrowserContainer(browser).prepend(element);
+      });
+    }
+    return browser._notificationBox;
   },
 
   getTabModalPromptBox(aBrowser) {
     let browser = (aBrowser || this.selectedBrowser);
     if (!browser.tabModalPromptBox) {
       browser.tabModalPromptBox = new TabModalPromptBox(browser);
     }
     return browser.tabModalPromptBox;
--- a/browser/base/content/test/general/browser_datachoices_notification.js
+++ b/browser/base/content/test/general/browser_datachoices_notification.js
@@ -40,17 +40,17 @@ function promiseNextTick() {
 
 /**
  * Wait for a notification to be shown in a notification box.
  * @param {Object} aNotificationBox The notification box.
  * @return {Promise} Resolved when the notification is displayed.
  */
 function promiseWaitForAlertActive(aNotificationBox) {
   let deferred = PromiseUtils.defer();
-  aNotificationBox.addEventListener("AlertActive", function() {
+  aNotificationBox.stack.addEventListener("AlertActive", function() {
     deferred.resolve();
   }, {once: true});
   return deferred.promise;
 }
 
 /**
  * Wait for a notification to be closed.
  * @param {Object} aNotification The notification.
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -8,31 +8,29 @@ ChromeUtils.defineModuleGetter(this, "Br
   "resource://testing-common/BrowserTestUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "TabCrashHandler",
   "resource:///modules/ContentCrashHandlers.jsm");
 
 /**
  * Wait for a <notification> to be closed then call the specified callback.
  */
 function waitForNotificationClose(notification, cb) {
-  let parent = notification.parentNode;
-
   let observer = new MutationObserver(function onMutatations(mutations) {
     for (let mutation of mutations) {
       for (let i = 0; i < mutation.removedNodes.length; i++) {
         let node = mutation.removedNodes.item(i);
         if (node != notification) {
           continue;
         }
         observer.disconnect();
         cb();
       }
     }
   });
-  observer.observe(parent, {childList: true});
+  observer.observe(notification.control.stack, {childList: true});
 }
 
 function closeAllNotifications() {
   if (!gNotificationBox.currentNotification) {
     return Promise.resolve();
   }
 
   return new Promise(resolve => {
--- a/browser/base/content/test/urlbar/browser_urlbarSearchSingleWordNotification.js
+++ b/browser/base/content/test/urlbar/browser_urlbarSearchSingleWordNotification.js
@@ -21,17 +21,17 @@ function promiseNotification(aBrowser, v
           notificationObserver = null;
           resolve();
         }
       };
       if (notificationObserver) {
         notificationObserver.disconnect();
       }
       notificationObserver = new MutationObserver(checkForNotification);
-      notificationObserver.observe(notificationBox, {childList: true});
+      notificationObserver.observe(notificationBox.stack, {childList: true});
     } else {
       setTimeout(() => {
         is(notificationBox.getNotificationWithValue(value), null,
            `We are expecting to not get a notification for ${input}`);
         resolve();
       }, 1000);
     }
   });
--- a/browser/modules/test/browser/browser_ProcessHangNotifications.js
+++ b/browser/modules/test/browser/browser_ProcessHangNotifications.js
@@ -3,17 +3,17 @@
 const { WebExtensionPolicy } =
   Cu.getGlobalForObject(ChromeUtils.import("resource://gre/modules/Services.jsm", {}));
 
 ChromeUtils.import("resource://gre/modules/UpdateUtils.jsm");
 
 function promiseNotificationShown(aWindow, aName) {
   return new Promise((resolve) => {
     let notificationBox = aWindow.gHighPriorityNotificationBox;
-    notificationBox.addEventListener("AlertActive", function() {
+    notificationBox.stack.addEventListener("AlertActive", function() {
       is(notificationBox.allNotifications.length, 1, "Notification Displayed.");
       resolve(notificationBox);
     }, {once: true});
   });
 }
 
 function pushPrefs(...aPrefs) {
   return SpecialPowers.pushPrefEnv({"set": aPrefs});
--- a/browser/modules/test/browser/browser_UnsubmittedCrashHandler.js
+++ b/browser/modules/test/browser/browser_UnsubmittedCrashHandler.js
@@ -259,17 +259,16 @@ add_task(async function test_no_pending_
  * Tests that there is a notification if there is one pending
  * crash report.
  */
 add_task(async function test_one_pending() {
   await createPendingCrashReports(1);
   let notification =
     await UnsubmittedCrashHandler.checkForUnsubmittedCrashReports();
   Assert.ok(notification, "There should be a notification");
-
   gNotificationBox.removeNotification(notification, true);
   clearPendingCrashReports();
 });
 
 /**
  * Tests that an ignored crash report does not suppress a notification that
  * would be trigged by another, unignored crash report.
  */
--- a/devtools/client/scratchpad/index.xul
+++ b/devtools/client/scratchpad/index.xul
@@ -406,23 +406,23 @@
     <menuitem id="sp-text-reloadAndRun"
               label="&reloadAndRun.label;"
               key="sp-key-reloadAndRun"
               accesskey="&reloadAndRun.accesskey;"
               command="sp-cmd-reloadAndRun"/>
   </menupopup>
 </popupset>
 
-<notificationbox id="scratchpad-notificationbox" flex="1">
+<vbox id="scratchpad-container" flex="1">
   <hbox flex="1">
     <vbox id="scratchpad-editor" flex="1"/>
     <splitter class="devtools-side-splitter"/>
     <tabbox id="scratchpad-sidebar" class="devtools-sidebar-tabs"
                                     width="300"
                                     hidden="true">
       <tabs/>
       <tabpanels flex="1"/>
     </tabbox>
   </hbox>
   <toolbar id="statusbar-line-col" class="devtools-toolbar"/>
-</notificationbox>
+</vbox>
 
 </window>
--- a/devtools/client/scratchpad/scratchpad.js
+++ b/devtools/client/scratchpad/scratchpad.js
@@ -325,24 +325,16 @@ var Scratchpad = {
     this._dirty = aValue;
     if (!aValue && this.editor) {
       this.editor.setClean();
     }
     this._updateTitle();
   },
 
   /**
-   * Retrieve the xul:notificationbox DOM element. It notifies the user when
-   * the current code execution context is SCRATCHPAD_CONTEXT_BROWSER.
-   */
-  get notificationBox() {
-    return document.getElementById("scratchpad-notificationbox");
-  },
-
-  /**
    * Hide the menu bar.
    */
   hideMenu: function SP_hideMenu() {
     document.getElementById("sp-menu-toolbar").style.display = "none";
   },
 
   /**
    * Show the menu bar.
@@ -1566,16 +1558,20 @@ var Scratchpad = {
    *
    * @param Event aEvent
    */
   onLoad: function SP_onLoad(aEvent) {
     if (aEvent.target != document) {
       return;
     }
 
+    this.notificationBox = new window.MozElements.NotificationBox(element => {
+      document.getElementById("scratchpad-container").prepend(element);
+    });
+
     const chrome = Services.prefs.getBoolPref(DEVTOOLS_CHROME_ENABLED);
     if (chrome) {
       const environmentMenu = document.getElementById("sp-environment-menu");
       const errorConsoleCommand = document.getElementById("sp-cmd-errorConsole");
       const chromeContextCommand = document.getElementById("sp-cmd-browserContext");
       environmentMenu.removeAttribute("hidden");
       chromeContextCommand.removeAttribute("disabled");
       errorConsoleCommand.removeAttribute("disabled");
@@ -1626,17 +1622,17 @@ var Scratchpad = {
 
       this.editor.on("change", this._onChanged);
       // Keep a reference to the bound version for use in onUnload.
       this.updateStatusBar = Scratchpad.updateStatusBar.bind(this);
       this.editor.on("cursorActivity", this.updateStatusBar);
       const okstring = this.strings.GetStringFromName("selfxss.okstring");
       const msg = this.strings.formatStringFromName("selfxss.msg", [okstring], 1);
       this._onPaste = WebConsoleUtils.pasteHandlerGen(this.editor.container.contentDocument.body,
-                                                      document.querySelector("#scratchpad-notificationbox"),
+                                                      this.notificationBox,
                                                       msg, okstring);
       editorElement.addEventListener("paste", this._onPaste, true);
       editorElement.addEventListener("drop", this._onPaste);
       this.editor.on("saveRequested", () => this.saveFile());
       this.editor.focus();
       this.editor.setCursor({ line: lines.length, ch: lines.pop().length });
 
       // Add the commands controller for the source-editor.
--- a/devtools/client/scratchpad/test/browser_scratchpad_edit_ui_updates.js
+++ b/devtools/client/scratchpad/test/browser_scratchpad_edit_ui_updates.js
@@ -56,17 +56,17 @@ function runTests() {
   let isContextMenu = false;
 
   const oldVal = sp.editor.getText();
 
   const testSelfXss = function(oldVal) {
     // Self xss prevention tests (bug 994134)
     info("Self xss paste tests");
     is(WebConsoleUtils.usageCount, 0, "Test for usage count getter");
-    const notificationbox = doc.getElementById("scratchpad-notificationbox");
+    const notificationbox = sp.notificationBox;
     const notification = notificationbox.getNotificationWithValue("selfxss-notification");
     ok(notification, "Self-xss notification shown");
     is(oldVal, sp.editor.getText(), "Paste blocked by self-xss prevention");
     Services.prefs.setIntPref("devtools.selfxss.count", 10);
     notificationbox.removeAllNotifications(true);
     openMenu(10, 10, firstShow);
   };
 
--- a/devtools/client/scratchpad/test/browser_scratchpad_open.js
+++ b/devtools/client/scratchpad/test/browser_scratchpad_open.js
@@ -73,18 +73,18 @@ function testOpenInvalidState() {
 function testOpenTestFile() {
   openScratchpad(function(win) {
     ok(win, "scratchpad opened for file open");
     try {
       win.Scratchpad.importFromFile(
         "http://example.com/browser/devtools/client/scratchpad/test/NS_ERROR_ILLEGAL_INPUT.txt",
         "silent",
         function(aStatus, content) {
-          const nb = win.document.querySelector("#scratchpad-notificationbox");
-          is(nb.querySelectorAll("notification").length, 1, "There is just one notification");
+          const nb = win.Scratchpad.notificationBox;
+          is(nb.allNotifications.length, 1, "There is just one notification");
           const cn = nb.currentNotification;
           is(cn.priority, nb.PRIORITY_WARNING_HIGH, "notification priority is correct");
           is(cn.value, "file-import-convert-failed", "notification value is corrent");
           is(cn.type, "warning", "notification type is correct");
           done();
         });
       ok(true, "importFromFile does not cause exception");
     } catch (exception) {
--- a/devtools/client/webide/content/webide.js
+++ b/devtools/client/webide/content/webide.js
@@ -58,16 +58,20 @@ window.addEventListener("unload", functi
 var UI = {
   init: function() {
     this._telemetry = new Telemetry();
 
     // webide is not connected with a toolbox so we pass -1 as the
     // toolbox session id.
     this._telemetry.toolOpened("webide", -1, this);
 
+    this.notificationBox = new window.MozElements.NotificationBox(element => {
+      document.getElementById("containerbox")
+              .insertAdjacentElement("afterbegin", element);
+    });
     AppManager.init();
 
     this.appManagerUpdate = this.appManagerUpdate.bind(this);
     AppManager.on("app-manager-update", this.appManagerUpdate);
 
     Cmds.showProjectPanel();
     Cmds.showRuntimePanel();
 
@@ -283,25 +287,24 @@ var UI = {
     const buttons = [{
       label: Strings.GetStringFromName("notification_showTroubleShooting_label"),
       accessKey: Strings.GetStringFromName("notification_showTroubleShooting_accesskey"),
       callback: function() {
         Cmds.showTroubleShooting();
       },
     }];
 
-    const nbox = document.querySelector("#notificationbox");
+    const nbox = this.notificationBox;
     nbox.removeAllNotifications(true);
     nbox.appendNotification(text, "webide:errornotification", null,
                             nbox.PRIORITY_WARNING_LOW, buttons);
   },
 
   dismissErrorNotification: function() {
-    const nbox = document.querySelector("#notificationbox");
-    nbox.removeAllNotifications(true);
+    this.notificationBox.removeAllNotifications(true);
   },
 
   /** ******** COMMANDS **********/
 
   /**
    * This module emits various events when state changes occur.
    *
    * The events this module may emit include:
@@ -814,17 +817,17 @@ var UI = {
 
     return this.busyUntil(this.toolboxPromise, "opening toolbox");
   },
 
   _showToolbox: function(target, iframe) {
     const splitter = document.querySelector(".devtools-horizontal-splitter");
     splitter.removeAttribute("hidden");
 
-    document.querySelector("notificationbox").insertBefore(iframe, splitter.nextSibling);
+    document.getElementById("containerbox").insertBefore(iframe, splitter.nextSibling);
     const host = Toolbox.HostType.CUSTOM;
     const options = { customIframe: iframe, zoom: false, uid: iframe.uid };
 
     document.querySelector("#action-button-debug").setAttribute("active", "true");
 
     return gDevTools.showToolbox(target, null, host, options);
   },
 };
--- a/devtools/client/webide/content/webide.xul
+++ b/devtools/client/webide/content/webide.xul
@@ -134,17 +134,17 @@
           <image class="panel-button-image"/>
           <label class="panel-button-label" value="&runtimeButton_label;"/>
         </toolbarbutton>
       </hbox>
 
     </vbox>
   </toolbar>
 
-  <notificationbox flex="1" id="notificationbox">
+  <vbox flex="1" id="containerbox">
     <div flex="1" id="deck-panels">
       <vbox id="project-listing-panel" class="project-listing panel-list" flex="1">
         <div id="project-listing-wrapper" class="panel-list-wrapper">
           <iframe id="project-listing-panel-details" flex="1" src="project-listing.xhtml" tooltip="aHTMLTooltip"/>
         </div>
       </vbox>
       <splitter class="devtools-side-splitter" id="project-listing-splitter"/>
       <deck flex="1" id="deck" selectedIndex="-1">
@@ -159,11 +159,11 @@
       <vbox id="runtime-listing-panel" class="runtime-listing panel-list" flex="1">
         <div id="runtime-listing-wrapper" class="panel-list-wrapper">
           <iframe id="runtime-listing-panel-details" flex="1" src="runtime-listing.xhtml" tooltip="aHTMLTooltip"/>
         </div>
       </vbox>
     </div>
     <splitter hidden="true" class="devtools-horizontal-splitter" orient="vertical"/>
     <!-- toolbox iframe will be inserted here -->
-  </notificationbox>
+  </vbox>
 
 </window>
--- a/devtools/client/webide/test/test_basic.html
+++ b/devtools/client/webide/test/test_basic.html
@@ -27,28 +27,28 @@
 
           ok(win, "Found a window");
           ok(win.AppManager, "App Manager accessible");
           const appmgr = win.AppManager;
           ok(appmgr.connection, "App Manager connection ready");
           ok(appmgr.runtimeList, "Runtime list ready");
 
             // test error reporting
-          const nbox = win.document.querySelector("#notificationbox");
-          let notification =  nbox.getNotificationWithValue("webide:errornotification");
+          const nbox = win.UI.notificationBox;
+          let notification = nbox.getNotificationWithValue("webide:errornotification");
           ok(!notification, "No notification yet");
           const deferred = new Promise((resolve, reject) => {
             nextTick().then(() => {
               reject("BOOM!");
             });
           });
           try {
             await win.UI.busyUntil(deferred, "xx");
           } catch (e) { /* This *will* fail */ }
-          notification =  nbox.getNotificationWithValue("webide:errornotification");
+          notification = nbox.getNotificationWithValue("webide:errornotification");
           ok(notification, "Error has been reported");
 
           await closeWebIDE(win);
 
           SimpleTest.finish();
         })();
       };
     </script>
--- a/devtools/client/webide/test/test_fullscreenToolbox.html
+++ b/devtools/client/webide/test/test_fullscreenToolbox.html
@@ -44,17 +44,17 @@
             docProject.querySelectorAll("#project-panel-runtimeapps .panel-item")[0].click();
           });
 
           await waitForUpdate(win, "project");
 
           ok(win.UI.toolboxPromise, "Toolbox promise exists");
           await win.UI.toolboxPromise;
 
-          const nbox = win.document.querySelector("#notificationbox");
+          const nbox = win.document.getElementById("containerbox");
           ok(!nbox.hasAttribute("toolboxfullscreen"), "Toolbox is not fullscreen");
 
           win.Cmds.showRuntimeDetails();
 
           ok(!nbox.hasAttribute("toolboxfullscreen"), "Toolbox is not fullscreen");
 
           await win.Cmds.disconnectRuntime();
 
--- a/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
+++ b/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
@@ -1589,17 +1589,17 @@ var BrowserTestUtils = {
   },
 
   waitForNotificationInNotificationBox(notificationBox, notificationValue) {
     return new Promise((resolve) => {
       let check = (event) => {
         return event.target.value == notificationValue;
       };
 
-      BrowserTestUtils.waitForEvent(notificationBox, "AlertActive",
+      BrowserTestUtils.waitForEvent(notificationBox.stack, "AlertActive",
                                     false, check).then((event) => {
         // The originalTarget of the AlertActive on a notificationbox
         // will be the notification itself.
         resolve(event.originalTarget);
       });
     });
   },
 
--- a/toolkit/components/normandy/test/browser/browser_Heartbeat.js
+++ b/toolkit/components/normandy/test/browser/browser_Heartbeat.js
@@ -38,17 +38,17 @@ function closeAllNotifications(targetWin
       }
       if (notificationSet.size === 0) {
         Assert.equal(notificationBox.allNotifications.length, 0, "No notifications left");
         observer.disconnect();
         resolve();
       }
     });
 
-    observer.observe(notificationBox, {childList: true});
+    observer.observe(notificationBox.stack, {childList: true});
 
     for (const notification of notificationBox.allNotifications) {
       notification.close();
     }
   });
 }
 
 /* Check that the correct telemetry was sent */
@@ -76,30 +76,30 @@ sandboxManager.addHold("test running");
 // Several of the behaviors of heartbeat prompt are mutually exclusive, so checks are broken up
 // into three batches.
 
 /* Batch #1 - General UI, Stars, and telemetry data */
 add_task(async function() {
   const targetWindow = Services.wm.getMostRecentWindow("navigator:browser");
   const notificationBox = targetWindow.gHighPriorityNotificationBox;
 
-  const preCount = notificationBox.childElementCount;
+  const preCount = notificationBox.allNotifications.length;
   const hb = new Heartbeat(targetWindow, sandboxManager, {
     testing: true,
     flowId: "test",
     message: "test",
     engagementButtonLabel: undefined,
     learnMoreMessage: "Learn More",
     learnMoreUrl: "https://example.org/learnmore",
   });
 
   // Check UI
   const learnMoreEl = hb.notice.querySelector(".text-link");
   const messageEl = targetWindow.document.getAnonymousElementByAttribute(hb.notice, "anonid", "messageText");
-  Assert.equal(notificationBox.childElementCount, preCount + 1, "Correct number of notifications open");
+  Assert.equal(notificationBox.allNotifications.length, preCount + 1, "Correct number of notifications open");
   Assert.equal(hb.notice.querySelectorAll(".star-x").length, 5, "Correct number of stars");
   Assert.equal(hb.notice.querySelectorAll(".notification-button").length, 0, "Engagement button not shown");
   Assert.equal(learnMoreEl.href, "https://example.org/learnmore", "Learn more url correct");
   Assert.equal(learnMoreEl.value, "Learn More", "Learn more label correct");
   Assert.equal(messageEl.textContent, "test", "Message is correct");
 
   // Check that when clicking the learn more link, a tab opens with the right URL
   let loadedPromise;
--- a/toolkit/content/customElements.js
+++ b/toolkit/content/customElements.js
@@ -290,16 +290,17 @@ window.MozXULElement = MozXULElement;
 window.MozElements = MozElements;
 
 // 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/progressmeter.js",
     "chrome://global/content/elements/radio.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
@@ -92,16 +92,17 @@ toolkit.jar:
    content/global/bindings/toolbarbutton.xml   (widgets/toolbarbutton.xml)
    content/global/bindings/tree.xml            (widgets/tree.xml)
    content/global/bindings/videocontrols.xml   (widgets/videocontrols.xml)
 *  content/global/bindings/wizard.xml          (widgets/wizard.xml)
    content/global/elements/datetimebox.js      (widgets/datetimebox.js)
    content/global/elements/findbar.js          (widgets/findbar.js)
    content/global/elements/editor.js          (widgets/editor.js)
    content/global/elements/general.js          (widgets/general.js)
+   content/global/elements/notificationbox.js  (widgets/notificationbox.js)
    content/global/elements/progressmeter.js    (widgets/progressmeter.js)
    content/global/elements/radio.js            (widgets/radio.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
--- a/toolkit/content/tests/chrome/test_bug457632.xul
+++ b/toolkit/content/tests/chrome/test_bug457632.xul
@@ -3,17 +3,17 @@
 <?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
 <!--
   XUL Widget Test for bug 457632
   -->
 <window title="Bug 457632" width="500" height="600"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>  
 
-  <notificationbox id="nb"/>
+  <vbox id="nb"/>
 
   <!-- test results are displayed in the html:body -->
   <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"
         onload="test()"/>
 
   <!-- test code goes here -->
 <script type="application/javascript">
 <![CDATA[
@@ -25,17 +25,19 @@ function completeAnimation(nextTest) {
     return;
   }
 
   setTimeout(completeAnimation, 50, nextTest);
 }
 
 function test() {
   SimpleTest.waitForExplicitFinish();
-  gNotificationBox = document.getElementById("nb");
+  gNotificationBox = new MozElements.NotificationBox(e => {
+    document.getElementById("nb").appendChild(e);
+  });
 
   is(gNotificationBox.allNotifications.length, 0, "There should be no initial notifications");
   gNotificationBox.appendNotification("Test notification",
                                       "notification1", null,
                                       gNotificationBox.PRIORITY_INFO_LOW,
                                       null);
   is(gNotificationBox.allNotifications.length, 1, "Notification exists while animating in");
   let notification = gNotificationBox.getNotificationWithValue("notification1");
--- a/toolkit/content/tests/chrome/test_bug509732.xul
+++ b/toolkit/content/tests/chrome/test_bug509732.xul
@@ -3,31 +3,33 @@
 <?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
 <!--
   XUL Widget Test for bug 509732
   -->
 <window title="Bug 509732" width="500" height="600"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>  
 
-    <notificationbox id="nb" hidden="true"/>
+  <vbox id="nb" hidden="true"/>
 
   <!-- test results are displayed in the html:body -->
   <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"
         onload="test()"/>
 
   <!-- test code goes here -->
 <script type="application/javascript">
 <![CDATA[
 var gNotificationBox;
 
 // Tests that a notification that is added in an hidden box didn't throw the animation
 function test() {
   SimpleTest.waitForExplicitFinish();
-  gNotificationBox = document.getElementById("nb");
+  gNotificationBox = new MozElements.NotificationBox(e => {
+    document.getElementById("nb").appendChild(e);
+  });
 
   is(gNotificationBox.allNotifications.length, 0, "There should be no initial notifications");
 
   gNotificationBox.appendNotification("Test notification",
                                       "notification1", null,
                                       gNotificationBox.PRIORITY_INFO_LOW,
                                       null);
 
--- a/toolkit/content/tests/chrome/test_notificationbox.xul
+++ b/toolkit/content/tests/chrome/test_notificationbox.xul
@@ -3,17 +3,17 @@
 <?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
 <!--
   XUL Widget Test for notificationbox
   -->
 <window title="Notification Box" width="500" height="600"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>  
 
-  <notificationbox id="nb"/>
+  <vbox id="nb"/>
   <menupopup id="menupopup" onpopupshown="this.hidePopup()" onpopuphidden="checkPopupClosed()">
     <menuitem label="One"/>
   </menupopup>
 
   <!-- test results are displayed in the html:body -->
   <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/>
 
   <!-- test code goes here -->
@@ -24,18 +24,16 @@ var testtag_notificationbox_buttons = [
   {
     label: "Button 1",
     accesskey: "u",
     callback: testtag_notificationbox_buttonpressed,
     popup: "menupopup"
   }
 ];
 
-var NSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-
 function testtag_notificationbox_buttonpressed(event)
 {
 }
 
 function testtag_notificationbox(nb)
 {
   testtag_notificationbox_State(nb, "initial", null, 0);
 
@@ -397,17 +395,16 @@ function testtag_notificationbox_State(n
 {
   SimpleTest.is(nb.currentNotification, expecteditem, testid + " currentNotification");
   SimpleTest.is(nb.allNotifications ? nb.allNotifications.length : "no value",
                 expectedcount, testid + " allNotifications");
 }
 
 function testtag_notification_State(nb, ntf, testid, label, value, image, priority)
 {
-  SimpleTest.is(ntf.control, nb, testid + " notification.control");
   SimpleTest.is(ntf.label, label, testid + " notification.label");
   SimpleTest.is(ntf.value, value, testid + " notification.value");
   SimpleTest.is(ntf.image, image, testid + " notification.image");
   SimpleTest.is(ntf.priority, priority, testid + " notification.priority");
 
   var type;
   switch (priority) {
     case nb.PRIORITY_INFO_LOW:
@@ -491,14 +488,18 @@ function runTimedTestsWait(tests, idx, e
   // use this secret property to check if the animation is still running. If it
   // is, then the notification hasn't fully opened or closed yet
   if (element._animating)
     setTimeout(runTimedTestsWait, 50, tests, idx, element, arg);
   else
     runTimedTests(tests, idx, element, arg);
 }
 
-setTimeout(testtag_notificationbox, 0, document.getElementById('nb'));
+setTimeout(() => {
+  testtag_notificationbox(new MozElements.NotificationBox(e => {
+    document.getElementById("nb").appendChild(e);
+  }));
+}, 0);
 ]]>
 </script>
 
 </window>
 
--- a/toolkit/content/widgets/notification.xml
+++ b/toolkit/content/widgets/notification.xml
@@ -10,388 +10,16 @@
 ]>
 
 <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">
 
-  <binding id="notificationbox">
-    <content>
-      <xul:stack xbl:inherits="hidden=notificationshidden"
-                 class="notificationbox-stack">
-        <xul:spacer/>
-        <children includes="notification"/>
-      </xul:stack>
-      <children/>
-    </content>
-
-    <implementation>
-      <field name="PRIORITY_INFO_LOW" readonly="true">1</field>
-      <field name="PRIORITY_INFO_MEDIUM" readonly="true">2</field>
-      <field name="PRIORITY_INFO_HIGH" readonly="true">3</field>
-      <field name="PRIORITY_WARNING_LOW" readonly="true">4</field>
-      <field name="PRIORITY_WARNING_MEDIUM" readonly="true">5</field>
-      <field name="PRIORITY_WARNING_HIGH" readonly="true">6</field>
-      <field name="PRIORITY_CRITICAL_LOW" readonly="true">7</field>
-      <field name="PRIORITY_CRITICAL_MEDIUM" readonly="true">8</field>
-      <field name="PRIORITY_CRITICAL_HIGH" readonly="true">9</field>
-      <field name="PRIORITY_CRITICAL_BLOCK" readonly="true">10</field>
-
-      <field name="currentNotification">null</field>
-
-      <field name="_closedNotification">null</field>
-      <field name="_blockingCanvas">null</field>
-      <field name="_animating">false</field>
-
-      <property name="_allowAnimation">
-        <getter>
-          <![CDATA[
-            var prefs = Cc["@mozilla.org/preferences-service;1"]
-                          .getService(Ci.nsIPrefBranch);
-            return prefs.getBoolPref("toolkit.cosmeticAnimations.enabled");
-          ]]>
-        </getter>
-      </property>
-
-      <property name="notificationsHidden"
-                onget="return this.getAttribute('notificationshidden') == 'true';">
-        <setter>
-          <![CDATA[
-            if (val)
-              this.setAttribute("notificationshidden", true);
-            else this.removeAttribute("notificationshidden");
-            return val;
-          ]]>
-        </setter>
-      </property>
-
-      <property name="allNotifications" readonly="true">
-        <getter>
-          <![CDATA[
-            var closedNotification = this._closedNotification;
-            var notifications = this.getElementsByTagName("notification");
-            return Array.filter(notifications, n => n != closedNotification);
-          ]]>
-        </getter>
-      </property>
-
-      <method name="getNotificationWithValue">
-        <parameter name="aValue"/>
-        <body>
-          <![CDATA[
-            var notifications = this.allNotifications;
-            for (var n = notifications.length - 1; n >= 0; n--) {
-              if (aValue == notifications[n].getAttribute("value"))
-                return notifications[n];
-            }
-            return null;
-          ]]>
-        </body>
-      </method>
-
-      <method name="appendNotification">
-        <parameter name="aLabel"/>
-        <parameter name="aValue"/>
-        <parameter name="aImage"/>
-        <parameter name="aPriority"/>
-        <parameter name="aButtons"/>
-        <parameter name="aEventCallback"/>
-        <body>
-          <![CDATA[
-            if (aPriority < this.PRIORITY_INFO_LOW ||
-                aPriority > this.PRIORITY_CRITICAL_BLOCK)
-              throw "Invalid notification priority " + aPriority;
-
-            // check for where the notification should be inserted according to
-            // priority. If two are equal, the existing one appears on top.
-            var notifications = this.allNotifications;
-            var insertPos = null;
-            for (var n = notifications.length - 1; n >= 0; n--) {
-              if (notifications[n].priority < aPriority)
-                break;
-              insertPos = notifications[n];
-            }
-
-            var newitem = document.createXULElement("notification");
-            // Can't use instanceof in case this was created from a different document:
-            let labelIsDocFragment = aLabel && typeof aLabel == "object" && aLabel.nodeType &&
-                                     aLabel.nodeType == aLabel.DOCUMENT_FRAGMENT_NODE;
-            if (!labelIsDocFragment)
-              newitem.setAttribute("label", aLabel);
-            newitem.setAttribute("value", aValue);
-            if (aImage)
-              newitem.setAttribute("image", aImage);
-            newitem.eventCallback = aEventCallback;
-
-            if (aButtons) {
-              // The notification-button-default class is added to the button
-              // with isDefault set to true. If there is no such button, it is
-              // added to the first button (unless that button has isDefault
-              // set to false). There cannot be multiple default buttons.
-              var defaultElem;
-
-              for (var b = 0; b < aButtons.length; b++) {
-                var button = aButtons[b];
-                var buttonElem = document.createXULElement("button");
-                buttonElem.setAttribute("label", button.label);
-                if (typeof button.accessKey == "string")
-                  buttonElem.setAttribute("accesskey", button.accessKey);
-                buttonElem.classList.add("notification-button");
-
-                if (button.isDefault ||
-                    b == 0 && !("isDefault" in button))
-                  defaultElem = buttonElem;
-
-                newitem.appendChild(buttonElem);
-                buttonElem.buttonInfo = button;
-              }
-
-              if (defaultElem)
-                defaultElem.classList.add("notification-button-default");
-            }
-
-            newitem.setAttribute("priority", aPriority);
-            if (aPriority >= this.PRIORITY_CRITICAL_LOW)
-              newitem.setAttribute("type", "critical");
-            else if (aPriority <= this.PRIORITY_INFO_HIGH)
-              newitem.setAttribute("type", "info");
-            else
-              newitem.setAttribute("type", "warning");
-
-            if (!insertPos) {
-              newitem.style.position = "fixed";
-              newitem.style.top = "100%";
-              newitem.style.marginTop = "-15px";
-              newitem.style.opacity = "0";
-            }
-            this.insertBefore(newitem, insertPos);
-            // Can only insert the document fragment after the item has been created because
-            // otherwise the XBL structure isn't there yet:
-            if (labelIsDocFragment) {
-              document.getAnonymousElementByAttribute(newitem, "anonid", "messageText")
-                .appendChild(aLabel);
-            }
-
-            if (!insertPos)
-              this._showNotification(newitem, true);
-
-            // Fire event for accessibility APIs
-            var event = document.createEvent("Events");
-            event.initEvent("AlertActive", true, true);
-            newitem.dispatchEvent(event);
-
-            return newitem;
-          ]]>
-        </body>
-      </method>
-
-      <method name="removeNotification">
-        <parameter name="aItem"/>
-        <parameter name="aSkipAnimation"/>
-        <body>
-          <![CDATA[
-            if (aItem == this.currentNotification)
-              this.removeCurrentNotification(aSkipAnimation);
-            else if (aItem != this._closedNotification)
-              this._removeNotificationElement(aItem);
-            return aItem;
-          ]]>
-        </body>
-      </method>
-
-      <method name="_removeNotificationElement">
-        <parameter name="aChild"/>
-        <body>
-          <![CDATA[
-            if (aChild.eventCallback)
-              aChild.eventCallback("removed");
-            this.removeChild(aChild);
-
-            // make sure focus doesn't get lost (workaround for bug 570835)
-            let fm = Cc["@mozilla.org/focus-manager;1"]
-                       .getService(Ci.nsIFocusManager);
-            if (!fm.getFocusedElementForWindow(window, false, {}))
-              fm.moveFocus(window, this, fm.MOVEFOCUS_FORWARD, 0);
-          ]]>
-        </body>
-      </method>
-
-      <method name="removeCurrentNotification">
-        <parameter name="aSkipAnimation"/>
-        <body>
-          <![CDATA[
-            this._showNotification(this.currentNotification, false, aSkipAnimation);
-          ]]>
-        </body>
-      </method>
-
-      <method name="removeAllNotifications">
-        <parameter name="aImmediate"/>
-        <body>
-          <![CDATA[
-            var notifications = this.allNotifications;
-            for (var n = notifications.length - 1; n >= 0; n--) {
-              if (aImmediate)
-                this._removeNotificationElement(notifications[n]);
-              else
-                this.removeNotification(notifications[n]);
-            }
-            this.currentNotification = null;
-
-            // Clean up any currently-animating notification; this is necessary
-            // if a notification was just opened and is still animating, but we
-            // want to close it *without* animating.  This can even happen if
-            // the user toggled `toolkit.cosmeticAnimations.enabled` to false
-            // and called this method immediately after an animated notification
-            // displayed (although this case isn't very likely).
-            if (aImmediate || !this._allowAnimation)
-              this._finishAnimation();
-          ]]>
-        </body>
-      </method>
-
-      <method name="removeTransientNotifications">
-        <body>
-          <![CDATA[
-            var notifications = this.allNotifications;
-            for (var n = notifications.length - 1; n >= 0; n--) {
-              var notification = notifications[n];
-              if (notification.persistence)
-                notification.persistence--;
-              else if (Date.now() > notification.timeout)
-                this.removeNotification(notification);
-            }
-          ]]>
-        </body>
-      </method>
-
-      <method name="_showNotification">
-        <parameter name="aNotification"/>
-        <parameter name="aSlideIn"/>
-        <parameter name="aSkipAnimation"/>
-        <body>
-          <![CDATA[
-            this._finishAnimation();
-
-            var height = aNotification.boxObject.height;
-            var skipAnimation = aSkipAnimation || height == 0 ||
-                                !this._allowAnimation;
-            aNotification.classList.toggle("animated", !skipAnimation);
-
-            if (aSlideIn) {
-              this.currentNotification = aNotification;
-              aNotification.style.removeProperty("position");
-              aNotification.style.removeProperty("top");
-              aNotification.style.removeProperty("margin-top");
-              aNotification.style.removeProperty("opacity");
-
-              if (skipAnimation) {
-                this._setBlockingState(this.currentNotification);
-                return;
-              }
-            } else {
-              this._closedNotification = aNotification;
-              var notifications = this.allNotifications;
-              var idx = notifications.length - 1;
-              this.currentNotification = (idx >= 0) ? notifications[idx] : null;
-
-              if (skipAnimation) {
-                this._removeNotificationElement(this._closedNotification);
-                this._closedNotification = null;
-                this._setBlockingState(this.currentNotification);
-                return;
-              }
-
-              aNotification.style.marginTop = -height + "px";
-              aNotification.style.opacity = 0;
-            }
-
-            this._animating = true;
-          ]]>
-        </body>
-      </method>
-
-      <method name="_finishAnimation">
-        <body><![CDATA[
-          if (this._animating) {
-            this._animating = false;
-            if (this._closedNotification) {
-              this._removeNotificationElement(this._closedNotification);
-              this._closedNotification = null;
-            }
-            this._setBlockingState(this.currentNotification);
-          }
-        ]]></body>
-      </method>
-
-      <method name="_setBlockingState">
-        <parameter name="aNotification"/>
-        <body>
-          <![CDATA[
-            var isblock = aNotification &&
-                          aNotification.priority == this.PRIORITY_CRITICAL_BLOCK;
-            var canvas = this._blockingCanvas;
-            if (isblock) {
-              if (!canvas)
-                canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
-              const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-              let content = this.firstChild;
-              if (!content ||
-                   content.namespaceURI != XULNS ||
-                   content.localName != "browser")
-                return;
-
-              var width = content.boxObject.width;
-              var height = content.boxObject.height;
-              content.collapsed = true;
-
-              canvas.setAttribute("width", width);
-              canvas.setAttribute("height", height);
-              canvas.setAttribute("flex", "1");
-
-              this.appendChild(canvas);
-              this._blockingCanvas = canvas;
-
-              var bgcolor = "white";
-              try {
-                var prefService = Cc["@mozilla.org/preferences-service;1"].
-                                    getService(Ci.nsIPrefBranch);
-                bgcolor = prefService.getCharPref("browser.display.background_color");
-
-                var win = content.contentWindow;
-                var context = canvas.getContext("2d");
-                context.globalAlpha = 0.5;
-                context.drawWindow(win, win.scrollX, win.scrollY,
-                                   width, height, bgcolor);
-              } catch (ex) { }
-            } else if (canvas) {
-              canvas.remove();
-              this._blockingCanvas = null;
-              let content = this.firstChild;
-              if (content)
-                content.collapsed = false;
-            }
-          ]]>
-        </body>
-      </method>
-
-    </implementation>
-
-    <handlers>
-      <handler event="transitionend"><![CDATA[
-        if (event.target.localName == "notification" &&
-            event.propertyName == "margin-top")
-          this._finishAnimation();
-      ]]></handler>
-    </handlers>
-
-  </binding>
-
   <binding id="notification">
     <content>
       <xul:hbox anonid="details" align="center" flex="1"
                 oncommand="this.parentNode._doButtonCommand(event);">
         <xul:image anonid="messageImage" class="messageImage" xbl:inherits="src=image,type,value"/>
         <xul:description anonid="messageText" class="messageText" flex="1" xbl:inherits="xbl:text=label"/>
         <xul:spacer flex="1"/>
         <children/>
@@ -416,23 +44,17 @@
                                 onset="this.setAttribute('priority', val); return val;"/>
       <property name="persistence" onget="return parseInt(this.getAttribute('persistence')) || 0;"
                                    onset="this.setAttribute('persistence', val); return val;"/>
       <field name="timeout">0</field>
 
       <property name="control" readonly="true">
         <getter>
           <![CDATA[
-            var parent = this.parentNode;
-            while (parent) {
-              if (parent.localName == "notificationbox")
-                return parent;
-              parent = parent.parentNode;
-            }
-            return null;
+            return this.closest(".notificationbox-stack")._notificationBox;
           ]]>
         </getter>
       </property>
 
       <!-- This method should only be called when the user has
            manually closed the notification. If you want to
            programmatically close the notification, you should
            call close() instead. -->
copy from toolkit/content/widgets/notification.xml
copy to toolkit/content/widgets/notificationbox.js
--- a/toolkit/content/widgets/notification.xml
+++ b/toolkit/content/widgets/notificationbox.js
@@ -1,574 +1,267 @@
-<?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/. -->
-
-
-<!DOCTYPE bindings [
-<!ENTITY % notificationDTD SYSTEM "chrome://global/locale/notification.dtd">
-%notificationDTD;
-]>
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-<bindings id="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">
+"use strict";
 
-  <binding id="notificationbox">
-    <content>
-      <xul:stack xbl:inherits="hidden=notificationshidden"
-                 class="notificationbox-stack">
-        <xul:spacer/>
-        <children includes="notification"/>
-      </xul:stack>
-      <children/>
-    </content>
+// This is loaded into chrome windows with the subscript loader. If you need to
+// define globals, wrap in a block to prevent leaking onto `window`.
 
-    <implementation>
-      <field name="PRIORITY_INFO_LOW" readonly="true">1</field>
-      <field name="PRIORITY_INFO_MEDIUM" readonly="true">2</field>
-      <field name="PRIORITY_INFO_HIGH" readonly="true">3</field>
-      <field name="PRIORITY_WARNING_LOW" readonly="true">4</field>
-      <field name="PRIORITY_WARNING_MEDIUM" readonly="true">5</field>
-      <field name="PRIORITY_WARNING_HIGH" readonly="true">6</field>
-      <field name="PRIORITY_CRITICAL_LOW" readonly="true">7</field>
-      <field name="PRIORITY_CRITICAL_MEDIUM" readonly="true">8</field>
-      <field name="PRIORITY_CRITICAL_HIGH" readonly="true">9</field>
-      <field name="PRIORITY_CRITICAL_BLOCK" readonly="true">10</field>
-
-      <field name="currentNotification">null</field>
-
-      <field name="_closedNotification">null</field>
-      <field name="_blockingCanvas">null</field>
-      <field name="_animating">false</field>
-
-      <property name="_allowAnimation">
-        <getter>
-          <![CDATA[
-            var prefs = Cc["@mozilla.org/preferences-service;1"]
-                          .getService(Ci.nsIPrefBranch);
-            return prefs.getBoolPref("toolkit.cosmeticAnimations.enabled");
-          ]]>
-        </getter>
-      </property>
-
-      <property name="notificationsHidden"
-                onget="return this.getAttribute('notificationshidden') == 'true';">
-        <setter>
-          <![CDATA[
-            if (val)
-              this.setAttribute("notificationshidden", true);
-            else this.removeAttribute("notificationshidden");
-            return val;
-          ]]>
-        </setter>
-      </property>
+MozElements.NotificationBox = class NotificationBox {
+  /**
+   * Creates a new class to handle a notification box, but does not add any
+   * elements to the DOM until a notification has to be displayed.
+   *
+   * @param insertElementFn
+   *        Called with the "notification-stack" element as an argument when the
+   *        first notification has to be displayed.
+   */
+  constructor(insertElementFn) {
+    this._insertElementFn = insertElementFn;
+    this._animating = false;
+    this.currentNotification = null;
+  }
 
-      <property name="allNotifications" readonly="true">
-        <getter>
-          <![CDATA[
-            var closedNotification = this._closedNotification;
-            var notifications = this.getElementsByTagName("notification");
-            return Array.filter(notifications, n => n != closedNotification);
-          ]]>
-        </getter>
-      </property>
-
-      <method name="getNotificationWithValue">
-        <parameter name="aValue"/>
-        <body>
-          <![CDATA[
-            var notifications = this.allNotifications;
-            for (var n = notifications.length - 1; n >= 0; n--) {
-              if (aValue == notifications[n].getAttribute("value"))
-                return notifications[n];
-            }
-            return null;
-          ]]>
-        </body>
-      </method>
+  get stack() {
+    if (!this._stack) {
+      let stack = document.createXULElement("stack");
+      stack._notificationBox = this;
+      stack.className = "notificationbox-stack";
+      stack.appendChild(document.createXULElement("spacer"));
+      stack.addEventListener("transitionend", event => {
+        if (event.target.localName == "notification" &&
+            event.propertyName == "margin-top") {
+          this._finishAnimation();
+        }
+      });
+      this._insertElementFn(stack);
+      this._stack = stack;
+    }
+    return this._stack;
+  }
 
-      <method name="appendNotification">
-        <parameter name="aLabel"/>
-        <parameter name="aValue"/>
-        <parameter name="aImage"/>
-        <parameter name="aPriority"/>
-        <parameter name="aButtons"/>
-        <parameter name="aEventCallback"/>
-        <body>
-          <![CDATA[
-            if (aPriority < this.PRIORITY_INFO_LOW ||
-                aPriority > this.PRIORITY_CRITICAL_BLOCK)
-              throw "Invalid notification priority " + aPriority;
-
-            // check for where the notification should be inserted according to
-            // priority. If two are equal, the existing one appears on top.
-            var notifications = this.allNotifications;
-            var insertPos = null;
-            for (var n = notifications.length - 1; n >= 0; n--) {
-              if (notifications[n].priority < aPriority)
-                break;
-              insertPos = notifications[n];
-            }
+  get _allowAnimation() {
+    return Services.prefs.getBoolPref("toolkit.cosmeticAnimations.enabled");
+  }
 
-            var newitem = document.createXULElement("notification");
-            // Can't use instanceof in case this was created from a different document:
-            let labelIsDocFragment = aLabel && typeof aLabel == "object" && aLabel.nodeType &&
-                                     aLabel.nodeType == aLabel.DOCUMENT_FRAGMENT_NODE;
-            if (!labelIsDocFragment)
-              newitem.setAttribute("label", aLabel);
-            newitem.setAttribute("value", aValue);
-            if (aImage)
-              newitem.setAttribute("image", aImage);
-            newitem.eventCallback = aEventCallback;
+  get allNotifications() {
+    // Don't create any DOM if no new notification has been added yet.
+    if (!this._stack) {
+      return [];
+    }
 
-            if (aButtons) {
-              // The notification-button-default class is added to the button
-              // with isDefault set to true. If there is no such button, it is
-              // added to the first button (unless that button has isDefault
-              // set to false). There cannot be multiple default buttons.
-              var defaultElem;
-
-              for (var b = 0; b < aButtons.length; b++) {
-                var button = aButtons[b];
-                var buttonElem = document.createXULElement("button");
-                buttonElem.setAttribute("label", button.label);
-                if (typeof button.accessKey == "string")
-                  buttonElem.setAttribute("accesskey", button.accessKey);
-                buttonElem.classList.add("notification-button");
+    var closedNotification = this._closedNotification;
+    var notifications = this.stack.getElementsByTagName("notification");
+    return Array.filter(notifications, n => n != closedNotification);
+  }
 
-                if (button.isDefault ||
-                    b == 0 && !("isDefault" in button))
-                  defaultElem = buttonElem;
-
-                newitem.appendChild(buttonElem);
-                buttonElem.buttonInfo = button;
-              }
-
-              if (defaultElem)
-                defaultElem.classList.add("notification-button-default");
-            }
+  getNotificationWithValue(aValue) {
+    var notifications = this.allNotifications;
+    for (var n = notifications.length - 1; n >= 0; n--) {
+      if (aValue == notifications[n].getAttribute("value"))
+        return notifications[n];
+    }
+    return null;
+  }
 
-            newitem.setAttribute("priority", aPriority);
-            if (aPriority >= this.PRIORITY_CRITICAL_LOW)
-              newitem.setAttribute("type", "critical");
-            else if (aPriority <= this.PRIORITY_INFO_HIGH)
-              newitem.setAttribute("type", "info");
-            else
-              newitem.setAttribute("type", "warning");
-
-            if (!insertPos) {
-              newitem.style.position = "fixed";
-              newitem.style.top = "100%";
-              newitem.style.marginTop = "-15px";
-              newitem.style.opacity = "0";
-            }
-            this.insertBefore(newitem, insertPos);
-            // Can only insert the document fragment after the item has been created because
-            // otherwise the XBL structure isn't there yet:
-            if (labelIsDocFragment) {
-              document.getAnonymousElementByAttribute(newitem, "anonid", "messageText")
-                .appendChild(aLabel);
-            }
+  appendNotification(aLabel, aValue, aImage, aPriority, aButtons, aEventCallback) {
+    if (aPriority < this.PRIORITY_INFO_LOW ||
+      aPriority > this.PRIORITY_CRITICAL_HIGH)
+      throw "Invalid notification priority " + aPriority;
 
-            if (!insertPos)
-              this._showNotification(newitem, true);
-
-            // Fire event for accessibility APIs
-            var event = document.createEvent("Events");
-            event.initEvent("AlertActive", true, true);
-            newitem.dispatchEvent(event);
-
-            return newitem;
-          ]]>
-        </body>
-      </method>
+    // check for where the notification should be inserted according to
+    // priority. If two are equal, the existing one appears on top.
+    var notifications = this.allNotifications;
+    var insertPos = null;
+    for (var n = notifications.length - 1; n >= 0; n--) {
+      if (notifications[n].priority < aPriority)
+        break;
+      insertPos = notifications[n];
+    }
 
-      <method name="removeNotification">
-        <parameter name="aItem"/>
-        <parameter name="aSkipAnimation"/>
-        <body>
-          <![CDATA[
-            if (aItem == this.currentNotification)
-              this.removeCurrentNotification(aSkipAnimation);
-            else if (aItem != this._closedNotification)
-              this._removeNotificationElement(aItem);
-            return aItem;
-          ]]>
-        </body>
-      </method>
-
-      <method name="_removeNotificationElement">
-        <parameter name="aChild"/>
-        <body>
-          <![CDATA[
-            if (aChild.eventCallback)
-              aChild.eventCallback("removed");
-            this.removeChild(aChild);
+    var newitem = document.createXULElement("notification");
+    // Can't use instanceof in case this was created from a different document:
+    let labelIsDocFragment = aLabel && typeof aLabel == "object" && aLabel.nodeType &&
+      aLabel.nodeType == aLabel.DOCUMENT_FRAGMENT_NODE;
+    if (!labelIsDocFragment)
+      newitem.setAttribute("label", aLabel);
+    newitem.setAttribute("value", aValue);
+    if (aImage)
+      newitem.setAttribute("image", aImage);
+    newitem.eventCallback = aEventCallback;
 
-            // make sure focus doesn't get lost (workaround for bug 570835)
-            let fm = Cc["@mozilla.org/focus-manager;1"]
-                       .getService(Ci.nsIFocusManager);
-            if (!fm.getFocusedElementForWindow(window, false, {}))
-              fm.moveFocus(window, this, fm.MOVEFOCUS_FORWARD, 0);
-          ]]>
-        </body>
-      </method>
+    if (aButtons) {
+      // The notification-button-default class is added to the button
+      // with isDefault set to true. If there is no such button, it is
+      // added to the first button (unless that button has isDefault
+      // set to false). There cannot be multiple default buttons.
+      var defaultElem;
 
-      <method name="removeCurrentNotification">
-        <parameter name="aSkipAnimation"/>
-        <body>
-          <![CDATA[
-            this._showNotification(this.currentNotification, false, aSkipAnimation);
-          ]]>
-        </body>
-      </method>
-
-      <method name="removeAllNotifications">
-        <parameter name="aImmediate"/>
-        <body>
-          <![CDATA[
-            var notifications = this.allNotifications;
-            for (var n = notifications.length - 1; n >= 0; n--) {
-              if (aImmediate)
-                this._removeNotificationElement(notifications[n]);
-              else
-                this.removeNotification(notifications[n]);
-            }
-            this.currentNotification = null;
+      for (var b = 0; b < aButtons.length; b++) {
+        var button = aButtons[b];
+        var buttonElem = document.createXULElement("button");
+        buttonElem.setAttribute("label", button.label);
+        if (typeof button.accessKey == "string")
+          buttonElem.setAttribute("accesskey", button.accessKey);
+        buttonElem.classList.add("notification-button");
 
-            // Clean up any currently-animating notification; this is necessary
-            // if a notification was just opened and is still animating, but we
-            // want to close it *without* animating.  This can even happen if
-            // the user toggled `toolkit.cosmeticAnimations.enabled` to false
-            // and called this method immediately after an animated notification
-            // displayed (although this case isn't very likely).
-            if (aImmediate || !this._allowAnimation)
-              this._finishAnimation();
-          ]]>
-        </body>
-      </method>
+        if (button.isDefault ||
+          b == 0 && !("isDefault" in button))
+          defaultElem = buttonElem;
+
+        newitem.appendChild(buttonElem);
+        buttonElem.buttonInfo = button;
+      }
 
-      <method name="removeTransientNotifications">
-        <body>
-          <![CDATA[
-            var notifications = this.allNotifications;
-            for (var n = notifications.length - 1; n >= 0; n--) {
-              var notification = notifications[n];
-              if (notification.persistence)
-                notification.persistence--;
-              else if (Date.now() > notification.timeout)
-                this.removeNotification(notification);
-            }
-          ]]>
-        </body>
-      </method>
+      if (defaultElem)
+        defaultElem.classList.add("notification-button-default");
+    }
 
-      <method name="_showNotification">
-        <parameter name="aNotification"/>
-        <parameter name="aSlideIn"/>
-        <parameter name="aSkipAnimation"/>
-        <body>
-          <![CDATA[
-            this._finishAnimation();
-
-            var height = aNotification.boxObject.height;
-            var skipAnimation = aSkipAnimation || height == 0 ||
-                                !this._allowAnimation;
-            aNotification.classList.toggle("animated", !skipAnimation);
-
-            if (aSlideIn) {
-              this.currentNotification = aNotification;
-              aNotification.style.removeProperty("position");
-              aNotification.style.removeProperty("top");
-              aNotification.style.removeProperty("margin-top");
-              aNotification.style.removeProperty("opacity");
+    newitem.setAttribute("priority", aPriority);
+    if (aPriority >= this.PRIORITY_CRITICAL_LOW)
+      newitem.setAttribute("type", "critical");
+    else if (aPriority <= this.PRIORITY_INFO_HIGH)
+      newitem.setAttribute("type", "info");
+    else
+      newitem.setAttribute("type", "warning");
 
-              if (skipAnimation) {
-                this._setBlockingState(this.currentNotification);
-                return;
-              }
-            } else {
-              this._closedNotification = aNotification;
-              var notifications = this.allNotifications;
-              var idx = notifications.length - 1;
-              this.currentNotification = (idx >= 0) ? notifications[idx] : null;
-
-              if (skipAnimation) {
-                this._removeNotificationElement(this._closedNotification);
-                this._closedNotification = null;
-                this._setBlockingState(this.currentNotification);
-                return;
-              }
-
-              aNotification.style.marginTop = -height + "px";
-              aNotification.style.opacity = 0;
-            }
-
-            this._animating = true;
-          ]]>
-        </body>
-      </method>
+    if (!insertPos) {
+      newitem.style.position = "fixed";
+      newitem.style.top = "100%";
+      newitem.style.marginTop = "-15px";
+      newitem.style.opacity = "0";
+    }
+    this.stack.insertBefore(newitem, insertPos);
+    // Can only insert the document fragment after the item has been created because
+    // otherwise the XBL structure isn't there yet:
+    if (labelIsDocFragment) {
+      document.getAnonymousElementByAttribute(newitem, "anonid", "messageText")
+        .appendChild(aLabel);
+    }
 
-      <method name="_finishAnimation">
-        <body><![CDATA[
-          if (this._animating) {
-            this._animating = false;
-            if (this._closedNotification) {
-              this._removeNotificationElement(this._closedNotification);
-              this._closedNotification = null;
-            }
-            this._setBlockingState(this.currentNotification);
-          }
-        ]]></body>
-      </method>
+    if (!insertPos)
+      this._showNotification(newitem, true);
+
+    // Fire event for accessibility APIs
+    var event = document.createEvent("Events");
+    event.initEvent("AlertActive", true, true);
+    newitem.dispatchEvent(event);
 
-      <method name="_setBlockingState">
-        <parameter name="aNotification"/>
-        <body>
-          <![CDATA[
-            var isblock = aNotification &&
-                          aNotification.priority == this.PRIORITY_CRITICAL_BLOCK;
-            var canvas = this._blockingCanvas;
-            if (isblock) {
-              if (!canvas)
-                canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
-              const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-              let content = this.firstChild;
-              if (!content ||
-                   content.namespaceURI != XULNS ||
-                   content.localName != "browser")
-                return;
+    return newitem;
+  }
 
-              var width = content.boxObject.width;
-              var height = content.boxObject.height;
-              content.collapsed = true;
-
-              canvas.setAttribute("width", width);
-              canvas.setAttribute("height", height);
-              canvas.setAttribute("flex", "1");
+  removeNotification(aItem, aSkipAnimation) {
+    if (aItem == this.currentNotification)
+      this.removeCurrentNotification(aSkipAnimation);
+    else if (aItem != this._closedNotification)
+      this._removeNotificationElement(aItem);
+    return aItem;
+  }
 
-              this.appendChild(canvas);
-              this._blockingCanvas = canvas;
-
-              var bgcolor = "white";
-              try {
-                var prefService = Cc["@mozilla.org/preferences-service;1"].
-                                    getService(Ci.nsIPrefBranch);
-                bgcolor = prefService.getCharPref("browser.display.background_color");
+  _removeNotificationElement(aChild) {
+    if (aChild.eventCallback)
+      aChild.eventCallback("removed");
+    this.stack.removeChild(aChild);
 
-                var win = content.contentWindow;
-                var context = canvas.getContext("2d");
-                context.globalAlpha = 0.5;
-                context.drawWindow(win, win.scrollX, win.scrollY,
-                                   width, height, bgcolor);
-              } catch (ex) { }
-            } else if (canvas) {
-              canvas.remove();
-              this._blockingCanvas = null;
-              let content = this.firstChild;
-              if (content)
-                content.collapsed = false;
-            }
-          ]]>
-        </body>
-      </method>
+    // make sure focus doesn't get lost (workaround for bug 570835)
+    if (!Services.focus.getFocusedElementForWindow(window, false, {})) {
+      Services.focus.moveFocus(window, this.stack,
+                               Services.focus.MOVEFOCUS_FORWARD, 0);
+    }
+  }
 
-    </implementation>
+  removeCurrentNotification(aSkipAnimation) {
+    this._showNotification(this.currentNotification, false, aSkipAnimation);
+  }
 
-    <handlers>
-      <handler event="transitionend"><![CDATA[
-        if (event.target.localName == "notification" &&
-            event.propertyName == "margin-top")
-          this._finishAnimation();
-      ]]></handler>
-    </handlers>
-
-  </binding>
+  removeAllNotifications(aImmediate) {
+    var notifications = this.allNotifications;
+    for (var n = notifications.length - 1; n >= 0; n--) {
+      if (aImmediate)
+        this._removeNotificationElement(notifications[n]);
+      else
+        this.removeNotification(notifications[n]);
+    }
+    this.currentNotification = null;
 
-  <binding id="notification">
-    <content>
-      <xul:hbox anonid="details" align="center" flex="1"
-                oncommand="this.parentNode._doButtonCommand(event);">
-        <xul:image anonid="messageImage" class="messageImage" xbl:inherits="src=image,type,value"/>
-        <xul:description anonid="messageText" class="messageText" flex="1" xbl:inherits="xbl:text=label"/>
-        <xul:spacer flex="1"/>
-        <children/>
-      </xul:hbox>
-      <xul:toolbarbutton ondblclick="event.stopPropagation();"
-                         class="messageCloseButton close-icon tabbable"
-                         anonid="close-button"
-                         xbl:inherits="hidden=hideclose"
-                         tooltiptext="&closeNotification.tooltip;"
-                         oncommand="document.getBindingParent(this).dismiss();"/>
-    </content>
-    <implementation>
-      <property name="label" onset="this.setAttribute('label', val); return val;"
-                             onget="return this.getAttribute('label');"/>
-      <property name="value" onset="this.setAttribute('value', val); return val;"
-                             onget="return this.getAttribute('value');"/>
-      <property name="image" onset="this.setAttribute('image', val); return val;"
-                             onget="return this.getAttribute('image');"/>
-      <property name="type" onset="this.setAttribute('type', val); return val;"
-                            onget="return this.getAttribute('type');"/>
-      <property name="priority" onget="return parseInt(this.getAttribute('priority')) || 0;"
-                                onset="this.setAttribute('priority', val); return val;"/>
-      <property name="persistence" onget="return parseInt(this.getAttribute('persistence')) || 0;"
-                                   onset="this.setAttribute('persistence', val); return val;"/>
-      <field name="timeout">0</field>
+    // Clean up any currently-animating notification; this is necessary
+    // if a notification was just opened and is still animating, but we
+    // want to close it *without* animating.  This can even happen if
+    // the user toggled `toolkit.cosmeticAnimations.enabled` to false
+    // and called this method immediately after an animated notification
+    // displayed (although this case isn't very likely).
+    if (aImmediate || !this._allowAnimation)
+      this._finishAnimation();
+  }
 
-      <property name="control" readonly="true">
-        <getter>
-          <![CDATA[
-            var parent = this.parentNode;
-            while (parent) {
-              if (parent.localName == "notificationbox")
-                return parent;
-              parent = parent.parentNode;
-            }
-            return null;
-          ]]>
-        </getter>
-      </property>
+  removeTransientNotifications() {
+    var notifications = this.allNotifications;
+    for (var n = notifications.length - 1; n >= 0; n--) {
+      var notification = notifications[n];
+      if (notification.persistence)
+        notification.persistence--;
+      else if (Date.now() > notification.timeout)
+        this.removeNotification(notification);
+    }
+  }
 
-      <!-- This method should only be called when the user has
-           manually closed the notification. If you want to
-           programmatically close the notification, you should
-           call close() instead. -->
-      <method name="dismiss">
-        <body>
-          <![CDATA[
-            if (this.eventCallback) {
-              this.eventCallback("dismissed");
-            }
-            this.close();
-          ]]>
-        </body>
-      </method>
+  _showNotification(aNotification, aSlideIn, aSkipAnimation) {
+    this._finishAnimation();
 
-      <method name="close">
-        <body>
-          <![CDATA[
-            var control = this.control;
-            if (control)
-              control.removeNotification(this);
-            else
-              this.hidden = true;
-          ]]>
-        </body>
-      </method>
-
-      <method name="_doButtonCommand">
-        <parameter name="aEvent"/>
-        <body>
-          <![CDATA[
-            if (!("buttonInfo" in aEvent.target))
-              return;
+    var height = aNotification.boxObject.height;
+    var skipAnimation = aSkipAnimation || height == 0 ||
+      !this._allowAnimation;
+    aNotification.classList.toggle("animated", !skipAnimation);
 
-            var button = aEvent.target.buttonInfo;
-            if (button.popup) {
-              document.getElementById(button.popup).
-                openPopup(aEvent.originalTarget, "after_start", 0, 0, false, false, aEvent);
-              aEvent.stopPropagation();
-            } else {
-              var callback = button.callback;
-              if (callback) {
-                var result = callback(this, button, aEvent.target, aEvent);
-                if (!result)
-                  this.close();
-                aEvent.stopPropagation();
-              }
-            }
-          ]]>
-        </body>
-      </method>
-    </implementation>
-  </binding>
+    if (aSlideIn) {
+      this.currentNotification = aNotification;
+      aNotification.style.removeProperty("position");
+      aNotification.style.removeProperty("top");
+      aNotification.style.removeProperty("margin-top");
+      aNotification.style.removeProperty("opacity");
+
+      if (skipAnimation) {
+        return;
+      }
+    } else {
+      this._closedNotification = aNotification;
+      var notifications = this.allNotifications;
+      var idx = notifications.length - 1;
+      this.currentNotification = (idx >= 0) ? notifications[idx] : null;
 
-  <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"/>
-              <!-- 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"/></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">
-        <children includes="button"/>
-        <xul:button anonid="secondarybutton"
-                    class="popup-notification-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"
-                    default="true"
-                    label="&defaultButton.label;"
-                    accesskey="&defaultButton.accesskey;"
-                    xbl:inherits="oncommand=buttoncommand,label=buttonlabel,accesskey=buttonaccesskey,highlight=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>
+      if (skipAnimation) {
+        this._removeNotificationElement(this._closedNotification);
+        delete this._closedNotification;
+        return;
+      }
+
+      aNotification.style.marginTop = -height + "px";
+      aNotification.style.opacity = 0;
+    }
+
+    this._animating = true;
+  }
+
+  _finishAnimation() {
+    if (this._animating) {
+      this._animating = false;
+      if (this._closedNotification) {
+        this._removeNotificationElement(this._closedNotification);
+        delete this._closedNotification;
+      }
+    }
+  }
+};
+
+// These are defined on the instance prototype for backwards compatibility.
+Object.assign(MozElements.NotificationBox.prototype, {
+  PRIORITY_INFO_LOW: 1,
+  PRIORITY_INFO_MEDIUM: 2,
+  PRIORITY_INFO_HIGH: 3,
+  PRIORITY_WARNING_LOW: 4,
+  PRIORITY_WARNING_MEDIUM: 5,
+  PRIORITY_WARNING_HIGH: 6,
+  PRIORITY_CRITICAL_LOW: 7,
+  PRIORITY_CRITICAL_MEDIUM: 8,
+  PRIORITY_CRITICAL_HIGH: 9,
+});
--- a/toolkit/content/xul.css
+++ b/toolkit/content/xul.css
@@ -173,33 +173,20 @@ iframe {
 }
 
 browser {
   -moz-binding: url("chrome://global/content/bindings/browser.xml#browser");
 }
 
 /********** notifications **********/
 
-notificationbox {
-  -moz-binding: url("chrome://global/content/bindings/notification.xml#notificationbox");
-  -moz-box-orient: vertical;
-}
-
-.notificationbox-stack {
-  overflow: -moz-hidden-unscrollable;
-}
-
 notification {
   -moz-binding: url("chrome://global/content/bindings/notification.xml#notification");
 }
 
-notification.animated {
-  transition: margin-top 300ms var(--animation-easing-function), opacity 300ms var(--animation-easing-function);
-}
-
 /*********** popup notification ************/
 popupnotification {
   -moz-binding: url("chrome://global/content/bindings/notification.xml#popup-notification");
 }
 
 .popup-notification-menubutton:not([label]) {
   display: none;
 }
--- a/toolkit/themes/shared/notification.inc.css
+++ b/toolkit/themes/shared/notification.inc.css
@@ -1,30 +1,38 @@
 /* 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/. */
 
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 
+.notificationbox-stack {
+  overflow: -moz-hidden-unscrollable;
+}
+
 notification {
   padding: 2px 0 3px;
   padding-inline-start: 12px;
   background: -moz-dialog;
   color: -moz-dialogText;
   border-color: ThreeDLightShadow;
   border-style: solid;
   border-width: 1px 0;
   text-shadow: none;
 }
 
-notificationbox[notificationside="top"] > notification {
+notification.animated {
+  transition: margin-top 300ms var(--animation-easing-function), opacity 300ms var(--animation-easing-function);
+}
+
+.notificationbox-stack[notificationside="top"] > notification {
   border-top-style: none;
 }
 
-notificationbox[notificationside="bottom"] > notification {
+.notificationbox-stack[notificationside="bottom"] > notification {
   border-bottom-style: none;
 }
 
 notification[type="warning"],
 notification[type="critical"] {
   border-color: rgba(12, 12, 13, 0.2);
 }