Bug 1113747 - New search UI breaks if too many open search providers are offered. r?florian draft
authorDrew Willcoxon <adw@mozilla.com>
Wed, 16 Nov 2016 10:00:51 -0800
changeset 439818 a305698a485ad0978423c3a65f76e2b46f7e1071
parent 430552 10a2b6ebcd44a3516673f51da14b760de7fd3bc0
child 441563 5ac6a0897d92e27ddf14cb64184a8ef37bb6d72d
push id36109
push userdwillcoxon@mozilla.com
push dateWed, 16 Nov 2016 18:01:41 +0000
reviewersflorian
bugs1113747
milestone52.0a1
Bug 1113747 - New search UI breaks if too many open search providers are offered. r?florian Based on an earlier patch by Nihanth Subramanya <nhnt11@gmail.com> MozReview-Commit-ID: 4TZzFgovIJm
browser/base/content/browser.css
browser/components/search/content/search.xml
browser/components/search/test/browser.ini
browser/components/search/test/browser_tooManyEnginesOffered.js
browser/components/search/test/tooManyEnginesOffered.html
browser/themes/linux/searchbar.css
browser/themes/osx/searchbar.css
browser/themes/windows/searchbar.css
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -485,17 +485,18 @@ toolbar:not(#TabsToolbar) > #personal-bo
 }
 
 #PopupSearchAutoComplete {
   -moz-binding: url("chrome://browser/content/search/search.xml#browser-search-autocomplete-result-popup");
 }
 
 /* Overlay a badge on top of the icon of additional open search providers
    in the search panel. */
-.addengine-item > .button-box > .button-icon {
+.addengine-item > .button-box > .button-icon,
+.addengine-item[type="menu"] > .button-box > .box-inherit > .button-icon {
   -moz-binding: url("chrome://browser/content/search/search.xml#addengine-icon");
   display: -moz-stack;
 }
 
 #PopupAutoCompleteRichResult {
   -moz-binding: url("chrome://browser/content/urlbarBindings.xml#urlbar-rich-result-popup");
 }
 
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -737,16 +737,34 @@
               document.getBindingParent(this).openSuggestionsPanel();
               return false;
             }
             return true;
           ]]>
         </body>
       </method>
 
+      <method name="handleEnter">
+        <parameter name="event"/>
+        <body><![CDATA[
+          // Toggle the open state of the add-engine menu button if it's
+          // selected.  We're using handleEnter for this instead of listening
+          // for the command event because a command event isn't fired.
+          if (this.selectedButton &&
+              this.selectedButton.getAttribute("anonid") ==
+                "addengine-menu-button") {
+            this.selectedButton.open = !this.selectedButton.open;
+            return true;
+          }
+          // Otherwise, "call super": do what the autocomplete binding's
+          // handleEnter implementation does.
+          return this.mController.handleEnter(false, event || null);
+        ]]></body>
+      </method>
+
       <!-- override |onTextEntered| in autocomplete.xml -->
       <method name="onTextEntered">
         <parameter name="aEvent"/>
         <body><![CDATA[
           let engine;
           let oneOff = this.selectedButton;
           if (oneOff) {
             if (!oneOff.engine) {
@@ -1355,52 +1373,17 @@
           this._updateAfterQueryChanged();
 
           let list = document.getAnonymousElementByAttribute(this, "anonid",
                                                              "search-panel-one-offs");
 
           // Handle opensearch items. This needs to be done before building the
           // list of one off providers, as that code will return early if all the
           // alternative engines are hidden.
-          let addEngineList =
-            document.getAnonymousElementByAttribute(this, "anonid", "add-engines");
-          while (addEngineList.firstChild)
-            addEngineList.firstChild.remove();
-
-          const kXULNS =
-            "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-
-          // Add a button for each engine that the page in the selected browser
-          // offers.  But not when the one-offs are compact.  Compact one-offs
-          // are shown in the urlbar, and the add-engine buttons span the width
-          // of the popup, so if we added all the engines that a site offers, it
-          // could effectively break the urlbar popup by offering a ton of
-          // engines.  We should probably make a smaller version of the buttons
-          // for compact one-offs.
-          if (!this.compact) {
-            for (let engine of gBrowser.selectedBrowser.engines || []) {
-              let button = document.createElementNS(kXULNS, "button");
-              let label = this.bundle.formatStringFromName("cmd_addFoundEngine",
-                                                           [engine.title], 1);
-              button.id = this.telemetryOrigin + "-add-engine-" +
-                          engine.title.replace(/ /g, '-');
-              button.setAttribute("class", "addengine-item");
-              button.setAttribute("label", label);
-              button.setAttribute("pack", "start");
-
-              button.setAttribute("crop", "end");
-              button.setAttribute("tooltiptext", engine.uri);
-              button.setAttribute("uri", engine.uri);
-              if (engine.icon) {
-                button.setAttribute("image", engine.icon);
-              }
-              button.setAttribute("title", engine.title);
-              addEngineList.appendChild(button);
-            }
-          }
+          this._rebuildAddEngineList();
 
           let settingsButton =
             document.getAnonymousElementByAttribute(this, "anonid",
                                                     "search-settings-compact");
           // Finally, build the list of one-off buttons.
           while (list.firstChild != settingsButton)
             list.firstChild.remove();
           // Remove the trailing empty text node introduced by the binding's
@@ -1448,16 +1431,19 @@
 
           // Ensure we can refer to the settings buttons by ID:
           let settingsEl = document.getAnonymousElementByAttribute(this, "anonid", "search-settings");
           settingsEl.id = this.telemetryOrigin + "-anon-search-settings";
           let compactSettingsEl = document.getAnonymousElementByAttribute(this, "anonid", "search-settings-compact");
           compactSettingsEl.id = this.telemetryOrigin +
                                  "-anon-search-settings-compact";
 
+          const kXULNS =
+            "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
           let dummyItems = enginesPerRow - (oneOffCount % enginesPerRow || enginesPerRow);
           for (let i = 0; i < engines.length; ++i) {
             let engine = engines[i];
             let button = document.createElementNS(kXULNS, "button");
             button.id = this._buttonIDForEngine(engine);
             let uri = "chrome://browser/skin/search-engine-placeholder.png";
             if (engine.iconURI) {
               uri = engine.iconURI.spec;
@@ -1504,21 +1490,137 @@
               let width = remainder + buttonWidth;
               let lastDummyItem = this.settingsButton.previousSibling;
               lastDummyItem.setAttribute("width", width);
             }
           }
         ]]></body>
       </method>
 
+      <!-- If a page offers more than this number of engines, the add-engines
+           menu button is shown, instead of showing the engines directly in the
+           popup. -->
+      <field name="_addEngineMenuThreshold">5</field>
+
+      <method name="_rebuildAddEngineList">
+        <body><![CDATA[
+        let list = document.getAnonymousElementByAttribute(this, "anonid",
+                                                           "add-engines");
+        while (list.firstChild) {
+          list.firstChild.remove();
+        }
+
+        // Add a button for each engine that the page in the selected browser
+        // offers, but with the following exceptions:
+        //
+        // (1) Not when the one-offs are compact.  Compact one-offs are shown in
+        // the urlbar, and the add-engine buttons span the width of the popup,
+        // so if we added all the engines that a page offers, it could break the
+        // urlbar popup by offering a ton of engines.  We should probably make a
+        // smaller version of the buttons for compact one-offs.
+        //
+        // (2) Not when there are too many offered engines.  The popup isn't
+        // designed to handle too many (by scrolling for example), so a page
+        // could break the popup by offering too many.  Instead, add a single
+        // menu button with a submenu of all the engines.
+
+        if (this.compact || !gBrowser.selectedBrowser.engines) {
+          return;
+        }
+
+        const kXULNS =
+          "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+        let engines = gBrowser.selectedBrowser.engines;
+        let tooManyEngines = engines.length > this._addEngineMenuThreshold;
+
+        if (tooManyEngines) {
+          // Make the top-level menu button.
+          let button = document.createElementNS(kXULNS, "button");
+          list.appendChild(button);
+          button.classList.add("addengine-item");
+          button.setAttribute("anonid", "addengine-menu-button");
+          button.setAttribute("type", "menu");
+          button.setAttribute("label",
+            this.bundle.GetStringFromName("cmd_addFoundEngineMenu"));
+          button.setAttribute("crop", "end");
+          button.setAttribute("pack", "start");
+
+          // Set the menu button's image to the image of the first engine.  The
+          // offered engines may have differing images, so there's no perfect
+          // choice here.
+          let engine = engines[0];
+          if (engine.icon) {
+            button.setAttribute("image", engine.icon);
+          }
+
+          // Now make the button's child menupopup.
+          list = document.createElementNS(kXULNS, "menupopup");
+          button.appendChild(list);
+          list.setAttribute("anonid", "addengine-menu");
+          list.setAttribute("position", "topright topleft");
+
+          // Events from child menupopups bubble up to the autocomplete binding,
+          // which breaks it, so prevent these events from propagating.
+          let suppressEventTypes = [
+            "popupshowing",
+            "popuphiding",
+            "popupshown",
+            "popuphidden",
+          ];
+          for (let type of suppressEventTypes) {
+            list.addEventListener(type, event => {
+              event.stopPropagation();
+            });
+          }
+        }
+
+        // Finally, add the engines to the list.  If there aren't too many
+        // engines, the list is the add-engines vbox.  Otherwise it's the
+        // menupopup created earlier.  In the latter case, create menuitem
+        // elements instead of buttons, because buttons don't get keyboard
+        // handling for free inside menupopups.
+        let eltType = tooManyEngines ? "menuitem" : "button";
+        for (let engine of engines) {
+          let button = document.createElementNS(kXULNS, eltType);
+          button.classList.add("addengine-item");
+          button.id = this.telemetryOrigin + "-add-engine-" +
+                      this._fixUpEngineNameForID(engine.title);
+          let label = this.bundle.formatStringFromName("cmd_addFoundEngine",
+                                                       [engine.title], 1);
+          button.setAttribute("label", label);
+          button.setAttribute("crop", "end");
+          button.setAttribute("tooltiptext", engine.uri);
+          button.setAttribute("uri", engine.uri);
+          button.setAttribute("title", engine.title);
+          if (engine.icon) {
+            button.setAttribute("image", engine.icon);
+          }
+          if (tooManyEngines) {
+            button.classList.add("menuitem-iconic");
+          } else {
+            button.setAttribute("pack", "start");
+          }
+          list.appendChild(button);
+        }
+        ]]></body>
+      </method>
+
       <method name="_buttonIDForEngine">
         <parameter name="engine"/>
         <body><![CDATA[
           return this.telemetryOrigin + "-engine-one-off-item-" +
-                 engine.name.replace(/ /g, '-');
+                 this._fixUpEngineNameForID(engine.name);
+        ]]></body>
+      </method>
+
+      <method name="_fixUpEngineNameForID">
+        <parameter name="name"/>
+        <body><![CDATA[
+          return name.replace(/ /g, "-");
         ]]></body>
       </method>
 
       <method name="_buttonForEngine">
         <parameter name="engine"/>
         <body><![CDATA[
           return document.getElementById(this._buttonIDForEngine(engine));
         ]]></body>
@@ -1837,16 +1939,32 @@
                   // The autocomplete controller should handle this case.
                 }
               }
             } else {
               stopEvent = this.advanceSelection(true, true, true);
             }
           }
 
+          // If the add-engine overflow menu item is selected and the user
+          // presses the right arrow key, open the submenu.  Unfortunately
+          // handling the left arrow key -- to close the popup -- isn't
+          // straightforward.  Once the popup is open, it consumes all key
+          // events.  Setting ignorekeys=handled on it doesn't help, since the
+          // popup handles all arrow keys.  Setting ignorekeys=true on it does
+          // mean that the popup no longer consumes the left arrow key, but then
+          // it no longer handles up/down keys to select items in the popup.
+          else if (this.selectedButton &&
+                   this.selectedButton.getAttribute("anonid") ==
+                     "addengine-menu-button" &&
+                   event.keyCode == KeyEvent.DOM_VK_RIGHT) {
+            this.selectedButton.open = true;
+            stopEvent = true;
+          }
+
           if (stopEvent) {
             event.preventDefault();
             event.stopPropagation();
             return true;
           }
           return false;
         ]]></body>
       </method>
@@ -1910,42 +2028,88 @@
                               aOpenUILinkParams.inBackground;
           let where = tabBackground ? "tab-background" : aOpenUILinkWhere;
           BrowserSearch.recordOneoffSearchInTelemetry(engine, source, type,
                                                       where);
           return true;
         ]]></body>
       </method>
 
+      <!-- All this stuff is to make the add-engines menu button behave like an
+           actual menu.  The add-engines menu button is shown when there are
+           many engines offered by the current site. -->
+      <field name="_addEngineMenuTimeoutMs">200</field>
+      <field name="_addEngineMenuTimeout">null</field>
+      <field name="_addEngineMenuShouldBeOpen">false</field>
+
+      <method name="_resetAddEngineMenuTimeout">
+        <body><![CDATA[
+        if (this._addEngineMenuTimeout) {
+          clearTimeout(this._addEngineMenuTimeout);
+        }
+        this._addEngineMenuTimeout = setTimeout(() => {
+          delete this._addEngineMenuTimeout;
+          let button = document.getAnonymousElementByAttribute(
+            this, "anonid", "addengine-menu-button"
+          );
+          button.open = this._addEngineMenuShouldBeOpen;
+        }, this._addEngineMenuTimeoutMs);
+        ]]></body>
+      </method>
+
     </implementation>
 
     <handlers>
 
       <handler event="mousedown"><![CDATA[
+        let target = event.originalTarget;
+        if (!target.classList.contains("searchbar-engine-one-off-item")) {
+          return;
+        }
         // Required to receive click events from the buttons on Linux.
         event.preventDefault();
       ]]></handler>
 
       <handler event="mousemove"><![CDATA[
         let target = event.originalTarget;
         if (target.localName != "button")
           return;
 
         // Ignore mouse events when the context menu is open.
          if (this._ignoreMouseEvents)
            return;
 
-        if ((target.classList.contains("searchbar-engine-one-off-item") &&
-             !target.classList.contains("dummy")) ||
+        let isOneOff =
+          target.classList.contains("searchbar-engine-one-off-item") &&
+          !target.classList.contains("dummy");
+        if (isOneOff ||
             target.classList.contains("addengine-item") ||
             target.classList.contains("search-setting-button")) {
           this._changeVisuallySelectedButton(target);
         }
       ]]></handler>
 
+      <handler event="mouseenter"><![CDATA[
+        let target = event.originalTarget;
+        if (target.getAttribute("anonid") == "addengine-menu-button") {
+          this._addEngineMenuShouldBeOpen = true;
+          this._resetAddEngineMenuTimeout();
+          return;
+        }
+      ]]></handler>
+
+      <handler event="mouseleave"><![CDATA[
+        let target = event.originalTarget;
+        if (target.getAttribute("anonid") == "addengine-menu-button") {
+          this._addEngineMenuShouldBeOpen = false;
+          this._resetAddEngineMenuTimeout();
+          return;
+        }
+      ]]></handler>
+
       <handler event="mouseout"><![CDATA[
         let target = event.originalTarget;
         if (target.localName != "button") {
           return;
         }
 
         // Don't deselect the current button if the context menu is open.
         if (this._ignoreMouseEvents)
--- a/browser/components/search/test/browser.ini
+++ b/browser/components/search/test/browser.ini
@@ -6,16 +6,17 @@ support-files =
   483086-2.xml
   head.js
   opensearch.html
   test.html
   testEngine.xml
   testEngine_diacritics.xml
   testEngine_dupe.xml
   testEngine_mozsearch.xml
+  tooManyEnginesOffered.html
   webapi.html
 
 [browser_426329.js]
 [browser_483086.js]
 [browser_addEngine.js]
 [browser_amazon.js]
 [browser_amazon_behavior.js]
 [browser_bing.js]
@@ -38,8 +39,9 @@ skip-if = os == "mac" # bug 967013
 [browser_abouthome_behavior.js]
 skip-if = true # Bug ??????, Bug 1100301 - leaks windows until shutdown when --run-by-dir
 [browser_aboutSearchReset.js]
 [browser_searchbar_openpopup.js]
 skip-if = os == "linux" # Linux has different focus behaviours.
 [browser_searchbar_keyboard_navigation.js]
 [browser_searchbar_smallpanel_keyboard_navigation.js]
 [browser_webapi.js]
+[browser_tooManyEnginesOffered.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/search/test/browser_tooManyEnginesOffered.js
@@ -0,0 +1,88 @@
+"use strict";
+
+// This test makes sure that when a page offers many search engines, the search
+// popup shows a submenu that lists them instead of showing them in the popup
+// itself.
+
+const searchbar = document.getElementById("searchbar");
+const searchPopup = document.getElementById("PopupSearchAutoComplete");
+const oneOffsContainer =
+  document.getAnonymousElementByAttribute(searchPopup, "anonid",
+                                          "search-one-off-buttons");
+
+add_task(function* test() {
+  let rootDir = getRootDirectory(gTestPath);
+  let url = rootDir + "tooManyEnginesOffered.html";
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+  // Open the search popup.
+  let promise = promiseEvent(searchPopup, "popupshown");
+  info("Opening search panel");
+  searchbar.focus();
+  EventUtils.synthesizeKey("VK_DOWN", {});
+  yield promise;
+
+  // Make sure it has only one add-engine menu button item.
+  let items = getOpenSearchItems();
+  Assert.equal(items.length, 1, "A single button")
+  let menuButton = items[0];
+  Assert.equal(menuButton.type, "menu", "A menu button");
+
+  // Mouse over the menu button to open it.
+  let buttonPopup = menuButton.firstChild;
+  promise = promiseEvent(buttonPopup, "popupshown");
+  EventUtils.synthesizeMouse(menuButton, 5, 5, { type: "mouseover" });
+  yield promise;
+
+  Assert.ok(menuButton.open, "Submenu should be open");
+
+  // Check the engines inside the submenu.
+  Assert.equal(buttonPopup.childNodes.length, 6, "Expected number of engines");
+  for (let i = 0; i < buttonPopup.childNodes.length; i++) {
+    let item = buttonPopup.childNodes[i];
+    Assert.equal(item.getAttribute("title"), "engine" + (i + 1),
+                 "Expected engine title");
+  }
+
+  // Mouse out of the menu button to close it.
+  promise = promiseEvent(buttonPopup, "popuphidden");
+  EventUtils.synthesizeMouse(searchbar, 5, 5, { type: "mousemove" });
+  yield promise;
+
+  Assert.ok(!menuButton.open, "Submenu should be closed");
+
+  // Key up until the menu button is selected.
+  for (let button = null;
+       button != menuButton;
+       button = searchbar.textbox.popup.oneOffButtons.selectedButton) {
+    EventUtils.synthesizeKey("VK_UP", {});
+  }
+
+  // Press the Right arrow key.  The submenu should open.
+  promise = promiseEvent(buttonPopup, "popupshown");
+  EventUtils.synthesizeKey("VK_RIGHT", {});
+  yield promise;
+
+  Assert.ok(menuButton.open, "Submenu should be open");
+
+  // Press the Esc key.  The submenu should close.
+  promise = promiseEvent(buttonPopup, "popuphidden");
+  EventUtils.synthesizeKey("VK_ESCAPE", {});
+  yield promise;
+
+  Assert.ok(!menuButton.open, "Submenu should be closed");
+
+  gBrowser.removeCurrentTab();
+});
+
+function getOpenSearchItems() {
+  let os = [];
+
+  let addEngineList =
+    document.getAnonymousElementByAttribute(oneOffsContainer, "anonid",
+                                            "add-engines");
+  for (let item = addEngineList.firstChild; item; item = item.nextSibling)
+    os.push(item);
+
+  return os;
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/search/test/tooManyEnginesOffered.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<link rel="search" type="application/opensearchdescription+xml" title="engine1" href="http://mochi.test:8888/browser/browser/components/search/test/engine1.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="engine2" href="http://mochi.test:8888/browser/browser/components/search/test/engine2.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="engine3" href="http://mochi.test:8888/browser/browser/components/search/test/engine3.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="engine4" href="http://mochi.test:8888/browser/browser/components/search/test/engine4.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="engine5" href="http://mochi.test:8888/browser/browser/components/search/test/engine5.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="engine6" href="http://mochi.test:8888/browser/browser/components/search/test/engine6.xml">
+</head>
+<body></body>
+</html>
--- a/browser/themes/linux/searchbar.css
+++ b/browser/themes/linux/searchbar.css
@@ -238,28 +238,35 @@ menuitem[cmd="cmd_clearhistory"][disable
   border-top: 1px solid var(--panel-separator-color);
 }
 
 .addengine-item[selected] {
   background-color: Highlight;
   color: HighlightText;
 }
 
+.addengine-item[type=menu][selected],
+.addengine-item[type=menu][open] {
+  color: inherit;
+  background-color: var(--arrowpanel-dimmed-further);
+}
+
 .addengine-icon {
   width: 16px;
 }
 
 .addengine-badge {
   width: 16px;
   height: 16px;
   margin: -7px -9px 7px 9px;
   list-style-image: url("chrome://browser/skin/badge-add-engine.png");
 }
 
-.addengine-item > .button-box > .button-text {
+.addengine-item > .button-box > .button-text,
+.addengine-item[type=menu] > .button-box > .box-inherit > .button-text {
   -moz-box-flex: 1;
   text-align: start;
   padding-inline-start: 10px;
 }
 
 .addengine-item:not([image]) {
   list-style-image: url("chrome://browser/skin/search-engine-placeholder.png");
 }
@@ -269,16 +276,22 @@ menuitem[cmd="cmd_clearhistory"][disable
     list-style-image: url("chrome://browser/skin/badge-add-engine@2x.png");
   }
 
   .addengine-item:not([image]) {
     list-style-image: url("chrome://browser/skin/search-engine-placeholder@2x.png");
   }
 }
 
+.addengine-item[type=menu] > .button-box > .button-menu-dropmarker {
+  display: -moz-box;
+  -moz-appearance: menuarrow !important;
+  list-style-image: none;
+}
+
 .search-panel-tree > .autocomplete-treebody::-moz-tree-cell {
   border-top: none !important;
 }
 
 .search-panel-tree > .autocomplete-treebody::-moz-tree-cell-text {
   padding-inline-start: 4px;
 }
 
--- a/browser/themes/osx/searchbar.css
+++ b/browser/themes/osx/searchbar.css
@@ -219,28 +219,35 @@
   border-top: 1px solid var(--panel-separator-color);
 }
 
 .addengine-item[selected] {
   background-color: Highlight;
   color: HighlightText;
 }
 
+.addengine-item[type=menu][selected],
+.addengine-item[type=menu][open] {
+  color: inherit;
+  background-color: var(--arrowpanel-dimmed-further);
+}
+
 .addengine-icon {
   width: 16px;
 }
 
 .addengine-badge {
   width: 16px;
   height: 16px;
   margin: -7px -9px 7px 9px;
   list-style-image: url("chrome://browser/skin/badge-add-engine.png");
 }
 
-.addengine-item > .button-box > .button-text {
+.addengine-item > .button-box > .button-text,
+.addengine-item[type=menu] > .button-box > .box-inherit > .button-text {
   -moz-box-flex: 1;
   text-align: start;
   padding-inline-start: 10px;
 }
 
 .addengine-item:not([image]) {
   list-style-image: url("chrome://browser/skin/search-engine-placeholder.png");
 }
@@ -250,16 +257,22 @@
     list-style-image: url("chrome://browser/skin/badge-add-engine@2x.png");
   }
 
   .addengine-item:not([image]) {
     list-style-image: url("chrome://browser/skin/search-engine-placeholder@2x.png");
   }
 }
 
+.addengine-item[type=menu] > .button-box > .button-menu-dropmarker {
+  display: -moz-box;
+  -moz-appearance: menuarrow !important;
+  list-style-image: none;
+}
+
 .search-panel-tree > .autocomplete-treebody::-moz-tree-cell {
   border-top: none !important;
 }
 
 .search-panel-tree > .autocomplete-treebody::-moz-tree-image {
   padding-inline-start: 4px;
   padding-inline-end: 2px;
   width: 14px;
--- a/browser/themes/windows/searchbar.css
+++ b/browser/themes/windows/searchbar.css
@@ -231,28 +231,35 @@
   border-top: 1px solid var(--panel-separator-color);
 }
 
 .addengine-item[selected] {
   background-color: Highlight;
   color: HighlightText;
 }
 
+.addengine-item[type=menu][selected],
+.addengine-item[type=menu][open] {
+  color: inherit;
+  background-color: var(--arrowpanel-dimmed-further);
+}
+
 .addengine-icon {
   width: 16px;
 }
 
 .addengine-badge {
   width: 16px;
   height: 16px;
   margin: -7px -9px 7px 9px;
   list-style-image: url("chrome://browser/skin/badge-add-engine.png");
 }
 
-.addengine-item > .button-box > .button-text {
+.addengine-item > .button-box > .button-text,
+.addengine-item[type=menu] > .button-box > .box-inherit > .button-text {
   -moz-box-flex: 1;
   text-align: start;
   padding-inline-start: 10px;
 }
 
 .addengine-item:not([image]) {
   list-style-image: url("chrome://browser/skin/search-engine-placeholder.png");
 }
@@ -262,16 +269,22 @@
     list-style-image: url("chrome://browser/skin/badge-add-engine@2x.png");
   }
 
   .addengine-item:not([image]) {
     list-style-image: url("chrome://browser/skin/search-engine-placeholder@2x.png");
   }
 }
 
+.addengine-item[type=menu] > .button-box > .button-menu-dropmarker {
+  display: -moz-box;
+  -moz-appearance: menuarrow !important;
+  list-style-image: none;
+}
+
 .search-panel-tree > .autocomplete-treebody::-moz-tree-cell {
   border-top: none !important;
 }
 
 .search-panel-tree > .autocomplete-treebody::-moz-tree-cell-text {
   padding-inline-start: 4px;
 }