Bug 1460982 - WIP - convert <searchbar> to a Custom Element draft
authorBrian Grinstead <bgrinstead@mozilla.com>
Fri, 11 May 2018 14:11:28 -0700
changeset 794373 5ead02b03baf64c1b3472e98c48fdee056520a1e
parent 794191 4303d49c53931385892231969e40048f096b4d4c
push id109665
push userbgrinstead@mozilla.com
push dateFri, 11 May 2018 21:11:36 +0000
bugs1460982
milestone62.0a1
Bug 1460982 - WIP - convert <searchbar> to a Custom Element MozReview-Commit-ID: CJjX3LjKgvv
browser/base/content/browser.css
browser/base/content/global-scripts.inc
browser/components/search/content/search.xml
browser/components/search/content/searchbar.js
browser/components/search/jar.mn
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -38,20 +38,16 @@
   min-width: 300px;
 %endif
 }
 
 #main-window[customize-entered] {
   min-width: -moz-fit-content;
 }
 
-searchbar {
-  -moz-binding: url("chrome://browser/content/search/search.xml#searchbar");
-}
-
 /* Prevent shrinking the page content to 0 height and width */
 .browserStack > browser {
   min-height: 25px;
   min-width: 25px;
 }
 
 .browserStack > browser {
   -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-browser");
--- a/browser/base/content/global-scripts.inc
+++ b/browser/base/content/global-scripts.inc
@@ -7,16 +7,17 @@
 # file so that ESLint works correctly:
 # tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js
 
 <script type="application/javascript">
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 for (let script of [
   "chrome://browser/content/browser.js",
+  "chrome://browser/content/search/searchbar.js",
 
   "chrome://browser/content/browser-captivePortal.js",
   "chrome://browser/content/browser-compacttheme.js",
   "chrome://browser/content/browser-feeds.js",
   "chrome://browser/content/browser-media.js",
   "chrome://browser/content/browser-pageActions.js",
   "chrome://browser/content/browser-places.js",
   "chrome://browser/content/browser-plugins.js",
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -489,33 +489,33 @@
 
     </handlers>
   </binding>
 
   <binding id="searchbar-textbox"
       extends="chrome://global/content/bindings/autocomplete.xml#autocomplete">
     <implementation>
       <constructor><![CDATA[
-        if (document.getBindingParent(this).parentNode.parentNode.localName ==
+        if (this.closest('searchbar').parentNode.parentNode.localName ==
             "toolbarpaletteitem")
           return;
 
         if (Services.prefs.getBoolPref("browser.urlbar.clickSelectsAll"))
           this.setAttribute("clickSelectsAll", true);
 
         var textBox = document.getAnonymousElementByAttribute(this,
                                               "anonid", "textbox-input-box");
         var cxmenu = document.getAnonymousElementByAttribute(textBox,
                                           "anonid", "input-box-contextmenu");
         cxmenu.addEventListener("popupshowing",
                                 () => { this.initContextMenu(cxmenu); },
                                 {capturing: true, once: true});
 
         this.setAttribute("aria-owns", this.popup.id);
-        document.getBindingParent(this)._textboxInitialized = true;
+        this.closest('searchbar')._textboxInitialized = true;
       ]]></constructor>
 
       <destructor><![CDATA[
         // If the context menu has never been opened, there won't be anything
         // to remove here.
         // Also, XBL and the customize toolbar code sometimes interact poorly.
         try {
           this.controllers.removeController(this.searchbarController);
@@ -524,17 +524,17 @@
 
       // Add items to context menu and attach controller to handle them the
       // first time the context menu is opened.
       <method name="initContextMenu">
         <parameter name="aMenu"/>
         <body><![CDATA[
           const kXULNS =
             "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-          let stringBundle = document.getBindingParent(this)._stringBundle;
+          let stringBundle = this.closest('searchbar')._stringBundle;
 
           let pasteAndSearch, suggestMenuItem;
           let element, label, akey;
 
           element = document.createElementNS(kXULNS, "menuseparator");
           aMenu.appendChild(element);
 
           let insertLocation = aMenu.firstChild;
@@ -680,17 +680,17 @@
           }
         ]]></body>
       </method>
 
       <method name="openSearch">
         <body>
           <![CDATA[
             if (!this.popupOpen) {
-              document.getBindingParent(this).openSuggestionsPanel();
+              this.closest('searchbar').openSuggestionsPanel();
               return false;
             }
             return true;
           ]]>
         </body>
       </method>
 
       <method name="handleEnter">
@@ -723,17 +723,17 @@
               return;
             }
             engine = oneOff.engine;
           }
           if (this._selectionDetails) {
             BrowserSearch.searchBar.telemetrySearchDetails = this._selectionDetails;
             this._selectionDetails = null;
           }
-          document.getBindingParent(this).handleSearchCommand(aEvent, engine);
+          this.closest('searchbar').handleSearchCommand(aEvent, engine);
         ]]></body>
       </method>
 
       <property name="selectedButton">
         <getter><![CDATA[
           return this.popup.oneOffButtons.selectedButton;
         ]]></getter>
         <setter><![CDATA[
@@ -798,21 +798,21 @@
         this.popup.removeAttribute("showonlysettings");
       ]]></handler>
 
       <handler event="keypress" phase="capturing"
                action="return this.handleKeyboardNavigation(event);"/>
 
       <handler event="keypress" keycode="VK_UP" modifiers="accel"
                phase="capturing"
-               action="document.getBindingParent(this).selectEngine(event, false);"/>
+               action="this.closest('searchbar').selectEngine(event, false);"/>
 
       <handler event="keypress" keycode="VK_DOWN" modifiers="accel"
                phase="capturing"
-               action="document.getBindingParent(this).selectEngine(event, true);"/>
+               action="this.closest('searchbar').selectEngine(event, true);"/>
 
       <handler event="keypress" keycode="VK_DOWN" modifiers="alt"
                phase="capturing"
                action="return this.openSearch();"/>
 
       <handler event="keypress" keycode="VK_UP" modifiers="alt"
                phase="capturing"
                action="return this.openSearch();"/>
@@ -829,17 +829,17 @@
       <![CDATA[
         var dataTransfer = event.dataTransfer;
         var data = dataTransfer.getData("text/plain");
         if (!data)
           data = dataTransfer.getData("text/x-moz-text-internal");
         if (data) {
           event.preventDefault();
           this.value = data;
-          document.getBindingParent(this).openSuggestionsPanel();
+          this.closest('searchbar').openSuggestionsPanel();
         }
       ]]>
       </handler>
 
     </handlers>
   </binding>
 
   <binding id="browser-search-autocomplete-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-rich-result-popup">
copy from browser/components/search/content/search.xml
copy to browser/components/search/content/searchbar.js
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/searchbar.js
@@ -1,2322 +1,431 @@
-<?xml version="1.0"?>
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
-<!-- XULCommandEvent is a specialised global. -->
-<!-- global XULCommandEvent -->
-
-<!DOCTYPE bindings [
-<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
-%browserDTD;
-]>
-
-<bindings id="SearchBindings"
-          xmlns="http://www.mozilla.org/xbl"
-          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-          xmlns:xbl="http://www.mozilla.org/xbl">
-
-  <binding id="searchbar">
-    <resources>
-      <stylesheet src="chrome://browser/content/search/searchbarBindings.css"/>
-      <stylesheet src="chrome://browser/skin/searchbar.css"/>
-    </resources>
-    <content>
-      <xul:stringbundle src="chrome://browser/locale/search.properties"
-                        anonid="searchbar-stringbundle"/>
-      <!--
-      There is a dependency between "maxrows" attribute and
-      "SuggestAutoComplete._historyLimit" (nsSearchSuggestions.js). Changing
-      one of them requires changing the other one.
-      -->
-      <xul:textbox class="searchbar-textbox"
-                   anonid="searchbar-textbox"
-                   type="autocomplete"
-                   inputtype="search"
-                   placeholder="&searchInput.placeholder;"
-                   flex="1"
-                   autocompletepopup="PopupSearchAutoComplete"
-                   autocompletesearch="search-autocomplete"
-                   autocompletesearchparam="searchbar-history"
-                   maxrows="10"
-                   completeselectedindex="true"
-                   minresultsforpopup="0"
-                   xbl:inherits="disabled,disableautocomplete,searchengine,src,newlines">
-        <!--
-        Empty <box> to properly position the icon within the autocomplete
-        binding's anonymous children (the autocomplete binding positions <box>
-        children differently)
-        -->
-        <xul:box>
-          <xul:hbox class="searchbar-search-button"
-                    anonid="searchbar-search-button"
-                    xbl:inherits="addengines"
-                    tooltiptext="&searchIcon.tooltip;">
-            <xul:image class="searchbar-search-icon"/>
-            <xul:image class="searchbar-search-icon-overlay"/>
-          </xul:hbox>
-        </xul:box>
-        <xul:hbox class="search-go-container">
-          <xul:image class="search-go-button urlbar-icon" hidden="true"
-                     anonid="search-go-button"
-                     onclick="handleSearchCommand(event);"
-                     tooltiptext="&contentSearchSubmit.tooltip;"/>
-        </xul:hbox>
-      </xul:textbox>
-    </content>
-
-    <implementation implements="nsIObserver">
-      <constructor><![CDATA[
-        if (this.parentNode.parentNode.localName == "toolbarpaletteitem")
-          return;
-
-        Services.obs.addObserver(this, "browser-search-engine-modified");
-
-        this._initialized = true;
-
-        (window.delayedStartupPromise || Promise.resolve()).then(() => {
-          window.requestIdleCallback(() => {
-            Services.search.init(aStatus => {
-              // Bail out if the binding's been destroyed
-              if (!this._initialized)
-                return;
-
-              if (Components.isSuccessCode(aStatus)) {
-                // Refresh the display (updating icon, etc)
-                this.updateDisplay();
-                BrowserSearch.updateOpenSearchBadge();
-              } else {
-                Cu.reportError("Cannot initialize search service, bailing out: " + aStatus);
-              }
-            });
-          });
-        });
-
-        // Wait until the popupshowing event to avoid forcing immediate
-        // attachment of the search-one-offs binding.
-        this.textbox.popup.addEventListener("popupshowing", () => {
-          let oneOffButtons = this.textbox.popup.oneOffButtons;
-          // Some accessibility tests create their own <searchbar> that doesn't
-          // use the popup binding below, so null-check oneOffButtons.
-          if (oneOffButtons) {
-            oneOffButtons.telemetryOrigin = "searchbar";
-            // Set .textbox first, since the popup setter will cause
-            // a _rebuild call that uses it.
-            oneOffButtons.textbox = this.textbox;
-            oneOffButtons.popup = this.textbox.popup;
-          }
-        }, {capturing: true, once: true});
-      ]]></constructor>
-
-      <destructor><![CDATA[
-        this.destroy();
-      ]]></destructor>
-
-      <method name="destroy">
-        <body><![CDATA[
-        if (this._initialized) {
-          this._initialized = false;
-
-          Services.obs.removeObserver(this, "browser-search-engine-modified");
-        }
-
-        // Make sure to break the cycle from _textbox to us. Otherwise we leak
-        // the world. But make sure it's actually pointing to us.
-        // Also make sure the textbox has ever been constructed, otherwise the
-        // _textbox getter will cause the textbox constructor to run, add an
-        // observer, and leak the world too.
-        if (this._textboxInitialized && this._textbox.mController.input == this)
-          this._textbox.mController.input = null;
-        ]]></body>
-      </method>
-
-      <field name="_ignoreFocus">false</field>
-      <field name="_clickClosedPopup">false</field>
-      <field name="_stringBundle">document.getAnonymousElementByAttribute(this,
-          "anonid", "searchbar-stringbundle");</field>
-      <field name="_textboxInitialized">false</field>
-      <field name="_textbox">document.getAnonymousElementByAttribute(this,
-          "anonid", "searchbar-textbox");</field>
-      <field name="_engines">null</field>
-      <field name="FormHistory" readonly="true">
-        (ChromeUtils.import("resource://gre/modules/FormHistory.jsm", {})).FormHistory;
-      </field>
-
-      <property name="engines" readonly="true">
-        <getter><![CDATA[
-          if (!this._engines)
-            this._engines = Services.search.getVisibleEngines();
-          return this._engines;
-        ]]></getter>
-      </property>
-
-      <property name="currentEngine">
-        <setter><![CDATA[
-          Services.search.currentEngine = val;
-          return val;
-        ]]></setter>
-        <getter><![CDATA[
-          var currentEngine = Services.search.currentEngine;
-          // Return a dummy engine if there is no currentEngine
-          return currentEngine || {name: "", uri: null};
-        ]]></getter>
-      </property>
-
-      <!-- textbox is used by sanitize.js to clear the undo history when
-           clearing form information. -->
-      <property name="textbox" readonly="true"
-                onget="return this._textbox;"/>
-
-      <property name="value" onget="return this._textbox.value;"
-                             onset="return this._textbox.value = val;"/>
-
-      <method name="focus">
-        <body><![CDATA[
-          this._textbox.focus();
-        ]]></body>
-      </method>
-
-      <method name="select">
-        <body><![CDATA[
-          this._textbox.select();
-        ]]></body>
-      </method>
-
-      <method name="observe">
-        <parameter name="aEngine"/>
-        <parameter name="aTopic"/>
-        <parameter name="aVerb"/>
-        <body><![CDATA[
-          if (aTopic == "browser-search-engine-modified") {
-            // Make sure the engine list is refetched next time it's needed
-            this._engines = null;
-
-            // Update the popup header and update the display after any modification.
-            this._textbox.popup.updateHeader();
-            this.updateDisplay();
-          }
-        ]]></body>
-      </method>
-
-      <method name="setIcon">
-        <parameter name="element"/>
-        <parameter name="uri"/>
-        <body><![CDATA[
-          element.setAttribute("src", uri);
-        ]]></body>
-      </method>
-
-      <method name="updateDisplay">
-        <body><![CDATA[
-          var uri = this.currentEngine.iconURI;
-          this.setIcon(this, uri ? uri.spec : "");
+{
 
-          var name = this.currentEngine.name;
-          var text = this._stringBundle.getFormattedString("searchtip", [name]);
-          this._textbox.label = text;
-          this._textbox.tooltipText = text;
-        ]]></body>
-      </method>
-
-      <method name="updateGoButtonVisibility">
-        <body><![CDATA[
-          document.getAnonymousElementByAttribute(this, "anonid",
-                                                  "search-go-button")
-                  .hidden = !this._textbox.value;
-        ]]></body>
-      </method>
-
-      <method name="openSuggestionsPanel">
-        <parameter name="aShowOnlySettingsIfEmpty"/>
-        <body><![CDATA[
-          if (this._textbox.open)
-            return;
-
-          this._textbox.showHistoryPopup();
-
-          if (this._textbox.value) {
-            // showHistoryPopup does a startSearch("") call, ensure the
-            // controller handles the text from the input box instead:
-            this._textbox.mController.handleText();
-          } else if (aShowOnlySettingsIfEmpty) {
-            this.setAttribute("showonlysettings", "true");
-          }
-        ]]></body>
-      </method>
-
-      <method name="selectEngine">
-        <parameter name="aEvent"/>
-        <parameter name="isNextEngine"/>
-        <body><![CDATA[
-          // Find the new index
-          var newIndex = this.engines.indexOf(this.currentEngine);
-          newIndex += isNextEngine ? 1 : -1;
-
-          if (newIndex >= 0 && newIndex < this.engines.length) {
-            this.currentEngine = this.engines[newIndex];
-          }
-
-          aEvent.preventDefault();
-          aEvent.stopPropagation();
-
-          this.openSuggestionsPanel();
-        ]]></body>
-      </method>
-
-      <method name="handleSearchCommand">
-        <parameter name="aEvent"/>
-        <parameter name="aEngine"/>
-        <parameter name="aForceNewTab"/>
-        <body><![CDATA[
-          var where = "current";
-          let params;
-
-          // Open ctrl/cmd clicks on one-off buttons in a new background tab.
-          if (aEvent && aEvent.originalTarget.getAttribute("anonid") == "search-go-button") {
-            if (aEvent.button == 2)
-              return;
-            where = whereToOpenLink(aEvent, false, true);
-          } else if (aForceNewTab) {
-            where = "tab";
-            if (Services.prefs.getBoolPref("browser.tabs.loadInBackground"))
-              where += "-background";
-          } else {
-            var newTabPref = Services.prefs.getBoolPref("browser.search.openintab");
-            if (((aEvent instanceof KeyboardEvent) && aEvent.altKey) ^ newTabPref)
-              where = "tab";
-            if ((aEvent instanceof MouseEvent) &&
-                (aEvent.button == 1 || aEvent.getModifierState("Accel"))) {
-              where = "tab";
-              params = {
-                inBackground: true,
-              };
-            }
-          }
-
-          this.handleSearchCommandWhere(aEvent, aEngine, where, params);
-        ]]></body>
-      </method>
-
-      <method name="handleSearchCommandWhere">
-        <parameter name="aEvent"/>
-        <parameter name="aEngine"/>
-        <parameter name="aWhere"/>
-        <parameter name="aParams"/>
-        <body><![CDATA[
-          var textBox = this._textbox;
-          var textValue = textBox.value;
-
-          let selection = this.telemetrySearchDetails;
-          let oneOffRecorded = false;
-
-          BrowserUsageTelemetry.recordSearchbarSelectedResultMethod(
-            aEvent,
-            selection ? selection.index : -1
-          );
-
-          if (!selection || (selection.index == -1)) {
-            oneOffRecorded = this.textbox.popup.oneOffButtons
-                                 .maybeRecordTelemetry(aEvent, aWhere, aParams);
-            if (!oneOffRecorded) {
-              let source = "unknown";
-              let type = "unknown";
-              let target = aEvent.originalTarget;
-              if (aEvent instanceof KeyboardEvent) {
-                type = "key";
-              } else if (aEvent instanceof MouseEvent) {
-                type = "mouse";
-                if (target.classList.contains("search-panel-header") ||
-                    target.parentNode.classList.contains("search-panel-header")) {
-                  source = "header";
-                }
-              } else if (aEvent instanceof XULCommandEvent) {
-                if (target.getAttribute("anonid") == "paste-and-search") {
-                  source = "paste";
-                }
-              }
-              if (!aEngine) {
-                aEngine = this.currentEngine;
-              }
-              BrowserSearch.recordOneoffSearchInTelemetry(aEngine, source, type,
-                                                          aWhere);
-            }
-          }
-
-          // This is a one-off search only if oneOffRecorded is true.
-          this.doSearch(textValue, aWhere, aEngine, aParams, oneOffRecorded);
-
-          if (aWhere == "tab" && aParams && aParams.inBackground)
-            this.focus();
-        ]]></body>
-      </method>
-
-      <method name="doSearch">
-        <parameter name="aData"/>
-        <parameter name="aWhere"/>
-        <parameter name="aEngine"/>
-        <parameter name="aParams"/>
-        <parameter name="aOneOff"/>
-        <body><![CDATA[
-          var textBox = this._textbox;
-
-          // Save the current value in the form history
-          if (aData && !PrivateBrowsingUtils.isWindowPrivate(window) && this.FormHistory.enabled) {
-            this.FormHistory.update(
-              { op: "bump",
-                fieldname: textBox.getAttribute("autocompletesearchparam"),
-                value: aData },
-              { handleError(aError) {
-                  Cu.reportError("Saving search to form history failed: " + aError.message);
-              }});
-          }
-
-          let engine = aEngine || this.currentEngine;
-          var submission = engine.getSubmission(aData, null, "searchbar");
-          let telemetrySearchDetails = this.telemetrySearchDetails;
-          this.telemetrySearchDetails = null;
-          if (telemetrySearchDetails && telemetrySearchDetails.index == -1) {
-            telemetrySearchDetails = null;
-          }
-          // If we hit here, we come either from a one-off, a plain search or a suggestion.
-          const details = {
-            isOneOff: aOneOff,
-            isSuggestion: (!aOneOff && telemetrySearchDetails),
-            selection: telemetrySearchDetails
-          };
-          BrowserSearch.recordSearchInTelemetry(engine, "searchbar", details);
-          // null parameter below specifies HTML response for search
-          let params = {
-            postData: submission.postData,
-          };
-          if (aParams) {
-            for (let key in aParams) {
-              params[key] = aParams[key];
-            }
-          }
-          openTrustedLinkIn(submission.uri.spec, aWhere, params);
-        ]]></body>
-      </method>
-    </implementation>
-
-    <handlers>
-      <handler event="command"><![CDATA[
-        const target = event.originalTarget;
-        if (target.engine) {
-          this.currentEngine = target.engine;
-        } else if (target.classList.contains("addengine-item")) {
-          // Select the installed engine if the installation succeeds
-          var installCallback = {
-            onSuccess: engine => this.currentEngine = engine
-          };
-          Services.search.addEngine(target.getAttribute("uri"), null,
-                                    target.getAttribute("src"), false,
-                                    installCallback);
-        } else
-          return;
-
-        this.focus();
-        this.select();
-      ]]></handler>
-
-      <handler event="DOMMouseScroll"
-               phase="capturing"
-               modifiers="accel"
-               action="this.selectEngine(event, (event.detail > 0));"/>
-
-      <handler event="input" action="this.updateGoButtonVisibility();"/>
-      <handler event="drop" action="this.updateGoButtonVisibility();"/>
-
-      <handler event="blur">
-      <![CDATA[
-        // If the input field is still focused then a different window has
-        // received focus, ignore the next focus event.
-        this._ignoreFocus = (document.activeElement == this._textbox.inputField);
-      ]]></handler>
+function parseXULDOM(str) {
+  const d = new DOMParser();
+  d.forceEnableXULXBL();
+  const doc = d.parseFromString(
+    `<box xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">${str}</box>`,
+    "application/xml");
+  const range = doc.createRange();
+  range.selectNodeContents(doc.firstChild);
+  return range.extractContents();
+}
 
-      <handler event="focus">
-      <![CDATA[
-        // Speculatively connect to the current engine's search URI (and
-        // suggest URI, if different) to reduce request latency
-        this.currentEngine.speculativeConnect({window,
-                                               originAttributes: gBrowser.contentPrincipal
-                                                                         .originAttributes});
-
-        if (this._ignoreFocus) {
-          // This window has been re-focused, don't show the suggestions
-          this._ignoreFocus = false;
-          return;
-        }
-
-        // Don't open the suggestions if there is no text in the textbox.
-        if (!this._textbox.value)
-          return;
-
-        // Don't open the suggestions if the mouse was used to focus the
-        // textbox, that will be taken care of in the click handler.
-        if (Services.focus.getLastFocusMethod(window) & Services.focus.FLAG_BYMOUSE)
-          return;
-
-        this.openSuggestionsPanel();
-      ]]></handler>
-
-      <handler event="mousedown" phase="capturing">
-      <![CDATA[
-        if (event.originalTarget.getAttribute("anonid") == "searchbar-search-button") {
-          this._clickClosedPopup = this._textbox.popup._isHiding;
-        }
-      ]]></handler>
-
-      <handler event="mousedown" button="0">
-      <![CDATA[
-        // Ignore clicks on the search go button.
-        if (event.originalTarget.getAttribute("anonid") == "search-go-button") {
-          return;
-        }
-
-        let isIconClick = event.originalTarget.getAttribute("anonid") == "searchbar-search-button";
-
-        // Ignore clicks on the icon if they were made to close the popup
-        if (isIconClick && this._clickClosedPopup) {
-          return;
-        }
-
-        // Open the suggestions whenever clicking on the search icon or if there
-        // is text in the textbox.
-        if (isIconClick || this._textbox.value) {
-          this.openSuggestionsPanel(true);
-        }
-      ]]></handler>
-
-    </handlers>
-  </binding>
-
-  <binding id="searchbar-textbox"
-      extends="chrome://global/content/bindings/autocomplete.xml#autocomplete">
-    <implementation>
-      <constructor><![CDATA[
-        if (document.getBindingParent(this).parentNode.parentNode.localName ==
-            "toolbarpaletteitem")
-          return;
-
-        if (Services.prefs.getBoolPref("browser.urlbar.clickSelectsAll"))
-          this.setAttribute("clickSelectsAll", true);
-
-        var textBox = document.getAnonymousElementByAttribute(this,
-                                              "anonid", "textbox-input-box");
-        var cxmenu = document.getAnonymousElementByAttribute(textBox,
-                                          "anonid", "input-box-contextmenu");
-        cxmenu.addEventListener("popupshowing",
-                                () => { this.initContextMenu(cxmenu); },
-                                {capturing: true, once: true});
-
-        this.setAttribute("aria-owns", this.popup.id);
-        document.getBindingParent(this)._textboxInitialized = true;
-      ]]></constructor>
-
-      <destructor><![CDATA[
-        // If the context menu has never been opened, there won't be anything
-        // to remove here.
-        // Also, XBL and the customize toolbar code sometimes interact poorly.
-        try {
-          this.controllers.removeController(this.searchbarController);
-        } catch (ex) { }
-      ]]></destructor>
-
-      // Add items to context menu and attach controller to handle them the
-      // first time the context menu is opened.
-      <method name="initContextMenu">
-        <parameter name="aMenu"/>
-        <body><![CDATA[
-          const kXULNS =
-            "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-          let stringBundle = document.getBindingParent(this)._stringBundle;
-
-          let pasteAndSearch, suggestMenuItem;
-          let element, label, akey;
-
-          element = document.createElementNS(kXULNS, "menuseparator");
-          aMenu.appendChild(element);
+class MozSearchbar extends XULElement {
+  connectedCallback() {
+    this.appendChild(parseXULDOM(`
+      <stringbundle src="chrome://browser/locale/search.properties" class="searchbar-stringbundle"></stringbundle>
+      <textbox class="searchbar-textbox"
+               type="autocomplete"
+               anonid="searchbar-textbox"
+               inputtype="search"
+               placeholder="FROM-DTD-searchInput-placeholder"
+               flex="1"
+               autocompletepopup="PopupSearchAutoComplete"
+               autocompletesearch="search-autocomplete"
+               autocompletesearchparam="searchbar-history"
+               maxrows="10"
+               completeselectedindex="true"
+               minresultsforpopup="0"
+               inherits="disabled,disableautocomplete,searchengine,src,newlines">
+        <box>
+          <hbox class="searchbar-search-button" anonid="searchbar-search-button" inherits="addengines" tooltiptext="FROM-DTD-searchIcon-tooltip">
+            <image class="searchbar-search-icon"></image>
+            <image class="searchbar-search-icon-overlay"></image>
+          </hbox>
+        </box>
+        <hbox class="search-go-container">
+          <image class="search-go-button urlbar-icon" hidden="true" anonid="search-go-button" onclick="handleSearchCommand(event);" tooltiptext="FROM-DTD-contentSearchSubmit-tooltip"></image>
+        </hbox>
+      </textbox>
+    `));
 
-          let insertLocation = aMenu.firstChild;
-          while (insertLocation.nextSibling &&
-                 insertLocation.getAttribute("cmd") != "cmd_paste")
-            insertLocation = insertLocation.nextSibling;
-          if (insertLocation) {
-            element = document.createElementNS(kXULNS, "menuitem");
-            label = stringBundle.getString("cmd_pasteAndSearch");
-            element.setAttribute("label", label);
-            element.setAttribute("anonid", "paste-and-search");
-            element.setAttribute("oncommand", "BrowserSearch.pasteAndSearch(event)");
-            aMenu.insertBefore(element, insertLocation.nextSibling);
-            pasteAndSearch = element;
-          }
-
-          element = document.createElementNS(kXULNS, "menuitem");
-          label = stringBundle.getString("cmd_clearHistory");
-          akey = stringBundle.getString("cmd_clearHistory_accesskey");
-          element.setAttribute("label", label);
-          element.setAttribute("accesskey", akey);
-          element.setAttribute("cmd", "cmd_clearhistory");
-          aMenu.appendChild(element);
-
-          element = document.createElementNS(kXULNS, "menuitem");
-          label = stringBundle.getString("cmd_showSuggestions");
-          akey = stringBundle.getString("cmd_showSuggestions_accesskey");
-          element.setAttribute("anonid", "toggle-suggest-item");
-          element.setAttribute("label", label);
-          element.setAttribute("accesskey", akey);
-          element.setAttribute("cmd", "cmd_togglesuggest");
-          element.setAttribute("type", "checkbox");
-          element.setAttribute("autocheck", "false");
-          suggestMenuItem = element;
-          aMenu.appendChild(element);
-
-          if (AppConstants.platform == "macosx") {
-            this.addEventListener("keypress", aEvent => {
-              if (aEvent.keyCode == KeyEvent.DOM_VK_F4)
-                this.openSearch();
-            }, true);
-          }
-
-          this.controllers.appendController(this.searchbarController);
-
-          let onpopupshowing = function() {
-            BrowserSearch.searchBar._textbox.closePopup();
-            if (suggestMenuItem) {
-              let enabled =
-                Services.prefs.getBoolPref("browser.search.suggest.enabled");
-              suggestMenuItem.setAttribute("checked", enabled);
-            }
+    this._ignoreFocus = false;
 
-            if (!pasteAndSearch)
-              return;
-            let controller = document.commandDispatcher.getControllerForCommand("cmd_paste");
-            let enabled = controller.isCommandEnabled("cmd_paste");
-            if (enabled)
-              pasteAndSearch.removeAttribute("disabled");
-            else
-              pasteAndSearch.setAttribute("disabled", "true");
-          };
-          aMenu.addEventListener("popupshowing", onpopupshowing);
-          onpopupshowing();
-        ]]></body>
-      </method>
-
-      <!--
-        This overrides the searchParam property in autocomplete.xml.  We're
-        hijacking this property as a vehicle for delivering the privacy
-        information about the window into the guts of nsSearchSuggestions.
-
-        Note that the setter is the same as the parent.  We were not sure whether
-        we can override just the getter.  If that proves to be the case, the setter
-        can be removed.
-      -->
-      <property name="searchParam"
-                onget="return this.getAttribute('autocompletesearchparam') +
-                       (PrivateBrowsingUtils.isWindowPrivate(window) ? '|private' : '');"
-                onset="this.setAttribute('autocompletesearchparam', val); return val;"/>
-
-      <!-- This is implemented so that when textbox.value is set directly (e.g.,
-           by tests), the one-off query is updated. -->
-      <method name="onBeforeValueSet">
-        <parameter name="aValue"/>
-        <body><![CDATA[
-          this.popup.oneOffButtons.query = aValue;
-          return aValue;
-        ]]></body>
-      </method>
-
-      <!--
-        This method overrides the autocomplete binding's openPopup (essentially
-        duplicating the logic from the autocomplete popup binding's
-        openAutocompletePopup method), modifying it so that the popup is aligned with
-        the inner textbox, but sized to not extend beyond the search bar border.
-      -->
-      <method name="openPopup">
-        <body><![CDATA[
-          // Entering customization mode after the search bar had focus causes
-          // the popup to appear again, due to focus returning after the
-          // hamburger panel closes. Don't open in that spurious event.
-          if (document.documentElement.getAttribute("customizing") == "true") {
-            return;
-          }
-
-          var popup = this.popup;
-          if (!popup.mPopupOpen) {
-            // Initially the panel used for the searchbar (PopupSearchAutoComplete
-            // in browser.xul) is hidden to avoid impacting startup / new
-            // window performance. The base binding's openPopup would normally
-            // call the overriden openAutocompletePopup in
-            // browser-search-autocomplete-result-popup binding to unhide the popup,
-            // but since we're overriding openPopup we need to unhide the panel
-            // ourselves.
-            popup.hidden = false;
+    this._clickClosedPopup = false;
 
-            // Don't roll up on mouse click in the anchor for the search UI.
-            if (popup.id == "PopupSearchAutoComplete") {
-              popup.setAttribute("norolluponanchor", "true");
-            }
-
-            popup.mInput = this;
-            // clear any previous selection, see bugs 400671 and 488357
-            popup.selectedIndex = -1;
-
-            document.popupNode = null;
-
-            const isRTL = getComputedStyle(this, "").direction == "rtl";
-
-            var outerRect = this.getBoundingClientRect();
-            var innerRect = this.inputField.getBoundingClientRect();
-            let width = isRTL ?
-                        innerRect.right - outerRect.left :
-                        outerRect.right - innerRect.left;
-            popup.setAttribute("width", width > 100 ? width : 100);
+    this._stringBundle = this.querySelector(".searchbar-stringbundle");
 
-            // invalidate() depends on the width attribute
-            popup._invalidate();
-
-            var yOffset = outerRect.bottom - innerRect.bottom;
-            popup.openPopup(this.inputField, "after_start", 0, yOffset, false, false);
-          }
-        ]]></body>
-      </method>
+    this._textboxInitialized = false;
 
-      <method name="openSearch">
-        <body>
-          <![CDATA[
-            if (!this.popupOpen) {
-              document.getBindingParent(this).openSuggestionsPanel();
-              return false;
-            }
-            return true;
-          ]]>
-        </body>
-      </method>
+    this._textbox = this.querySelector(".searchbar-textbox");
 
-      <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>
+    this._engines = null;
 
-      <!-- override |onTextEntered| in autocomplete.xml -->
-      <method name="onTextEntered">
-        <parameter name="aEvent"/>
-        <body><![CDATA[
-          let engine;
-          let oneOff = this.selectedButton;
-          if (oneOff) {
-            if (!oneOff.engine) {
-              oneOff.doCommand();
-              return;
-            }
-            engine = oneOff.engine;
-          }
-          if (this._selectionDetails) {
-            BrowserSearch.searchBar.telemetrySearchDetails = this._selectionDetails;
-            this._selectionDetails = null;
-          }
-          document.getBindingParent(this).handleSearchCommand(aEvent, engine);
-        ]]></body>
-      </method>
+    this.FormHistory = (ChromeUtils.import("resource://gre/modules/FormHistory.jsm", {})).FormHistory;
 
-      <property name="selectedButton">
-        <getter><![CDATA[
-          return this.popup.oneOffButtons.selectedButton;
-        ]]></getter>
-        <setter><![CDATA[
-          return this.popup.oneOffButtons.selectedButton = val;
-        ]]></setter>
-      </property>
-
-      <method name="handleKeyboardNavigation">
-        <parameter name="aEvent"/>
-        <body><![CDATA[
-          let popup = this.popup;
-          if (!popup.popupOpen)
-            return;
-
-          // accel + up/down changes the default engine and shouldn't affect
-          // the selection on the one-off buttons.
-          if (aEvent.getModifierState("Accel"))
-            return;
-
-          let suggestionsHidden =
-            popup.richlistbox.getAttribute("collapsed") == "true";
-          let numItems = suggestionsHidden ? 0 : this.popup.matchCount;
-          this.popup.oneOffButtons.handleKeyPress(aEvent, numItems, true);
-        ]]></body>
-      </method>
+    if (this.parentNode.parentNode.localName == "toolbarpaletteitem")
+      return;
 
-      <!-- nsIController -->
-      <field name="searchbarController" readonly="true"><![CDATA[({
-        _self: this,
-        supportsCommand(aCommand) {
-          return aCommand == "cmd_clearhistory" ||
-                 aCommand == "cmd_togglesuggest";
-        },
-
-        isCommandEnabled(aCommand) {
-          return true;
-        },
-
-        doCommand(aCommand) {
-          switch (aCommand) {
-            case "cmd_clearhistory":
-              var param = this._self.getAttribute("autocompletesearchparam");
-
-              BrowserSearch.searchBar.FormHistory.update({ op: "remove", fieldname: param }, null);
-              this._self.value = "";
-              break;
-            case "cmd_togglesuggest":
-              let enabled =
-                Services.prefs.getBoolPref("browser.search.suggest.enabled");
-              Services.prefs.setBoolPref("browser.search.suggest.enabled",
-                                         !enabled);
-              break;
-            default:
-              // do nothing with unrecognized command
-          }
-        }
-      })]]></field>
-    </implementation>
-
-    <handlers>
-      <handler event="input"><![CDATA[
-        this.popup.removeAttribute("showonlysettings");
-      ]]></handler>
-
-      <handler event="keypress" phase="capturing"
-               action="return this.handleKeyboardNavigation(event);"/>
-
-      <handler event="keypress" keycode="VK_UP" modifiers="accel"
-               phase="capturing"
-               action="document.getBindingParent(this).selectEngine(event, false);"/>
-
-      <handler event="keypress" keycode="VK_DOWN" modifiers="accel"
-               phase="capturing"
-               action="document.getBindingParent(this).selectEngine(event, true);"/>
-
-      <handler event="keypress" keycode="VK_DOWN" modifiers="alt"
-               phase="capturing"
-               action="return this.openSearch();"/>
-
-      <handler event="keypress" keycode="VK_UP" modifiers="alt"
-               phase="capturing"
-               action="return this.openSearch();"/>
+    this.observer = {
+      observe: (aEngine, aTopic, aVerb) => {
+        if (aTopic == "browser-search-engine-modified") {
+          // Make sure the engine list is refetched next time it's needed
+          this._engines = null;
 
-      <handler event="dragover">
-      <![CDATA[
-        var types = event.dataTransfer.types;
-        if (types.includes("text/plain") || types.includes("text/x-moz-text-internal"))
-          event.preventDefault();
-      ]]>
-      </handler>
-
-      <handler event="drop">
-      <![CDATA[
-        var dataTransfer = event.dataTransfer;
-        var data = dataTransfer.getData("text/plain");
-        if (!data)
-          data = dataTransfer.getData("text/x-moz-text-internal");
-        if (data) {
-          event.preventDefault();
-          this.value = data;
-          document.getBindingParent(this).openSuggestionsPanel();
+          // Update the popup header and update the display after any modification.
+          this._textbox.popup.updateHeader();
+          this.updateDisplay();
         }
-      ]]>
-      </handler>
-
-    </handlers>
-  </binding>
+      }
+    };
 
-  <binding id="browser-search-autocomplete-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-rich-result-popup">
-    <resources>
-      <stylesheet src="chrome://browser/content/search/searchbarBindings.css"/>
-      <stylesheet src="chrome://browser/skin/searchbar.css"/>
-    </resources>
-    <content ignorekeys="true" level="top" consumeoutsideclicks="never">
-      <xul:hbox anonid="searchbar-engine" xbl:inherits="showonlysettings"
-                class="search-panel-header search-panel-current-engine">
-        <xul:image class="searchbar-engine-image" xbl:inherits="src"/>
-        <xul:label anonid="searchbar-engine-name" flex="1" crop="end"
-                   role="presentation"/>
-      </xul:hbox>
-      <xul:richlistbox anonid="richlistbox" class="autocomplete-richlistbox search-panel-tree" flex="1"/>
-      <xul:vbox anonid="search-one-off-buttons" class="search-one-offs"/>
-    </content>
-    <implementation>
-      <method name="openAutocompletePopup">
-        <parameter name="aInput"/>
-        <parameter name="aElement"/>
-        <body><![CDATA[
-          // initially the panel is hidden
-          // to avoid impacting startup / new window performance
-          aInput.popup.hidden = false;
+    Services.obs.addObserver(this.observer, "browser-search-engine-modified");
+
+    this._initialized = true;
 
-          // this method is defined on the base binding
-          this._openAutocompletePopup(aInput, aElement);
-        ]]></body>
-      </method>
-
-      <method name="onPopupClick">
-        <parameter name="aEvent"/>
-        <body><![CDATA[
-          // Ignore all right-clicks
-          if (aEvent.button == 2)
+    (window.delayedStartupPromise || Promise.resolve()).then(() => {
+      window.requestIdleCallback(() => {
+        Services.search.init(aStatus => {
+          // Bail out if the binding's been destroyed
+          if (!this._initialized)
             return;
 
-          var searchBar = BrowserSearch.searchBar;
-          var popupForSearchBar = searchBar && searchBar.textbox == this.mInput;
-          if (popupForSearchBar) {
-            searchBar.telemetrySearchDetails = {
-              index: this.selectedIndex,
-              kind: "mouse"
-            };
-          }
-
-          // Check for unmodified left-click, and use default behavior
-          if (aEvent.button == 0 && !aEvent.shiftKey && !aEvent.ctrlKey &&
-              !aEvent.altKey && !aEvent.metaKey) {
-            this.input.controller.handleEnter(true, aEvent);
-            return;
+          if (Components.isSuccessCode(aStatus)) {
+            // Refresh the display (updating icon, etc)
+            this.updateDisplay();
+            BrowserSearch.updateOpenSearchBadge();
+          } else {
+            Cu.reportError("Cannot initialize search service, bailing out: " + aStatus);
           }
-
-          // Check for middle-click or modified clicks on the search bar
-          if (popupForSearchBar) {
-            BrowserUsageTelemetry.recordSearchbarSelectedResultMethod(
-              aEvent,
-              this.selectedIndex
-            );
-
-            // Handle search bar popup clicks
-            var search = this.input.controller.getValueAt(this.selectedIndex);
-
-            // open the search results according to the clicking subtlety
-            var where = whereToOpenLink(aEvent, false, true);
-            let params = {};
-
-            // But open ctrl/cmd clicks on autocomplete items in a new background tab.
-            let modifier = AppConstants.platform == "macosx" ?
-                           aEvent.metaKey :
-                           aEvent.ctrlKey;
-            if (where == "tab" && (aEvent instanceof MouseEvent) &&
-                (aEvent.button == 1 || modifier))
-              params.inBackground = true;
-
-            // leave the popup open for background tab loads
-            if (!(where == "tab" && params.inBackground)) {
-              // close the autocomplete popup and revert the entered search term
-              this.closePopup();
-              this.input.controller.handleEscape();
-            }
-
-            searchBar.doSearch(search, where, null, params);
-            if (where == "tab" && params.inBackground)
-              searchBar.focus();
-            else
-              searchBar.value = search;
-          }
-        ]]></body>
-      </method>
-
-      <!-- Popup rollup is triggered by native events before the mousedown event
-           reaches the DOM. The will be set to true by the popuphiding event and
-           false after the mousedown event has been triggered to detect what
-           caused rollup. -->
-      <field name="_isHiding">false</field>
-      <field name="_bundle">null</field>
-      <property name="bundle" readonly="true">
-        <getter>
-          <![CDATA[
-            if (!this._bundle) {
-              const kBundleURI = "chrome://browser/locale/search.properties";
-              this._bundle = Services.strings.createBundle(kBundleURI);
-            }
-            return this._bundle;
-          ]]>
-        </getter>
-      </property>
-
-      <field name="oneOffButtons" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid",
-                                                "search-one-off-buttons");
-      </field>
-
-      <method name="updateHeader">
-        <body><![CDATA[
-          let currentEngine = Services.search.currentEngine;
-          let uri = currentEngine.iconURI;
-          if (uri) {
-            this.setAttribute("src", uri.spec);
-          } else {
-            // If the default has just been changed to a provider without icon,
-            // avoid showing the icon of the previous default provider.
-            this.removeAttribute("src");
-          }
+        });
+      });
+    });
 
-          let headerText = this.bundle.formatStringFromName("searchHeader",
-                                                            [currentEngine.name], 1);
-          document.getAnonymousElementByAttribute(this, "anonid", "searchbar-engine-name")
-                  .setAttribute("value", headerText);
-          document.getAnonymousElementByAttribute(this, "anonid", "searchbar-engine")
-                  .engine = currentEngine;
-        ]]></body>
-      </method>
-
-      <!-- This is called when a one-off is clicked and when "search in new tab"
-           is selected from a one-off context menu. -->
-      <method name="handleOneOffSearch">
-        <parameter name="event"/>
-        <parameter name="engine"/>
-        <parameter name="where"/>
-        <parameter name="params"/>
-        <body><![CDATA[
-          let searchbar = document.getElementById("searchbar");
-          searchbar.handleSearchCommandWhere(event, engine, where, params);
-        ]]></body>
-      </method>
-    </implementation>
-
-    <handlers>
-      <handler event="popupshowing"><![CDATA[
-        // Force the panel to have the width of the searchbar rather than
-        // the width of the textfield.
-        let DOMUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                             .getInterface(Ci.nsIDOMWindowUtils);
-        let textboxRect = DOMUtils.getBoundsWithoutFlushing(this.mInput);
-        let inputRect = DOMUtils.getBoundsWithoutFlushing(this.mInput.inputField);
-
-        // Ensure the panel is wide enough to fit at least 3 engines.
-        let minWidth = Math.max(textboxRect.width,
-                                this.oneOffButtons.buttonWidth * 3);
-        this.style.minWidth = Math.round(minWidth) + "px";
-        // Alignment of the panel with the searchbar is obtained with negative
-        // margins.
-        this.style.marginLeft = (textboxRect.left - inputRect.left) + "px";
-        // This second margin is needed when the direction is reversed,
-        // eg. when using command+shift+X.
-        this.style.marginRight = (inputRect.right - textboxRect.right) + "px";
+    // Wait until the popupshowing event to avoid forcing immediate
+    // attachment of the search-one-offs binding.
+    this.textbox.popup.addEventListener("popupshowing", () => {
+      let oneOffButtons = this.textbox.popup.oneOffButtons;
+      // Some accessibility tests create their own <searchbar> that doesn't
+      // use the popup binding below, so null-check oneOffButtons.
+      if (oneOffButtons) {
+        oneOffButtons.telemetryOrigin = "searchbar";
+        // Set .textbox first, since the popup setter will cause
+        // a _rebuild call that uses it.
+        oneOffButtons.textbox = this.textbox;
+        oneOffButtons.popup = this.textbox.popup;
+      }
+    }, { capturing: true, once: true });
 
-        // First handle deciding if we are showing the reduced version of the
-        // popup containing only the preferences button. We do this if the
-        // glass icon has been clicked if the text field is empty.
-        let searchbar = document.getElementById("searchbar");
-        if (searchbar.hasAttribute("showonlysettings")) {
-          searchbar.removeAttribute("showonlysettings");
-          this.setAttribute("showonlysettings", "true");
-
-          // Setting this with an xbl-inherited attribute gets overridden the
-          // second time the user clicks the glass icon for some reason...
-          this.richlistbox.collapsed = true;
-        } else {
-          this.removeAttribute("showonlysettings");
-          // Uncollapse as long as we have a view which has >= 1 row.
-          // The autocomplete binding itself will take care of uncollapsing later,
-          // if we currently have no rows but end up having some in the future
-          // when the search string changes
-          this.richlistbox.collapsed = (this.matchCount == 0);
-        }
-
-        // Show the current default engine in the top header of the panel.
-        this.updateHeader();
-      ]]></handler>
+    this._setupEventListeners();
+  }
 
-      <handler event="popuphiding"><![CDATA[
-        this._isHiding = true;
-        Services.tm.dispatchToMainThread(() => {
-          this._isHiding = false;
-        });
-      ]]></handler>
+  get engines() {
+    if (!this._engines)
+      this._engines = Services.search.getVisibleEngines();
+    return this._engines;
+  }
 
-      <!-- This handles clicks on the topmost "Foo Search" header in the
-           popup (hbox[anonid="searchbar-engine"]). -->
-      <handler event="click"><![CDATA[
-        if (event.button == 2) {
-          // Ignore right clicks.
-          return;
-        }
-        let button = event.originalTarget;
-        let engine = button.parentNode.engine;
-        if (!engine) {
-          return;
-        }
-        this.oneOffButtons.handleSearchCommand(event, engine);
-      ]]></handler>
-    </handlers>
-
-  </binding>
+  set currentEngine(val) {
+    Services.search.currentEngine = val;
+    return val;
+  }
 
-  <binding id="search-one-offs">
-    <content context="_child">
-      <xul:deck anonid="search-panel-one-offs-header"
-                selectedIndex="0"
-                class="search-panel-header search-panel-current-input">
-        <xul:label anonid="searchbar-oneoffheader-search"
-                   value="&searchWithHeader.label;"/>
-        <xul:hbox anonid="search-panel-searchforwith"
-                  class="search-panel-current-input">
-          <xul:label anonid="searchbar-oneoffheader-before"
-                     value="&searchFor.label;"/>
-          <xul:label anonid="searchbar-oneoffheader-searchtext"
-                     class="search-panel-input-value"
-                     flex="1"
-                     crop="end"/>
-          <xul:label anonid="searchbar-oneoffheader-after"
-                     flex="10000"
-                     value="&searchWith.label;"/>
-        </xul:hbox>
-        <xul:hbox anonid="search-panel-searchonengine"
-                  class="search-panel-current-input">
-          <xul:label anonid="searchbar-oneoffheader-beforeengine"
-                     value="&search.label;"/>
-          <xul:label anonid="searchbar-oneoffheader-engine"
-                     class="search-panel-input-value"
-                     flex="1"
-                     crop="end"/>
-          <xul:label anonid="searchbar-oneoffheader-afterengine"
-                     flex="10000"
-                     value="&searchAfter.label;"/>
-        </xul:hbox>
-      </xul:deck>
-      <xul:description anonid="search-panel-one-offs"
-                       role="group"
-                       class="search-panel-one-offs"
-                       xbl:inherits="compact">
-        <xul:button anonid="search-settings-compact"
-                    oncommand="showSettings();"
-                    class="searchbar-engine-one-off-item search-setting-button-compact"
-                    tooltiptext="&changeSearchSettings.tooltip;"
-                    xbl:inherits="compact"/>
-      </xul:description>
-      <xul:vbox anonid="add-engines" class="search-add-engines"/>
-      <xul:button anonid="search-settings"
-                  oncommand="showSettings();"
-                  class="search-setting-button search-panel-header"
-                  label="&changeSearchSettings.button;"
-                  xbl:inherits="compact"/>
-      <xul:menupopup anonid="search-one-offs-context-menu">
-        <xul:menuitem anonid="search-one-offs-context-open-in-new-tab"
-                      label="&searchInNewTab.label;"
-                      accesskey="&searchInNewTab.accesskey;"/>
-        <xul:menuitem anonid="search-one-offs-context-set-default"
-                      label="&searchSetAsDefault.label;"
-                      accesskey="&searchSetAsDefault.accesskey;"/>
-      </xul:menupopup>
-    </content>
+  get currentEngine() {
+    var currentEngine = Services.search.currentEngine;
+    // Return a dummy engine if there is no currentEngine
+    return currentEngine || { name: "", uri: null };
+  }
+  /**
+   * textbox is used by sanitize.js to clear the undo history when
+   * clearing form information.
+   */
+  get textbox() {
+    return this._textbox;
+  }
 
-    <implementation implements="nsIObserver,nsIWeakReference">
-
-      <!-- Width in pixels of the one-off buttons.  49px is the min-width of
-           each search engine button, adapt this const when changing the css.
-           It's actually 48px + 1px of right border. -->
-      <property name="buttonWidth" readonly="true" onget="return 49;"/>
-
-      <field name="_popup">null</field>
+  set value(val) {
+    return this._textbox.value = val;
+  }
 
-      <!-- The popup that contains the one-offs.  This is required, so it should
-           never be null or undefined, except possibly before the one-offs are
-           used. -->
-      <property name="popup">
-        <getter><![CDATA[
-          return this._popup;
-        ]]></getter>
-        <setter><![CDATA[
-          let events = [
-            "popupshowing",
-            "popuphidden",
-          ];
-          if (this._popup) {
-            for (let event of events) {
-              this._popup.removeEventListener(event, this);
-            }
-          }
-          if (val) {
-            for (let event of events) {
-              val.addEventListener(event, this);
-            }
-          }
-          this._popup = val;
+  get value() {
+    return this._textbox.value;
+  }
+
+  destroy() {
+    if (this._initialized) {
+      this._initialized = false;
 
-          // If the popup is already open, rebuild the one-offs now.  The
-          // popup may be opening, so check that the state is not closed
-          // instead of checking popupOpen.
-          if (val && val.state != "closed") {
-            this._rebuild();
-          }
-          return val;
-        ]]></setter>
-      </property>
+      Services.obs.removeObserver(this.observer, "browser-search-engine-modified");
+    }
 
-      <field name="_textbox">null</field>
-      <field name="_textboxWidth">0</field>
+    this.textContent = "";
 
-      <!-- The textbox associated with the one-offs.  Set this to a textbox to
-           automatically keep the related one-offs UI up to date.  Otherwise you
-           can leave it null/undefined, and in that case you should update the
-           query property manually. -->
-      <property name="textbox">
-        <getter><![CDATA[
-          return this._textbox;
-        ]]></getter>
-        <setter><![CDATA[
-          if (this._textbox) {
-            this._textbox.removeEventListener("input", this);
-          }
-          if (val) {
-            val.addEventListener("input", this);
-          }
-          return this._textbox = val;
-        ]]></setter>
-      </property>
-
-      <!-- Set this to a string that identifies your one-offs consumer.  It'll
-           be appended to telemetry recorded with maybeRecordTelemetry(). -->
-      <field name="telemetryOrigin">""</field>
-
-      <field name="_query">""</field>
+    // Make sure to break the cycle from _textbox to us. Otherwise we leak
+    // the world. But make sure it's actually pointing to us.
+    // Also make sure the textbox has ever been constructed, otherwise the
+    // _textbox getter will cause the textbox constructor to run, add an
+    // observer, and leak the world too.
+    if (this._textboxInitialized && this._textbox.mController.input == this)
+      this._textbox.mController.input = null;
+  }
 
-      <!-- The query string currently shown in the one-offs.  If the textbox
-           property is non-null, then this is automatically updated on
-           input. -->
-      <property name="query">
-        <getter><![CDATA[
-          return this._query;
-        ]]></getter>
-        <setter><![CDATA[
-          this._query = val;
-          if (this.popup && this.popup.popupOpen) {
-            this._updateAfterQueryChanged();
-          }
-          return val;
-        ]]></setter>
-      </property>
+  focus() {
+    this._textbox.focus();
+  }
 
-      <field name="_selectedButton">null</field>
+  select() {
+    this._textbox.select();
+  }
 
-      <!-- The selected one-off, a xul:button, including the add-engine button
-           and the search-settings button.  Null if no one-off is selected. -->
-      <property name="selectedButton">
-        <getter><![CDATA[
-          return this._selectedButton;
-        ]]></getter>
-        <setter><![CDATA[
-          if (val && val.classList.contains("dummy")) {
-            // Never select dummy buttons.
-            val = null;
-          }
-          if (this._selectedButton) {
-            this._selectedButton.removeAttribute("selected");
-          }
-          if (val) {
-            val.setAttribute("selected", "true");
-          }
-          this._selectedButton = val;
-          this._updateStateForButton(null);
-          if (val && !val.engine) {
-            // If the button doesn't have an engine, then clear the popup's
-            // selection to indicate that pressing Return while the button is
-            // selected will do the button's command, not search.
-            this.popup.selectedIndex = -1;
-          }
-          let event = document.createEvent("Events");
-          event.initEvent("SelectedOneOffButtonChanged", true, false);
-          this.dispatchEvent(event);
-          return val;
-        ]]></setter>
-      </property>
+  setIcon(element, uri) {
+    element.setAttribute("src", uri);
+  }
 
-      <!-- The index of the selected one-off, including the add-engine button
-           and the search-settings button.  -1 if no one-off is selected. -->
-      <property name="selectedButtonIndex">
-        <getter><![CDATA[
-          let buttons = this.getSelectableButtons(true);
-          for (let i = 0; i < buttons.length; i++) {
-            if (buttons[i] == this._selectedButton) {
-              return i;
-            }
-          }
-          return -1;
-        ]]></getter>
-        <setter><![CDATA[
-          let buttons = this.getSelectableButtons(true);
-          this.selectedButton = buttons[val];
-          return val;
-        ]]></setter>
-      </property>
+  updateDisplay() {
+    var uri = this.currentEngine.iconURI;
+    this.setIcon(this, uri ? uri.spec : "");
 
-      <property name="compact" readonly="true">
-        <getter><![CDATA[
-          return this.getAttribute("compact") == "true";
-        ]]></getter>
-      </property>
-
-      <field name="buttons" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "search-panel-one-offs");
-      </field>
-      <field name="header" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "search-panel-one-offs-header");
-      </field>
-      <field name="addEngines" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "add-engines");
-      </field>
-      <field name="settingsButton" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "search-settings");
-      </field>
-      <field name="settingsButtonCompact" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "search-settings-compact");
-      </field>
-
-      <field name="_bundle">null</field>
-
-      <property name="bundle" readonly="true">
-        <getter><![CDATA[
-          if (!this._bundle) {
-            const kBundleURI = "chrome://browser/locale/search.properties";
-            this._bundle = Services.strings.createBundle(kBundleURI);
-          }
-          return this._bundle;
-        ]]></getter>
-      </property>
-
-      <!-- When a context menu is opened on a one-off button, this is set to the
-           engine of that button for use with the context menu actions. -->
-      <field name="_contextEngine">null</field>
+    var name = this.currentEngine.name;
+    var text = this._stringBundle.getFormattedString("searchtip", [name]);
+    this._textbox.label = text;
+    this._textbox.tooltipText = text;
+  }
 
-      <constructor><![CDATA[
-        // Prevent popup events from the context menu from reaching the autocomplete
-        // binding (or other listeners).
-        let menu = document.getAnonymousElementByAttribute(this, "anonid", "search-one-offs-context-menu");
-        let listener = aEvent => aEvent.stopPropagation();
-        menu.addEventListener("popupshowing", listener);
-        menu.addEventListener("popuphiding", listener);
-        menu.addEventListener("popupshown", aEvent => {
-          this._ignoreMouseEvents = true;
-          aEvent.stopPropagation();
-        });
-        menu.addEventListener("popuphidden", aEvent => {
-          this._ignoreMouseEvents = false;
-          aEvent.stopPropagation();
-        });
-
-        // Add weak referenced observers to invalidate our cached list of engines.
-        Services.prefs.addObserver("browser.search.hiddenOneOffs", this, true);
-        Services.obs.addObserver(this, "browser-search-engine-modified", true);
-
-        // Rebuild the buttons when the theme changes.  See bug 1357800 for
-        // details.  Summary: On Linux, switching between themes can cause a row
-        // of buttons to disappear.
-        Services.obs.addObserver(this, "lightweight-theme-changed", true);
-      ]]></constructor>
+  updateGoButtonVisibility() {
+    this.querySelector(".search-go-button")
+      .hidden = !this._textbox.value;
+  }
 
-      <!-- This handles events outside the one-off buttons, like on the popup
-           and textbox. -->
-      <method name="handleEvent">
-        <parameter name="event"/>
-        <body><![CDATA[
-          switch (event.type) {
-            case "input":
-              // Allow the consumer's input to override its value property with
-              // a oneOffSearchQuery property.  That way if the value is not
-              // actually what the user typed (e.g., it's autofilled, or it's a
-              // mozaction URI), the consumer has some way of providing it.
-              this.query = event.target.oneOffSearchQuery || event.target.value;
-              break;
-            case "popupshowing":
-              this._rebuild();
-              break;
-            case "popuphidden":
-              Services.tm.dispatchToMainThread(() => {
-                this.selectedButton = null;
-                this._contextEngine = null;
-              });
-              break;
-          }
-        ]]></body>
-      </method>
+  openSuggestionsPanel(aShowOnlySettingsIfEmpty) {
+    if (this._textbox.open)
+      return;
+
+    this._textbox.showHistoryPopup();
 
-      <method name="observe">
-        <parameter name="aEngine"/>
-        <parameter name="aTopic"/>
-        <parameter name="aData"/>
-        <body><![CDATA[
-          // Make sure the engine list is refetched next time it's needed.
-          this._engines = null;
-        ]]></body>
-      </method>
+    if (this._textbox.value) {
+      // showHistoryPopup does a startSearch("") call, ensure the
+      // controller handles the text from the input box instead:
+      this._textbox.mController.handleText();
+    } else if (aShowOnlySettingsIfEmpty) {
+      this.setAttribute("showonlysettings", "true");
+    }
+  }
 
-      <method name="showSettings">
-        <body><![CDATA[
-          BrowserUITelemetry.countSearchSettingsEvent(this.telemetryOrigin);
-
-          openPreferences("paneSearch", {origin: "contentSearch"});
-
-          // If the preference tab was already selected, the panel doesn't
-          // close itself automatically.
-          this.popup.hidePopup();
-        ]]></body>
-      </method>
+  selectEngine(aEvent, isNextEngine) {
+    // Find the new index
+    var newIndex = this.engines.indexOf(this.currentEngine);
+    newIndex += isNextEngine ? 1 : -1;
 
-      <!-- Updates the parts of the UI that show the query string. -->
-      <method name="_updateAfterQueryChanged">
-        <body><![CDATA[
-          let headerSearchText =
-            document.getAnonymousElementByAttribute(this, "anonid",
-                                                    "searchbar-oneoffheader-searchtext");
-          headerSearchText.setAttribute("value", this.query);
-          let groupText;
-          let isOneOffSelected =
-            this.selectedButton &&
-            this.selectedButton.classList.contains("searchbar-engine-one-off-item");
-          // Typing de-selects the settings or opensearch buttons at the bottom
-          // of the search panel, as typing shows the user intends to search.
-          if (this.selectedButton && !isOneOffSelected)
-            this.selectedButton = null;
-          if (this.query) {
-            groupText = headerSearchText.previousSibling.value +
-                        '"' + headerSearchText.value + '"' +
-                        headerSearchText.nextSibling.value;
-            if (!isOneOffSelected)
-              this.header.selectedIndex = 1;
-          } else {
-            let noSearchHeader =
-              document.getAnonymousElementByAttribute(this, "anonid",
-                                                      "searchbar-oneoffheader-search");
-            groupText = noSearchHeader.value;
-            if (!isOneOffSelected)
-              this.header.selectedIndex = 0;
-          }
-          this.buttons.setAttribute("aria-label", groupText);
-        ]]></body>
-      </method>
+    if (newIndex >= 0 && newIndex < this.engines.length) {
+      this.currentEngine = this.engines[newIndex];
+    }
+
+    aEvent.preventDefault();
+    aEvent.stopPropagation();
 
-      <field name="_engines">null</field>
-      <property name="engines" readonly="true">
-        <getter><![CDATA[
-          if (this._engines)
-            return this._engines;
-          let currentEngineNameToIgnore;
-          if (!this.getAttribute("includecurrentengine"))
-            currentEngineNameToIgnore = Services.search.currentEngine.name;
-
-          let pref = Services.prefs.getStringPref("browser.search.hiddenOneOffs");
-          let hiddenList = pref ? pref.split(",") : [];
+    this.openSuggestionsPanel();
+  }
 
-          this._engines = Services.search.getVisibleEngines().filter(e => {
-            let name = e.name;
-            return (!currentEngineNameToIgnore ||
-                    name != currentEngineNameToIgnore) &&
-                   !hiddenList.includes(name);
-          });
-
-          return this._engines;
-        ]]></getter>
-      </property>
-
-      <!-- Builds all the UI. -->
-      <method name="_rebuild">
-        <body><![CDATA[
-          // Update the 'Search for <keywords> with:" header.
-          this._updateAfterQueryChanged();
-
-          // 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.
-          // Skip this in compact mode, ie. for the urlbar.
-          if (!this.compact)
-            this._rebuildAddEngineList();
+  handleSearchCommand(aEvent, aEngine, aForceNewTab) {
+    var where = "current";
+    let params;
 
-          // Check if the one-off buttons really need to be rebuilt.
-          if (this._textbox) {
-            // We can't get a reliable value for the popup width without flushing,
-            // but the popup width won't change if the textbox width doesn't.
-            let DOMUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                                 .getInterface(Ci.nsIDOMWindowUtils);
-            let textboxWidth =
-              DOMUtils.getBoundsWithoutFlushing(this._textbox).width;
-            // We can return early if neither the list of engines nor the panel
-            // width has changed.
-            if (this._engines && this._textboxWidth == textboxWidth) {
-              return;
-            }
-            this._textboxWidth = textboxWidth;
-          }
-
-          // Finally, build the list of one-off buttons.
-          while (this.buttons.firstChild != this.settingsButtonCompact)
-            this.buttons.firstChild.remove();
-          // Remove the trailing empty text node introduced by the binding's
-          // content markup above.
-          if (this.settingsButtonCompact.nextSibling)
-            this.settingsButtonCompact.nextSibling.remove();
-
-          let engines = this.engines;
-          let oneOffCount = engines.length;
-          let collapsed = !oneOffCount ||
-                          (oneOffCount == 1 && engines[0].name == Services.search.currentEngine.name);
-
-          // header is a xul:deck so collapsed doesn't work on it, see bug 589569.
-          this.header.hidden = this.buttons.collapsed = collapsed;
-
-          if (collapsed)
-            return;
-
-          let panelWidth = parseInt(this.popup.clientWidth);
+    // Open ctrl/cmd clicks on one-off buttons in a new background tab.
+    if (aEvent && aEvent.originalTarget.getAttribute("anonid") == "search-go-button") {
+      if (aEvent.button == 2)
+        return;
+      where = whereToOpenLink(aEvent, false, true);
+    } else if (aForceNewTab) {
+      where = "tab";
+      if (Services.prefs.getBoolPref("browser.tabs.loadInBackground"))
+        where += "-background";
+    } else {
+      var newTabPref = Services.prefs.getBoolPref("browser.search.openintab");
+      if (((aEvent instanceof KeyboardEvent) && aEvent.altKey) ^ newTabPref)
+        where = "tab";
+      if ((aEvent instanceof MouseEvent) &&
+        (aEvent.button == 1 || aEvent.getModifierState("Accel"))) {
+        where = "tab";
+        params = {
+          inBackground: true,
+        };
+      }
+    }
 
-          // There's one weird thing to guard against: when layout pixels
-          // aren't an integral multiple of device pixels, the last button
-          // of each row sometimes gets pushed to the next row, depending on the
-          // panel and button widths.
-          // This is likely because the clientWidth getter rounds the value, but
-          // the panel's border width is not an integer.
-          // As a workaround, decrement the width if the scale is not an integer.
-          let scale = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                            .getInterface(Ci.nsIDOMWindowUtils)
-                            .screenPixelsPerCSSPixel;
-          if (Math.floor(scale) != scale) {
-            --panelWidth;
-          }
-
-          // The + 1 is because the last button doesn't have a right border.
-          let enginesPerRow = Math.floor((panelWidth + 1) / this.buttonWidth);
-          let buttonWidth = Math.floor(panelWidth / enginesPerRow);
-          // There will be an emtpy area of:
-          //   panelWidth - enginesPerRow * buttonWidth  px
-          // at the end of each row.
-
-          // If the <description> tag with the list of search engines doesn't have
-          // a fixed height, the panel will be sized incorrectly, causing the bottom
-          // of the suggestion <tree> to be hidden.
-          if (this.compact)
-            ++oneOffCount;
-          let rowCount = Math.ceil(oneOffCount / enginesPerRow);
-          let height = rowCount * 33; // 32px per row, 1px border.
-          this.buttons.setAttribute("height", height + "px");
-
-          // Ensure we can refer to the settings buttons by ID:
-          let origin = this.telemetryOrigin;
-          this.settingsButton.id = origin + "-anon-search-settings";
-          this.settingsButtonCompact.id = origin + "-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;
-            }
-            button.setAttribute("image", uri);
-            button.setAttribute("class", "searchbar-engine-one-off-item");
-            button.setAttribute("tooltiptext", engine.name);
-            button.setAttribute("width", buttonWidth);
-            button.engine = engine;
-
-            if ((i + 1) % enginesPerRow == 0)
-              button.classList.add("last-of-row");
-
-            if (i + 1 == engines.length)
-              button.classList.add("last-engine");
-
-            if (i >= oneOffCount + dummyItems - enginesPerRow)
-              button.classList.add("last-row");
+    this.handleSearchCommandWhere(aEvent, aEngine, where, params);
+  }
 
-            this.buttons.insertBefore(button, this.settingsButtonCompact);
-          }
-
-          let hasDummyItems = !!dummyItems;
-          while (dummyItems) {
-            let button = document.createElementNS(kXULNS, "button");
-            button.setAttribute("class", "searchbar-engine-one-off-item dummy last-row");
-            button.setAttribute("width", buttonWidth);
-
-            if (!--dummyItems)
-              button.classList.add("last-of-row");
-
-            this.buttons.insertBefore(button, this.settingsButtonCompact);
-          }
+  handleSearchCommandWhere(aEvent, aEngine, aWhere, aParams) {
+    var textBox = this._textbox;
+    var textValue = textBox.value;
 
-          if (this.compact) {
-            this.settingsButtonCompact.setAttribute("width", buttonWidth);
-            if (rowCount == 1 && hasDummyItems) {
-              // When there's only one row, make the compact settings button
-              // hug the right edge of the panel.  It may not due to the panel's
-              // width not being an integral multiple of the button width.  (See
-              // the "There will be an emtpy area" comment above.)  Increase the
-              // width of the last dummy item by the remainder.
-              let remainder = panelWidth - (enginesPerRow * buttonWidth);
-              let width = remainder + buttonWidth;
-              let lastDummyItem = this.settingsButtonCompact.previousSibling;
-              lastDummyItem.setAttribute("width", width);
-            }
-          }
-        ]]></body>
-      </method>
+    let selection = this.telemetrySearchDetails;
+    let oneOffRecorded = false;
 
-      <!-- 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 = this.addEngines;
-        while (list.firstChild) {
-          list.firstChild.remove();
-        }
+    BrowserUsageTelemetry.recordSearchbarSelectedResultMethod(
+      aEvent,
+      selection ? selection.index : -1
+    );
 
-        // Add a button for each engine that the page in the selected browser
-        // offers, except 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 (!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, "toolbarbutton");
-          list.appendChild(button);
-          button.classList.add("addengine-item", "badged-button");
-          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);
+    if (!selection || (selection.index == -1)) {
+      oneOffRecorded = this.textbox.popup.oneOffButtons
+        .maybeRecordTelemetry(aEvent, aWhere, aParams);
+      if (!oneOffRecorded) {
+        let source = "unknown";
+        let type = "unknown";
+        let target = aEvent.originalTarget;
+        if (aEvent instanceof KeyboardEvent) {
+          type = "key";
+        } else if (aEvent instanceof MouseEvent) {
+          type = "mouse";
+          if (target.classList.contains("search-panel-header") ||
+            target.parentNode.classList.contains("search-panel-header")) {
+            source = "header";
           }
-
-          // 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();
-            });
+        } else if (aEvent instanceof XULCommandEvent) {
+          if (target.getAttribute("anonid") == "paste-and-search") {
+            source = "paste";
           }
         }
-
-        // 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" : "toolbarbutton";
-        for (let engine of engines) {
-          let button = document.createElementNS(kXULNS, eltType);
-          button.classList.add("addengine-item");
-          if (!tooManyEngines) {
-            button.classList.add("badged-button");
-          }
-          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.title + "\n" + 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);
+        if (!aEngine) {
+          aEngine = this.currentEngine;
         }
-        ]]></body>
-      </method>
-
-      <method name="_buttonIDForEngine">
-        <parameter name="engine"/>
-        <body><![CDATA[
-          return this.telemetryOrigin + "-engine-one-off-item-" +
-                 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>
-      </method>
-
-      <!--
-        Updates the popup and textbox for the currently selected or moused-over
-        button.
-
-        @param mousedOverButton
-               The currently moused-over button, or null if there isn't one.
-      -->
-      <method name="_updateStateForButton">
-        <parameter name="mousedOverButton"/>
-        <body><![CDATA[
-          let button = mousedOverButton;
+        BrowserSearch.recordOneoffSearchInTelemetry(aEngine, source, type,
+          aWhere);
+      }
+    }
 
-          // Ignore dummy buttons.
-          if (button && button.classList.contains("dummy")) {
-            button = null;
-          }
-
-          // If there's no moused-over button, then the one-offs should reflect
-          // the selected button, if any.
-          button = button || this.selectedButton;
-
-          if (!button) {
-            this.header.selectedIndex = this.query ? 1 : 0;
-            if (this.textbox) {
-              this.textbox.removeAttribute("aria-activedescendant");
-            }
-            return;
-          }
-
-          if (button.classList.contains("searchbar-engine-one-off-item") &&
-              button.engine) {
-            let headerEngineText =
-              document.getAnonymousElementByAttribute(this, "anonid",
-                                                      "searchbar-oneoffheader-engine");
-            this.header.selectedIndex = 2;
-            headerEngineText.value = button.engine.name;
-          } else {
-            this.header.selectedIndex = this.query ? 1 : 0;
-          }
-          if (this.textbox) {
-            this.textbox.setAttribute("aria-activedescendant", button.id);
-          }
-        ]]></body>
-      </method>
+    // This is a one-off search only if oneOffRecorded is true.
+    this.doSearch(textValue, aWhere, aEngine, aParams, oneOffRecorded);
 
-      <method name="getSelectableButtons">
-        <parameter name="aIncludeNonEngineButtons"/>
-        <body><![CDATA[
-          let buttons = [];
-          for (let oneOff = this.buttons.firstChild; oneOff; oneOff = oneOff.nextSibling) {
-            // oneOff may be a text node since the list xul:description contains
-            // whitespace and the compact settings button.  See the markup
-            // above.  _rebuild removes text nodes, but it may not have been
-            // called yet (because e.g. the popup hasn't been opened yet).
-            if (oneOff.nodeType == Node.ELEMENT_NODE) {
-              if (oneOff.classList.contains("dummy") ||
-                  oneOff.classList.contains("search-setting-button-compact"))
-                break;
-              buttons.push(oneOff);
-            }
-          }
+    if (aWhere == "tab" && aParams && aParams.inBackground)
+      this.focus();
+  }
 
-          if (aIncludeNonEngineButtons) {
-            for (let addEngine = this.addEngines.firstChild; addEngine; addEngine = addEngine.nextSibling) {
-              buttons.push(addEngine);
-            }
-            buttons.push(this.compact ? this.settingsButtonCompact : this.settingsButton);
-          }
-
-          return buttons;
-        ]]></body>
-      </method>
-
-      <method name="handleSearchCommand">
-        <parameter name="aEvent"/>
-        <parameter name="aEngine"/>
-        <parameter name="aForceNewTab"/>
-        <body><![CDATA[
-          let where = "current";
-          let params;
+  doSearch(aData, aWhere, aEngine, aParams, aOneOff) {
+    var textBox = this._textbox;
 
-          // Open ctrl/cmd clicks on one-off buttons in a new background tab.
-          if (aForceNewTab) {
-            where = "tab";
-            if (Services.prefs.getBoolPref("browser.tabs.loadInBackground")) {
-              params = {
-                inBackground: true,
-              };
-            }
-          } else {
-            var newTabPref = Services.prefs.getBoolPref("browser.search.openintab");
-            if (((aEvent instanceof KeyboardEvent) && aEvent.altKey) ^ newTabPref)
-              where = "tab";
-            if ((aEvent instanceof MouseEvent) &&
-                (aEvent.button == 1 || aEvent.getModifierState("Accel"))) {
-              where = "tab";
-              params = {
-                inBackground: true,
-              };
-            }
-          }
-
-          this.popup.handleOneOffSearch(aEvent, aEngine, where, params);
-        ]]></body>
-      </method>
-
-      <!--
-        Increments or decrements the index of the currently selected one-off.
-
-        @param aForward
-               If true, the index is incremented, and if false, the index is
-               decremented.
-        @param aIncludeNonEngineButtons
-               If true, non-dummy buttons that do not have engines are included.
-               These buttons include the OpenSearch and settings buttons.  For
-               example, if the currently selected button is an engine button,
-               the next button is the settings button, and you pass true for
-               aForward, then passing true for this value would cause the
-               settings to be selected.  Passing false for this value would
-               cause the selection to clear or wrap around, depending on what
-               value you passed for the aWrapAround parameter.
-        @param aWrapAround
-               If true, the selection wraps around between the first and last
-               buttons.
-        @return True if the selection can continue to advance after this method
-                returns and false if not.
-      -->
-      <method name="advanceSelection">
-        <parameter name="aForward"/>
-        <parameter name="aIncludeNonEngineButtons"/>
-        <parameter name="aWrapAround"/>
-        <body><![CDATA[
-          let buttons = this.getSelectableButtons(aIncludeNonEngineButtons);
-          let index;
-          if (this.selectedButton) {
-            let inc = aForward ? 1 : -1;
-            let oldIndex = buttons.indexOf(this.selectedButton);
-            index = ((oldIndex + inc) + buttons.length) % buttons.length;
-            if (!aWrapAround &&
-                ((aForward && index <= oldIndex) ||
-                 (!aForward && oldIndex <= index))) {
-              // The index has wrapped around, but wrapping around isn't
-              // allowed.
-              index = -1;
-            }
-          } else {
-            index = aForward ? 0 : buttons.length - 1;
-          }
-          this.selectedButton = index < 0 ? null : buttons[index];
-        ]]></body>
-      </method>
-
-      <!--
-        This handles key presses specific to the one-off buttons like Tab and
-        Alt+Up/Down, and Up/Down keys within the buttons.  Since one-off buttons
-        are always used in conjunction with a list of some sort (in this.popup),
-        it also handles Up/Down keys that cross the boundaries between list
-        items and the one-off buttons.
-
-        If this method handles the key press, then event.defaultPrevented will
-        be true when it returns.
+    // Save the current value in the form history
+    if (aData && !PrivateBrowsingUtils.isWindowPrivate(window) && this.FormHistory.enabled) {
+      this.FormHistory.update({
+        op: "bump",
+        fieldname: textBox.getAttribute("autocompletesearchparam"),
+        value: aData
+      }, {
+        handleError(aError) {
+          Cu.reportError("Saving search to form history failed: " + aError.message);
+        }
+      });
+    }
 
-        @param event
-               The key event.
-        @param numListItems
-               The number of items in the list.  The reason that this is a
-               parameter at all is that the list may contain items at the end
-               that should be ignored, depending on the consumer.  That's true
-               for the urlbar for example.
-        @param allowEmptySelection
-               Pass true if it's OK that neither the list nor the one-off
-               buttons contains a selection.  Pass false if either the list or
-               the one-off buttons (or both) should always contain a selection.
-        @param textboxUserValue
-               When the last list item is selected and the user presses Down,
-               the first one-off becomes selected and the textbox value is
-               restored to the value that the user typed.  Pass that value here.
-               However, if you pass true for allowEmptySelection, you don't need
-               to pass anything for this parameter.  (Pass undefined or null.)
-      -->
-      <method name="handleKeyPress">
-        <parameter name="event"/>
-        <parameter name="numListItems"/>
-        <parameter name="allowEmptySelection"/>
-        <parameter name="textboxUserValue"/>
-        <body><![CDATA[
-          if (!this.popup) {
-            return;
-          }
-          let handled = this._handleKeyPress(event, numListItems,
-                                             allowEmptySelection,
-                                             textboxUserValue);
-          if (handled) {
-            event.preventDefault();
-            event.stopPropagation();
-          }
-        ]]></body>
-      </method>
+    let engine = aEngine || this.currentEngine;
+    var submission = engine.getSubmission(aData, null, "searchbar");
+    let telemetrySearchDetails = this.telemetrySearchDetails;
+    this.telemetrySearchDetails = null;
+    if (telemetrySearchDetails && telemetrySearchDetails.index == -1) {
+      telemetrySearchDetails = null;
+    }
+    // If we hit here, we come either from a one-off, a plain search or a suggestion.
+    const details = {
+      isOneOff: aOneOff,
+      isSuggestion: (!aOneOff && telemetrySearchDetails),
+      selection: telemetrySearchDetails
+    };
+    BrowserSearch.recordSearchInTelemetry(engine, "searchbar", details);
+    // null parameter below specifies HTML response for search
+    let params = {
+      postData: submission.postData,
+    };
+    if (aParams) {
+      for (let key in aParams) {
+        params[key] = aParams[key];
+      }
+    }
+    openTrustedLinkIn(submission.uri.spec, aWhere, params);
+  }
 
-      <method name="_handleKeyPress">
-        <parameter name="event"/>
-        <parameter name="numListItems"/>
-        <parameter name="allowEmptySelection"/>
-        <parameter name="textboxUserValue"/>
-        <body><![CDATA[
-          if (this.compact && this.buttons.collapsed)
-            return false;
-          if (event.keyCode == KeyEvent.DOM_VK_RIGHT &&
-              this.selectedButton &&
-              this.selectedButton.getAttribute("anonid") ==
-                "addengine-menu-button") {
-            // 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.
-            this.selectedButton.open = true;
-            return true;
-          }
-
-          // Handle the Tab key, but only if non-Shift modifiers aren't also
-          // pressed to avoid clobbering other shortcuts (like the Alt+Tab
-          // browser tab switcher).  The reason this uses getModifierState() and
-          // checks for "AltGraph" is that when you press Shift-Alt-Tab,
-          // event.altKey is actually false for some reason, at least on macOS.
-          // getModifierState("Alt") is also false, but "AltGraph" is true.
-          if (event.keyCode == KeyEvent.DOM_VK_TAB &&
-              !event.getModifierState("Alt") &&
-              !event.getModifierState("AltGraph") &&
-              !event.getModifierState("Control") &&
-              !event.getModifierState("Meta")) {
-            if (this.getAttribute("disabletab") == "true" ||
-                (event.shiftKey &&
-                  this.selectedButtonIndex <= 0) ||
-                (!event.shiftKey &&
-                 this.selectedButtonIndex ==
-                   this.getSelectableButtons(true).length - 1)) {
-              this.selectedButton = null;
-              return false;
-            }
-            this.popup.selectedIndex = -1;
-            this.advanceSelection(!event.shiftKey, true, false);
-            return !!this.selectedButton;
-          }
+  disconnectedCallback() {
+    this.destroy();
+  }
 
-          if (event.keyCode == KeyboardEvent.DOM_VK_UP) {
-            if (event.altKey) {
-              // Keep the currently selected result in the list (if any) as a
-              // secondary "alt" selection and move the selection up within the
-              // buttons.
-              this.advanceSelection(false, false, false);
-              return true;
-            }
-            if (numListItems == 0) {
-              this.advanceSelection(false, true, false);
-              return true;
-            }
-            if (this.popup.selectedIndex > 0) {
-              // Moving up within the list.  The autocomplete controller should
-              // handle this case.  A button may be selected, so null it.
-              this.selectedButton = null;
-              return false;
-            }
-            if (this.popup.selectedIndex == 0) {
-              // Moving up from the top of the list.
-              if (allowEmptySelection) {
-                // Let the autocomplete controller remove selection in the list
-                // and revert the typed text in the textbox.
-                return false;
-              }
-              // Wrap selection around to the last button.
-              if (this.textbox && typeof(textboxUserValue) == "string") {
-                this.textbox.value = textboxUserValue;
-              }
-              this.advanceSelection(false, true, true);
-              return true;
-            }
-            if (!this.selectedButton) {
-              // Moving up from no selection in the list or the buttons, back
-              // down to the last button.
-              this.advanceSelection(false, true, true);
-              return true;
-            }
-            if (this.selectedButtonIndex == 0) {
-              // Moving up from the buttons to the bottom of the list.
-              this.selectedButton = null;
-              return false;
-            }
-            // Moving up/left within the buttons.
-            this.advanceSelection(false, true, false);
-            return true;
-          }
+  _setupEventListeners() {
+    this.addEventListener("command", (event) => {
+      const target = event.originalTarget;
+      if (target.engine) {
+        this.currentEngine = target.engine;
+      } else if (target.classList.contains("addengine-item")) {
+        // Select the installed engine if the installation succeeds
+        var installCallback = {
+          onSuccess: engine => this.currentEngine = engine
+        };
+        Services.search.addEngine(target.getAttribute("uri"), null,
+          target.getAttribute("src"), false,
+          installCallback);
+      } else
+        return;
+
+      this.focus();
+      this.select();
+    });
 
-          if (event.keyCode == KeyboardEvent.DOM_VK_DOWN) {
-            if (event.altKey) {
-              // Keep the currently selected result in the list (if any) as a
-              // secondary "alt" selection and move the selection down within
-              // the buttons.
-              this.advanceSelection(true, false, false);
-              return true;
-            }
-            if (numListItems == 0) {
-              this.advanceSelection(true, true, false);
-              return true;
-            }
-            if (this.popup.selectedIndex >= 0 &&
-                this.popup.selectedIndex < numListItems - 1) {
-              // Moving down within the list.  The autocomplete controller
-              // should handle this case.  A button may be selected, so null it.
-              this.selectedButton = null;
-              return false;
-            }
-            if (this.popup.selectedIndex == numListItems - 1) {
-              // Moving down from the last item in the list to the buttons.
-              this.selectedButtonIndex = 0;
-              if (allowEmptySelection) {
-                // Let the autocomplete controller remove selection in the list
-                // and revert the typed text in the textbox.
-                return false;
-              }
-              if (this.textbox && typeof(textboxUserValue) == "string") {
-                this.textbox.value = textboxUserValue;
-              }
-              this.popup.selectedIndex = -1;
-              return true;
-            }
-            if (this.selectedButton) {
-              let buttons = this.getSelectableButtons(true);
-              if (this.selectedButtonIndex == buttons.length - 1) {
-                // Moving down from the buttons back up to the top of the list.
-                this.selectedButton = null;
-                if (allowEmptySelection) {
-                  // Prevent the selection from wrapping around to the top of
-                  // the list by returning true, since the list currently has no
-                  // selection.  Nothing should be selected after handling this
-                  // Down key.
-                  return true;
-                }
-                return false;
-              }
-              // Moving down/right within the buttons.
-              this.advanceSelection(true, true, false);
-              return true;
-            }
-            return false;
-          }
+    this.addEventListener("DOMMouseScroll", (event) => { this.selectEngine(event, (event.detail > 0)); }, true);
 
-          if (event.keyCode == KeyboardEvent.DOM_VK_LEFT) {
-            if (this.selectedButton &&
-                (this.compact || this.selectedButton.engine)) {
-              // Moving left within the buttons.
-              this.advanceSelection(false, this.compact, true);
-              return true;
-            }
-            return false;
-          }
+    this.addEventListener("input", (event) => { this.updateGoButtonVisibility(); });
+
+    this.addEventListener("drop", (event) => { this.updateGoButtonVisibility(); });
 
-          if (event.keyCode == KeyboardEvent.DOM_VK_RIGHT) {
-            if (this.selectedButton &&
-                (this.compact || this.selectedButton.engine)) {
-              // Moving right within the buttons.
-              this.advanceSelection(true, this.compact, true);
-              return true;
-            }
-            return false;
-          }
-
-          return false;
-        ]]></body>
-      </method>
-
-      <!--
-        If the given event is related to the one-offs, this method records
-        one-off telemetry for it.  this.telemetryOrigin will be appended to the
-        computed source, so make sure you set that first.
+    this.addEventListener("blur", (event) => {
+      // If the input field is still focused then a different window has
+      // received focus, ignore the next focus event.
+      this._ignoreFocus = (document.activeElement == this._textbox.inputField);
+    });
 
-        @param aEvent
-               An event, like a click on a one-off button.
-        @param aOpenUILinkWhere
-               The "where" passed to openUILink.
-        @param aOpenUILinkParams
-               The "params" passed to openUILink.
-        @return True if telemetry was recorded and false if not.
-      -->
-      <method name="maybeRecordTelemetry">
-        <parameter name="aEvent"/>
-        <parameter name="aOpenUILinkWhere"/>
-        <parameter name="aOpenUILinkParams"/>
-        <body><![CDATA[
-          if (!aEvent) {
-            return false;
-          }
-
-          let source = null;
-          let type = "unknown";
-          let engine = null;
-          let target = aEvent.originalTarget;
-
-          if (aEvent instanceof KeyboardEvent) {
-            type = "key";
-            if (this.selectedButton) {
-              source = "oneoff";
-              engine = this.selectedButton.engine;
-            }
-          } else if (aEvent instanceof MouseEvent) {
-            type = "mouse";
-            if (target.classList.contains("searchbar-engine-one-off-item")) {
-              source = "oneoff";
-              engine = target.engine;
-            }
-          } else if ((aEvent instanceof XULCommandEvent) &&
-                     target.getAttribute("anonid") ==
-                       "search-one-offs-context-open-in-new-tab") {
-            source = "oneoff-context";
-            engine = this._contextEngine;
-          }
-
-          if (!source) {
-            return false;
-          }
-
-          if (this.telemetryOrigin) {
-            source += "-" + this.telemetryOrigin;
-          }
+    this.addEventListener("focus", (event) => {
+      // Speculatively connect to the current engine's search URI (and
+      // suggest URI, if different) to reduce request latency
+      this.currentEngine.speculativeConnect({
+        window,
+        originAttributes: gBrowser.contentPrincipal
+          .originAttributes
+      });
 
-          let tabBackground = aOpenUILinkWhere == "tab" &&
-                              aOpenUILinkParams &&
-                              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>
+      if (this._ignoreFocus) {
+        // This window has been re-focused, don't show the suggestions
+        this._ignoreFocus = false;
+        return;
+      }
 
-      <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.getAttribute("anonid") == "addengine-menu-button") {
-          return;
-        }
-        // Required to receive click events from the buttons on Linux.
-        event.preventDefault();
-      ]]></handler>
-
-      <handler event="mousemove"><![CDATA[
-        let target = event.originalTarget;
+      // Don't open the suggestions if there is no text in the textbox.
+      if (!this._textbox.value)
+        return;
 
-        // Handle mouseover on the add-engine menu button and its popup items.
-        if (target.getAttribute("anonid") == "addengine-menu-button" ||
-            (target.localName == "menuitem" &&
-             target.classList.contains("addengine-item"))) {
-          let menuButton = document.getAnonymousElementByAttribute(
-            this, "anonid", "addengine-menu-button"
-          );
-          this._updateStateForButton(menuButton);
-          this._addEngineMenuShouldBeOpen = true;
-          this._resetAddEngineMenuTimeout();
-          return;
-        }
-
-        if (target.localName != "button")
-          return;
-
-        // Ignore mouse events when the context menu is open.
-         if (this._ignoreMouseEvents)
-           return;
+      // Don't open the suggestions if the mouse was used to focus the
+      // textbox, that will be taken care of in the click handler.
+      if (Services.focus.getLastFocusMethod(window) & Services.focus.FLAG_BYMOUSE)
+        return;
 
-        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._updateStateForButton(target);
-        }
-      ]]></handler>
-
-      <handler event="mouseout"><![CDATA[
-
-        let target = event.originalTarget;
+      this.openSuggestionsPanel();
+    });
 
-        // Handle mouseout on the add-engine menu button and its popup items.
-        if (target.getAttribute("anonid") == "addengine-menu-button" ||
-            (target.localName == "menuitem" &&
-             target.classList.contains("addengine-item"))) {
-          this._updateStateForButton(null);
-          this._addEngineMenuShouldBeOpen = false;
-          this._resetAddEngineMenuTimeout();
-          return;
-        }
-
-        if (target.localName != "button") {
-          return;
-        }
+    this.addEventListener("mousedown", (event) => {
+      if (event.originalTarget.getAttribute("anonid") == "searchbar-search-button") {
+        this._clickClosedPopup = this._textbox.popup._isHiding;
+      }
+    }, true);
 
-        // Don't update the mouseover state if the context menu is open.
-        if (this._ignoreMouseEvents)
-          return;
-
-        this._updateStateForButton(null);
-      ]]></handler>
-
-      <handler event="click"><![CDATA[
-        if (event.button == 2)
-          return; // ignore right clicks.
+    this.addEventListener("mousedown", (event) => {
+      // Ignore clicks on the search go button.
+      if (event.originalTarget.getAttribute("anonid") == "search-go-button") {
+        return;
+      }
 
-        let button = event.originalTarget;
-        let engine = button.engine;
-
-        if (!engine)
-          return;
+      let isIconClick = event.originalTarget.getAttribute("anonid") == "searchbar-search-button";
 
-        // Select the clicked button so that consumers can easily tell which
-        // button was acted on.
-        this.selectedButton = button;
-        this.handleSearchCommand(event, engine);
-      ]]></handler>
+      // Ignore clicks on the icon if they were made to close the popup
+      if (isIconClick && this._clickClosedPopup) {
+        return;
+      }
 
-      <handler event="command"><![CDATA[
-        let target = event.originalTarget;
-        if (target.classList.contains("addengine-item")) {
-          // On success, hide the panel and tell event listeners to reshow it to
-          // show the new engine.
-          let installCallback = {
-            onSuccess: engine => {
-              this._rebuild();
-            },
-            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);
-              let brandBundle = document.getElementById("bundle_brand");
-              let brandName = brandBundle.getString("brandShortName");
-              let title = searchBundle.GetStringFromName("error_invalid_engine_title");
-              let text = searchBundle.formatStringFromName("error_duplicate_engine_msg",
-                                                           [brandName, target.getAttribute("uri")], 2);
-              Services.prompt.QueryInterface(Ci.nsIPromptFactory);
-              let prompt = Services.prompt.getPrompt(gBrowser.contentWindow, Ci.nsIPrompt);
-              prompt.QueryInterface(Ci.nsIWritablePropertyBag2);
-              prompt.setPropertyAsBool("allowTabModal", true);
-              prompt.alert(title, text);
-            }
-          };
-          Services.search.addEngine(target.getAttribute("uri"), null,
-                                    target.getAttribute("image"), false,
-                                    installCallback);
-        }
-        let anonid = target.getAttribute("anonid");
-        if (anonid == "search-one-offs-context-open-in-new-tab") {
-          // Select the context-clicked button so that consumers can easily
-          // tell which button was acted on.
-          this.selectedButton = this._buttonForEngine(this._contextEngine);
-          this.handleSearchCommand(event, this._contextEngine, true);
-        }
-        if (anonid == "search-one-offs-context-set-default") {
-          let currentEngine = Services.search.currentEngine;
+      // Open the suggestions whenever clicking on the search icon or if there
+      // is text in the textbox.
+      if (isIconClick || this._textbox.value) {
+        this.openSuggestionsPanel(true);
+      }
+    });
 
-          if (!this.getAttribute("includecurrentengine")) {
-            // Make the target button of the context menu reflect the current
-            // search engine first. Doing this as opposed to rebuilding all the
-            // one-off buttons avoids flicker.
-            let button = this._buttonForEngine(this._contextEngine);
-            button.id = this._buttonIDForEngine(currentEngine);
-            let uri = "chrome://browser/skin/search-engine-placeholder.png";
-            if (currentEngine.iconURI)
-              uri = currentEngine.iconURI.spec;
-            button.setAttribute("image", uri);
-            button.setAttribute("tooltiptext", currentEngine.name);
-            button.engine = currentEngine;
-          }
-
-          Services.search.currentEngine = this._contextEngine;
-        }
-      ]]></handler>
+  }
+}
 
-      <handler event="contextmenu"><![CDATA[
-        let target = event.originalTarget;
-        // Prevent the context menu from appearing except on the one off buttons.
-        if (!target.classList.contains("searchbar-engine-one-off-item") ||
-            target.classList.contains("dummy")) {
-          event.preventDefault();
-          return;
-        }
-        document.getAnonymousElementByAttribute(this, "anonid", "search-one-offs-context-set-default")
-                .setAttribute("disabled", target.engine == Services.search.currentEngine);
-
-        this._contextEngine = target.engine;
-      ]]></handler>
-    </handlers>
-
-  </binding>
-
-</bindings>
+customElements.define("searchbar", MozSearchbar);
+}
\ No newline at end of file
--- a/browser/components/search/jar.mn
+++ b/browser/components/search/jar.mn
@@ -1,9 +1,10 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 browser.jar:
+        content/browser/search/searchbar.js                         (content/searchbar.js)
         content/browser/search/search.xml                           (content/search.xml)
         content/browser/search/searchbarBindings.css                (content/searchbarBindings.css)
         content/browser/search/searchReset.xhtml                    (content/searchReset.xhtml)
         content/browser/search/searchReset.js                       (content/searchReset.js)