Bug 1459907 - Implement new bookmark confirmation. r=mak draft
authorDão Gottwald <dao@mozilla.com>
Sat, 23 Jun 2018 15:28:47 +0200
changeset 809885 836ba1340189
parent 809880 368ae05266bd
push id113834
push userdgottwald@mozilla.com
push dateSat, 23 Jun 2018 13:29:37 +0000
reviewersmak
bugs1459907
milestone62.0a1
Bug 1459907 - Implement new bookmark confirmation. r=mak MozReview-Commit-ID: DsTXTKgX72y
browser/base/content/browser-pageActions.js
browser/base/content/browser-places.js
browser/base/content/browser.js
browser/base/content/browser.xul
browser/base/content/test/urlbar/browser_page_action_menu.js
browser/base/content/test/urlbar/browser_page_action_menu_add_search_engine.js
browser/locales/en-US/chrome/browser/browser.dtd
browser/locales/en-US/chrome/browser/browser.properties
browser/locales/en-US/chrome/browser/search.properties
browser/themes/shared/customizableui/panelUI.inc.css
--- a/browser/base/content/browser-pageActions.js
+++ b/browser/base/content/browser-pageActions.js
@@ -941,79 +941,31 @@ var BrowserPageActions = {
   onLocationChange() {
     for (let action of PageActions.actions) {
       action.onLocationChange(window);
     }
   },
 };
 
 
-var BrowserPageActionFeedback = {
-  /**
-   * The feedback page action panel DOM node (DOM node)
-   */
-  get panelNode() {
-    delete this.panelNode;
-    return this.panelNode = document.getElementById("pageActionFeedback");
-  },
-
-  get feedbackAnimationBox() {
-    delete this.feedbackAnimationBox;
-    return this.feedbackAnimationBox = document.getElementById("pageActionFeedbackAnimatableBox");
-  },
-
-  get feedbackLabel() {
-    delete this.feedbackLabel;
-    return this.feedbackLabel = document.getElementById("pageActionFeedbackMessage");
-  },
+/**
+ * Shows the feedback popup for an action.
+ *
+ * @param  action (PageActions.Action, required)
+ *         The action associated with the feedback.
+ * @param  event (DOM event, optional)
+ *         The event that triggered the feedback.
+ * @param  messageId (string, optional)
+ *         Can be used to set a message id that is different from the action id.
+ */
+function showBrowserPageActionFeedback(action, event = null, messageId = null) {
+  let anchor = BrowserPageActions.panelAnchorNodeForAction(action, event);
 
-  /**
-   * Shows the feedback popup for an action.
-   *
-   * @param  action (PageActions.Action, required)
-   *         The action associated with the feedback.
-   * @param  opts (object, optional)
-   *         An object with the following optional properties:
-   *         - event (DOM event): The event that triggered the feedback.
-   *         - textAttributeOverride (string): Normally the feedback text is
-   *           taken from an attribute on the feedback panel.  The attribute's
-   *           name is `${action.id}Feedback`.  Use this to override the
-   *           action.id part of the name.
-   *         - text (string): The text string.  If not given, an attribute on
-   *           panel is assumed to contain the text, as described above.
-   */
-  show(action, opts = {}) {
-    this.feedbackLabel.textContent =
-      opts.text ||
-      this.panelNode.getAttribute((opts.textAttributeOverride || action.id) +
-                                  "Feedback");
-    this.panelNode.hidden = false;
-
-    let event = opts.event || null;
-    let anchor = BrowserPageActions.panelAnchorNodeForAction(action, event);
-    PanelMultiView.openPopup(this.panelNode, anchor, {
-      position: "bottomcenter topright",
-      triggerEvent: event,
-    }).catch(Cu.reportError);
-
-    this.panelNode.addEventListener("popupshown", () => {
-      this.feedbackAnimationBox.setAttribute("animate", "true");
-
-      // The timeout value used here allows the panel to stay open for
-      // 1 second after the text transition (duration=120ms) has finished.
-      setTimeout(() => {
-        this.panelNode.hidePopup(true);
-      }, Services.prefs.getIntPref("browser.pageActions.feedbackTimeoutMS", 1120));
-    }, {once: true});
-    this.panelNode.addEventListener("popuphidden", () => {
-      this.feedbackAnimationBox.removeAttribute("animate");
-    }, {once: true});
-  },
-};
-
+  ConfirmationHint.show(anchor, messageId || action.id, {event, hideArrow: true});
+}
 
 // built-in actions below //////////////////////////////////////////////////////
 
 // bookmark
 BrowserPageActions.bookmark = {
   onShowingInPanel(buttonNode) {
     // Update the button label via the bookmark observer.
     BookmarkingUI.updateBookmarkPageMenuItem();
@@ -1033,19 +985,17 @@ BrowserPageActions.copyURL = {
   },
 
   onCommand(event, buttonNode) {
     PanelMultiView.hidePopup(BrowserPageActions.panelNode);
     Cc["@mozilla.org/widget/clipboardhelper;1"]
       .getService(Ci.nsIClipboardHelper)
       .copyString(gURLBar.makeURIReadable(gBrowser.selectedBrowser.currentURI).displaySpec);
     let action = PageActions.actionForID("copyURL");
-    BrowserPageActionFeedback.show(action, {
-      event,
-    });
+    showBrowserPageActionFeedback(action, event);
   },
 };
 
 // email link
 BrowserPageActions.emailLink = {
   onPlacedInPanel(buttonNode) {
     let action = PageActions.actionForID("emailLink");
     BrowserPageActions.takeActionTitleFromPanel(action);
@@ -1112,21 +1062,18 @@ BrowserPageActions.sendToDevice = {
       item.addEventListener("command", event => {
         if (panelNode) {
           PanelMultiView.hidePopup(panelNode);
         }
         // There are items in the subview that don't represent devices: "Sign
         // in", "Learn about Sync", etc.  Device items will be .sendtab-target.
         if (event.target.classList.contains("sendtab-target")) {
           let action = PageActions.actionForID("sendToDevice");
-          let textAttributeOverride = gSync.offline && "sendToDeviceOffline";
-          BrowserPageActionFeedback.show(action, {
-            event,
-            textAttributeOverride,
-          });
+          let messageId = gSync.offline && "sendToDeviceOffline";
+          showBrowserPageActionFeedback(action, event, messageId);
         }
       });
       return item;
     });
 
     bodyNode.removeAttribute("state");
     // In the first ~10 sec after startup, Sync may not be loaded and the list
     // of devices will be empty.
@@ -1228,19 +1175,17 @@ BrowserPageActions.addSearchEngine = {
     // above.)
     let engine = this.engines[0];
     this._installEngine(engine.uri, engine.icon);
   },
 
   _installEngine(uri, image) {
     Services.search.addEngine(uri, null, image, false, {
       onSuccess: engine => {
-        BrowserPageActionFeedback.show(this.action, {
-          text: this.strings.GetStringFromName("searchAddedFoundEngine2"),
-        });
+        showBrowserPageActionFeedback(this.action);
       },
       onError(errorCode) {
         if (errorCode != Ci.nsISearchInstallCallback.ERROR_DUPLICATE_ENGINE) {
           // Download error is shown by the search service
           return;
         }
         const kSearchBundleURI = "chrome://global/locale/search/search.properties";
         let searchBundle = Services.strings.createBundle(kSearchBundleURI);
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -114,16 +114,17 @@ var StarUI = {
               PlacesTransactions.undo().catch(Cu.reportError);
               break;
             }
             // Remove all bookmarks for the bookmark's url, this also removes
             // the tags for the url.
             PlacesTransactions.Remove(guidsForRemoval)
                               .transact().catch(Cu.reportError);
           } else if (this._isNewBookmark) {
+            this._showConfirmation();
             LibraryUI.triggerLibraryAnimation("bookmark");
           }
 
           if (!removeBookmarksOnPopupHidden) {
             this._storeRecentlyUsedFolder(selectedFolderGuid).catch(console.error);
           }
         }
         break;
@@ -398,16 +399,36 @@ var StarUI = {
       lastUsedFolderGuids.unshift(selectedFolderGuid);
     }
     if (lastUsedFolderGuids.length > 5) {
       lastUsedFolderGuids.pop();
     }
 
     await PlacesUtils.metadata.set(PlacesUIUtils.LAST_USED_FOLDERS_META_KEY,
                                    lastUsedFolderGuids);
+  },
+
+  _showConfirmation() {
+    let anchor;
+    if (window.toolbar.visible) {
+      for (let id of ["library-button", "bookmarks-menu-button"]) {
+        let element = document.getElementById(id);
+        if (element &&
+            element.getAttribute("cui-areatype") != "menu-panel" &&
+            element.getAttribute("overflowedItem") != "true") {
+          anchor = element;
+          break;
+        }
+      }
+    }
+    if (!anchor) {
+      anchor = document.getElementById("PanelUI-menu-button");
+    }
+
+    ConfirmationHint.show(anchor, "pageBookmarked");
   }
 };
 
 var PlacesCommandHook = {
   /**
    * Adds a bookmark to the page loaded in the given browser.
    *
    * @param aBrowser
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -8517,8 +8517,76 @@ TabModalPromptBox.prototype = {
   get browser() {
     let browser = this._weakBrowserRef.get();
     if (!browser) {
       throw "Stale promptbox! The associated browser is gone.";
     }
     return browser;
   },
 };
+
+var ConfirmationHint = {
+  /**
+   * Shows a transient, non-interactive confirmation hint anchored to an
+   * element, usually used in response to a user action to reaffirm that it was
+   * successful and potentially provide extra context. Examples for such hints:
+   * - "Saved to Library!" after bookmarking a page
+   * - "Sent!" after sending a tab to another device
+   * - "Queued (offline)" when attempting to send a tab to another device
+   *   while offline
+   *
+   * @param  anchor (DOM node, required)
+   *         The anchor for the panel.
+   * @param  messageId (string, required)
+   *         For getting the message string from browser.properties:
+   *         confirmationHint.<messageId>.label
+   * @param  options (object, optional)
+   *         An object with the following optional properties:
+   *         - event (DOM event): The event that triggered the feedback.
+   *         - hideArrow (boolean): Optionally hide the arrow.
+   */
+  show(anchor, messageId, options = {}) {
+    this._message.textContent =
+      gBrowserBundle.GetStringFromName("confirmationHint." + messageId + ".label");
+
+    if (options.hideArrow) {
+      this._panel.setAttribute("hidearrow", "true");
+    }
+
+    this._panel.addEventListener("popupshown", () => {
+      this._animationBox.setAttribute("animate", "true");
+
+      // The timeout value used here allows the panel to stay open for
+      // X second after the text transition (duration=120ms) has finished.
+      const DURATION = 1500;
+      setTimeout(() => {
+        this._panel.hidePopup(true);
+      }, DURATION + 120);
+    }, {once: true});
+
+    this._panel.addEventListener("popuphidden", () => {
+      this._panel.removeAttribute("hidearrow");
+      this._animationBox.removeAttribute("animate");
+    }, {once: true});
+
+    this._panel.hidden = false;
+    this._panel.openPopup(anchor, {
+      position: "bottomcenter topright",
+      triggerEvent: options.event,
+    });
+  },
+
+  get _panel() {
+    delete this._panel;
+    return this._panel = document.getElementById("confirmation-hint");
+  },
+
+  get _animationBox() {
+    delete this._animationBox;
+    return this._animationBox = document.getElementById("confirmation-hint-checkmark-animation-container");
+  },
+
+  get _message() {
+    delete this._message;
+    return this._message = document.getElementById("confirmation-hint-message");
+  },
+};
+
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -459,31 +459,29 @@
                       viewCacheId="appMenu-viewCache">
         <panelview id="pageActionPanelMainView"
                    context="pageActionContextMenu"
                    class="PanelUI-subView">
           <vbox class="panel-subview-body"/>
         </panelview>
       </panelmultiview>
     </panel>
-    <panel id="pageActionFeedback"
+
+    <panel id="confirmation-hint"
            role="alert"
            type="arrow"
            hidden="true"
            flip="slide"
            position="bottomcenter topright"
            tabspecific="true"
-           noautofocus="true"
-           copyURLFeedback="&copyURLFeedback.label;"
-           sendToDeviceFeedback="&sendToDeviceFeedback.label;"
-           sendToDeviceOfflineFeedback="&sendToDeviceOfflineFeedback.label;">
-      <hbox id="pageActionFeedbackAnimatableBox">
-        <image id="pageActionFeedbackAnimatableImage"/>
+           noautofocus="true">
+      <hbox id="confirmation-hint-checkmark-animation-container">
+        <image id="confirmation-hint-checkmark-image"/>
       </hbox>
-      <label id="pageActionFeedbackMessage"/>
+      <label id="confirmation-hint-message"/>
     </panel>
 
     <menupopup id="pageActionContextMenu"
                onpopupshowing="BrowserPageActions.onContextMenuShowing(event, this);">
       <menuitem class="pageActionContextMenuItem builtInUnpinned"
                 label="&pageAction.addToUrlbar.label;"
                 oncommand="BrowserPageActions.togglePinningForContextAction();"/>
       <menuitem class="pageActionContextMenuItem builtInPinned"
--- a/browser/base/content/test/urlbar/browser_page_action_menu.js
+++ b/browser/base/content/test/urlbar/browser_page_action_menu.js
@@ -132,21 +132,21 @@ add_task(async function copyURLFromPanel
     Assert.ok(true, "page action panel opened");
 
     let copyURLButton =
       document.getElementById("pageAction-panel-copyURL");
     let hiddenPromise = promisePageActionPanelHidden();
     EventUtils.synthesizeMouseAtCenter(copyURLButton, {});
     await hiddenPromise;
 
-    let feedbackPanel = document.getElementById("pageActionFeedback");
+    let feedbackPanel = document.getElementById("confirmation-hint");
     let feedbackShownPromise = BrowserTestUtils.waitForEvent(feedbackPanel, "popupshown");
     await feedbackShownPromise;
     Assert.equal(feedbackPanel.anchorNode.id, "pageActionButton", "Feedback menu should be anchored on the main Page Action button");
-    let feedbackHiddenPromise = promisePanelHidden("pageActionFeedback");
+    let feedbackHiddenPromise = promisePanelHidden("confirmation-hint");
     await feedbackHiddenPromise;
 
     action.pinnedToUrlbar = false;
   });
 });
 
 add_task(async function copyURLFromURLBar() {
   // Open an actionable page so that the main page action button appears.  (It
@@ -155,23 +155,23 @@ add_task(async function copyURLFromURLBa
   await BrowserTestUtils.withNewTab(url, async () => {
     // Add action to URL bar.
     let action = PageActions._builtInActions.find(a => a.id == "copyURL");
     action.pinnedToUrlbar = true;
     registerCleanupFunction(() => action.pinnedToUrlbar = false);
 
     let copyURLButton =
       document.getElementById("pageAction-urlbar-copyURL");
-    let feedbackShownPromise = promisePanelShown("pageActionFeedback");
+    let feedbackShownPromise = promisePanelShown("confirmation-hint");
     EventUtils.synthesizeMouseAtCenter(copyURLButton, {});
 
     await feedbackShownPromise;
-    let panel = document.getElementById("pageActionFeedback");
+    let panel = document.getElementById("confirmation-hint");
     Assert.equal(panel.anchorNode.id, "pageAction-urlbar-copyURL", "Feedback menu should be anchored on the main URL bar button");
-    let feedbackHiddenPromise = promisePanelHidden("pageActionFeedback");
+    let feedbackHiddenPromise = promisePanelHidden("confirmation-hint");
     await feedbackHiddenPromise;
 
     action.pinnedToUrlbar = false;
   });
 });
 
 add_task(async function sendToDevice_nonSendable() {
   // Open a tab that's not sendable but where the page action buttons still
@@ -612,23 +612,23 @@ add_task(async function sendToDevice_inU
     info("Waiting for Send to Device panel to close after clicking a device");
     await hiddenPromise;
     Assert.ok(!urlbarButton.hasAttribute("open"),
       "URL bar button no longer has open attribute");
 
     // And then the "Sent!" notification panel should open and close by itself
     // after a moment.
     info("Waiting for the Sent! notification panel to open");
-    await promisePanelShown(BrowserPageActionFeedback.panelNode.id);
+    await promisePanelShown(ConfirmationHint._panel.id);
     Assert.equal(
-      BrowserPageActionFeedback.panelNode.anchorNode.id,
+      ConfirmationHint._panel.anchorNode.id,
       urlbarButton.id
     );
     info("Waiting for the Sent! notification panel to close");
-    await promisePanelHidden(BrowserPageActionFeedback.panelNode.id);
+    await promisePanelHidden(ConfirmationHint._panel.id);
 
     // Remove Send to Device from the urlbar.
     action.pinnedToUrlbar = false;
 
     cleanUp();
   });
 });
 
--- a/browser/base/content/test/urlbar/browser_page_action_menu_add_search_engine.js
+++ b/browser/base/content/test/urlbar/browser_page_action_menu_add_search_engine.js
@@ -44,17 +44,17 @@ add_task(async function one() {
     let enginePromise =
       promiseEngine("engine-added", "page_action_menu_add_search_engine_0");
     let hiddenPromise = promisePageActionPanelHidden();
     let feedbackPromise = promiseFeedbackPanelShownAndHidden();
     EventUtils.synthesizeMouseAtCenter(button, {});
     await hiddenPromise;
     let engine = await enginePromise;
     let feedbackText = await feedbackPromise;
-    Assert.equal(feedbackText, "Added Search Engine");
+    Assert.equal(feedbackText, "Search engine added!");
 
     // Open the panel again.
     await promisePageActionPanelOpen();
     EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
     await promisePageActionPanelHidden();
 
     // The action should be gone.
     actions = PageActions.actionsInPanel(window);
@@ -133,17 +133,17 @@ add_task(async function many() {
     let hiddenPromise = promisePageActionPanelHidden();
     let feedbackPromise = promiseFeedbackPanelShownAndHidden();
     EventUtils.synthesizeMouseAtCenter(body.childNodes[0], {});
     await hiddenPromise;
     let engines = [];
     let engine = await enginePromise;
     engines.push(engine);
     let feedbackText = await feedbackPromise;
-    Assert.equal(feedbackText, "Added Search Engine", "Feedback text");
+    Assert.equal(feedbackText, "Search engine added!", "Feedback text");
 
     // Open the panel and show the subview again.  The installed engine should
     // be gone.
     await promisePageActionPanelOpen();
     viewPromise = promisePageActionViewShown();
     EventUtils.synthesizeMouseAtCenter(button, {});
     await viewPromise;
     Assert.deepEqual(
@@ -160,17 +160,17 @@ add_task(async function many() {
       promiseEngine("engine-added", "page_action_menu_add_search_engine_1");
     hiddenPromise = promisePageActionPanelHidden();
     feedbackPromise = promiseFeedbackPanelShownAndHidden();
     EventUtils.synthesizeMouseAtCenter(body.childNodes[0], {});
     await hiddenPromise;
     engine = await enginePromise;
     engines.push(engine);
     feedbackText = await feedbackPromise;
-    Assert.equal(feedbackText, "Added Search Engine", "Feedback text");
+    Assert.equal(feedbackText, "Search engine added!", "Feedback text");
 
     // Open the panel again.  This time the action button should show the one
     // remaining engine.
     await promisePageActionPanelOpen();
     actions = PageActions.actionsInPanel(window);
     action = actions.find(a => a.id == "addSearchEngine");
     Assert.ok(action, "Action should be present in panel");
     expectedTitle = "Add Search Engine";
@@ -186,17 +186,17 @@ add_task(async function many() {
       promiseEngine("engine-added", "page_action_menu_add_search_engine_2");
     hiddenPromise = promisePageActionPanelHidden();
     feedbackPromise = promiseFeedbackPanelShownAndHidden();
     EventUtils.synthesizeMouseAtCenter(button, {});
     await hiddenPromise;
     engine = await enginePromise;
     engines.push(engine);
     feedbackText = await feedbackPromise;
-    Assert.equal(feedbackText, "Added Search Engine", "Feedback text");
+    Assert.equal(feedbackText, "Search engine added!", "Feedback text");
 
     // All engines are installed at this point.  Open the panel and make sure
     // the action is gone.
     await promisePageActionPanelOpen();
     EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
     await promisePageActionPanelHidden();
     actions = PageActions.actionsInPanel(window);
     action = actions.find(a => a.id == "addSearchEngine");
@@ -305,17 +305,17 @@ add_task(async function urlbarOne() {
 
     // Click the action's button.
     let enginePromise =
       promiseEngine("engine-added", "page_action_menu_add_search_engine_0");
     let feedbackPromise = promiseFeedbackPanelShownAndHidden();
     EventUtils.synthesizeMouseAtCenter(button, {});
     let engine = await enginePromise;
     let feedbackText = await feedbackPromise;
-    Assert.equal(feedbackText, "Added Search Engine");
+    Assert.equal(feedbackText, "Search engine added!");
 
     // The action should be gone.
     actions = PageActions.actionsInUrlbar(window);
     action = actions.find(a => a.id == "addSearchEngine");
     Assert.ok(!action, "Action should not be present in urlbar");
     button = BrowserPageActions.urlbarButtonNodeForActionID("addSearchEngine");
     Assert.ok(!button, "Action button should not be in urlbar");
 
@@ -384,17 +384,17 @@ add_task(async function urlbarMany() {
       promisePanelHidden(BrowserPageActions.activatedActionPanelNode);
     let feedbackPromise = promiseFeedbackPanelShownAndHidden();
     EventUtils.synthesizeMouseAtCenter(body.childNodes[0], {});
     await hiddenPromise;
     let engines = [];
     let engine = await enginePromise;
     engines.push(engine);
     let feedbackText = await feedbackPromise;
-    Assert.equal(feedbackText, "Added Search Engine", "Feedback text");
+    Assert.equal(feedbackText, "Search engine added!", "Feedback text");
 
     // Open the panel again.  The installed engine should be gone.
     EventUtils.synthesizeMouseAtCenter(button, {});
     view = await waitForActivatedActionPanel();
     body = view.firstChild;
     Assert.deepEqual(
       Array.map(body.childNodes, n => n.label),
       [
@@ -410,28 +410,28 @@ add_task(async function urlbarMany() {
     hiddenPromise =
       promisePanelHidden(BrowserPageActions.activatedActionPanelNode);
     feedbackPromise = promiseFeedbackPanelShownAndHidden();
     EventUtils.synthesizeMouseAtCenter(body.childNodes[0], {});
     await hiddenPromise;
     engine = await enginePromise;
     engines.push(engine);
     feedbackText = await feedbackPromise;
-    Assert.equal(feedbackText, "Added Search Engine", "Feedback text");
+    Assert.equal(feedbackText, "Search engine added!", "Feedback text");
 
     // Now there's only one engine left, so clicking the button should simply
     // install it instead of opening the activated-action panel.
     enginePromise =
       promiseEngine("engine-added", "page_action_menu_add_search_engine_2");
     feedbackPromise = promiseFeedbackPanelShownAndHidden();
     EventUtils.synthesizeMouseAtCenter(button, {});
     engine = await enginePromise;
     engines.push(engine);
     feedbackText = await feedbackPromise;
-    Assert.equal(feedbackText, "Added Search Engine", "Feedback text");
+    Assert.equal(feedbackText, "Search engine added!", "Feedback text");
 
     // All engines are installed at this point.  The action should be gone.
     actions = PageActions.actionsInUrlbar(window);
     action = actions.find(a => a.id == "addSearchEngine");
     Assert.ok(!action, "Action should be gone");
     button = BrowserPageActions.urlbarButtonNodeForActionID("addSearchEngine");
     Assert.ok(!button, "Button should not be in urlbar");
 
@@ -516,20 +516,20 @@ function promiseEngine(expectedData, exp
     info(`Got engine ${engine.wrappedJSObject.name} ${data}`);
     return expectedData == data &&
            expectedEngineName == engine.wrappedJSObject.name;
   }).then(([engine, data]) => engine);
 }
 
 function promiseFeedbackPanelShownAndHidden() {
   info("Waiting for feedback panel popupshown");
-  return BrowserTestUtils.waitForEvent(BrowserPageActionFeedback.panelNode, "popupshown").then(() => {
+  return BrowserTestUtils.waitForEvent(ConfirmationHint._panel, "popupshown").then(() => {
     info("Got feedback panel popupshown. Now waiting for popuphidden");
-    return BrowserTestUtils.waitForEvent(BrowserPageActionFeedback.panelNode, "popuphidden")
-          .then(() => BrowserPageActionFeedback.feedbackLabel.textContent);
+    return BrowserTestUtils.waitForEvent(ConfirmationHint._panel, "popuphidden")
+          .then(() => ConfirmationHint._message.textContent);
   });
 }
 
 function promisePlacedInUrlbar() {
   let action = PageActions.actionForID("addSearchEngine");
   return new Promise(resolve => {
     let onPlaced = action._onPlacedInUrlbar;
     action._onPlacedInUrlbar = button => {
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -47,18 +47,16 @@ can reach it easily. -->
 <!ENTITY  unpinTab.label                     "Unpin Tab">
 <!ENTITY  unpinTab.accesskey                 "b">
 <!ENTITY  sendTabToDevice.label              "Send Tab to Device">
 <!ENTITY  sendTabToDevice.accesskey          "n">
 <!ENTITY  sendPageToDevice.label             "Send Page to Device">
 <!ENTITY  sendPageToDevice.accesskey         "n">
 <!ENTITY  sendLinkToDevice.label             "Send Link to Device">
 <!ENTITY  sendLinkToDevice.accesskey         "n">
-<!ENTITY  sendToDeviceFeedback.label         "Sent!">
-<!ENTITY  sendToDeviceOfflineFeedback.label  "Queued (offline)">
 <!ENTITY  moveToNewWindow.label              "Move to New Window">
 <!ENTITY  moveToNewWindow.accesskey          "W">
 <!ENTITY  reopenInContainer.label            "Reopen in Container">
 <!ENTITY  reopenInContainer.accesskey        "e">
 <!ENTITY  bookmarkAllTabs.label              "Bookmark All Tabs…">
 <!ENTITY  bookmarkAllTabs.accesskey          "T">
 <!ENTITY  undoCloseTab.label                 "Undo Close Tab">
 <!ENTITY  undoCloseTab.accesskey             "U">
@@ -575,17 +573,16 @@ These should match what Safari and other
 <!ENTITY setDesktopBackgroundCmd.accesskey  "S">
 <!ENTITY bookmarkPageCmd2.label       "Bookmark This Page">
 <!ENTITY bookmarkPageCmd2.accesskey   "m">
 <!ENTITY bookmarkThisLinkCmd.label      "Bookmark This Link">
 <!ENTITY bookmarkThisLinkCmd.accesskey  "L">
 <!ENTITY bookmarkThisFrameCmd.label      "Bookmark This Frame">
 <!ENTITY bookmarkThisFrameCmd.accesskey  "m">
 <!ENTITY pageAction.copyLink.label    "Copy Link">
-<!ENTITY copyURLFeedback.label        "Copied!">
 <!ENTITY emailPageCmd.label           "Email Link…">
 <!ENTITY emailPageCmd.accesskey       "E">
 <!ENTITY savePageCmd.label            "Save Page As…">
 <!ENTITY savePageCmd.accesskey        "A">
 <!-- alternate for content area context menu -->
 <!ENTITY savePageCmd.accesskey2       "P">
 <!ENTITY savePageCmd.commandkey       "s">
 <!ENTITY saveFrameCmd.label           "Save Frame As…">
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -965,8 +965,14 @@ autoplay.DontAllow.accesskey = n
 autoplay.remember = Remember this decision
 # LOCALIZATION NOTE (autoplay.message): %S is the name of the site URL (https://...) trying to autoplay media
 autoplay.message = Will you allow %S to autoplay media with sound?
 autoplay.messageWithFile = Will you allow this file to autoplay media with sound?
 # LOCALIZATION NOTE (panel.back):
 # This is used by screen readers to label the "back" button in various browser
 # popup panels, including the sliding subviews of the main menu.
 panel.back = Back
+
+confirmationHint.sendToDevice.label = Sent!
+confirmationHint.sendToDeviceOffline.label = Queued (offline)
+confirmationHint.copyURL.label = Copied to clipboard!
+confirmationHint.pageBookmarked.label = Saved to Library!
+confirmationHint.addSearchEngine.label = Search engine added!
--- a/browser/locales/en-US/chrome/browser/search.properties
+++ b/browser/locales/en-US/chrome/browser/search.properties
@@ -29,17 +29,16 @@ cmd_showSuggestions_accesskey=S
 cmd_addFoundEngine=Add “%S”
 # LOCALIZATION NOTE (cmd_addFoundEngineMenu): When more than 5 engines
 # are offered by a web page, instead of listing all of them in the
 # search panel using the cmd_addFoundEngine string, they will be
 # grouped in a submenu using cmd_addFoundEngineMenu as a label.
 cmd_addFoundEngineMenu=Add search engine
 
 searchAddFoundEngine2=Add Search Engine
-searchAddedFoundEngine2=Added Search Engine
 
 # LOCALIZATION NOTE (searchForSomethingWith2):
 # This string is used to build the header above the list of one-click
 # search providers:  "Search for <user-typed string> with:"
 searchForSomethingWith2=Search for %S with:
 
 # LOCALIZATION NOTE (searchWithHeader):
 # The wording of this string should be as close as possible to
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -214,88 +214,90 @@ panel[photon] > .panel-arrowcontainer > 
 #wrapper-zoom-controls:-moz-any([place="palette"],[place="menu-panel"]) > #zoom-controls {
   margin-inline-start: 0;
 }
 
 #BMB_bookmarksPopup {
   max-width: @standaloneSubviewWidth@;
 }
 
-#pageActionFeedback > .panel-arrowcontainer > .panel-arrowbox {
+#confirmation-hint {
+  --arrowpanel-background: #058b00;
+  --arrowpanel-border-color: #046b00;
+  --arrowpanel-color: #fff;
+}
+
+#confirmation-hint[hidearrow] > .panel-arrowcontainer > .panel-arrowbox {
   /* Don't display the arrow but keep the popup at the same vertical
      offset as other arrow panels. */
   visibility: hidden;
 }
 
-#pageActionFeedback > .panel-arrowcontainer > .panel-arrowcontent {
-  background-color: #058b00;
-  background-image: none;
-  border-radius: 2px;
-  color: #fff;
+#confirmation-hint > .panel-arrowcontainer > .panel-arrowcontent {
   font-weight: 400;
   font-size: 1.1rem;
   -moz-box-align: center;
   padding: 6px 10px;
 }
 
-#pageActionFeedbackAnimatableBox {
+#confirmation-hint-checkmark-animation-container {
   position: relative;
   overflow: hidden;
   width: 14px;
   height: 14px;
 }
 
-#pageActionFeedbackAnimatableBox[animate] > #pageActionFeedbackAnimatableImage {
+#confirmation-hint-checkmark-animation-container[animate] > #confirmation-hint-checkmark-image {
   position: absolute;
   background-image: url(chrome://browser/skin/check-animation.svg);
   background-repeat: no-repeat;
   min-width: 266px;
   max-width: 266px;
   min-height: 14px;
   max-height: 14px;
-  animation-name: page-action-feedback-animation;
+  animation-name: confirmation-hint-checkmark-animation;
   animation-duration: 300ms;
   animation-delay: 60ms;
   animation-fill-mode: forwards;
   animation-timing-function: steps(18);
 }
 
-#pageActionFeedbackAnimatableBox[animate] > #pageActionFeedbackAnimatableImage:-moz-locale-dir(rtl) {
-  animation-name: page-action-feedback-animation-rtl;
+#confirmation-hint-checkmark-animation-container[animate] > #confirmation-hint-checkmark-image:-moz-locale-dir(rtl) {
+  animation-name: confirmation-hint-checkmark-animation-rtl;
   transform: translateX(252px);
 }
 
-@keyframes page-action-feedback-animation {
+@keyframes confirmation-hint-checkmark-animation {
   from {
     transform: translateX(0);
   }
   to {
     transform: translateX(-252px);
   }
 }
 
-@keyframes page-action-feedback-animation-rtl {
+@keyframes confirmation-hint-checkmark-animation-rtl {
   from {
     transform: translateX(252px);
   }
   to {
     transform: translateX(0);
   }
 }
 
-#pageActionFeedbackMessage {
+#confirmation-hint-message {
   margin-inline-start: 7px;
   margin-inline-end: 0;
   transform: scale(.8);
   opacity: 0;
   transition: transform 120ms cubic-bezier(.25,1.27,.35,1.18),
               opacity 60ms linear;
 }
 
-#pageActionFeedbackAnimatableBox[animate] + #pageActionFeedbackMessage {
+#confirmation-hint-checkmark-animation-container[animate] + #confirmation-hint-message {
   transform: scale(1);
   opacity: 1;
 }
 
 .cui-widget-panel[viewId^=PanelUI-webext-] > .panel-arrowcontainer > .panel-arrowcontent {
   padding: 0;
 }