Bug 893505 - clean up js and simplify css for PanelUI notifications draft
authorDoug Thayer <dougathayer@gmail.com>
Wed, 04 Jan 2017 12:37:17 -0800
changeset 456079 bbe581b369218c7c623447398ff8acaa4fa597e4
parent 455429 16fe8dc50d181c56c4be309f579429caf35059d1
child 541127 5f2808b6abca73186c88c4d93dfe30fd504dc96d
push id40384
push userbmo:dothayer@mozilla.com
push dateWed, 04 Jan 2017 21:28:00 +0000
bugs893505
milestone53.0a1
Bug 893505 - clean up js and simplify css for PanelUI notifications MozReview-Commit-ID: 3WZMgQ6sKcL
browser/base/content/browser.css
browser/base/content/browser.js
browser/base/content/test/general/browser.ini
browser/base/content/test/general/browser_menuButtonBadgeManager.js
browser/base/content/test/general/browser_panelUINotifications.js
browser/components/customizableui/content/panelUI.inc.xul
browser/components/customizableui/content/panelUI.js
browser/components/customizableui/test/browser.ini
browser/components/customizableui/test/browser_panelUINotifications.js
browser/locales/en-US/chrome/browser/browser.properties
browser/themes/shared/customizableui/panelUI.inc.css
browser/themes/shared/notification-icons.inc.css
toolkit/themes/osx/global/toolbarbutton.css
toolkit/themes/windows/global/toolbarbutton.css
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -1139,19 +1139,17 @@ toolbarpaletteitem[place="palette"][hidd
 
 #customization-palette .toolbarpaletteitem-box {
   -moz-box-pack: center;
   -moz-box-flex: 1;
   width: 10em;
   max-width: 10em;
 }
 
-#main-window[customizing=true] #PanelUI-update-available-menu-item,
-#main-window[customizing=true] #PanelUI-download-available-menu-item,
-#main-window[customizing=true] #PanelUI-update-restart-menu-item {
+#main-window[customizing=true] #PanelUI-notification-menu-item {
   display: none;
 }
 
 /* UI Tour */
 
 @keyframes uitour-wobble {
   from {
     transform: rotate(0deg) translateX(3px) rotate(0deg);
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -2659,44 +2659,42 @@ var gMenuButtonUpdateBadge = {
       this.reset();
       return;
     }
 
     if (status == "failed") {
       // Background update has failed, let's show the UI responsible for
       // prompting the user to update manually.
       this.uninit();
-      let openManualUpdateUrl = this.openManualUpdateUrl;
       let action = {
-        callback: () => { openManualUpdateUrl(); }
+        callback: () => { gMenuButtonUpdateBadge.openManualUpdateUrl(); }
       };
 
-      PanelUI.showNotification("update-manual", action, [], {});
+      PanelUI.showNotification("update-manual", action);
       return;
     }
 
     // Give the user badgeWaitTime seconds to react before prompting.
     this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
     this.timer.initWithCallback(this, this.badgeWaitTime * 1000,
                                 this.timer.TYPE_ONE_SHOT);
     // The timer callback will call uninit() when it completes.
   },
 
   notify: function() {
     // If the update is successfully applied, or if the updater has fallen back
     // to non-staged updates, add a badge to the hamburger menu to indicate an
     // update will be applied once the browser restarts.
     this.uninit();
 
-    let requestRestart = this.requestRestart;
     let action = {
-      callback: () => { requestRestart(); }
+      callback: () => { gMenuButtonUpdateBadge.requestRestart(); }
     };
 
-    PanelUI.showNotification("update-restart", action, [], {});
+    PanelUI.showNotification("update-restart", action);
   },
 
   reset: function() {
     PanelUI.removeNotification(/^update-/);
     this.uninit();
     this.init();
   }
 };
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -354,17 +354,16 @@ support-files =
 [browser_popupUI.js]
 [browser_popup_blocker.js]
 skip-if = (os == 'linux') || (e10s && debug) # Frequent bug 1081925 and bug 1125520 failures
 [browser_printpreview.js]
 [browser_private_browsing_window.js]
 [browser_private_no_prompt.js]
 [browser_purgehistory_clears_sh.js]
 [browser_PageMetaData_pushstate.js]
-[browser_panelUINotifications.js]
 [browser_refreshBlocker.js]
 support-files =
   refresh_header.sjs
   refresh_meta.sjs
 [browser_relatedTabs.js]
 [browser_remoteTroubleshoot.js]
 skip-if = !updater
 reason = depends on UpdateUtils .Locale
@@ -481,17 +480,16 @@ tags = mcb
 tags = psm
 [browser_mcb_redirect.js]
 tags = mcb
 [browser_windowactivation.js]
 [browser_contextmenu_childprocess.js]
 [browser_bug963945.js]
 [browser_domFullscreen_fullscreenMode.js]
 tags = fullscreen
-[browser_menuButtonBadgeManager.js]
 [browser_newTabDrop.js]
 [browser_newWindowDrop.js]
 [browser_csp_block_all_mixedcontent.js]
 tags = mcb
 [browser_newwindow_focus.js]
 skip-if = (os == "linux" && !e10s) # Bug 1263254 - Perma fails on Linux without e10s for some reason.
 [browser_bug1299667.js]
 [browser_close_dependent_tabs.js]
deleted file mode 100644
--- a/browser/base/content/test/general/browser_menuButtonBadgeManager.js
+++ /dev/null
@@ -1,43 +0,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/. */
-
-var menuButton = document.getElementById("PanelUI-menu-button");
-
-add_task(function* testButtonActivities() {
-  is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
-  is(menuButton.hasAttribute("badge"), false, "Should not have the badge attribute set");
-
-  PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
-  is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
-
-  PanelUI.showBadgeOnlyNotification("update-succeeded");
-  is(menuButton.getAttribute("badge-status"), "update-succeeded", "Should have update-succeeded badge status (update > fxa)");
-
-  PanelUI.showBadgeOnlyNotification("update-failed");
-  is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
-
-  PanelUI.showBadgeOnlyNotification("download-severe");
-  is(menuButton.getAttribute("badge-status"), "download-severe", "Should have download-severe badge status");
-
-  PanelUI.showBadgeOnlyNotification("download-warning");
-  is(menuButton.getAttribute("badge-status"), "download-warning", "Should have download-warning badge status");
-
-  PanelUI.removeNotification(/^download-/);
-  is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
-
-  PanelUI.removeNotification(/^update-/);
-  is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
-
-  PanelUI.removeNotification(/^fxa-/);
-  is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
-
-  yield PanelUI.show();
-  is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status (Hamburger menu opened)");
-  PanelUI.hide();
-
-  PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
-  PanelUI.showBadgeOnlyNotification("update-succeeded");
-  PanelUI.removeNotification(/.*/);
-  is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status (clearBadges called)");
-});
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -431,18 +431,20 @@
                    closebuttonhidden="true"
                    secondarybuttonlabel="&updateAvailable.cancelButton.label;"
                    secondarybuttonaccesskey="&updateAvailable.cancelButton.accesskey;"
                    dropmarkerhidden="true"
                    checkboxhidden="true"
                    hidden="true">
   <popupnotificationcontent id="update-available-notification-content" orient="vertical">
     <description id="update-available-description">&updateAvailable.message;
+#ifndef NIGHTLY_BUILD
       <label class="text-link" value="&updateAvailable.whatsnew.label;"
-             href="&updateAvailable.whatsnew.href;"/>
+#expand      href="https://www.mozilla.org/firefox/__MOZ_APP_VERSION__/releasenotes/"/>
+#endif
     </description>
   </popupnotificationcontent>
 </popupnotification>
 
 <popupnotification id="PanelUI-update-manual-notification"
                    popupid="update-manual"
                    label="&updateManual.header.message;"
                    buttonlabel="&updateManual.acceptButton.label;"
@@ -450,18 +452,20 @@
                    closebuttonhidden="true"
                    secondarybuttonlabel="&updateManual.cancelButton.label;"
                    secondarybuttonaccesskey="&updateManual.cancelButton.accesskey;"
                    dropmarkerhidden="true"
                    checkboxhidden="true"
                    hidden="true">
   <popupnotificationcontent id="update-manual-notification-content" orient="vertical">
     <description id="update-manual-description">&updateManual.message;
+#ifndef NIGHTLY_BUILD
       <label class="text-link" value="&updateManual.whatsnew.label;"
-             href="&updateManual.whatsnew.href;"/>
+#expand      href="https://www.mozilla.org/firefox/__MOZ_APP_VERSION__/releasenotes/"/>
+#endif
     </description>
   </popupnotificationcontent>
 </popupnotification>
 
 <popupnotification id="PanelUI-update-restart-notification"
                    popupid="update-restart"
                    label="&updateRestart.header.message;"
                    buttonlabel="&updateRestart.acceptButton.label;"
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -165,35 +165,47 @@ const PanelUI = {
         anchor = this._getPanelAnchor(anchor);
         this.panel.openPopup(anchor);
       }, (reason) => {
         console.error("Error showing the PanelUI menu", reason);
       });
     });
   },
 
-  showNotification: function(id, mainAction, secondaryActions, options) {
-    let notification = new Notification(id, mainAction, secondaryActions, options);
+  showNotification(id, mainAction, secondaryActions = [], options = {}) {
+    let notification = new PanelUINotification(id, mainAction, secondaryActions, options);
     let existingIndex = this.notifications.findIndex(n => n.id == id);
     if (existingIndex != -1) {
       this.notifications.splice(existingIndex, 1);
     }
-    this.notifications.forEach(n => { n.dismissed = true; });
+
+    // we don't want to clobber doorhanger notifications just to show a badge,
+    // so don't dismiss any of them and the badge will show once the doorhanger
+    // gets resolved.
+    if (!options.badgeOnly) {
+      this.notifications.forEach(n => { n.dismissed = true; });
+    }
+
+    // since notifications are generally somewhat pressing, the ideal case is that
+    // we never have two notifications at once. However, in the event that we do,
+    // it's more likely that the older notification has been sitting around for a
+    // bit, and so we don't want to hide the new notification behind it. Thus,
+    // we want our notifications to behave like a stack instead of a queue.
     this.notifications.unshift(notification);
     this._updateNotifications();
     return notification;
   },
 
-  showBadgeOnlyNotification: function(id) {
+  showBadgeOnlyNotification(id) {
     return this.showNotification(id, null, null, { badgeOnly: true });
   },
 
-  removeNotification: function(id) {
+  removeNotification(id) {
     let notifications;
-    if (typeof(id) == "string") {
+    if (typeof id == "string") {
       notifications = this.notifications.filter(n => n.id == id);
     } else {
       // if it's not a string, assume RegExp
       notifications = this.notifications.filter(n => id.test(n.id));
     }
 
     notifications.forEach(n => {
       this._removeNotification(n);
@@ -432,19 +444,19 @@ const PanelUI = {
       }
 
       viewShown = true;
       CustomizableUI.addPanelCloseListeners(tempPanel);
       tempPanel.addEventListener("popuphidden", panelRemover);
 
       let anchor = this._getPanelAnchor(aAnchor);
 
-      if ((aAnchor != anchor) && aAnchor.id) {
+      if ((aAnchor != anchor) && aAnchor.id)
         anchor.setAttribute("consumeanchor", aAnchor.id);
-      }
+
       tempPanel.openPopup(anchor, "bottomcenter topright");
     }
   }),
 
   /**
    * NB: The enable- and disableSingleSubviewPanelAnimations methods only
    * affect the hiding/showing animations of single-subview panels (tempPanel
    * in the showSubView method).
@@ -574,17 +586,17 @@ const PanelUI = {
   },
 
   _overlayScrollListenerBoundFn: null,
   _overlayScrollListener: function(aMQL) {
     ScrollbarSampler.resetSystemScrollbarWidth();
     this._scrollWidth = null;
   },
 
-  _updateNotifications: function() {
+  _updateNotifications() {
     if (!this.notifications.length) {
       this._clearAllNotifications();
       this.notificationPanel.hidePopup();
       return;
     }
 
     let doorhangers =
       this.notifications.filter(n => !n.dismissed && !n.options.badgeOnly);
@@ -603,43 +615,43 @@ const PanelUI = {
       this._showNotificationPanel(doorhangers[0]);
     } else {
       this.notificationPanel.hidePopup();
       this._showBadge(this.notifications[0]);
       this._showMenuItem(this.notifications[0]);
     }
   },
 
-  _showNotificationPanel: function(notification) {
+  _showNotificationPanel(notification) {
     this._refreshNotificationPanel(notification);
 
     if (this.isNotificationPanelOpen) {
       return;
     }
 
     let anchor = this._getPanelAnchor(this.menuButton);
 
     this.notificationPanel.hidden = false;
     this.notificationPanel.openPopup(anchor, "bottomcenter topright");
   },
 
-  _clearNotificationPanel: function() {
+  _clearNotificationPanel() {
     for (let popupnotification of this.notificationPanel.children) {
       popupnotification.hidden = true;
       popupnotification.notification = null;
     }
   },
 
-  _clearAllNotifications: function() {
+  _clearAllNotifications() {
     this._clearNotificationPanel();
     this._clearBadges();
     this._clearMenuItems();
   },
 
-  _refreshNotificationPanel: function(notification) {
+  _refreshNotificationPanel(notification) {
     this._clearNotificationPanel();
 
     let popupnotificationID = this._getPopupId(notification);
     let popupnotification = document.getElementById(popupnotificationID);
 
     popupnotification.setAttribute("id", popupnotificationID);
     popupnotification.setAttribute("buttoncommand", "PanelUI._onNotificationButtonEvent(event, 'buttoncommand');");
     popupnotification.setAttribute("secondarybuttoncommand", "PanelUI._onNotificationButtonEvent(event, 'secondarybuttoncommand');");
@@ -651,58 +663,59 @@ const PanelUI = {
     popupnotification.hidden = false;
   },
 
   _showBadge(notification) {
     let badgeStatus = this._getBadgeStatus(notification);
     this.menuButton.setAttribute("badge-status", badgeStatus);
   },
 
-  _showMenuItem: function(notification) {
+  _showMenuItem(notification) {
     this._clearMenuItems();
 
     let menuItemId = this._getMenuItemId(notification);
     let menuItem = document.getElementById(menuItemId);
     if (menuItem) {
       menuItem.notification = notification;
       menuItem.setAttribute("oncommand", "PanelUI._onNotificationMenuItemSelected(event)");
+      menuItem.className += " PanelUI-notification-menu-item";
       menuItem.hidden = false;
       menuItem.fromPanelUINotifications = true;
     }
   },
 
-  _clearBadges: function() {
+  _clearBadges() {
     this.menuButton.removeAttribute("badge-status");
   },
 
-  _clearMenuItems: function() {
+  _clearMenuItems() {
     for (let child of this.footer.children) {
       if (child.fromPanelUINotifications) {
         child.notification = null;
         child.hidden = true;
       }
     }
   },
 
-  _removeNotification: function(notification) {
+  _removeNotification(notification) {
     // This notification may already be removed, in which case let's just fail
     // silently.
     let notifications = this.notifications;
     if (!notifications)
       return;
 
     var index = notifications.indexOf(notification);
     if (index == -1)
       return;
 
     // remove the notification
     notifications.splice(index, 1);
   },
 
-  _onNotificationButtonEvent: function(event, type) {
+  _onNotificationButtonEvent(event, type) {
     let notificationEl = getNotificationFromElement(event.originalTarget);
 
     if (!notificationEl)
       throw "PanelUI._onNotificationButtonEvent: couldn't find notification element";
 
     if (!notificationEl.notification)
       throw "PanelUI._onNotificationButtonEvent: couldn't find notification";
 
@@ -712,56 +725,56 @@ const PanelUI = {
 
     if (type == "secondarybuttoncommand") {
       action = notification.secondaryActions[0];
     }
 
     let dismiss = true;
     if (action) {
       try {
-        action.callback.call(undefined);
+        action.callback();
       } catch (error) {
         Cu.reportError(error);
       }
 
       dismiss = action.dismiss;
     }
 
     if (dismiss) {
       notification.dismissed = true;
     } else {
       this._removeNotification(notification);
     }
     this._updateNotifications();
   },
 
-  _onNotificationMenuItemSelected: function(event) {
+  _onNotificationMenuItemSelected(event) {
     let target = event.originalTarget;
     if (!target.notification)
       throw "menucommand target has no associated action/notification";
 
     event.stopPropagation();
 
     try {
-      target.notification.mainAction.callback.call(undefined);
+      target.notification.mainAction.callback();
     } catch (error) {
       Cu.reportError(error);
     }
 
     this._removeNotification(target.notification);
     this._updateNotifications();
   },
 
-  _getPopupId: function(notification) { return "PanelUI-" + notification.id + "-notification"; },
+  _getPopupId(notification) { return "PanelUI-" + notification.id + "-notification"; },
 
-  _getBadgeStatus: function(notification) { return notification.id; },
+  _getBadgeStatus(notification) { return notification.id; },
 
-  _getMenuItemId: function(notification) { return "PanelUI-" + notification.id + "-menu-item"; },
+  _getMenuItemId(notification) { return "PanelUI-" + notification.id + "-menu-item"; },
 
-  _getPanelAnchor: function(candidate) {
+  _getPanelAnchor(candidate) {
     let iconAnchor =
       document.getAnonymousElementByAttribute(candidate, "class",
                                               "toolbarbutton-icon");
     return iconAnchor || candidate;
   }
 };
 
 XPCOMUtils.defineConstant(this, "PanelUI", PanelUI);
@@ -775,38 +788,28 @@ function getLocale() {
     let chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"]
                            .getService(Ci.nsIXULChromeRegistry);
     return chromeRegistry.getSelectedLocale("browser");
   } catch (ex) {
     return "en-US";
   }
 }
 
-function Notification(id, mainAction, secondaryActions, options) {
+function PanelUINotification(id, mainAction, secondaryActions = [], options = {}) {
   this.id = id;
   this.mainAction = mainAction;
-  this.secondaryActions = secondaryActions || [];
-  this.options = options || {};
+  this.secondaryActions = secondaryActions;
+  this.options = options;
   this.dismissed = this.options.dismissed || false;
 }
 
-Notification.prototype = {
-  id: null,
-  mainAction: null,
-  secondaryActions: null,
-  browser: null,
-  owner: null,
-  options: null,
-  timeShown: null,
-  dismissed: null,
-};
-
 function getNotificationFromElement(aElement) {
   // Need to find the associated notification object, which is a bit tricky
   // since it isn't associated with the element directly - this is kind of
   // gross and very dependent on the structure of the popupnotification
   // binding's content.
   let notificationEl;
   let parent = aElement;
-  while (parent && (parent = aElement.ownerDocument.getBindingParent(parent)))
+  while (parent && (parent = aElement.ownerDocument.getBindingParent(parent))) {
     notificationEl = parent;
+  }
   return notificationEl;
 }
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -145,10 +145,11 @@ tags = fullscreen
 skip-if = os == "mac"
 [browser_1087303_button_preferences.js]
 [browser_1089591_still_customizable_after_reset.js]
 [browser_1096763_seen_widgets_post_reset.js]
 [browser_1161838_inserted_new_default_buttons.js]
 [browser_bootstrapped_custom_toolbar.js]
 [browser_customizemode_contextmenu_menubuttonstate.js]
 [browser_panel_toggle.js]
+[browser_panelUINotifications.js]
 [browser_switch_to_customize_mode.js]
 [browser_check_tooltips_in_navbar.js]
rename from browser/base/content/test/general/browser_panelUINotifications.js
rename to browser/components/customizableui/test/browser_panelUINotifications.js
--- a/browser/base/content/test/general/browser_panelUINotifications.js
+++ b/browser/components/customizableui/test/browser_panelUINotifications.js
@@ -1,68 +1,276 @@
-add_task(function*() {
-  yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank"},
-    function*(browser) {
-      let doc = browser.ownerDocument;
-
-      let mainActionCalled = false;
-      let mainAction = {
-        callback: () => { mainActionCalled = true; }
-      };
-      PanelUI.showNotification("update-manual", mainAction, [], {});
-
-      isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
-      let notifications = PanelUI.notificationPanel.children;
-      is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
-      let doorhanger = notifications[0];
-      is(doorhanger.id, "PanelUI-update-manual-notification","PanelUI is displaying the update-manual notification.")
-
-      let mainActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
-      mainActionButton.click();
-
-      ok(mainActionCalled, "Main action callback was called")
-      is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
-    });
-});
-
-add_task(function*() {
-  yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank"},
-    function*(browser) {
-      let doc = browser.ownerDocument;
-
-      PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
-      is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
-
-      let mainActionCalled = false;
-      let mainAction = {
-        callback: () => { mainActionCalled = true; },
-      };
-      PanelUI.showNotification("update-manual", mainAction, [], {});
-      isnot(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is hidden on PanelUI button.");
-
-      isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
-      let notifications = PanelUI.notificationPanel.children;
-      is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
-      let doorhanger = notifications[0];
-      is(doorhanger.id, "PanelUI-update-manual-notification", "PanelUI is displaying the update-manual notification.")
-
-      let secondaryActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
-      secondaryActionButton.click();
-
-      is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
-
-      is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is displaying on PanelUI button.");
-
-      yield PanelUI.show();
-      isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is hidden on PanelUI button.");
-      let menuItem = doc.getElementById("PanelUI-update-manual-menu-item");
-      is(menuItem.hidden, false, "update-manual menu item is showing.")
-
-      yield PanelUI.hide();
-      is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is shown on PanelUI button.");
-
-      yield PanelUI.show();
-      menuItem.click();
-      ok(mainActionCalled, "Main action callback was called")
-
-      is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
-    });
-});
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Tests that when we click on the main call-to-action of the doorhanger, the provided
+ * action is called, and the doorhanger removed.
+ */
+add_task(function* testMainActionCalled() {
+  yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
+    let doc = browser.ownerDocument;
+
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+    let mainActionCalled = false;
+    let mainAction = {
+      callback: () => { mainActionCalled = true; }
+    };
+    PanelUI.showNotification("update-manual", mainAction);
+
+    isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
+    let notifications = PanelUI.notificationPanel.children;
+    is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+    let doorhanger = notifications[0];
+    is(doorhanger.id, "PanelUI-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+    let mainActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
+    mainActionButton.click();
+
+    ok(mainActionCalled, "Main action callback was called");
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+    is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+  });
+});
+
+/**
+ * This tests that when we click the secondary action for a notification,
+ * it will display the badge for that notification on the PanelUI menu button.
+ * Once we click on this button, we should see an item in the menu which will
+ * call our main action.
+ */
+add_task(function* testSecondaryActionWorkflow() {
+  yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
+    let doc = browser.ownerDocument;
+
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+
+    let mainActionCalled = false;
+    let mainAction = {
+      callback: () => { mainActionCalled = true; },
+    };
+    PanelUI.showNotification("update-manual", mainAction);
+
+    isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
+    let notifications = PanelUI.notificationPanel.children;
+    is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+    let doorhanger = notifications[0];
+    is(doorhanger.id, "PanelUI-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+    let secondaryActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
+    secondaryActionButton.click();
+
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+
+    is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is displaying on PanelUI button.");
+
+    yield PanelUI.show();
+    isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is hidden on PanelUI button.");
+    let menuItem = doc.getElementById("PanelUI-update-manual-menu-item");
+    is(menuItem.hidden, false, "update-manual menu item is showing.");
+
+    yield PanelUI.hide();
+    is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is shown on PanelUI button.");
+
+    yield PanelUI.show();
+    menuItem.click();
+    ok(mainActionCalled, "Main action callback was called");
+
+    PanelUI.removeNotification(/.*/);
+  });
+});
+
+/**
+ * We want to ensure a few things with this:
+ * - Adding a doorhanger will make a badge disappear
+ * - once the notification for the doorhanger is resolved (removed, not just dismissed),
+ *   then we display any other badges that are remaining.
+ */
+add_task(function* testInteractionWithBadges() {
+  yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
+    let doc = browser.ownerDocument;
+
+    PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
+    is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+
+    let mainActionCalled = false;
+    let mainAction = {
+      callback: () => { mainActionCalled = true; },
+    };
+    PanelUI.showNotification("update-manual", mainAction);
+
+    isnot(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is hidden on PanelUI button.");
+    isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
+    let notifications = PanelUI.notificationPanel.children;
+    is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+    let doorhanger = notifications[0];
+    is(doorhanger.id, "PanelUI-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+    let secondaryActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
+    secondaryActionButton.click();
+
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+
+    is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is displaying on PanelUI button.");
+
+    yield PanelUI.show();
+    isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is hidden on PanelUI button.");
+    let menuItem = doc.getElementById("PanelUI-update-manual-menu-item");
+    is(menuItem.hidden, false, "update-manual menu item is showing.");
+
+    menuItem.click();
+    ok(mainActionCalled, "Main action callback was called");
+
+    is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
+    PanelUI.removeNotification(/.*/);
+    is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+  });
+});
+
+/**
+ * This tests that adding a badge will not dismiss any existing doorhangers.
+ */
+add_task(function* testAddingBadgeWhileDoorhangerIsShowing() {
+  yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
+    let doc = browser.ownerDocument;
+
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+    let mainActionCalled = false;
+    let mainAction = {
+      callback: () => { mainActionCalled = true; }
+    };
+    PanelUI.showNotification("update-manual", mainAction);
+    PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
+
+    isnot(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is hidden on PanelUI button.");
+    isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
+    let notifications = PanelUI.notificationPanel.children;
+    is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+    let doorhanger = notifications[0];
+    is(doorhanger.id, "PanelUI-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+    let mainActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
+    mainActionButton.click();
+
+    ok(mainActionCalled, "Main action callback was called");
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+    is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
+    PanelUI.removeNotification(/.*/);
+    is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+  });
+});
+
+/**
+ * Tests that badges operate like a stack.
+ */
+add_task(function* testMultipleBadges() {
+  yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
+    let doc = browser.ownerDocument;
+    let menuButton = doc.getElementById("PanelUI-menu-button");
+
+    is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+    is(menuButton.hasAttribute("badge"), false, "Should not have the badge attribute set");
+
+    PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
+    is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
+
+    PanelUI.showBadgeOnlyNotification("update-succeeded");
+    is(menuButton.getAttribute("badge-status"), "update-succeeded", "Should have update-succeeded badge status (update > fxa)");
+
+    PanelUI.showBadgeOnlyNotification("update-failed");
+    is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
+
+    PanelUI.showBadgeOnlyNotification("download-severe");
+    is(menuButton.getAttribute("badge-status"), "download-severe", "Should have download-severe badge status");
+
+    PanelUI.showBadgeOnlyNotification("download-warning");
+    is(menuButton.getAttribute("badge-status"), "download-warning", "Should have download-warning badge status");
+
+    PanelUI.removeNotification(/^download-/);
+    is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
+
+    PanelUI.removeNotification(/^update-/);
+    is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
+
+    PanelUI.removeNotification(/^fxa-/);
+    is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+
+    yield PanelUI.show();
+    is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status (Hamburger menu opened)");
+    PanelUI.hide();
+
+    PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
+    PanelUI.showBadgeOnlyNotification("update-succeeded");
+    PanelUI.removeNotification(/.*/);
+    is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+  });
+});
+
+/**
+ * Tests that non-badges also operate like a stack.
+ */
+add_task(function* testMultipleNonBadges() {
+  yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
+    let doc = browser.ownerDocument;
+
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+
+    let updateManualAction = {
+        called: false,
+        callback: () => { updateManualAction.called = true; },
+    };
+    let updateRestartAction = {
+        called: false,
+        callback: () => { updateRestartAction.called = true; },
+    };
+
+    PanelUI.showNotification("update-manual", updateManualAction);
+
+    let notifications;
+    let doorhanger;
+
+    isnot(PanelUI.notificationPanel.state, "closed", "Doorhanger is showing.");
+    notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
+    is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+    doorhanger = notifications[0];
+    is(doorhanger.id, "PanelUI-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+    PanelUI.showNotification("update-restart", updateRestartAction);
+
+    isnot(PanelUI.notificationPanel.state, "closed", "Doorhanger is showing.");
+    notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
+    is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+    doorhanger = notifications[0];
+    is(doorhanger.id, "PanelUI-update-restart-notification", "PanelUI is displaying the update-restart notification.");
+
+    let secondaryActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
+    secondaryActionButton.click();
+
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+    is(PanelUI.menuButton.getAttribute("badge-status"), "update-restart", "update-restart badge is displaying on PanelUI button.");
+
+    let menuItem
+
+    yield PanelUI.show();
+    isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-restart", "update-restart badge is hidden on PanelUI button.");
+    menuItem = doc.getElementById("PanelUI-update-restart-menu-item");
+    is(menuItem.hidden, false, "update-restart menu item is showing.");
+
+    menuItem.click();
+    ok(updateRestartAction.called, "update-restart main action callback was called");
+
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+    is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "update-manual badge is displaying on PanelUI button.");
+
+    yield PanelUI.show();
+    isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "update-manual badge is hidden on PanelUI button.");
+    menuItem = doc.getElementById("PanelUI-update-manual-menu-item");
+    is(menuItem.hidden, false, "update-manual menu item is showing.");
+
+    menuItem.click();
+    ok(updateManualAction.called, "update-manual main action callback was called");
+  });
+});
+
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -622,25 +622,16 @@ flashHang.helpButton.accesskey = L
 # be replaced with a hyperlink containing the text defined in customizeTips.tip0.learnMore.
 customizeTips.tip0 = %1$S: You can customize %2$S to work the way you do. Simply drag any of the above to the menu or toolbar. %3$S about customizing %2$S.
 customizeTips.tip0.hint = Hint
 customizeTips.tip0.learnMore = Learn more
 
 # LOCALIZATION NOTE (customizeMode.tabTitle): %S is brandShortName
 customizeMode.tabTitle = Customize %S
 
-# LOCALIZATION NOTE(appmenu.*.description, appmenu.*.label): these are used for
-# the appmenu labels and buttons that appear when an update is staged for
-# installation or a background update has failed and a manual download is required.
-# %S is brandShortName
-appmenu.restartNeeded.description = Restart %S to apply updates
-appmenu.updateFailed.description = Background update failed, please download update
-appmenu.restartBrowserButton.label = Restart %S
-appmenu.downloadUpdateButton.label = Download %S Update
-
 # LOCALIZATION NOTE : FILE Reader View is a feature name and therefore typically used as a proper noun.
 
 readingList.promo.firstUse.readerView.title = Reader View
 readingList.promo.firstUse.readerView.body = Remove clutter so you can focus exactly on what you want to read.
 
 # LOCALIZATION NOTE (appMenuRemoteTabs.mobilePromo.text2):
 # %1$S will be replaced with a link, the text of which is
 # appMenuRemoteTabs.mobilePromo.android and the link will be to
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -103,47 +103,41 @@
   height: 10px;
   width: 10px;
   background-size: contain;
 }
 
 #PanelUI-menu-button[badge-status="download-success"] > .toolbarbutton-badge-stack > .toolbarbutton-badge {
   display: none;
 }
-#PanelUI-menu-button[badge-status="update-succeeded"] > .toolbarbutton-badge-stack > .toolbarbutton-badge {
-  background: #74BF43 url(chrome://browser/skin/update-badge.svg) no-repeat center;
-  height: 13px;
-}
 
 #PanelUI-menu-button[badge-status="update-available"] > .toolbarbutton-badge-stack > .toolbarbutton-badge,
 #PanelUI-menu-button[badge-status="update-manual"] > .toolbarbutton-badge-stack > .toolbarbutton-badge,
 #PanelUI-menu-button[badge-status="update-restart"] > .toolbarbutton-badge-stack > .toolbarbutton-badge {
   background: #74BF43 url(chrome://browser/skin/update-badge.svg) no-repeat center;
-  height: 13px;
-}
-
-#PanelUI-menu-button[badge-status="update-failed"] > .toolbarbutton-badge-stack > .toolbarbutton-badge {
-  background: #D90000 url(chrome://browser/skin/update-badge-failed.svg) no-repeat center;
-  height: 13px;
+  border-radius: 50%;
+  box-shadow: none;
+  border: 1px solid -moz-dialog;
+  margin: -9px 0 0 !important;
+  margin-inline-end: -6px !important;
+  min-width: 16px;
+  min-height: 16px;
 }
 
 #PanelUI-menu-button[badge-status="download-warning"] > .toolbarbutton-badge-stack > .toolbarbutton-badge,
 #PanelUI-menu-button[badge-status="fxa-needs-authentication"] > .toolbarbutton-badge-stack > .toolbarbutton-badge {
   box-shadow: none;
   filter: drop-shadow(0 1px 0 hsla(206, 50%, 10%, .15));
-  border-radius: 2px;
-  border: none;
 }
 
 #PanelUI-menu-button[badge-status="download-warning"] > .toolbarbutton-badge-stack > .toolbarbutton-badge,
 #PanelUI-menu-button[badge-status="download-severe"] > .toolbarbutton-badge-stack > .toolbarbutton-badge {
   width: 7px;
   height: 7px;
   min-width: 0;
-  min-height: 0;
   /* "!important" is necessary to override the rule in toolbarbutton.css */
   margin-top: -1px !important;
   margin-right: -2px !important;
 }
 
 #PanelUI-menu-button[badge-status="download-warning"] > .toolbarbutton-badge-stack > .toolbarbutton-badge {
   background: #FFBF00;
 }
@@ -439,27 +433,25 @@ toolbaritem[cui-areatype="menu-panel"][s
 toolbaritem[cui-areatype="menu-panel"][sdkstylewidget="true"] > iframe {
   margin: 4px auto;
 }
 
 #PanelUI-multiView[viewtype="subview"] > .panel-viewcontainer > .panel-viewstack > .panel-mainview >  #PanelUI-mainView {
   background-color: var(--arrowpanel-dimmed);
 }
 
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-contents-scroller > #PanelUI-contents > .panel-wide-item,
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-contents-scroller > #PanelUI-contents > .toolbarbutton-1:not([panel-multiview-anchor="true"]),
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-update-available-menu-item,
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-update-manual-menu-item,
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-update-restart-menu-item,
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-fxa > #PanelUI-fxa-status > #PanelUI-fxa-avatar,
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-fxa > #PanelUI-fxa-status > #PanelUI-fxa-label,
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-fxa > #PanelUI-fxa-icon,
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-inner > toolbarseparator,
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-inner > #PanelUI-customize,
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-inner > #PanelUI-help:not([panel-multiview-anchor="true"]) {
+#PanelUI-multiView[viewtype="subview"] #PanelUI-contents > .panel-wide-item,
+#PanelUI-multiView[viewtype="subview"] #PanelUI-contents > .toolbarbutton-1:not([panel-multiview-anchor="true"]),
+#PanelUI-multiView[viewtype="subview"] .PanelUI-notification-menu-item,
+#PanelUI-multiView[viewtype="subview"] #PanelUI-fxa-avatar,
+#PanelUI-multiView[viewtype="subview"] #PanelUI-fxa-label,
+#PanelUI-multiView[viewtype="subview"] #PanelUI-fxa-icon,
+#PanelUI-multiView[viewtype="subview"] #PanelUI-footer-inner > toolbarseparator,
+#PanelUI-multiView[viewtype="subview"] #PanelUI-customize,
+#PanelUI-multiView[viewtype="subview"] #PanelUI-help:not([panel-multiview-anchor="true"]) {
   opacity: .5;
 }
 
 /*
  * XXXgijs: this is a workaround for a layout issue that was caused by these iframes,
  * which was affecting subview display. Because of this, we're hiding the iframe *only*
  * when displaying a subview. The discerning user might notice this, but it's not nearly
  * as bad as the brokenness.
@@ -565,19 +557,17 @@ toolbarpaletteitem[place="palette"] > to
   width: 47px;
   padding-top: 1px;
   display: block;
   text-align: center;
   position: relative;
   top: 25%;
 }
 
-#PanelUI-update-available-menu-item::after,
-#PanelUI-update-manual-menu-item::after,
-#PanelUI-update-restart-menu-item::after {
+.PanelUI-notification-menu-item::after {
   content: "";
   width: 16px;
   height: 16px;
   margin-inline-end: 16.5px;
   border-radius: 50%;
   display: -moz-box;
   background: #74BF43 url(chrome://browser/skin/update-badge.svg) no-repeat center;
 }
@@ -607,19 +597,17 @@ toolbarpaletteitem[place="palette"] > to
   -moz-appearance: none;
 }
 
 #PanelUI-footer-inner:hover > toolbarseparator,
 #PanelUI-footer-fxa:hover > toolbarseparator {
   margin: 0;
 }
 
-#PanelUI-update-available-menu-item,
-#PanelUI-update-manual-menu-item,
-#PanelUI-update-restart-menu-item,
+.PanelUI-notification-menu-item,
 #PanelUI-help,
 #PanelUI-fxa-label,
 #PanelUI-fxa-icon,
 #PanelUI-customize,
 #PanelUI-quit {
   margin: 0;
   padding: 11px 0;
   box-sizing: border-box;
@@ -627,54 +615,46 @@ toolbarpaletteitem[place="palette"] > to
   -moz-appearance: none;
   box-shadow: none;
   border: none;
   border-radius: 0;
   transition: background-color;
   -moz-box-orient: horizontal;
 }
 
-#PanelUI-update-available-menu-item,
-#PanelUI-update-manual-menu-item,
-#PanelUI-update-restart-menu-item {
+.PanelUI-notification-menu-item {
   border-top: 1px solid var(--panel-separator-color);
   border-bottom: 1px solid transparent;
   margin-bottom: -1px;
 }
 
-#PanelUI-update-available-menu-item > .toolbarbutton-text,
-#PanelUI-update-manual-menu-item > .toolbarbutton-text,
-#PanelUI-update-restart-menu-item > .toolbarbutton-text {
+.PanelUI-notification-menu-item > .toolbarbutton-text {
   width: 0; /* Fancy cropping solution for flexbox. */
 }
 
 #PanelUI-help,
 #PanelUI-quit {
   min-width: 46px;
 }
 
-#PanelUI-update-available-menu-item > .toolbarbutton-text,
-#PanelUI-update-manual-menu-item > .toolbarbutton-text,
-#PanelUI-update-restart-menu-item > .toolbarbutton-text,
+.PanelUI-notification-menu-item > .toolbarbutton-text,
 #PanelUI-fxa-label > .toolbarbutton-text,
 #PanelUI-customize > .toolbarbutton-text {
   margin: 0;
   padding: 0 6px;
   text-align: start;
 }
 
 #PanelUI-help > .toolbarbutton-text,
 #PanelUI-quit > .toolbarbutton-text,
 #PanelUI-fxa-avatar > .toolbarbutton-text {
   display: none;
 }
 
-#PanelUI-update-available-menu-item > .toolbarbutton-icon,
-#PanelUI-update-manual-menu-item > .toolbarbutton-icon,
-#PanelUI-update-restart-menu-item > .toolbarbutton-icon,
+.PanelUI-notification-menu-item > .toolbarbutton-icon,
 #PanelUI-fxa-label > .toolbarbutton-icon,
 #PanelUI-fxa-icon > .toolbarbutton-icon,
 #PanelUI-customize > .toolbarbutton-icon,
 #PanelUI-help > .toolbarbutton-icon,
 #PanelUI-quit > .toolbarbutton-icon {
   margin-inline-end: 0;
 }
 
@@ -690,19 +670,19 @@ toolbarpaletteitem[place="palette"] > to
   border-inline-start-style: none;
 }
 
 #PanelUI-footer-fxa[fxaprofileimage="set"] > #PanelUI-fxa-status > #PanelUI-fxa-label,
 #PanelUI-footer-fxa[fxaprofileimage="enabled"]:not([fxastatus="error"]) > #PanelUI-fxa-status > #PanelUI-fxa-label {
   padding-inline-start: 0px;
 }
 
-#PanelUI-update-available-menu-item,
-#PanelUI-update-manual-menu-item,
-#PanelUI-update-restart-menu-item {
+/* descend from #PanelUI-footer to add specificity, or else the
+   padding-inline-start will be overridden */
+#PanelUI-footer > .PanelUI-notification-menu-item {
   width: calc(@menuPanelWidth@ + 30px);
   padding-inline-start: 15px;
   border-inline-start-style: none;
   list-style-image: url(chrome://branding/content/icon16.png);
 }
 
 #PanelUI-fxa-label,
 #PanelUI-fxa-icon {
@@ -945,32 +925,26 @@ toolbarpaletteitem[place="palette"] > to
   background-color: hsl(42,94%,85%);
 }
 
 #PanelUI-footer-fxa[fxastatus="error"] > #PanelUI-fxa-status:hover:active {
   background-color: hsl(42,94%,82%);
   box-shadow: 0 1px 0 hsla(210,4%,10%,.05) inset;
 }
 
-#PanelUI-update-available-menu-item,
-#PanelUI-update-manual-menu-item,
-#PanelUI-update-restart-menu-item {
+.PanelUI-notification-menu-item {
   color: black;
   background-color: hsla(96,65%,75%,.5);
 }
 
-#PanelUI-update-available-menu-item:not([disabled]):hover,
-#PanelUI-update-manual-menu-item:not([disabled]):hover,
-#PanelUI-update-restart-menu-item:not([disabled]):hover {
+.PanelUI-notification-menu-item:not([disabled]):hover {
   background-color: hsla(96,65%,75%,.8);
 }
 
-#PanelUI-update-available-menu-item:not([disabled]):hover:active,
-#PanelUI-update-manual-menu-item:not([disabled]):hover:active,
-#PanelUI-update-restart-menu-item:not([disabled]):hover:active {
+.PanelUI-notification-menu-item:not([disabled]):hover:active {
   background-color: hsl(96,65%,75%);
 }
 
 #PanelUI-quit:not([disabled]):hover {
   background-color: #d94141;
   outline-color: #c23a3a;
 }
 
@@ -1667,19 +1641,17 @@ menuitem[checked="true"].subviewbutton >
   }
 
   #PanelUI-help[panel-multiview-anchor="true"]:-moz-locale-dir(rtl)::after,
   toolbarbutton[panel-multiview-anchor="true"]:-moz-locale-dir(rtl) {
     background-image: url(chrome://browser/skin/customizableui/subView-arrow-back-inverted-rtl@2x.png),
                       linear-gradient(rgba(255,255,255,0.3), transparent);
   }
 
-  #PanelUI-update-available-menu-item,
-  #PanelUI-update-manual-menu-item,
-  #PanelUI-update-restart-menu-item {
+  .PanelUI-notification-menu-item {
     list-style-image: url(chrome://branding/content/icon32.png);
   }
 
   #PanelUI-fxa-label,
   #PanelUI-fxa-icon {
     list-style-image: url(chrome://browser/skin/sync-horizontalbar@2x.png);
   }
 
@@ -1706,19 +1678,17 @@ menuitem[checked="true"].subviewbutton >
   #PanelUI-fxa-label,
   #PanelUI-fxa-icon,
   #PanelUI-customize,
   #PanelUI-help,
   #PanelUI-quit {
     -moz-image-region: rect(0, 32px, 32px, 0);
   }
 
-  #PanelUI-update-available-menu-item > .toolbarbutton-icon,
-  #PanelUI-update-manual-menu-item > .toolbarbutton-icon,
-  #PanelUI-update-restart-menu-item > .toolbarbutton-icon,
+  .PanelUI-notification-menu-item > .toolbarbutton-icon,
   #PanelUI-fxa-label > .toolbarbutton-icon,
   #PanelUI-fxa-icon > .toolbarbutton-icon,
   #PanelUI-customize > .toolbarbutton-icon,
   #PanelUI-help > .toolbarbutton-icon,
   #PanelUI-quit > .toolbarbutton-icon {
     width: 16px;
   }
 
--- a/browser/themes/shared/notification-icons.inc.css
+++ b/browser/themes/shared/notification-icons.inc.css
@@ -341,14 +341,18 @@ html|*#webRTC-previewVideo {
 /* UPDATE */
 
 #update-available-description,
 #update-manual-description,
 #update-restart-description {
   margin: 0;
 }
 
+
+/* the popup-notification-icon class is added to an element in the
+   popup-notification binding (see notification.xml), and that element
+   inherits the popupid that we pass into the <popupnotification> el */
 .popup-notification-icon[popupid="update-available"],
 .popup-notification-icon[popupid="update-manual"],
 .popup-notification-icon[popupid="update-restart"] {
   background: #74BF43 url(chrome://browser/skin/notification-icons.svg#update) no-repeat center;
   border-radius: 50%;
 }
\ No newline at end of file
--- a/toolkit/themes/osx/global/toolbarbutton.css
+++ b/toolkit/themes/osx/global/toolbarbutton.css
@@ -76,24 +76,24 @@ toolbarbutton[type="menu-button"][disabl
 
 /* ::::: toolbarbutton badged ::::: */
 
 .toolbarbutton-badge {
   background-color: #d90000;
   font-size: 9px;
   padding: 1px 2px;
   color: #fff;
-  border-radius: 50%;
-  border: 1px solid hsl(0,0%,95%);
-  margin: -9px 0 0 !important;
+  border-radius: 2px;
+  box-shadow: 0 1px 0 hsla(0, 100%, 100%, .2) inset,
+              0 -1px 0 hsla(0, 0%, 0%, .1) inset,
+              0 1px 0 hsla(206, 50%, 10%, .2);
+  margin: -6px 0 0 !important;
   margin-inline-end: -6px !important;
-  min-width: 15px;
+  min-width: 14px;
   max-width: 28px;
-  min-height: 15px;
-  max-height: 28px;
   line-height: 10px;
   text-align: center;
   -moz-stack-sizing: ignore;
 }
 
 .toolbarbutton-badge:-moz-window-inactive {
   background-color: rgb(230,230,230);
   box-shadow: none;
--- a/toolkit/themes/windows/global/toolbarbutton.css
+++ b/toolkit/themes/windows/global/toolbarbutton.css
@@ -144,24 +144,24 @@ toolbarbutton[type="menu-button"][disabl
 }
 
 .toolbarbutton-badge {
   background-color: #d90000;
   font-size: 10px;
   font-weight: bold;
   padding: 0 2px 1px;
   color: #fff;
-  border-radius: 50%;
-  border: 1px solid hsl(0,0%,96%);
-  margin: -9px 0 0 !important;
+  border-radius: 2px;
+  box-shadow: 0 1px 0 hsla(0, 100%, 100%, .2) inset,
+              0 -1px 0 hsla(0, 0%, 0%, .1) inset,
+              0 1px 0 hsla(206, 50%, 10%, .2);
+  margin: -6px 0 0 !important;
   margin-inline-end: -8px !important;
-  min-width: 16px;
+  min-width: 14px;
   max-width: 28px;
-  min-height: 16px;
-  max-height: 28px;
   line-height: 10px;
   text-align: center;
   -moz-stack-sizing: ignore;
 }
 
 /* .......... dropmarker .......... */
 
 .toolbarbutton-menubutton-dropmarker {