Bug 1496827 - Remove the "notification" binding. r=bgrins
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Tue, 13 Nov 2018 12:56:42 +0000
changeset 446395 91320b1c87ceea9d1097209a6dba3ac8406272a4
parent 446394 c45b58286c4750314cef6991e3d97361ed24bc72
child 446396 eb7c8ac30b9e1c1f791a8b200fa13b563bb2caf5
push id35039
push userrmaries@mozilla.com
push dateWed, 14 Nov 2018 22:17:41 +0000
treeherdermozilla-central@b0a40093b6b7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1496827
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 1496827 - Remove the "notification" binding. r=bgrins Differential Revision: https://phabricator.services.mozilla.com/D11650
browser/base/content/browser-captivePortal.js
browser/base/content/browser-plugins.js
browser/base/content/browser.css
browser/base/content/browser.js
browser/base/content/test/general/browser_decoderDoctor.js
browser/base/content/test/general/browser_refreshBlocker.js
browser/base/content/test/plugins/browser_enable_DRM_prompt.js
browser/base/content/test/plugins/browser_globalplugin_crashinfobar.js
browser/base/content/test/urlbar/browser_urlbarSearchSingleWordNotification.js
browser/components/extensions/test/browser/browser_ext_slow_script.js
browser/components/feeds/test/browser/browser_registerProtocolHandler_notification.js
browser/components/tests/browser/browser_bug538331.js
browser/components/translation/Translation.jsm
browser/components/translation/content/.eslintrc.js
browser/components/translation/content/jar.mn
browser/components/translation/content/microsoft-translator-attribution.png
browser/components/translation/content/moz.build
browser/components/translation/content/translation-notification.js
browser/components/translation/jar.mn
browser/components/translation/microsoft-translator-attribution.png
browser/components/translation/moz.build
browser/components/translation/translation-infobar.xml
browser/modules/test/browser/browser_UnsubmittedCrashHandler.js
browser/themes/shared/translation/infobar.inc.css
devtools/client/scratchpad/test/browser_scratchpad_open.js
devtools/client/scratchpad/test/browser_scratchpad_recent_files.js
devtools/shared/fronts/csscoverage.js
testing/firefox-ui/tests/functional/safebrowsing/test_notification.py
testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
toolkit/components/normandy/lib/Heartbeat.jsm
toolkit/components/normandy/skin/shared/Heartbeat.css
toolkit/components/normandy/test/browser/browser_Heartbeat.js
toolkit/content/tests/chrome/test_notificationbox.xul
toolkit/content/widgets/notification.xml
toolkit/content/widgets/notificationbox.js
toolkit/content/xul.css
toolkit/themes/linux/global/global.css
toolkit/themes/osx/global/global.css
toolkit/themes/shared/notification.inc.css
uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js
--- a/browser/base/content/browser-captivePortal.js
+++ b/browser/base/content/browser-captivePortal.js
@@ -210,17 +210,16 @@ var CaptivePortalWatcher = {
       {
         label: this._browserBundle.GetStringFromName("captivePortal.showLoginPage2"),
         callback: () => {
           this.ensureCaptivePortalTab();
 
           // Returning true prevents the notification from closing.
           return true;
         },
-        isDefault: true,
       },
     ];
 
     let message = this._browserBundle.GetStringFromName("captivePortal.infoMessage3");
 
     let closeHandler = (aEventName) => {
       if (aEventName != "removed") {
         return;
--- a/browser/base/content/browser-plugins.js
+++ b/browser/base/content/browser-plugins.js
@@ -477,14 +477,13 @@ var gPluginHandler = {
 
     // Add the "learn more" link.
     let link = notification.ownerDocument.createXULElement("label");
     link.className = "text-link";
     link.setAttribute("value", gNavigatorBundle.getString("crashedpluginsMessage.learnMore"));
     let crashurl = formatURL("app.support.baseURL", true);
     crashurl += "plugin-crashed-notificationbar";
     link.href = crashurl;
-    let description = notification.ownerDocument.getAnonymousElementByAttribute(notification, "anonid", "messageText");
-    description.appendChild(link);
+    notification.messageText.appendChild(link);
   },
 };
 
 gPluginHandler.init();
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -1053,21 +1053,16 @@ browser[tabmodalPromptShowing] {
 }
 
 #statuspanel-inner {
   height: 3em;
   width: 100%;
   -moz-box-align: end;
 }
 
-/* Translation */
-notification[value="translation"] {
-  -moz-binding: url("chrome://browser/content/translation-infobar.xml#translationbar");
-}
-
 /*** Visibility of downloads indicator controls ***/
 
 /* Bug 924050: If we've loaded the indicator, for now we hide it in the menu panel,
    and just show the icon. This is a hack to side-step very weird layout bugs that
    seem to be caused by the indicator stack interacting with the menu panel. */
 #downloads-button[indicator]:not([cui-areatype="menu-panel"]) > .toolbarbutton-badge-stack > image.toolbarbutton-icon,
 #downloads-button[indicator][cui-areatype="menu-panel"] > #downloads-indicator-anchor {
   display: none;
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -237,16 +237,21 @@ XPCOMUtils.defineLazyGetter(this, "Win7F
         }
       },
       handledOpening: false,
     };
   }
   return null;
 });
 
+customElements.setElementCreationCallback("translation-notification", () => {
+  Services.scriptloader.loadSubScript(
+    "chrome://browser/content/translation-notification.js", window);
+});
+
 var gBrowser;
 var gLastValidURLStr = "";
 var gInPrintPreviewMode = false;
 var gContextMenu = null; // nsContextMenu instance
 var gMultiProcessBrowser =
   window.docShell
         .QueryInterface(Ci.nsILoadContext)
         .useRemoteTabs;
--- a/browser/base/content/test/general/browser_decoderDoctor.js
+++ b/browser/base/content/test/general/browser_decoderDoctor.js
@@ -42,20 +42,20 @@ async function test_decoder_doctor_notif
     try {
       notification = await awaitNotificationBar;
     } catch (ex) {
       ok(false, ex);
       return;
     }
     ok(notification, "Got decoder-doctor-notification notification");
 
-    is(notification.getAttribute("label"), notificationMessage,
+    is(notification.messageText.textContent, notificationMessage,
        "notification message should match expectation");
 
-    let button = notification.children[0];
+    let button = notification.querySelector("button");
     if (!label) {
       ok(!button, "There should not be button");
       return;
     }
 
     is(button.getAttribute("label"),
        label,
        `notification button should be '${label}'`);
--- a/browser/base/content/test/general/browser_refreshBlocker.js
+++ b/browser/base/content/test/general/browser_refreshBlocker.js
@@ -95,17 +95,17 @@ async function testRealRefresh(refreshPa
     await BrowserTestUtils.browserLoaded(browser);
 
     // Once browserLoaded resolves, all nsIWebProgressListener callbacks
     // should have fired, so the notification should be visible.
     let notificationBox = gBrowser.getNotificationBox(browser);
     let notification = notificationBox.currentNotification;
 
     ok(notification, "Notification should be visible");
-    is(notification.value, "refresh-blocked",
+    is(notification.getAttribute("value"), "refresh-blocked",
        "Should be showing the right notification");
 
     // Then click the button to allow the refresh.
     let buttons = notification.querySelectorAll(".notification-button");
     is(buttons.length, 1, "Should have one button.");
 
     // Prepare a Promise that should resolve when the refresh goes through
     let refreshPromise = BrowserTestUtils.browserLoaded(browser);
--- a/browser/base/content/test/plugins/browser_enable_DRM_prompt.js
+++ b/browser/base/content/test/plugins/browser_enable_DRM_prompt.js
@@ -40,17 +40,17 @@ add_task(async function() {
     });
     is(result.rejected, true, "EME request should be denied because EME disabled.");
 
     // Verify the UI prompt showed.
     let box = gBrowser.getNotificationBox(browser);
     let notification = box.currentNotification;
 
     ok(notification, "Notification should be visible");
-    is(notification.value, "drmContentDisabled",
+    is(notification.getAttribute("value"), "drmContentDisabled",
        "Should be showing the right notification");
 
     // Verify the "Enable DRM" button is there.
     let buttons = notification.querySelectorAll(".notification-button");
     is(buttons.length, 1, "Should have one button.");
 
     // Prepare a Promise that should resolve when the "Enable DRM" button's
     // page reload completes.
--- a/browser/base/content/test/plugins/browser_globalplugin_crashinfobar.js
+++ b/browser/base/content/test/plugins/browser_globalplugin_crashinfobar.js
@@ -22,13 +22,13 @@ add_task(async function() {
     });
 
     let notification = await waitForNotificationBar("plugin-crashed", browser);
 
     let notificationBox = gBrowser.getNotificationBox(browser);
     ok(notification, "Infobar was shown.");
     is(notification.priority, notificationBox.PRIORITY_WARNING_MEDIUM,
        "Correct priority.");
-    is(notification.getAttribute("label"),
+    is(notification.messageText.textContent,
        "The GlobalTestPlugin plugin has crashed.",
        "Correct message.");
   });
 });
--- a/browser/base/content/test/urlbar/browser_urlbarSearchSingleWordNotification.js
+++ b/browser/base/content/test/urlbar/browser_urlbarSearchSingleWordNotification.js
@@ -9,29 +9,18 @@ registerCleanupFunction(function() {
   }
 });
 
 function promiseNotification(aBrowser, value, expected, input) {
   return new Promise(resolve => {
     let notificationBox = aBrowser.getNotificationBox(aBrowser.selectedBrowser);
     if (expected) {
       info("Waiting for " + value + " notification");
-      let checkForNotification = function() {
-        if (notificationBox.getNotificationWithValue(value)) {
-          info("Saw the notification");
-          notificationObserver.disconnect();
-          notificationObserver = null;
-          resolve();
-        }
-      };
-      if (notificationObserver) {
-        notificationObserver.disconnect();
-      }
-      notificationObserver = new MutationObserver(checkForNotification);
-      notificationObserver.observe(notificationBox.stack, {childList: true});
+      resolve(BrowserTestUtils.waitForNotificationInNotificationBox(
+        notificationBox, value));
     } else {
       setTimeout(() => {
         is(notificationBox.getNotificationWithValue(value), null,
            `We are expecting to not get a notification for ${input}`);
         resolve();
       }, 1000);
     }
   });
@@ -147,17 +136,17 @@ function get_test_function_for_localhost
       expectSearch: true,
       expectNotification: true,
       aWindow: win,
     });
 
     let notificationBox = browser.getNotificationBox(tab.linkedBrowser);
     let notification = notificationBox.getNotificationWithValue("keyword-uri-fixup");
     let docLoadPromise = waitForDocLoadAndStopIt("http://" + hostName + "/", tab.linkedBrowser);
-    notification.querySelector(".notification-button-default").click();
+    notification.querySelector("button").click();
 
     // check pref value
     let prefValue = Services.prefs.getBoolPref(pref);
     is(prefValue, !isPrivate, "Pref should have the correct state.");
 
     await docLoadPromise;
     browser.removeTab(tab);
 
--- a/browser/components/extensions/test/browser/browser_ext_slow_script.js
+++ b/browser/components/extensions/test/browser/browser_ext_slow_script.js
@@ -44,17 +44,17 @@ add_task(async function test_slow_conten
     },
   });
 
   await extension.startup();
 
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
 
   let notification = await BrowserTestUtils.waitForGlobalNotificationBar(window, "process-hang");
-  let text = document.getAnonymousElementByAttribute(notification, "anonid", "messageText").textContent;
+  let text = notification.messageText.textContent;
 
   ok(text.includes("\u201cSlow Script Extension\u201d"),
      "Label is correct");
 
   let stopButton = notification.querySelector("[label='Stop It']");
   stopButton.click();
 
   BrowserTestUtils.removeTab(tab);
--- a/browser/components/feeds/test/browser/browser_registerProtocolHandler_notification.js
+++ b/browser/components/feeds/test/browser/browser_registerProtocolHandler_notification.js
@@ -17,21 +17,20 @@ add_task(async function() {
 
   let notificationBox = window.gBrowser.getNotificationBox();
   let notification = notificationBox.getNotificationWithValue(notificationValue);
   ok(notification, "Notification box should be displayed");
   if (notification == null) {
     finish();
     return;
   }
-  is(notification.type, "info", "We expect this notification to have the type of 'info'.");
-  isnot(notification.image, null, "We expect this notification to have an icon.");
+  is(notification.getAttribute("type"), "info",
+     "We expect this notification to have the type of 'info'.");
+  ok(notification.messageImage.getAttribute("src"),
+     "We expect this notification to have an icon.");
 
-  let buttons = notification.getElementsByClassName("notification-button-default");
-  is(buttons.length, 1, "We expect see one default button.");
-
-  buttons = notification.getElementsByClassName("notification-button");
+  let buttons = notification.getElementsByClassName("notification-button");
   is(buttons.length, 1, "We expect see one button.");
 
   let button = buttons[0];
   isnot(button.label, null, "We expect the add button to have a label.");
   todo_isnot(button.accesskey, null, "We expect the add button to have a accesskey.");
 });
--- a/browser/components/tests/browser/browser_bug538331.js
+++ b/browser/components/tests/browser/browser_bug538331.js
@@ -328,17 +328,18 @@ function testShowNotification() {
 
     let updateBox = gHighPriorityNotificationBox.getNotificationWithValue(
                                                     "post-update-notification");
     if (testCase.actions && testCase.actions.includes("showNotification") &&
         !testCase.actions.includes("silent")) {
       ok(updateBox, "Update notification box should have been displayed");
       if (updateBox) {
         if (testCase.notificationText) {
-          is(updateBox.label, testCase.notificationText, "Update notification box " +
+          is(updateBox.messageText.textContent, testCase.notificationText,
+             "Update notification box " +
              "should have the label provided by the update");
         }
         if (testCase.notificationButtonLabel) {
           var button = updateBox.getElementsByTagName("button").item(0);
           is(button.label, testCase.notificationButtonLabel, "Update notification " +
              "box button should have the label provided by the update");
           if (testCase.notificationButtonAccessKey) {
             let accessKey = button.getAttribute("accesskey");
--- a/browser/components/translation/Translation.jsm
+++ b/browser/components/translation/Translation.jsm
@@ -223,17 +223,18 @@ TranslationUI.prototype = {
 
   get notificationBox() {
     return this.browser.ownerGlobal.gBrowser.getNotificationBox(this.browser);
   },
 
   showTranslationInfoBar() {
     let notificationBox = this.notificationBox;
     let notif = notificationBox.appendNotification("", "translation", null,
-                                                   notificationBox.PRIORITY_INFO_HIGH);
+      notificationBox.PRIORITY_INFO_HIGH, null, null,
+      "translation-notification");
     notif.init(this);
     return notif;
   },
 
   shouldShowInfoBar(aURI) {
     // Never show the infobar automatically while the translation
     // service is temporarily unavailable.
     if (Translation.serviceUnavailable)
copy from browser/components/customizableui/content/.eslintrc.js
copy to browser/components/translation/content/.eslintrc.js
rename from browser/components/translation/jar.mn
rename to browser/components/translation/content/jar.mn
--- a/browser/components/translation/jar.mn
+++ b/browser/components/translation/content/jar.mn
@@ -1,6 +1,6 @@
 # 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/.
 browser.jar:
-       content/browser/translation-infobar.xml
+       content/browser/translation-notification.js
        content/browser/microsoft-translator-attribution.png
rename from browser/components/translation/microsoft-translator-attribution.png
rename to browser/components/translation/content/microsoft-translator-attribution.png
copy from browser/components/customizableui/content/moz.build
copy to browser/components/translation/content/moz.build
rename from browser/components/translation/translation-infobar.xml
rename to browser/components/translation/content/translation-notification.js
--- a/browser/components/translation/translation-infobar.xml
+++ b/browser/components/translation/content/translation-notification.js
@@ -1,439 +1,349 @@
-<?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;
-<!ENTITY % translationDTD SYSTEM "chrome://browser/locale/translation.dtd" >
-%translationDTD;
-]>
-
-<bindings id="translationBindings"
-          xmlns="http://www.mozilla.org/xbl"
-          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-          xmlns:xbl="http://www.mozilla.org/xbl">
-  <binding id="translationbar" extends="chrome://global/content/bindings/notification.xml#notification">
-    <content>
-      <xul:hbox anonid="details" align="center" flex="1">
-        <xul:image class="translate-infobar-element messageImage"
-                   anonid="messageImage"/>
-        <xul:panel anonid="welcomePanel" class="translation-welcome-panel"
-                   type="arrow" align="start">
-          <xul:image class="translation-welcome-logo"/>
-          <xul:vbox flex="1" class="translation-welcome-content">
-            <xul:description class="translation-welcome-headline"
-                             anonid="welcomeHeadline"/>
-            <xul:description class="translation-welcome-body" anonid="welcomeBody"/>
-            <xul:hbox align="center">
-              <xul:label anonid="learnMore" class="plain text-link"
-                         onclick="openTrustedLinkIn('https://support.mozilla.org/kb/automatic-translation', 'tab'); this.parentNode.parentNode.parentNode.hidePopup();"/>
-              <xul:spacer flex="1"/>
-              <xul:button class="translate-infobar-element" anonid="thanksButton"
-                          onclick="this.parentNode.parentNode.parentNode.hidePopup();"/>
-            </xul:hbox>
-          </xul:vbox>
-        </xul:panel>
-        <xul:deck anonid="translationStates" selectedIndex="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/. */
 
-          <!-- offer to translate -->
-          <xul:hbox class="translate-offer-box" align="center">
-            <xul:label class="translate-infobar-element" value="&translation.thisPageIsIn.label;"/>
-            <xul:menulist class="translate-infobar-element" anonid="detectedLanguage">
-              <xul:menupopup/>
-            </xul:menulist>
-            <xul:label class="translate-infobar-element" value="&translation.translateThisPage.label;"/>
-            <xul:button class="translate-infobar-element"
-                        label="&translation.translate.button;"
-                        anonid="translate"
-                        oncommand="document.getBindingParent(this).translate();"/>
-            <xul:button class="translate-infobar-element"
-                        label="&translation.notNow.button;" anonid="notNow"
-                        oncommand="document.getBindingParent(this).closeCommand();"/>
-          </xul:hbox>
-
-          <!-- translating -->
-          <xul:vbox class="translating-box" pack="center">
-            <xul:label class="translate-infobar-element"
-                       value="&translation.translatingContent.label;"/>
-          </xul:vbox>
-
-          <!-- translated -->
-          <xul:hbox class="translated-box" align="center">
-            <xul:label class="translate-infobar-element"
-                       value="&translation.translatedFrom.label;"/>
-            <xul:menulist class="translate-infobar-element"
-                          anonid="fromLanguage"
-                          oncommand="document.getBindingParent(this).translate()">
-              <xul:menupopup/>
-            </xul:menulist>
-            <xul:label class="translate-infobar-element"
-                       value="&translation.translatedTo.label;"/>
-            <xul:menulist class="translate-infobar-element"
-                          anonid="toLanguage"
-                          oncommand="document.getBindingParent(this).translate()">
-              <xul:menupopup/>
-            </xul:menulist>
-            <xul:label class="translate-infobar-element"
-                       value="&translation.translatedToSuffix.label;"/>
-            <xul:button anonid="showOriginal"
-                        class="translate-infobar-element"
-                        label="&translation.showOriginal.button;"
-                        oncommand="document.getBindingParent(this).showOriginal();"/>
-            <xul:button anonid="showTranslation"
-                        class="translate-infobar-element"
-                        label="&translation.showTranslation.button;"
-                        oncommand="document.getBindingParent(this).showTranslation();"/>
-          </xul:hbox>
-
-          <!-- error -->
-          <xul:hbox class="translation-error" align="center">
-            <xul:label class="translate-infobar-element"
-                       value="&translation.errorTranslating.label;"/>
-            <xul:button class="translate-infobar-element"
-                        label="&translation.tryAgain.button;"
-                        anonid="tryAgain"
-                        oncommand="document.getBindingParent(this).translate();"/>
-          </xul:hbox>
-
-          <!-- unavailable -->
-          <xul:vbox class="translation-unavailable" pack="center">
-            <xul:label class="translate-infobar-element"
-                       value="&translation.serviceUnavailable.label;"/>
-          </xul:vbox>
-
-        </xul:deck>
-        <xul:spacer flex="1"/>
+"use strict";
 
-        <xul:button type="menu"
-                    class="translate-infobar-element options-menu-button"
-                    anonid="options"
-                    label="&translation.options.menu;">
-          <xul:menupopup class="translation-menupopup cui-widget-panel cui-widget-panelview
-                                cui-widget-panelWithFooter PanelUI-subView"
-                         onpopupshowing="document.getBindingParent(this).optionsShowing();">
-            <xul:menuitem anonid="neverForLanguage"
-                          oncommand="document.getBindingParent(this).neverForLanguage();"/>
-            <xul:menuitem anonid="neverForSite"
-                          oncommand="document.getBindingParent(this).neverForSite();"
-                          label="&translation.options.neverForSite.label;"
-                          accesskey="&translation.options.neverForSite.accesskey;"/>
-            <xul:menuseparator/>
-            <xul:menuitem oncommand="openPreferences('paneGeneral', {origin:'translationInfobar'});"
-                          label="&translation.options.preferences.label;"
-                          accesskey="&translation.options.preferences.accesskey;"/>
-            <xul:menuitem class="subviewbutton panel-subview-footer"
-                          oncommand="document.getBindingParent(this).openProviderAttribution();">
-              <xul:deck anonid="translationEngine" selectedIndex="0">
-                <xul:hbox class="translation-attribution">
-                  <xul:label>&translation.options.attribution.beforeLogo;</xul:label>
-                  <xul:image src="chrome://browser/content/microsoft-translator-attribution.png"
-                             aria-label="Microsoft Translator"/>
-                  <xul:label>&translation.options.attribution.afterLogo;</xul:label>
-                </xul:hbox>
-                <xul:label class="translation-attribution">&translation.options.attribution.yandexTranslate;</xul:label>
-              </xul:deck>
-            </xul:menuitem>
-          </xul:menupopup>
-        </xul:button>
-
-      </xul:hbox>
-      <xul:toolbarbutton ondblclick="event.stopPropagation();"
-                         anonid="closeButton"
-                         class="messageCloseButton close-icon tabbable"
-                         xbl:inherits="hidden=hideclose"
-                         tooltiptext="&closeNotification.tooltip;"
-                         oncommand="document.getBindingParent(this).closeCommand();"/>
-    </content>
-    <implementation>
-      <property name="state"
-                onget="return this._getAnonElt('translationStates').selectedIndex;">
-        <setter>
-          <![CDATA[
-          let deck = this._getAnonElt("translationStates");
-
-          let activeElt = document.activeElement;
-          if (activeElt && deck.contains(activeElt))
-            activeElt.blur();
+class MozTranslationNotification extends MozElements.Notification {
+  connectedCallback() {
+    this.appendChild(MozXULElement.parseXULToFragment(`
+      <hbox anonid="details" align="center" flex="1">
+        <image class="translate-infobar-element messageImage"/>
+        <panel anonid="welcomePanel" class="translation-welcome-panel" type="arrow" align="start">
+          <image class="translation-welcome-logo"/>
+          <vbox flex="1" class="translation-welcome-content">
+            <description class="translation-welcome-headline" anonid="welcomeHeadline"/>
+            <description class="translation-welcome-body" anonid="welcomeBody"/>
+            <hbox align="center">
+              <label anonid="learnMore" class="plain text-link" onclick="openTrustedLinkIn('https://support.mozilla.org/kb/automatic-translation', 'tab'); this.parentNode.parentNode.parentNode.hidePopup();"/>
+              <spacer flex="1"/>
+              <button class="translate-infobar-element" anonid="thanksButton" onclick="this.parentNode.parentNode.parentNode.hidePopup();"/>
+            </hbox>
+          </vbox>
+        </panel>
+        <deck anonid="translationStates" selectedIndex="0">
+          <hbox class="translate-offer-box" align="center">
+            <label class="translate-infobar-element" value="&translation.thisPageIsIn.label;"/>
+            <menulist class="translate-infobar-element" anonid="detectedLanguage">
+              <menupopup/>
+            </menulist>
+            <label class="translate-infobar-element" value="&translation.translateThisPage.label;"/>
+            <button class="translate-infobar-element" label="&translation.translate.button;" anonid="translate" oncommand="this.closest('notification').translate();"/>
+            <button class="translate-infobar-element" label="&translation.notNow.button;" anonid="notNow" oncommand="this.closest('notification').closeCommand();"/>
+          </hbox>
+          <vbox class="translating-box" pack="center">
+            <label class="translate-infobar-element" value="&translation.translatingContent.label;"/>
+          </vbox>
+          <hbox class="translated-box" align="center">
+            <label class="translate-infobar-element" value="&translation.translatedFrom.label;"/>
+            <menulist class="translate-infobar-element" anonid="fromLanguage" oncommand="this.closest('notification').translate();">
+              <menupopup/>
+            </menulist>
+            <label class="translate-infobar-element" value="&translation.translatedTo.label;"/>
+            <menulist class="translate-infobar-element" anonid="toLanguage" oncommand="this.closest('notification').translate();">
+              <menupopup/>
+            </menulist>
+            <label class="translate-infobar-element" value="&translation.translatedToSuffix.label;"/>
+            <button anonid="showOriginal" class="translate-infobar-element" label="&translation.showOriginal.button;" oncommand="this.closest('notification').showOriginal();"/>
+            <button anonid="showTranslation" class="translate-infobar-element" label="&translation.showTranslation.button;" oncommand="this.closest('notification').showTranslation();"/>
+          </hbox>
+          <hbox class="translation-error" align="center">
+            <label class="translate-infobar-element" value="&translation.errorTranslating.label;"/>
+            <button class="translate-infobar-element" label="&translation.tryAgain.button;" anonid="tryAgain" oncommand="this.closest('notification').translate();"/>
+          </hbox>
+          <vbox class="translation-unavailable" pack="center">
+            <label class="translate-infobar-element" value="&translation.serviceUnavailable.label;"/>
+          </vbox>
+        </deck>
+        <spacer flex="1"/>
+        <button type="menu" class="translate-infobar-element options-menu-button" anonid="options" label="&translation.options.menu;">
+          <menupopup class="translation-menupopup cui-widget-panel cui-widget-panelview
+                                cui-widget-panelWithFooter PanelUI-subView" onpopupshowing="this.closest('notification').optionsShowing();">
+            <menuitem anonid="neverForLanguage" oncommand="this.closest('notification').neverForLanguage();"/>
+            <menuitem anonid="neverForSite" oncommand="this.closest('notification').neverForSite();" label="&translation.options.neverForSite.label;" accesskey="&translation.options.neverForSite.accesskey;"/>
+            <menuseparator/>
+            <menuitem oncommand="openPreferences('paneGeneral', {origin:'translationInfobar'});" label="&translation.options.preferences.label;" accesskey="&translation.options.preferences.accesskey;"/>
+            <menuitem class="subviewbutton panel-subview-footer" oncommand="this.closest('notification').openProviderAttribution();">
+              <deck anonid="translationEngine" selectedIndex="0">
+                <hbox class="translation-attribution">
+                  <label/>
+                  <image src="chrome://browser/content/microsoft-translator-attribution.png" aria-label="Microsoft Translator"/>
+                  <label/>
+                </hbox>
+                <label class="translation-attribution"/>
+              </deck>
+            </menuitem>
+          </menupopup>
+        </button>
+      </hbox>
+      <toolbarbutton anonid="closeButton" ondblclick="event.stopPropagation();"
+                     class="messageCloseButton close-icon tabbable"
+                     tooltiptext="&closeNotification.tooltip;"
+                     oncommand="this.parentNode.closeCommand();"/>
+    `, [
+      "chrome://global/locale/notification.dtd",
+      "chrome://browser/locale/translation.dtd",
+    ]));
 
-          let stateName;
-          for (let name of ["OFFER", "TRANSLATING", "TRANSLATED", "ERROR"]) {
-            if (Translation["STATE_" + name] == val) {
-              stateName = name.toLowerCase();
-              break;
-            }
-          }
-          this.setAttribute("state", stateName);
+    for (let [propertyName, selector] of [
+      ["details", "[anonid=details]"],
+      ["messageImage", ".messageImage"],
+      ["spacer", "[anonid=spacer]"],
+    ]) {
+      this[propertyName] = this.querySelector(selector);
+    }
+  }
+
+  set state(val) {
+    let deck = this._getAnonElt("translationStates");
+
+    let activeElt = document.activeElement;
+    if (activeElt && deck.contains(activeElt))
+      activeElt.blur();
 
-          if (val == Translation.STATE_TRANSLATED)
-            this._handleButtonHiding();
+    let stateName;
+    for (let name of ["OFFER", "TRANSLATING", "TRANSLATED", "ERROR"]) {
+      if (Translation["STATE_" + name] == val) {
+        stateName = name.toLowerCase();
+        break;
+      }
+    }
+    this.setAttribute("state", stateName);
 
-          deck.selectedIndex = val;
-          ]]>
-        </setter>
-      </property>
+    if (val == Translation.STATE_TRANSLATED)
+      this._handleButtonHiding();
 
-      <method name="init">
-        <parameter name="aTranslation"/>
-        <body>
-          <![CDATA[
-            this.translation = aTranslation;
+    deck.selectedIndex = val;
+  }
+
+  get state() {
+    return this._getAnonElt("translationStates").selectedIndex;
+  }
 
-            let sortByLocalizedName = function(aList) {
-              let names = Services.intl.getLanguageDisplayNames(undefined, aList);
-              return aList.map((code, i) => [code, names[i]])
-                          .sort((a, b) => a[1].localeCompare(b[1]));
-            };
+  init(aTranslation) {
+    this.translation = aTranslation;
 
-            // Fill the lists of supported source languages.
-            let detectedLanguage = this._getAnonElt("detectedLanguage");
-            let fromLanguage = this._getAnonElt("fromLanguage");
-            let sourceLanguages =
-              sortByLocalizedName(Translation.supportedSourceLanguages);
-            for (let [code, name] of sourceLanguages) {
-              detectedLanguage.appendItem(name, code);
-              fromLanguage.appendItem(name, code);
-            }
-            detectedLanguage.value = this.translation.detectedLanguage;
+    let sortByLocalizedName = function(aList) {
+      let names = Services.intl.getLanguageDisplayNames(undefined, aList);
+      return aList.map((code, i) => [code, names[i]])
+        .sort((a, b) => a[1].localeCompare(b[1]));
+    };
 
-            // translatedFrom is only set if we have already translated this page.
-            if (aTranslation.translatedFrom)
-              fromLanguage.value = aTranslation.translatedFrom;
+    // Fill the lists of supported source languages.
+    let detectedLanguage = this._getAnonElt("detectedLanguage");
+    let fromLanguage = this._getAnonElt("fromLanguage");
+    let sourceLanguages =
+      sortByLocalizedName(Translation.supportedSourceLanguages);
+    for (let [code, name] of sourceLanguages) {
+      detectedLanguage.appendItem(name, code);
+      fromLanguage.appendItem(name, code);
+    }
+    detectedLanguage.value = this.translation.detectedLanguage;
 
-            // Fill the list of supported target languages.
-            let toLanguage = this._getAnonElt("toLanguage");
-            let targetLanguages =
-              sortByLocalizedName(Translation.supportedTargetLanguages);
-            for (let [code, name] of targetLanguages)
-              toLanguage.appendItem(name, code);
+    // translatedFrom is only set if we have already translated this page.
+    if (aTranslation.translatedFrom)
+      fromLanguage.value = aTranslation.translatedFrom;
 
-            if (aTranslation.translatedTo)
-              toLanguage.value = aTranslation.translatedTo;
+    // Fill the list of supported target languages.
+    let toLanguage = this._getAnonElt("toLanguage");
+    let targetLanguages =
+      sortByLocalizedName(Translation.supportedTargetLanguages);
+    for (let [code, name] of targetLanguages)
+      toLanguage.appendItem(name, code);
 
-            if (aTranslation.state)
-              this.state = aTranslation.state;
+    if (aTranslation.translatedTo)
+      toLanguage.value = aTranslation.translatedTo;
+
+    if (aTranslation.state)
+      this.state = aTranslation.state;
 
-            // Show attribution for the preferred translator.
-            let engineIndex = Object.keys(Translation.supportedEngines)
-              .indexOf(Translation.translationEngine);
-            // We currently only have attribution for the Bing and Yandex engines.
-            if (engineIndex >= 0) {
-              --engineIndex;
-            }
-            let attributionNode = this._getAnonElt("translationEngine");
-            if (engineIndex != -1) {
-              attributionNode.selectedIndex = engineIndex;
-            } else {
-              // Hide the attribution menuitem
-              let footer = attributionNode.parentNode;
-              footer.hidden = true;
-              // Make the 'Translation preferences' item the new footer.
-              footer = footer.previousSibling;
-              footer.setAttribute("class", "subviewbutton panel-subview-footer");
-              // And hide the menuseparator.
-              footer.previousSibling.hidden = true;
-            }
+    // Show attribution for the preferred translator.
+    let engineIndex = Object.keys(Translation.supportedEngines)
+      .indexOf(Translation.translationEngine);
+    // We currently only have attribution for the Bing and Yandex engines.
+    if (engineIndex >= 0) {
+      --engineIndex;
+    }
+    let attributionNode = this._getAnonElt("translationEngine");
+    if (engineIndex != -1) {
+      attributionNode.selectedIndex = engineIndex;
+    } else {
+      // Hide the attribution menuitem
+      let footer = attributionNode.parentNode;
+      footer.hidden = true;
+      // Make the 'Translation preferences' item the new footer.
+      footer = footer.previousSibling;
+      footer.setAttribute("class", "subviewbutton panel-subview-footer");
+      // And hide the menuseparator.
+      footer.previousSibling.hidden = true;
+    }
 
-            const kWelcomePref = "browser.translation.ui.welcomeMessageShown";
-            if (Services.prefs.prefHasUserValue(kWelcomePref) ||
-                this.translation.browser != gBrowser.selectedBrowser)
-              return;
+    const kWelcomePref = "browser.translation.ui.welcomeMessageShown";
+    if (Services.prefs.prefHasUserValue(kWelcomePref) ||
+      this.translation.browser != gBrowser.selectedBrowser)
+      return;
 
-            this.addEventListener("transitionend", function() {
-              // These strings are hardcoded because they need to reach beta
-              // without riding the trains.
-              let localizedStrings = {
-                en: ["Hey look! It's something new!",
-                     "Now the Web is even more accessible with our new in-page translation feature. Click the translate button to try it!",
-                     "Learn more.",
-                     "Thanks"],
-                "es-AR": ["\xA1Mir\xE1! \xA1Hay algo nuevo!",
-                          "Ahora la web es a\xFAn m\xE1s accesible con nuestra nueva funcionalidad de traducci\xF3n integrada. \xA1Hac\xE9 clic en el bot\xF3n traducir para probarla!",
-                          "Conoc\xE9 m\xE1s.",
-                          "Gracias"],
-                "es-ES": ["\xA1Mira! \xA1Hay algo nuevo!",
-                          "Con la nueva funcionalidad de traducci\xF3n integrada, ahora la Web es a\xFAn m\xE1s accesible. \xA1Pulsa el bot\xF3n Traducir y pru\xE9bala!",
-                          "M\xE1s informaci\xF3n.",
-                          "Gracias"],
-                pl: ["Sp\xF3jrz tutaj! To co\u015B nowego!",
-                     "Sie\u0107 sta\u0142a si\u0119 w\u0142a\u015Bnie jeszcze bardziej dost\u0119pna dzi\u0119ki opcji bezpo\u015Bredniego t\u0142umaczenia stron. Kliknij przycisk t\u0142umaczenia, aby spr\xF3bowa\u0107!",
-                     "Dowiedz si\u0119 wi\u0119cej",
-                     "Dzi\u0119kuj\u0119"],
-                tr: ["Bak\u0131n, burada yeni bir \u015Fey var!",
-                     "Yeni sayfa i\xE7i \xE7eviri \xF6zelli\u011Fimiz sayesinde Web art\u0131k \xE7ok daha anla\u015F\u0131l\u0131r olacak. Denemek i\xE7in \xC7evir d\xFC\u011Fmesine t\u0131klay\u0131n!",
-                     "Daha fazla bilgi al\u0131n.",
-                     "Te\u015Fekk\xFCrler"],
-                vi: ["Nh\xECn n\xE0y! \u0110\u1ED3 m\u1EDBi!",
-                     "Gi\u1EDD \u0111\xE2y ch\xFAng ta c\xF3 th\u1EC3 ti\u1EBFp c\u1EADn web d\u1EC5 d\xE0ng h\u01A1n n\u1EEFa v\u1EDBi t\xEDnh n\u0103ng d\u1ECBch ngay trong trang.  Hay nh\u1EA5n n\xFAt d\u1ECBch \u0111\u1EC3 th\u1EED!",
-                     "T\xECm hi\u1EC3u th\xEAm.",
-                     "C\u1EA3m \u01A1n"],
-              };
-
-              let locale = Services.locale.appLocaleAsLangTag;
-              if (!(locale in localizedStrings))
-                locale = "en";
-              let strings = localizedStrings[locale];
+    this.addEventListener("transitionend", function() {
+      // These strings are hardcoded because they need to reach beta
+      // without riding the trains.
+      let localizedStrings = {
+        en: ["Hey look! It's something new!",
+          "Now the Web is even more accessible with our new in-page translation feature. Click the translate button to try it!",
+          "Learn more.",
+          "Thanks",
+        ],
+        "es-AR": ["\xA1Mir\xE1! \xA1Hay algo nuevo!",
+          "Ahora la web es a\xFAn m\xE1s accesible con nuestra nueva funcionalidad de traducci\xF3n integrada. \xA1Hac\xE9 clic en el bot\xF3n traducir para probarla!",
+          "Conoc\xE9 m\xE1s.",
+          "Gracias",
+        ],
+        "es-ES": ["\xA1Mira! \xA1Hay algo nuevo!",
+          "Con la nueva funcionalidad de traducci\xF3n integrada, ahora la Web es a\xFAn m\xE1s accesible. \xA1Pulsa el bot\xF3n Traducir y pru\xE9bala!",
+          "M\xE1s informaci\xF3n.",
+          "Gracias",
+        ],
+        pl: ["Sp\xF3jrz tutaj! To co\u015B nowego!",
+          "Sie\u0107 sta\u0142a si\u0119 w\u0142a\u015Bnie jeszcze bardziej dost\u0119pna dzi\u0119ki opcji bezpo\u015Bredniego t\u0142umaczenia stron. Kliknij przycisk t\u0142umaczenia, aby spr\xF3bowa\u0107!",
+          "Dowiedz si\u0119 wi\u0119cej",
+          "Dzi\u0119kuj\u0119",
+        ],
+        tr: ["Bak\u0131n, burada yeni bir \u015Fey var!",
+          "Yeni sayfa i\xE7i \xE7eviri \xF6zelli\u011Fimiz sayesinde Web art\u0131k \xE7ok daha anla\u015F\u0131l\u0131r olacak. Denemek i\xE7in \xC7evir d\xFC\u011Fmesine t\u0131klay\u0131n!",
+          "Daha fazla bilgi al\u0131n.",
+          "Te\u015Fekk\xFCrler",
+        ],
+        vi: ["Nh\xECn n\xE0y! \u0110\u1ED3 m\u1EDBi!",
+          "Gi\u1EDD \u0111\xE2y ch\xFAng ta c\xF3 th\u1EC3 ti\u1EBFp c\u1EADn web d\u1EC5 d\xE0ng h\u01A1n n\u1EEFa v\u1EDBi t\xEDnh n\u0103ng d\u1ECBch ngay trong trang.  Hay nh\u1EA5n n\xFAt d\u1ECBch \u0111\u1EC3 th\u1EED!",
+          "T\xECm hi\u1EC3u th\xEAm.",
+          "C\u1EA3m \u01A1n",
+        ],
+      };
 
-              this._getAnonElt("welcomeHeadline").setAttribute("value", strings[0]);
-              this._getAnonElt("welcomeBody").textContent = strings[1];
-              this._getAnonElt("learnMore").setAttribute("value", strings[2]);
-              this._getAnonElt("thanksButton").setAttribute("label", strings[3]);
+      let locale = Services.locale.appLocaleAsLangTag;
+      if (!(locale in localizedStrings))
+        locale = "en";
+      let strings = localizedStrings[locale];
 
-              let panel = this._getAnonElt("welcomePanel");
-              panel.openPopup(this._getAnonElt("messageImage"),
-                              "bottomcenter topleft");
+      this._getAnonElt("welcomeHeadline").setAttribute("value", strings[0]);
+      this._getAnonElt("welcomeBody").textContent = strings[1];
+      this._getAnonElt("learnMore").setAttribute("value", strings[2]);
+      this._getAnonElt("thanksButton").setAttribute("label", strings[3]);
 
-              Services.prefs.setBoolPref(kWelcomePref, true);
-            }, {once: true});
-          ]]>
-        </body>
-      </method>
+      let panel = this._getAnonElt("welcomePanel");
+      panel.openPopup(this._getAnonElt("messageImage"),
+        "bottomcenter topleft");
 
-      <method name="_getAnonElt">
-        <parameter name="aAnonId"/>
-        <body>
-          return document.getAnonymousElementByAttribute(this, "anonid", aAnonId);
-        </body>
-      </method>
+      Services.prefs.setBoolPref(kWelcomePref, true);
+    }, { once: true });
+  }
+
+  _getAnonElt(aAnonId) {
+    return this.querySelector("[anonid=" + aAnonId + "]");
+  }
 
-      <method name="translate">
-        <body>
-          <![CDATA[
-            if (this.state == Translation.STATE_OFFER) {
-              this._getAnonElt("fromLanguage").value =
-                this._getAnonElt("detectedLanguage").value;
-              this._getAnonElt("toLanguage").value =
-                Translation.defaultTargetLanguage;
-            }
-
-            this.translation.translate(this._getAnonElt("fromLanguage").value,
-                                       this._getAnonElt("toLanguage").value);
-          ]]>
-        </body>
-      </method>
+  translate() {
+    if (this.state == Translation.STATE_OFFER) {
+      this._getAnonElt("fromLanguage").value =
+        this._getAnonElt("detectedLanguage").value;
+      this._getAnonElt("toLanguage").value =
+        Translation.defaultTargetLanguage;
+    }
 
-      <!-- To be called when the infobar should be closed per user's wish (e.g.
-           by clicking the notification's close button -->
-      <method name="closeCommand">
-        <body>
-          <![CDATA[
-            this.close();
-            this.translation.infobarClosed();
-          ]]>
-        </body>
-      </method>
-      <method name="_handleButtonHiding">
-        <body>
-          <![CDATA[
-            let originalShown = this.translation.originalShown;
-            this._getAnonElt("showOriginal").hidden = originalShown;
-            this._getAnonElt("showTranslation").hidden = !originalShown;
-          ]]>
-        </body>
-      </method>
+    this.translation.translate(this._getAnonElt("fromLanguage").value,
+      this._getAnonElt("toLanguage").value);
+  }
+
+  /**
+   * To be called when the infobar should be closed per user's wish (e.g.
+   * by clicking the notification's close button
+   */
+  closeCommand() {
+    this.close();
+    this.translation.infobarClosed();
+  }
+
+  _handleButtonHiding() {
+    let originalShown = this.translation.originalShown;
+    this._getAnonElt("showOriginal").hidden = originalShown;
+    this._getAnonElt("showTranslation").hidden = !originalShown;
+  }
 
-      <method name="showOriginal">
-        <body>
-          <![CDATA[
-            this.translation.showOriginalContent();
-            this._handleButtonHiding();
-          ]]>
-        </body>
-      </method>
+  showOriginal() {
+    this.translation.showOriginalContent();
+    this._handleButtonHiding();
+  }
 
-      <method name="showTranslation">
-        <body>
-          <![CDATA[
-            this.translation.showTranslatedContent();
-            this._handleButtonHiding();
-          ]]>
-        </body>
-      </method>
+  showTranslation() {
+    this.translation.showTranslatedContent();
+    this._handleButtonHiding();
+  }
 
-      <method name="optionsShowing">
-        <body>
-          <![CDATA[
-            // Get the source language name.
-            let lang;
-            if (this.state == Translation.STATE_OFFER)
-              lang = this._getAnonElt("detectedLanguage").value;
-            else {
-              lang = this._getAnonElt("fromLanguage").value;
+  optionsShowing() {
+    // Get the source language name.
+    let lang;
+    if (this.state == Translation.STATE_OFFER)
+      lang = this._getAnonElt("detectedLanguage").value;
+    else {
+      lang = this._getAnonElt("fromLanguage").value;
 
-              // If we have never attempted to translate the page before the
-              // service became unavailable, "fromLanguage" isn't set.
-              if (!lang && this.state == Translation.STATE_UNAVAILABLE)
-                lang = this.translation.detectedLanguage;
-            }
+      // If we have never attempted to translate the page before the
+      // service became unavailable, "fromLanguage" isn't set.
+      if (!lang && this.state == Translation.STATE_UNAVAILABLE)
+        lang = this.translation.detectedLanguage;
+    }
 
-            let langName = Services.intl.getLanguageDisplayNames(undefined, [lang])[0];
+    let langName = Services.intl.getLanguageDisplayNames(undefined, [lang])[0];
 
-            // Set the label and accesskey on the menuitem.
-            let bundle =
-              Services.strings.createBundle("chrome://browser/locale/translation.properties");
-            let item = this._getAnonElt("neverForLanguage");
-            const kStrId = "translation.options.neverForLanguage";
-            item.setAttribute("label",
-                              bundle.formatStringFromName(kStrId + ".label",
-                                                          [langName], 1));
-            item.setAttribute("accesskey",
-                              bundle.GetStringFromName(kStrId + ".accesskey"));
-            item.langCode = lang;
+    // Set the label and accesskey on the menuitem.
+    let bundle =
+      Services.strings.createBundle("chrome://browser/locale/translation.properties");
+    let item = this._getAnonElt("neverForLanguage");
+    const kStrId = "translation.options.neverForLanguage";
+    item.setAttribute("label",
+      bundle.formatStringFromName(kStrId + ".label", [langName], 1));
+    item.setAttribute("accesskey",
+      bundle.GetStringFromName(kStrId + ".accesskey"));
+    item.langCode = lang;
 
-            // We may need to disable the menuitems if they have already been used.
-            // Check if translation is already disabled for this language:
-            let neverForLangs =
-              Services.prefs.getCharPref("browser.translation.neverForLanguages");
-            item.disabled = neverForLangs.split(",").includes(lang);
+    // We may need to disable the menuitems if they have already been used.
+    // Check if translation is already disabled for this language:
+    let neverForLangs =
+      Services.prefs.getCharPref("browser.translation.neverForLanguages");
+    item.disabled = neverForLangs.split(",").includes(lang);
 
-            // Check if translation is disabled for the domain:
-            let uri = this.translation.browser.currentURI;
-            let perms = Services.perms;
-            item = this._getAnonElt("neverForSite");
-            item.disabled =
-              perms.testExactPermission(uri, "translate") == perms.DENY_ACTION;
-          ]]>
-        </body>
-      </method>
-
-      <method name="neverForLanguage">
-        <body>
-          <![CDATA[
-            const kPrefName = "browser.translation.neverForLanguages";
+    // Check if translation is disabled for the domain:
+    let uri = this.translation.browser.currentURI;
+    let perms = Services.perms;
+    item = this._getAnonElt("neverForSite");
+    item.disabled =
+      perms.testExactPermission(uri, "translate") == perms.DENY_ACTION;
+  }
 
-            let val = Services.prefs.getCharPref(kPrefName);
-            if (val)
-              val += ",";
-            val += this._getAnonElt("neverForLanguage").langCode;
+  neverForLanguage() {
+    const kPrefName = "browser.translation.neverForLanguages";
 
-            Services.prefs.setCharPref(kPrefName, val);
+    let val = Services.prefs.getCharPref(kPrefName);
+    if (val)
+      val += ",";
+    val += this._getAnonElt("neverForLanguage").langCode;
 
-            this.closeCommand();
-          ]]>
-        </body>
-      </method>
+    Services.prefs.setCharPref(kPrefName, val);
+
+    this.closeCommand();
+  }
 
-      <method name="neverForSite">
-        <body>
-          <![CDATA[
-            let uri = this.translation.browser.currentURI;
-            let perms = Services.perms;
-            perms.add(uri, "translate", perms.DENY_ACTION);
+  neverForSite() {
+    let uri = this.translation.browser.currentURI;
+    let perms = Services.perms;
+    perms.add(uri, "translate", perms.DENY_ACTION);
 
-            this.closeCommand();
-          ]]>
-        </body>
-      </method>
+    this.closeCommand();
+  }
 
-      <method name="openProviderAttribution">
-        <body>
-          <![CDATA[
-            Translation.openProviderAttribution();
-          ]]>
-        </body>
-      </method>
+  openProviderAttribution() {
+    Translation.openProviderAttribution();
+  }
+}
 
-    </implementation>
-  </binding>
-</bindings>
+customElements.define("translation-notification", MozTranslationNotification,
+                      { extends: "notification" });
--- a/browser/components/translation/moz.build
+++ b/browser/components/translation/moz.build
@@ -1,28 +1,30 @@
 # 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/.
 
+DIRS += [
+    'content',
+]
+
 with Files("**"):
     BUG_COMPONENT = ("Firefox", "Translation")
 
 EXTRA_JS_MODULES.translation = [
     'BingTranslator.jsm',
     'cld2/cld-worker.js',
     'cld2/cld-worker.js.mem',
     'GoogleTranslator.jsm',
     'LanguageDetector.jsm',
     'Translation.jsm',
     'TranslationContentHandler.jsm',
     'TranslationDocument.jsm',
     'YandexTranslator.jsm'
 ]
 
-JAR_MANIFESTS += ['jar.mn']
-
 BROWSER_CHROME_MANIFESTS += [
     'test/browser.ini'
 ]
 
 XPCSHELL_TESTS_MANIFESTS += [
     'test/unit/xpcshell.ini'
 ]
--- a/browser/modules/test/browser/browser_UnsubmittedCrashHandler.js
+++ b/browser/modules/test/browser/browser_UnsubmittedCrashHandler.js
@@ -274,19 +274,17 @@ add_task(async function test_one_pending
  */
 add_task(async function test_other_ignored() {
   let toIgnore = await createPendingCrashReports(1);
   let notification =
     await UnsubmittedCrashHandler.checkForUnsubmittedCrashReports();
   Assert.ok(notification, "There should be a notification");
 
   // Dismiss notification, creating the .dmp.ignore file
-  let closeButton =
-    document.getAnonymousElementByAttribute(notification, "anonid", "close-button");
-  closeButton.click();
+  notification.querySelector(".messageCloseButton").click();
   gNotificationBox.removeNotification(notification, true);
   await waitForIgnoredReports(toIgnore);
 
   notification =
     await UnsubmittedCrashHandler.checkForUnsubmittedCrashReports();
   Assert.ok(!notification, "There should not be a notification");
 
   await createPendingCrashReports(1);
@@ -466,19 +464,17 @@ add_task(async function test_can_auto_su
  */
 add_task(async function test_can_ignore() {
   let reportIDs = await createPendingCrashReports(3);
   let notification =
     await UnsubmittedCrashHandler.checkForUnsubmittedCrashReports();
   Assert.ok(notification, "There should be a notification");
 
   // Dismiss the notification by clicking on the "X" button.
-  let closeButton =
-    document.getAnonymousElementByAttribute(notification, "anonid", "close-button");
-  closeButton.click();
+  notification.querySelector(".messageCloseButton").click();
   // We'll not wait for the notification to finish its transition -
   // we'll just remove it right away.
   gNotificationBox.removeNotification(notification, true);
   await waitForIgnoredReports(reportIDs);
 
   notification =
     await UnsubmittedCrashHandler.checkForUnsubmittedCrashReports();
   Assert.equal(notification, null, "There should be no notification");
@@ -540,19 +536,17 @@ add_task(async function test_shutdown_wh
  */
 add_task(async function test_shutdown_while_not_showing() {
   let reportIDs = await createPendingCrashReports(1);
   let notification =
     await UnsubmittedCrashHandler.checkForUnsubmittedCrashReports();
   Assert.ok(notification, "There should be a notification");
 
   // Dismiss the notification by clicking on the "X" button.
-  let closeButton =
-    document.getAnonymousElementByAttribute(notification, "anonid", "close-button");
-  closeButton.click();
+  notification.querySelector(".messageCloseButton").click();
   // We'll not wait for the notification to finish its transition -
   // we'll just remove it right away.
   gNotificationBox.removeNotification(notification, true);
 
   await waitForIgnoredReports(reportIDs);
 
   UnsubmittedCrashHandler.uninit();
   Assert.throws(() => {
--- a/browser/themes/shared/translation/infobar.inc.css
+++ b/browser/themes/shared/translation/infobar.inc.css
@@ -1,33 +1,33 @@
 %if 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/. */
 %endif
 notification[value="translation"] .messageImage {
-  list-style-image: url(chrome://browser/skin/translation-16.png);
+  list-style-image: url(chrome://browser/skin/translation-16.png) !important;
   -moz-image-region: rect(0, 32px, 16px, 16px);
 }
 
 @media (min-resolution: 1.25dppx) {
   notification[value="translation"] .messageImage {
-    list-style-image: url(chrome://browser/skin/translation-16@2x.png);
+    list-style-image: url(chrome://browser/skin/translation-16@2x.png) !important;
     -moz-image-region: rect(0, 64px, 32px, 32px);
   }
 }
 
 notification[value="translation"][state="translating"] .messageImage {
-  list-style-image: url(chrome://browser/skin/translating-16.png);
+  list-style-image: url(chrome://browser/skin/translating-16.png) !important;
   -moz-image-region: auto;
 }
 
 @media (min-resolution: 1.25dppx) {
   notification[value="translation"][state="translating"] .messageImage {
-    list-style-image: url(chrome://browser/skin/translating-16@2x.png);
+    list-style-image: url(chrome://browser/skin/translating-16@2x.png) !important;
   }
 }
 
 notification[value="translation"] hbox[anonid="details"] {
   overflow: hidden;
 }
 
 notification[value="translation"] button,
--- a/devtools/client/scratchpad/test/browser_scratchpad_open.js
+++ b/devtools/client/scratchpad/test/browser_scratchpad_open.js
@@ -77,18 +77,20 @@ function testOpenTestFile() {
       win.Scratchpad.importFromFile(
         "http://example.com/browser/devtools/client/scratchpad/test/NS_ERROR_ILLEGAL_INPUT.txt",
         "silent",
         function(aStatus, content) {
           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");
+          is(cn.getAttribute("value"), "file-import-convert-failed",
+             "notification value is corrent");
+          is(cn.getAttribute("type"), "warning",
+             "notification type is correct");
           done();
         });
       ok(true, "importFromFile does not cause exception");
     } catch (exception) {
       ok(false, "importFromFile causes exception " + DevToolsUtils.safeErrorString(exception));
     }
   }, {noFocus: true});
 }
--- a/devtools/client/scratchpad/test/browser_scratchpad_recent_files.js
+++ b/devtools/client/scratchpad/test/browser_scratchpad_recent_files.js
@@ -147,17 +147,17 @@ function testOpenDeletedFile() {
   is(gScratchpad.getRecentFiles().length, 1,
      "The missing file was successfully removed from the list.");
   // The number of recent files stored, plus the separator and the
   // clearRecentMenuItems-item.
   is(popup.children.length, 3,
      "The missing file was successfully removed from the menu.");
   ok(gScratchpad.notificationBox.currentNotification,
      "The notification was successfully displayed.");
-  is(gScratchpad.notificationBox.currentNotification.label,
+  is(gScratchpad.notificationBox.currentNotification.messageText.textContent,
      gScratchpad.strings.GetStringFromName("fileNoLongerExists.notification"),
      "The notification label is correct.");
 
   gScratchpad.clearRecentFiles();
 }
 
 // We have cleared the last file. Test to see if the last file was removed,
 // the menu is empty and was disabled successfully.
--- a/devtools/shared/fronts/csscoverage.js
+++ b/devtools/shared/fronts/csscoverage.js
@@ -58,17 +58,17 @@ const CSSUsageFront = protocol.FrontClas
         notification = gnb.appendNotification(msg, "csscoverage-running",
                                               "",
                                               gnb.PRIORITY_INFO_HIGH,
                                               null,
                                               notifyStop);
       }
     } else {
       if (notification) {
-        notification.remove();
+        notification.close();
         notification = undefined;
       }
 
       gDevTools.showToolbox(target, "styleeditor");
       target = undefined;
     }
   }),
 
--- a/testing/firefox-ui/tests/functional/safebrowsing/test_notification.py
+++ b/testing/firefox-ui/tests/functional/safebrowsing/test_notification.py
@@ -141,16 +141,15 @@ class TestSafeBrowsingNotificationBar(Pu
             message='The default home page has not been loaded',
         )
 
     def check_x_button(self):
         with self.marionette.using_context('chrome'):
             # TODO: update to use safe browsing notification bar class when bug 1139544 lands
             button = (self.marionette.find_element(By.ID, 'tabbrowser-tabbox')
                       .find_element(By.CSS_SELECTOR, 'notification[value=blocked-badware-page]')
-                      .find_element('anon attribute',
-                                    {'class': 'messageCloseButton close-icon tabbable'}))
+                      .find_element(By.CSS_SELECTOR, '.messageCloseButton'))
             button.click()
 
             Wait(self.marionette, timeout=self.marionette.timeout.page_load).until(
                 expected.element_stale(button),
                 message='The notification bar has not been closed',
             )
--- a/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
+++ b/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
@@ -1586,17 +1586,17 @@ var BrowserTestUtils = {
   waitForGlobalNotificationBar(win, notificationValue) {
     return this.waitForNotificationInNotificationBox(
                        win.gHighPriorityNotificationBox, notificationValue);
   },
 
   waitForNotificationInNotificationBox(notificationBox, notificationValue) {
     return new Promise((resolve) => {
       let check = (event) => {
-        return event.target.value == notificationValue;
+        return event.target.getAttribute("value") == notificationValue;
       };
 
       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/lib/Heartbeat.jsm
+++ b/toolkit/components/normandy/lib/Heartbeat.jsm
@@ -168,18 +168,18 @@ var Heartbeat = class {
     );
 
     // Holds the rating UI
     const frag = this.chromeWindow.document.createDocumentFragment();
 
     // Build the heartbeat stars
     if (!this.options.engagementButtonLabel) {
       const numStars = this.options.engagementButtonLabel ? 0 : 5;
-      const ratingContainer = this.chromeWindow.document.createXULElement("hbox");
-      ratingContainer.id = "star-rating-container";
+      this.ratingContainer = this.chromeWindow.document.createXULElement("hbox");
+      this.ratingContainer.id = "star-rating-container";
 
       for (let i = 0; i < numStars; i++) {
         // create a star rating element
         const ratingElement = this.chromeWindow.document.createXULElement("toolbarbutton");
 
         // style it
         const starIndex = numStars - i;
         ratingElement.className = "plain star-x";
@@ -188,53 +188,48 @@ var Heartbeat = class {
 
         // Add the click handler
         ratingElement.addEventListener("click", ev => {
           const rating = parseInt(ev.target.getAttribute("data-score"));
           this.maybeNotifyHeartbeat("Voted", {score: rating});
           this.userEngaged({type: "stars", score: rating, flowId: this.options.flowId});
         });
 
-        ratingContainer.appendChild(ratingElement);
+        this.ratingContainer.appendChild(ratingElement);
       }
 
-      frag.appendChild(ratingContainer);
+      frag.appendChild(this.ratingContainer);
     }
 
-    const details = this.chromeWindow.document.getAnonymousElementByAttribute(this.notice, "anonid", "details");
-    details.style.overflow = "hidden";
-
-    this.messageImage = this.chromeWindow.document.getAnonymousElementByAttribute(this.notice, "anonid", "messageImage");
-    this.messageImage.classList.add("heartbeat", "pulse-onshow");
-
-    this.messageText = this.chromeWindow.document.getAnonymousElementByAttribute(this.notice, "anonid", "messageText");
-    this.messageText.classList.add("heartbeat");
+    this.notice.messageDetails.style.overflow = "hidden";
+    this.notice.messageImage.classList.add("heartbeat", "pulse-onshow");
+    this.notice.messageText.classList.add("heartbeat");
 
     // Make sure the stars are not pushed to the right by the spacer.
-    const rightSpacer = this.chromeWindow.document.createXULElement("spacer");
-    rightSpacer.flex = 20;
-    frag.appendChild(rightSpacer);
+    this.rightSpacer = this.chromeWindow.document.createXULElement("spacer");
+    this.rightSpacer.flex = 20;
+    frag.appendChild(this.rightSpacer);
 
     // collapse the space before the stars
-    this.messageText.flex = 0;
-    const leftSpacer = this.messageText.nextSibling;
-    leftSpacer.flex = 0;
+    this.notice.messageText.flex = 0;
+    this.notice.spacer.flex = 0;
 
     // Add Learn More Link
     if (this.options.learnMoreMessage && this.options.learnMoreUrl) {
-      const learnMore = this.chromeWindow.document.createXULElement("label");
-      learnMore.className = "text-link";
-      learnMore.href = this.options.learnMoreUrl.toString();
-      learnMore.setAttribute("value", this.options.learnMoreMessage);
-      learnMore.addEventListener("click", () => this.maybeNotifyHeartbeat("LearnMore"));
-      frag.appendChild(learnMore);
+      this.learnMore = this.chromeWindow.document.createXULElement("label");
+      this.learnMore.className = "text-link";
+      this.learnMore.href = this.options.learnMoreUrl.toString();
+      this.learnMore.setAttribute("value", this.options.learnMoreMessage);
+      this.learnMore.addEventListener("click",
+        () => this.maybeNotifyHeartbeat("LearnMore"));
+      frag.appendChild(this.learnMore);
     }
 
     // Append the fragment and apply the styling
-    this.notice.appendChild(frag);
+    this.notice.messageDetails.appendChild(frag);
     this.notice.classList.add("heartbeat");
 
     // Let the consumer know the notification was shown.
     this.maybeNotifyHeartbeat("NotificationOffered");
     this.chromeWindow.addEventListener("SSWindowClosing", this.handleWindowClosed);
 
     const surveyDuration = Preferences.get(PREF_SURVEY_DURATION, 300) * 1000;
     this.surveyEndTimer = setTimeout(() => {
@@ -325,22 +320,27 @@ var Heartbeat = class {
     if (cleanup) {
       this.cleanup();
     }
   }
 
   userEngaged(engagementParams) {
     // Make the heartbeat icon pulse twice
     this.notice.label = this.options.thanksMessage;
-    this.messageImage.classList.remove("pulse-onshow");
-    this.messageImage.classList.add("pulse-twice");
+    this.notice.messageImage.classList.remove("pulse-onshow");
+    this.notice.messageImage.classList.add("pulse-twice");
 
-    // Remove all the children of the notice (rating container, and the flex)
-    while (this.notice.firstChild) {
-      this.notice.firstChild.remove();
+    // Remove the custom contents of the notice and the buttons
+    if (this.ratingContainer) {
+      this.ratingContainer.remove();
+    }
+    this.rightSpacer.remove();
+    this.learnMore.remove();
+    for (let button of this.notice.querySelectorAll("button")) {
+      button.remove();
     }
 
     // Open the engagement tab if we have a valid engagement URL.
     if (this.options.postAnswerUrl) {
       for (const key in engagementParams) {
         this.options.postAnswerUrl.searchParams.append(key, engagementParams[key]);
       }
       // Open the engagement URL in a new tab.
@@ -376,16 +376,18 @@ var Heartbeat = class {
     this.endTimerIfPresent("engagementCloseTimer");
 
     this.sandboxManager.removeHold("heartbeat");
     // remove listeners
     this.chromeWindow.removeEventListener("SSWindowClosing", this.handleWindowClosed);
     // remove references for garbage collection
     this.chromeWindow = null;
     this.notificationBox = null;
-    this.notification = null;
     this.notice = null;
+    this.ratingContainer = null;
+    this.rightSpacer = null;
+    this.learnMore = null;
     this.eventEmitter = null;
     this.sandboxManager = null;
     // Ensure we don't re-enter and release the CleanupManager's reference to us:
     CleanupManager.removeCleanupHandler(this.close);
   }
 };
--- a/toolkit/components/normandy/skin/shared/Heartbeat.css
+++ b/toolkit/components/normandy/skin/shared/Heartbeat.css
@@ -72,56 +72,56 @@ notification.heartbeat {
 .messageImage.heartbeat.pulse-twice {
   animation-duration: 1s;
   animation-iteration-count: 2;
   animation-name: pulse-twice;
   animation-timing-function: linear;
 }
 
 /* Learn More link styles */
-.heartbeat > .text-link {
+.heartbeat > hbox > .text-link {
   color: #0095DD !important;
   margin-inline-start: 0 !important;
 }
 
-.heartbeat > .text-link:hover {
+.heartbeat > hbox > .text-link:hover {
   color: #008ACB !important;
   text-decoration: none !important;
 }
 
-.heartbeat > .text-link:hover:active {
+.heartbeat > hbox > .text-link:hover:active {
   color: #006B9D !important;
 }
 
 /* Heartbeat UI Rating Star Classes */
-.heartbeat > #star-rating-container {
+#star-rating-container {
   display: -moz-box;
   margin-bottom: 4px;
 }
 
-.heartbeat > #star-rating-container > #star5 {
+#star-rating-container > #star5 {
   -moz-box-ordinal-group: 5;
 }
 
-.heartbeat > #star-rating-container > #star4 {
+#star-rating-container > #star4 {
   -moz-box-ordinal-group: 4;
 }
 
-.heartbeat > #star-rating-container > #star3 {
+#star-rating-container > #star3 {
   -moz-box-ordinal-group: 3;
 }
 
-.heartbeat > #star-rating-container > #star2 {
+#star-rating-container > #star2 {
   -moz-box-ordinal-group: 2;
 }
 
-.heartbeat > #star-rating-container > .star-x {
+#star-rating-container > .star-x {
   background: url("resource://normandy/skin/shared/heartbeat-star-off.svg");
   cursor: pointer;
   height: 16px;
   margin-inline-end: 4px !important; /* Overrides the margin-inline-end for all platforms defined in the .plain class */
   width: 16px;
 }
 
-.heartbeat > #star-rating-container > .star-x:hover,
-.heartbeat > #star-rating-container > .star-x:hover ~ .star-x {
+#star-rating-container > .star-x:hover,
+#star-rating-container > .star-x:hover ~ .star-x {
   background: url("resource://normandy/skin/shared/heartbeat-star-lit.svg");
 }
--- a/toolkit/components/normandy/test/browser/browser_Heartbeat.js
+++ b/toolkit/components/normandy/test/browser/browser_Heartbeat.js
@@ -88,23 +88,22 @@ add_task(async function() {
     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.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");
+  Assert.equal(hb.notice.messageText.textContent, "test", "Message is correct");
 
   // Check that when clicking the learn more link, a tab opens with the right URL
   let loadedPromise;
   const tabOpenPromise = new Promise(resolve => {
     gBrowser.tabContainer.addEventListener("TabOpen", event => {
       let tab = event.target;
       loadedPromise = BrowserTestUtils.browserLoaded(
         tab.linkedBrowser, true, url => url && url !== "about:blank");
--- a/toolkit/content/tests/chrome/test_notificationbox.xul
+++ b/toolkit/content/tests/chrome/test_notificationbox.xul
@@ -296,18 +296,18 @@ var tests =
       nb.appendNotification("Six", "6", null, nb.PRIORITY_WARNING_HIGH, null);
       nb.appendNotification("One", "1", null, nb.PRIORITY_INFO_LOW, null);
       nb.appendNotification("Nine", "9", null, nb.PRIORITY_CRITICAL_MEDIUM, null);
       var ntf = nb.appendNotification("Ten", "10", null, nb.PRIORITY_CRITICAL_HIGH, null);
       nb.appendNotification("Three", "3", null, nb.PRIORITY_INFO_MEDIUM, null);
       return ntf;
     },
     result: function(nb, ntf) {
-      SimpleTest.is(nb.currentNotification == ntf ?
-                    nb.currentNotification.value : null, "10", "appendNotification order");
+      is(nb.currentNotification, ntf, "appendNotification last notification");
+      is(nb.currentNotification.getAttribute("value"), "10", "appendNotification order");
       return 1;
     }
   },
   {
     // test closing notifications to make sure that the current notification is still set properly
     repeat: true,
     test: function(nb, testidx) {
       switch (testidx) {
@@ -328,18 +328,18 @@ var tests =
           return [5, 7];
         case 6:
           nb.removeCurrentNotification();
           return [6, 4];
       }
     },
     result: function(nb, arr) {
       // arr is [testindex, expectedvalue]
-      SimpleTest.is(nb.currentNotification.value, "" + arr[1], "close order " + arr[0]);
-      SimpleTest.is(nb.allNotifications.length, 10 - arr[0], "close order " + arr[0] + " count");
+      is(nb.currentNotification.getAttribute("value"), "" + arr[1], "close order " + arr[0]);
+      is(nb.allNotifications.length, 10 - arr[0], "close order " + arr[0] + " count");
       if (arr[0] == 6)
         this.repeat = false;
       return ++arr[0];
     }
   },
   {
     test: function(nb, ntf) {
       var exh = false;
@@ -395,20 +395,20 @@ 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.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");
+  is(ntf.messageText.textContent, label, testid + " notification label");
+  is(ntf.getAttribute("value"), value, testid + " notification value");
+  is(ntf.messageImage.getAttribute("src"), image, testid + " notification image");
+  is(ntf.priority, priority, testid + " notification priority");
 
   var type;
   switch (priority) {
     case nb.PRIORITY_INFO_LOW:
     case nb.PRIORITY_INFO_MEDIUM:
     case nb.PRIORITY_INFO_HIGH:
       type = "info";
       break;
@@ -419,17 +419,17 @@ function testtag_notification_State(nb, 
       break;
     case nb.PRIORITY_CRITICAL_LOW:
     case nb.PRIORITY_CRITICAL_MEDIUM:
     case nb.PRIORITY_CRITICAL_HIGH:
       type = "critical";
       break;
   }
 
-  SimpleTest.is(ntf.type, type, testid + " notification.type");
+  is(ntf.getAttribute("type"), type, testid + " notification type");
 }
 
 function checkPopupTest(nb, ntf)
 {
   if (nb._animating)
     setTimeout(checkPopupTest, ntf);
   else {
     var evt = new Event("");
--- a/toolkit/content/widgets/notification.xml
+++ b/toolkit/content/widgets/notification.xml
@@ -10,109 +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="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>
-
-      <property name="control" readonly="true">
-        <getter>
-          <![CDATA[
-            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. -->
-      <method name="dismiss">
-        <body>
-          <![CDATA[
-            if (this.eventCallback) {
-              this.eventCallback("dismissed");
-            }
-            this.close();
-          ]]>
-        </body>
-      </method>
-
-      <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 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>
-
   <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"/>
--- a/toolkit/content/widgets/notificationbox.js
+++ b/toolkit/content/widgets/notificationbox.js
@@ -59,93 +59,122 @@ MozElements.NotificationBox = class Noti
     var notifications = this.allNotifications;
     for (var n = notifications.length - 1; n >= 0; n--) {
       if (aValue == notifications[n].getAttribute("value"))
         return notifications[n];
     }
     return null;
   }
 
-  appendNotification(aLabel, aValue, aImage, aPriority, aButtons, aEventCallback) {
+  /**
+   * Creates a <notification> element and shows it. The calling code can modify
+   * the element synchronously to add features to the notification.
+   *
+   * @param aLabel
+   *        The main message text, or a DocumentFragment containing elements to
+   *        add as children of the notification's main <description> element.
+   * @param aValue
+   *        String identifier of the notification.
+   * @param aImage
+   *        URL of the icon image to display. If not specified, a default icon
+   *        based on the priority will be shown.
+   * @param aPriority
+   *        One of the PRIORITY_ constants. These determine the appearance of
+   *        the notification based on severity (using the "type" attribute), and
+   *        only the notification with the highest priority is displayed.
+   * @param aButtons
+   *        Array of objects defining action buttons:
+   *        {
+   *          label:
+   *            Label of the <button> element.
+   *          accessKey:
+   *            Access key character for the <button> element.
+   *          callback:
+   *            When the button is used, this is called with the arguments:
+   *             1. The <notification> element.
+   *             2. This button object definition.
+   *             3. The <button> element.
+   *             4. The "command" event.
+   *          popup:
+   *            If specified, the button will open the popup element with this
+   *            ID, anchored to the button. This is alternative to "callback".
+   *        }
+   * @param aEventCallback
+   *        This may be called with the "removed" or "dismissed" parameter.
+   * @param aNotificationIs
+   *        Defines a Custom Element name to use as the "is" value on creation.
+   *        This allows subclassing the created element.
+   *
+   * @return The <notification> element that is shown.
+   */
+  appendNotification(aLabel, aValue, aImage, aPriority, aButtons,
+                     aEventCallback, aNotificationIs) {
     if (aPriority < this.PRIORITY_INFO_LOW ||
       aPriority > this.PRIORITY_CRITICAL_HIGH)
       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);
+    // Create the Custom Element and connect it to the document immediately.
+    var newitem = document.createXULElement("notification",
+      aNotificationIs ? { is: aNotificationIs } : {});
+    this.stack.insertBefore(newitem, insertPos);
+
+    // Custom notification classes may not have the messageText property.
+    if (newitem.messageText) {
+      // Can't use instanceof in case this was created from a different document:
+      if (aLabel && typeof aLabel == "object" &&
+          aLabel.nodeType && aLabel.nodeType == aLabel.DOCUMENT_FRAGMENT_NODE) {
+        newitem.messageText.appendChild(aLabel);
+      } else {
+        newitem.messageText.textContent = aLabel;
+      }
+    }
     newitem.setAttribute("value", aValue);
-    if (aImage)
-      newitem.setAttribute("image", aImage);
+    if (aImage) {
+      newitem.messageImage.setAttribute("src", 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);
+        newitem.messageDetails.appendChild(buttonElem);
         buttonElem.buttonInfo = button;
       }
-
-      if (defaultElem)
-        defaultElem.classList.add("notification-button-default");
     }
 
-    newitem.setAttribute("priority", aPriority);
+    newitem.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._showNotification(newitem, true);
     }
-    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);
-    }
-
-    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;
   }
@@ -260,8 +289,85 @@ Object.assign(MozElements.NotificationBo
   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,
 });
+
+MozElements.Notification = class Notification extends MozXULElement {
+  constructor() {
+    super();
+    this.persistence = 0;
+    this.priority = 0;
+    this.timeout = 0;
+  }
+
+  connectedCallback() {
+    this.appendChild(MozXULElement.parseXULToFragment(`
+      <hbox class="messageDetails" align="center" flex="1"
+            oncommand="this.parentNode._doButtonCommand(event);">
+        <image class="messageImage"/>
+        <description class="messageText" flex="1"/>
+        <spacer flex="1"/>
+      </hbox>
+      <toolbarbutton ondblclick="event.stopPropagation();"
+                     class="messageCloseButton close-icon tabbable"
+                     tooltiptext="&closeNotification.tooltip;"
+                     oncommand="this.parentNode.dismiss();"/>
+    `, ["chrome://global/locale/notification.dtd"]));
+
+    for (let [propertyName, selector] of [
+      ["messageDetails", ".messageDetails"],
+      ["messageImage", ".messageImage"],
+      ["messageText", ".messageText"],
+      ["spacer", "spacer"],
+    ]) {
+      this[propertyName] = this.querySelector(selector);
+    }
+  }
+
+  get control() {
+    return this.closest(".notificationbox-stack")._notificationBox;
+  }
+
+  /**
+   * 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.
+   */
+  dismiss() {
+    if (this.eventCallback) {
+      this.eventCallback("dismissed");
+    }
+    this.close();
+  }
+
+  close() {
+    this.control.removeNotification(this);
+  }
+
+  _doButtonCommand(event) {
+    if (!("buttonInfo" in event.target)) {
+      return;
+    }
+
+    var button = event.target.buttonInfo;
+    if (button.popup) {
+      document.getElementById(button.popup).
+      openPopup(event.originalTarget, "after_start", 0, 0, false, false, event);
+      event.stopPropagation();
+    } else {
+      var callback = button.callback;
+      if (callback) {
+        var result = callback(this, button, event.target, event);
+        if (!result) {
+          this.close();
+        }
+        event.stopPropagation();
+      }
+    }
+  }
+};
+
+customElements.define("notification", MozElements.Notification);
--- a/toolkit/content/xul.css
+++ b/toolkit/content/xul.css
@@ -171,22 +171,16 @@ iframe {
     display: block;
   }
 }
 
 browser {
   -moz-binding: url("chrome://global/content/bindings/browser.xml#browser");
 }
 
-/********** notifications **********/
-
-notification {
-  -moz-binding: url("chrome://global/content/bindings/notification.xml#notification");
-}
-
 /*********** popup notification ************/
 popupnotification {
   -moz-binding: url("chrome://global/content/bindings/notification.xml#popup-notification");
 }
 
 .popup-notification-menubutton:not([label]) {
   display: none;
 }
--- a/toolkit/themes/linux/global/global.css
+++ b/toolkit/themes/linux/global/global.css
@@ -252,17 +252,17 @@ label[disabled="true"] {
 .text-link:hover {
   text-decoration: underline;
 }
 
 .text-link:-moz-focusring {
   outline: 1px dotted;
 }
 
-notification > button {
+notification > hbox > button {
   margin-top: 0;
   margin-bottom: 0;
 }
 
 popupnotificationcontent {
   margin-top: .5em;
 }
 
--- a/toolkit/themes/osx/global/global.css
+++ b/toolkit/themes/osx/global/global.css
@@ -213,45 +213,45 @@ label[disabled="true"] {
 .text-link:hover {
   text-decoration: underline;
 }
 
 .text-link:-moz-focusring {
   box-shadow: var(--focus-ring-box-shadow);
 }
 
-notification > button {
+notification > hbox > button {
   margin: 0 3px;
   padding: 1px 10px;
   min-width: 60px;
   min-height: 16px;
   -moz-appearance: none;
   border-radius: 10000px;
   border: @roundButtonBorderWidth@ @roundButtonBorderStyle@ @roundButtonBorderColor@;
   text-shadow: @loweredShadow@;
   color: @roundButtonColor@;
   background: @roundButtonBackground@;
   box-shadow: @roundButtonShadow@;
 }
 
-notification > button:active:hover {
+notification > hbox > button:active:hover {
   color: @roundButtonColor@;
   background: @roundButtonPressedBackground@;
   box-shadow: @roundButtonPressedShadow@;
 }
 
-notification > button:-moz-focusring {
+notification > hbox > button:-moz-focusring {
   box-shadow: var(--focus-ring-box-shadow), @roundButtonShadow@;
 }
 
-notification > button:active:hover:-moz-focusring {
+notification > hbox > button:active:hover:-moz-focusring {
   box-shadow: var(--focus-ring-box-shadow), @roundButtonPressedShadow@;
 }
 
-notification > button > .button-box > .button-text {
+notification > hbox > button > .button-box > .button-text {
   margin: 0 !important;
 }
 
 popupnotificationcontent {
   margin-top: .5em;
 }
 
 %include ../../shared/notification-popup.inc.css
--- a/toolkit/themes/shared/notification.inc.css
+++ b/toolkit/themes/shared/notification.inc.css
@@ -58,25 +58,25 @@ notification[type="critical"] {
 
 .messageImage {
   width: 16px;
   height: 16px;
   -moz-context-properties: fill;
   fill: currentColor;
 }
 
-.messageImage[type="info"] {
+notification[type="info"] > hbox > .messageImage {
   list-style-image: url("chrome://global/skin/icons/help.svg");
 }
 
-.messageImage[type="warning"] {
+notification[type="warning"] > hbox > .messageImage {
   list-style-image: url("chrome://global/skin/icons/warning.svg");
 }
 
-.messageImage[type="critical"] {
+notification[type="critical"] > hbox > .messageImage {
   list-style-image: url("chrome://global/skin/icons/error.svg");
 }
 
 .messageCloseButton {
   margin: 0;
   padding: 0;
 }
 
--- a/uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js
+++ b/uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js
@@ -8,18 +8,17 @@ add_task(async function() {
   await BrowserTestUtils.browserLoaded(browser, false, testURL);
 
   // Register the protocol handler by clicking the notificationbar button.
   let notificationValue = "Protocol Registration: web+testprotocol";
   let getNotification = () =>
     gBrowser.getNotificationBox().getNotificationWithValue(notificationValue);
   await BrowserTestUtils.waitForCondition(getNotification);
   let notification = getNotification();
-  let button =
-    notification.getElementsByClassName("notification-button-default")[0];
+  let button = notification.querySelector("button");
   ok(button, "got registration button");
   button.click();
 
   // Set the new handler as default.
   const protoSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"].
                      getService(Ci.nsIExternalProtocolService);
   let protoInfo = protoSvc.getProtocolHandlerInfo("web+testprotocol");
   is(protoInfo.preferredAction, protoInfo.useHelperApp,