Bug 1525101 - Convert browser-search-autocomplete-result-popup into a Custom Element, r=MattN
authorAlexander Surkov <surkov.alexander@gmail.com>
Wed, 06 Mar 2019 21:06:41 +0000
changeset 520587 ebd4899255b6c93864f17ce68eba5b6122987890
parent 520586 49ae69e7af10c707071280555aee53262f0f45d5
child 520588 67bda35b975de19314623a9ee2ff89b3048081f6
push id10862
push userffxbld-merge
push dateMon, 11 Mar 2019 13:01:11 +0000
treeherdermozilla-beta@a2e7f5c935da [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1525101
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1525101 - Convert browser-search-autocomplete-result-popup into a Custom Element, r=MattN Differential Revision: https://phabricator.services.mozilla.com/D20819
browser/base/content/browser.css
browser/base/content/browser.xul
browser/components/enterprisepolicies/tests/browser/browser_policy_search_engine.js
browser/components/search/content/autocomplete-popup.js
browser/components/search/content/search-one-offs.js
browser/components/search/content/search.xml
browser/components/search/jar.mn
browser/components/search/test/browser/browser_oneOffHeader.js
browser/components/search/test/browser/browser_searchbar_keyboard_navigation.js
browser/components/search/test/browser/browser_searchbar_smallpanel_keyboard_navigation.js
browser/components/search/test/browser/browser_tooManyEnginesOffered.js
browser/components/search/test/browser/head.js
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -747,20 +747,16 @@ html|input.urlbar-input {
   text-overflow: initial;
   white-space: initial;
 }
 
 #PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] > .ac-title > label {
   margin-inline-start: 0;
 }
 
-#PopupSearchAutoComplete {
-  -moz-binding: url("chrome://browser/content/search/search.xml#browser-search-autocomplete-result-popup");
-}
-
 #PopupAutoCompleteRichResult {
   -moz-binding: url("chrome://browser/content/urlbarBindings.xml#urlbar-rich-result-popup");
 }
 
 #PopupAutoCompleteRichResult.showSearchSuggestionsNotification {
   transition: height 100ms;
 }
 
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -98,16 +98,17 @@ xmlns="http://www.w3.org/1999/xhtml"
   Services.scriptloader.loadSubScript("chrome://browser/content/browser-development-helpers.js", this);
 #endif
   Services.scriptloader.loadSubScript("chrome://browser/content/browser-media.js", this);
   Services.scriptloader.loadSubScript("chrome://browser/content/browser-pageActions.js", this);
   Services.scriptloader.loadSubScript("chrome://browser/content/browser-plugins.js", this);
   Services.scriptloader.loadSubScript("chrome://browser/content/browser-sidebar.js", this);
   Services.scriptloader.loadSubScript("chrome://browser/content/browser-tabsintitlebar.js", this);
   Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser.js", this);
+  Services.scriptloader.loadSubScript("chrome://browser/content/search/autocomplete-popup.js", this);
   Services.scriptloader.loadSubScript("chrome://browser/content/search/searchbar.js", this);
 
   window.onload = gBrowserInit.onLoad.bind(gBrowserInit);
   window.onunload = gBrowserInit.onUnload.bind(gBrowserInit);
   window.onclose = WindowIsClosing;
 
 #ifdef BROWSER_XHTML
   window.addEventListener("readystatechange", () => {
@@ -242,17 +243,18 @@ xmlns="http://www.w3.org/1999/xhtml"
            role="group"
            noautofocus="true"
            hidden="true"
            overflowpadding="4"
            norolluponanchor="true"
            nomaxresults="true" />
 
     <!-- for search with one-off buttons -->
-    <panel type="autocomplete-richlistbox"
+    <panel is="search-autocomplete-richlistbox-popup"
+           type="autocomplete-richlistbox"
            id="PopupSearchAutoComplete"
            role="group"
            noautofocus="true"
            hidden="true" />
 
     <!-- for url bar autocomplete -->
     <panel type="autocomplete-richlistbox"
            id="PopupAutoCompleteRichResult"
--- a/browser/components/enterprisepolicies/tests/browser/browser_policy_search_engine.js
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_search_engine.js
@@ -25,19 +25,17 @@ async function test_opensearch(shouldWor
   let rootDir = getRootDirectory(gTestPath);
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, rootDir + "opensearch.html");
   let searchPopup = document.getElementById("PopupSearchAutoComplete");
   let promiseSearchPopupShown = BrowserTestUtils.waitForEvent(searchPopup, "popupshown");
   let searchBarButton = searchBar.querySelector(".searchbar-search-button");
 
   searchBarButton.click();
   await promiseSearchPopupShown;
-  let oneOffsContainer = document.getAnonymousElementByAttribute(searchPopup,
-                                                                 "anonid",
-                                                                 "search-one-off-buttons");
+  let oneOffsContainer = searchPopup.searchOneOffsContainer;
   let engineListElement = oneOffsContainer.querySelector(".search-add-engines");
   if (shouldWork) {
     ok(engineListElement.firstElementChild,
        "There should be search engines available to add");
     ok(searchBar.getAttribute("addengines"),
        "Search bar should have addengines attribute");
   } else {
     is(engineListElement.firstElementChild, null,
--- a/browser/components/search/content/autocomplete-popup.js
+++ b/browser/components/search/content/autocomplete-popup.js
@@ -1,213 +1,256 @@
-  <binding id="browser-search-autocomplete-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-rich-result-popup">
-    <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:hbox 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;
+/* This Source Code Form is subject to the terms of the Mozilla Public
+  * License, v. 2.0. If a copy of the MPL was not distributed with this
+  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Wrap in a block to prevent leaking to window scope.
+{
+/**
+ * A richlistbox popup custom element for for a browser search autocomplete
+ * widget.
+ */
+class MozSearchAutocompleteRichlistboxPopup extends MozElements.MozAutocompleteRichlistboxPopup {
+  constructor() {
+    super();
+
+    this.addEventListener("popupshowing", (event) => {
+      // Force the panel to have the width of the searchbar rather than
+      // the width of the textfield.
+      let DOMUtils = window.windowUtils;
+      let textboxRect = DOMUtils.getBoundsWithoutFlushing(this.mInput);
+      let inputRect = DOMUtils.getBoundsWithoutFlushing(this.mInput.inputField);
 
-          // 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)
-            return;
+      // 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";
 
-          let searchBar = BrowserSearch.searchBar;
-          let popupForSearchBar = searchBar && searchBar.textbox == this.mInput;
-          if (popupForSearchBar) {
-            searchBar.telemetrySearchDetails = {
-              index: this.selectedIndex,
-              kind: "mouse",
-            };
-          }
+      // 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");
 
-          // 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;
-          }
+        // 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();
+    });
 
-          // Check for middle-click or modified clicks on the search bar
-          if (popupForSearchBar) {
-            BrowserUsageTelemetry.recordSearchbarSelectedResultMethod(
-              aEvent,
-              this.selectedIndex
-            );
-
-            // Handle search bar popup clicks
-            let search = this.input.controller.getValueAt(this.selectedIndex);
+    this.addEventListener("popuphiding", (event) => {
+      this._isHiding = true;
+      Services.tm.dispatchToMainThread(() => {
+        this._isHiding = false;
+      });
+    });
 
-            // open the search results according to the clicking subtlety
-            let where = whereToOpenLink(aEvent, false, true);
-            let params = {};
+    /**
+     * This handles clicks on the topmost "Foo Search" header in the
+     * popup (hbox.search-panel-header]).
+     */
+    this.addEventListener("click", (event) => {
+      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);
+    });
 
-            // 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;
+    /**
+     * 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.
+     */
+    this._isHiding = false;
+
+    this._bundle = null;
+  }
 
-            // 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();
-            }
+  static get inheritedAttributes() {
+    return {
+      ".search-panel-current-engine": "showonlysettings",
+      ".searchbar-engine-image": "src",
+    };
+  }
 
-            searchBar.doSearch(search, where, null, params);
-            if (where == "tab" && params.inBackground)
-              searchBar.focus();
-            else
-              searchBar.value = search;
-          }
-        ]]></body>
-      </method>
+  initialize() {
+    super.initialize();
+    this.initializeAttributeInheritance();
+
+    this._searchOneOffsContainer = this.querySelector(".search-one-offs");
+    this._searchbarEngine = this.querySelector(".search-panel-header");
+    this._searchbarEngineName = this.querySelector(".searchbar-engine-name");
+    this._oneOffButtons = new SearchOneOffs(this._searchOneOffsContainer);
+  }
 
-      <!-- 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>
+  get oneOffButtons() {
+    if (!this._oneOffButtons) {
+      this.initialize();
+    }
+    return this._oneOffButtons;
+  }
+
+  get _markup() {
+    return `
+      <hbox class="search-panel-header search-panel-current-engine">
+        <image class="searchbar-engine-image"></image>
+        <label class="searchbar-engine-name" flex="1" crop="end" role="presentation"></label>
+      </hbox>
+      <richlistbox class="autocomplete-richlistbox search-panel-tree" flex="1"></richlistbox>
+      <hbox class="search-one-offs"></hbox>
+    `;
+  }
 
-      <field name="oneOffButtons" readonly="true">
-        new SearchOneOffs(
-          document.getAnonymousElementByAttribute(this, "anonid",
-                                                  "search-one-off-buttons"));
-      </field>
+  get searchOneOffsContainer() {
+    if (!this._searchOneOffsContainer) {
+      this.initialize();
+    }
+    return this._searchOneOffsContainer;
+  }
+
+  get searchbarEngine() {
+    if (!this._searchbarEngine) {
+      this.initialize();
+    }
+    return this._searchbarEngine;
+  }
 
-      <method name="updateHeader">
-        <body><![CDATA[
-          Services.search.getDefault().then(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");
-            }
+  get searchbarEngineName() {
+    if (!this._searchbarEngineName) {
+      this.initialize();
+    }
+    return this._searchbarEngineName;
+  }
+
+  get bundle() {
+    if (!this._bundle) {
+      const kBundleURI = "chrome://browser/locale/search.properties";
+      this._bundle = Services.strings.createBundle(kBundleURI);
+    }
+    return this._bundle;
+  }
 
-            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>
+  openAutocompletePopup(aInput, aElement) {
+    // initially the panel is hidden
+    // to avoid impacting startup / new window performance
+    aInput.popup.hidden = false;
+
+    // this method is defined on the base binding
+    this._openAutocompletePopup(aInput, aElement);
+  }
+
+  onPopupClick(aEvent) {
+    // Ignore all right-clicks
+    if (aEvent.button == 2)
+      return;
 
-      <!-- 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>
+    let searchBar = BrowserSearch.searchBar;
+    let popupForSearchBar = searchBar && searchBar.textbox == this.mInput;
+    if (popupForSearchBar) {
+      searchBar.telemetrySearchDetails = {
+        index: this.selectedIndex,
+        kind: "mouse",
+      };
+    }
 
-    <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.windowUtils;
-        let textboxRect = DOMUtils.getBoundsWithoutFlushing(this.mInput);
-        let inputRect = DOMUtils.getBoundsWithoutFlushing(this.mInput.inputField);
+    // 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;
+    }
 
-        // 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";
+    // Check for middle-click or modified clicks on the search bar
+    if (popupForSearchBar) {
+      BrowserUsageTelemetry.recordSearchbarSelectedResultMethod(
+        aEvent,
+        this.selectedIndex
+      );
+
+      // Handle search bar popup clicks
+      let search = this.input.controller.getValueAt(this.selectedIndex);
+
+      // open the search results according to the clicking subtlety
+      let where = whereToOpenLink(aEvent, false, true);
+      let params = {};
 
-        // 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");
+      // 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();
+      }
 
-          // 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);
-        }
+      searchBar.doSearch(search, where, null, params);
+      if (where == "tab" && params.inBackground)
+        searchBar.focus();
+      else
+        searchBar.value = search;
+    }
+  }
 
-        // Show the current default engine in the top header of the panel.
-        this.updateHeader();
-      ]]></handler>
+  updateHeader() {
+    Services.search.getDefault().then(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");
+      }
 
-      <handler event="popuphiding"><![CDATA[
-        this._isHiding = true;
-        Services.tm.dispatchToMainThread(() => {
-          this._isHiding = false;
-        });
-      ]]></handler>
+      let headerText = this.bundle.formatStringFromName("searchHeader", [currentEngine.name], 1);
+      this.searchbarEngineName.setAttribute("value", headerText);
+      this.searchbarEngine.engine = currentEngine;
+    });
+  }
 
-      <!-- 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>
+  /**
+   * This is called when a one-off is clicked and when "search in new tab"
+   * is selected from a one-off context menu.
+   */
+  /* eslint-disable-next-line valid-jsdoc */
+  handleOneOffSearch(event, engine, where, params) {
+    let searchbar = document.getElementById("searchbar");
+    searchbar.handleSearchCommandWhere(event, engine, where, params);
+  }
+}
 
-  </binding>
+customElements.define("search-autocomplete-richlistbox-popup", MozSearchAutocompleteRichlistboxPopup, {
+  extends: "panel",
+});
+}
--- a/browser/components/search/content/search-one-offs.js
+++ b/browser/components/search/content/search-one-offs.js
@@ -674,18 +674,18 @@ class SearchOneOffs {
     return this.telemetryOrigin + "-engine-one-off-item-" + this._fixUpEngineNameForID(engine.name);
   }
 
   _fixUpEngineNameForID(name) {
     return name.replace(/ /g, "-");
   }
 
   _buttonForEngine(engine) {
-    return this._popup &&
-      document.getAnonymousElementByAttribute(this._popup, "id", this._buttonIDForEngine(engine));
+    let id = this._buttonIDForEngine(engine);
+    return this._popup && document.getElementById(id);
   }
 
   /**
    * Updates the popup and textbox for the currently selected or moused-over
    * button.
    *
    * @param {DOMElement} mousedOverButton
    *        The currently moused-over button, or null if there isn't one.
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -360,224 +360,9 @@
           this.value = data;
           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">
-    <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:hbox 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;
-
-          // 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)
-            return;
-
-          let searchBar = BrowserSearch.searchBar;
-          let 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;
-          }
-
-          // Check for middle-click or modified clicks on the search bar
-          if (popupForSearchBar) {
-            BrowserUsageTelemetry.recordSearchbarSelectedResultMethod(
-              aEvent,
-              this.selectedIndex
-            );
-
-            // Handle search bar popup clicks
-            let search = this.input.controller.getValueAt(this.selectedIndex);
-
-            // open the search results according to the clicking subtlety
-            let 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">
-        new SearchOneOffs(
-          document.getAnonymousElementByAttribute(this, "anonid",
-                                                  "search-one-off-buttons"));
-      </field>
-
-      <method name="updateHeader">
-        <body><![CDATA[
-          Services.search.getDefault().then(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.windowUtils;
-        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";
-
-        // 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>
-
-      <handler event="popuphiding"><![CDATA[
-        this._isHiding = true;
-        Services.tm.dispatchToMainThread(() => {
-          this._isHiding = false;
-        });
-      ]]></handler>
-
-      <!-- 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>
-
 </bindings>
--- a/browser/components/search/jar.mn
+++ b/browser/components/search/jar.mn
@@ -1,12 +1,13 @@
 # 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/search.xml                           (content/search.xml)
+        content/browser/search/autocomplete-popup.js                (content/autocomplete-popup.js)
         content/browser/search/searchbar.js                         (content/searchbar.js)
         content/browser/search/search-one-offs.js                   (content/search-one-offs.js)
 
         searchplugins/                                              (searchplugins/**)
 
 % resource search-plugins %searchplugins/ contentaccessible=yes
--- a/browser/components/search/test/browser/browser_oneOffHeader.js
+++ b/browser/components/search/test/browser/browser_oneOffHeader.js
@@ -2,19 +2,17 @@
  * 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/. */
 // Tests that keyboard navigation in the search panel works as designed.
 
 const isMac = ("nsILocalFileMac" in Ci);
 
 const searchPopup = document.getElementById("PopupSearchAutoComplete");
 
-const oneOffsContainer =
-  document.getAnonymousElementByAttribute(searchPopup, "anonid",
-                                          "search-one-off-buttons");
+const oneOffsContainer = searchPopup.searchOneOffsContainer;
 const searchSettings = oneOffsContainer.querySelector(".search-setting-button");
 
 var header = oneOffsContainer.querySelector(".search-panel-one-offs-header");
 
 function getHeaderText() {
   let headerChild = header.selectedPanel;
   while (headerChild.hasChildNodes()) {
     headerChild = headerChild.firstElementChild;
@@ -117,20 +115,17 @@ add_task(async function test_text() {
   await synthesizeNativeMouseMove(searchSettings);
   is(header.getAttribute("selectedIndex"), 1,
      "Header has the correct index selected when search terms have been entered and the Change Search Settings button is selected.");
   is(getHeaderText(), "Search for foo with:",
      "Header has the correct text when search terms have been entered and the Change Search Settings button is selected.");
 
   // Click the "Foo Search" header at the top of the popup and make sure it
   // loads the search results.
-  let searchbarEngine =
-    document.getAnonymousElementByAttribute(searchPopup, "anonid",
-                                            "searchbar-engine");
-
+  let searchbarEngine = searchPopup.searchbarEngine;
   await synthesizeNativeMouseMove(searchbarEngine);
   SimpleTest.executeSoon(() => {
     EventUtils.synthesizeMouseAtCenter(searchbarEngine, {});
   });
 
   let url = (await Services.search.getDefault()).getSubmission(searchbar.textbox.value).uri.spec;
   await promiseTabLoadEvent(gBrowser.selectedTab, url);
 
--- a/browser/components/search/test/browser/browser_searchbar_keyboard_navigation.js
+++ b/browser/components/search/test/browser/browser_searchbar_keyboard_navigation.js
@@ -1,14 +1,12 @@
 // Tests that keyboard navigation in the search panel works as designed.
 
 const searchPopup = document.getElementById("PopupSearchAutoComplete");
-const oneOffsContainer =
-  document.getAnonymousElementByAttribute(searchPopup, "anonid",
-                                          "search-one-off-buttons");
+const oneOffsContainer = searchPopup.searchOneOffsContainer;
 
 const kValues = ["foo1", "foo2", "foo3"];
 const kUserValue = "foo";
 
 function getOpenSearchItems() {
   let os = [];
 
   let addEngineList =
--- a/browser/components/search/test/browser/browser_searchbar_smallpanel_keyboard_navigation.js
+++ b/browser/components/search/test/browser/browser_searchbar_smallpanel_keyboard_navigation.js
@@ -1,14 +1,12 @@
 // Tests that keyboard navigation in the search panel works as designed.
 
 const searchPopup = document.getElementById("PopupSearchAutoComplete");
-const oneOffsContainer =
-  document.getAnonymousElementByAttribute(searchPopup, "anonid",
-                                          "search-one-off-buttons");
+const oneOffsContainer = searchPopup.searchOneOffsContainer;
 
 const kValues = ["foo1", "foo2", "foo3"];
 
 function getOpenSearchItems() {
   let os = [];
 
   let addEngineList =
     oneOffsContainer.querySelector(".search-add-engines");
--- a/browser/components/search/test/browser/browser_tooManyEnginesOffered.js
+++ b/browser/components/search/test/browser/browser_tooManyEnginesOffered.js
@@ -1,18 +1,16 @@
 "use strict";
 
 // This test makes sure that when a page offers many search engines, the search
 // popup shows a submenu that lists them instead of showing them in the popup
 // itself.
 
 const searchPopup = document.getElementById("PopupSearchAutoComplete");
-const oneOffsContainer =
-  document.getAnonymousElementByAttribute(searchPopup, "anonid",
-                                          "search-one-off-buttons");
+const oneOffsContainer = searchPopup.searchOneOffsContainer;
 
 add_task(async function test_setup() {
   await gCUITestUtils.addSearchBar();
   registerCleanupFunction(() => {
     gCUITestUtils.removeSearchBar();
   });
 });
 
--- a/browser/components/search/test/browser/head.js
+++ b/browser/components/search/test/browser/head.js
@@ -170,19 +170,17 @@ function promiseTabLoadEvent(tab, url) {
 
   return loaded;
 }
 
 // Get an array of the one-off buttons.
 function getOneOffs() {
   let oneOffs = [];
   let searchPopup = document.getElementById("PopupSearchAutoComplete");
-  let oneOffsContainer =
-    document.getAnonymousElementByAttribute(searchPopup, "anonid",
-                                            "search-one-off-buttons");
+  let oneOffsContainer = searchPopup.searchOneOffsContainer;
   let oneOff =
     oneOffsContainer.querySelector(".search-panel-one-offs");
   for (oneOff = oneOff.firstChild; oneOff; oneOff = oneOff.nextSibling) {
     if (oneOff.nodeType == Node.ELEMENT_NODE) {
       if (oneOff.classList.contains("dummy") ||
           oneOff.classList.contains("search-setting-button-compact"))
         break;
       oneOffs.push(oneOff);