Backed out changeset 10a5af6d30df (bug 1506261) for causing window leaks on OSX debug. CLOSED TREE
authorCosmin Sabou <csabou@mozilla.com>
Thu, 15 Nov 2018 10:53:29 +0200
changeset 446550 d76f007bee63b3730068f1a5549bd4d6513593c7
parent 446549 eff95fc19f19f108a7554f74e456f9fd7664541a
child 446551 ba1aae6c2949a2c1b9cbb0e4aad403c5abe2bb69
push id35043
push userebalazs@mozilla.com
push dateThu, 15 Nov 2018 16:12:36 +0000
treeherdermozilla-central@59026ada59bd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1506261
milestone65.0a1
backs out10a5af6d30df698b0f03cd62c0948d26089cf2af
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
Backed out changeset 10a5af6d30df (bug 1506261) for causing window leaks on OSX debug. CLOSED TREE
browser/base/content/browser.xul
browser/base/content/global-scripts.inc
browser/base/content/test/urlbar/browser_urlbarOneOffs.js
browser/base/content/test/urlbar/browser_urlbarSearchFunction.js
browser/base/content/urlbarBindings.xml
browser/components/search/content/search-one-offs.js
browser/components/search/content/search.xml
browser/components/search/test/browser_oneOffContextMenu.js
browser/components/search/test/browser_oneOffContextMenu_setDefault.js
browser/components/urlbar/UrlbarView.jsm
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -84,17 +84,16 @@
 
 <script type="application/javascript"
 #ifdef BROWSER_XHTML
 xmlns="http://www.w3.org/1999/xhtml"
 #endif
 >
   Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", this);
   Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser.js", this);
-  Services.scriptloader.loadSubScript("chrome://browser/content/search/search-one-offs.js", this);
 
   window.onload = gBrowserInit.onLoad.bind(gBrowserInit);
   window.onunload = gBrowserInit.onUnload.bind(gBrowserInit);
   window.onclose = WindowIsClosing;
 
 #ifdef BROWSER_XHTML
   window.addEventListener("readystatechange", () => {
     // We initially hide the window to prevent layouts during parse. This lets us
@@ -256,24 +255,24 @@ xmlns="http://www.w3.org/1999/xhtml"
            flip="none"
            level="parent">
       <html:div class="urlbarView-body-outer">
         <html:div class="urlbarView-body-inner">
           <!-- TODO: add search suggestions notification -->
           <html:div class="urlbarView-results"/>
         </html:div>
       </html:div>
-      <hbox class="search-one-offs"
-            compact="true"
-            includecurrentengine="true"
-            disabletab="true"/>
+      <search-one-offs class="search-one-offs"
+                       compact="true"
+                       includecurrentengine="true"
+                       disabletab="true"/>
     </panel>
 
-    <!-- for date/time picker. consumeoutsideclicks is set to never, so that
-         clicks on the anchored input box are never consumed. -->
+   <!-- for date/time picker. consumeoutsideclicks is set to never, so that
+        clicks on the anchored input box are never consumed. -->
     <panel id="DateTimePickerPanel"
            type="arrow"
            hidden="true"
            orient="vertical"
            noautofocus="true"
            norolluponanchor="true"
            consumeoutsideclicks="never"
            level="parent"
--- a/browser/base/content/global-scripts.inc
+++ b/browser/base/content/global-scripts.inc
@@ -12,16 +12,17 @@
 xmlns="http://www.w3.org/1999/xhtml"
 #endif
 >
 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/search/search-one-offs.js",
 
   "chrome://browser/content/browser-captivePortal.js",
   "chrome://browser/content/browser-compacttheme.js",
   "chrome://browser/content/browser-contentblocking.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/base/content/test/urlbar/browser_urlbarOneOffs.js
+++ b/browser/base/content/test/urlbar/browser_urlbarOneOffs.js
@@ -255,27 +255,27 @@ add_task(async function collapsedOneOffs
 // The one-offs should be hidden when searching with an "@engine" search engine
 // alias.
 add_task(async function hiddenWhenUsingSearchAlias() {
   let typedValue = "@example";
   await promiseAutocompleteResultPopup(typedValue, window, true);
   await waitForAutocompleteResultAt(0);
   Assert.equal(gURLBar.popup.oneOffSearchesEnabled, false);
   Assert.equal(
-    window.getComputedStyle(gURLBar.popup.oneOffSearchButtons.container).display,
+    window.getComputedStyle(gURLBar.popup.oneOffSearchButtons).display,
     "none"
   );
   await hidePopup();
 
   typedValue = "not an engine alias";
   await promiseAutocompleteResultPopup(typedValue, window, true);
   await waitForAutocompleteResultAt(0);
   Assert.equal(gURLBar.popup.oneOffSearchesEnabled, true);
   Assert.equal(
-    window.getComputedStyle(gURLBar.popup.oneOffSearchButtons.container).display,
+    window.getComputedStyle(gURLBar.popup.oneOffSearchButtons).display,
     "-moz-box"
   );
   await hidePopup();
 });
 
 
 function assertState(result, oneOff, textValue = undefined) {
   Assert.equal(gURLBar.popup.selectedIndex, result,
--- a/browser/base/content/test/urlbar/browser_urlbarSearchFunction.js
+++ b/browser/base/content/test/urlbar/browser_urlbarSearchFunction.js
@@ -122,17 +122,17 @@ function assertSearchSuggestionsNotifica
  * Asserts that the one-off search buttons are or aren't visible.
  *
  * @param visible
  *        True if they should be visible, false if not.
  */
 function assertOneOffButtonsVisible(visible) {
   Assert.equal(gURLBar.popup.oneOffSearchesEnabled, visible);
   Assert.equal(
-    window.getComputedStyle(gURLBar.popup.oneOffSearchButtons.container).display,
+    window.getComputedStyle(gURLBar.popup.oneOffSearchButtons).display,
     visible ? "-moz-box" : "none"
   );
 }
 
 /**
  * Asserts that the urlbar's input value is the given value.  Also asserts that
  * the first (heuristic) result in the popup is a search suggestion whose search
  * query is the given value.
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -1868,22 +1868,22 @@ file, You can obtain one at http://mozil
                      onclick="openPreferences('paneSearch', {origin: 'searchChangeSettings'});"
                      control="search-suggestions-change-settings"/>
         </xul:hbox>
       </xul:deck>
       <xul:richlistbox anonid="richlistbox" class="autocomplete-richlistbox"
                        flex="1"/>
       <xul:hbox anonid="footer">
         <children/>
-        <xul:hbox anonid="one-off-search-buttons"
-                  class="search-one-offs"
-                  compact="true"
-                  includecurrentengine="true"
-                  disabletab="true"
-                  flex="1"/>
+        <xul:search-one-offs anonid="one-off-search-buttons"
+                             class="search-one-offs"
+                             compact="true"
+                             includecurrentengine="true"
+                             disabletab="true"
+                             flex="1"/>
       </xul:hbox>
     </content>
 
     <implementation>
       <!--
         For performance reasons we want to limit the size of the text runs we
         build and show to the user.
       -->
@@ -1907,19 +1907,18 @@ file, You can obtain one at http://mozil
         document.getAnonymousElementByAttribute(this, "anonid", "footer");
       </field>
 
       <field name="shrinkDelay" readonly="true">
         250
       </field>
 
       <field name="oneOffSearchButtons" readonly="true">
-        new window.SearchOneOffs(
-          document.getAnonymousElementByAttribute(this, "anonid",
-                                                  "one-off-search-buttons"));
+        document.getAnonymousElementByAttribute(this, "anonid",
+                                                "one-off-search-buttons");
       </field>
 
       <field name="_overrideValue">null</field>
       <property name="overrideValue"
                 onget="return this._overrideValue;"
                 onset="this._overrideValue = val; return val;"/>
 
       <method name="onPopupClick">
--- a/browser/components/search/content/search-one-offs.js
+++ b/browser/components/search/content/search-one-offs.js
@@ -1,46 +1,230 @@
 /* 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/. */
 
-/* eslint-env mozilla/browser-window */
+"use strict";
 
-"use strict";
+/* eslint-env mozilla/browser-window */
 
 {
 
-class SearchOneOffs {
-  constructor(container) {
-    this.container = container;
+let sharedFragment;
+function getFragment() {
+  if (!sharedFragment) {
+    sharedFragment = MozXULElement.parseXULToFragment(`
+    <deck class="search-panel-one-offs-header search-panel-header search-panel-current-input">
+      <label class="searchbar-oneoffheader-search" value="&searchWithHeader.label;"></label>
+      <hbox class="search-panel-searchforwith search-panel-current-input">
+        <label value="&searchFor.label;"></label>
+        <label class="searchbar-oneoffheader-searchtext search-panel-input-value" flex="1" crop="end"></label>
+        <label flex="10000" value="&searchWith.label;"></label>
+      </hbox>
+      <hbox class="search-panel-searchonengine search-panel-current-input">
+        <label value="&search.label;"></label>
+        <label class="searchbar-oneoffheader-engine search-panel-input-value" flex="1" crop="end"></label>
+        <label flex="10000" value="&searchAfter.label;"></label>
+      </hbox>
+    </deck>
+    <description role="group" class="search-panel-one-offs">
+      <button oncommand="showSettings();" class="searchbar-engine-one-off-item search-setting-button-compact" tooltiptext="&changeSearchSettings.tooltip;"></button>
+    </description>
+    <vbox class="search-add-engines"></vbox>
+    <button oncommand="showSettings();" class="search-setting-button search-panel-header" label="&changeSearchSettings.button;"></button>
+    <menupopup class="search-one-offs-context-menu">
+      <menuitem class="search-one-offs-context-open-in-new-tab" label="&searchInNewTab.label;" accesskey="&searchInNewTab.accesskey;"></menuitem>
+      <menuitem class="search-one-offs-context-set-default" label="&searchSetAsDefault.label;" accesskey="&searchSetAsDefault.accesskey;"></menuitem>
+    </menupopup>
+    `, ["chrome://browser/locale/browser.dtd"]);
+  }
+
+  return document.importNode(sharedFragment, true);
+}
+
+class MozSearchOneOffs extends MozXULElement {
+  constructor() {
+    super();
+
+    this.addEventListener("mousedown", event => {
+      let target = event.originalTarget;
+      if (target.classList.contains("addengine-menu-button")) {
+        return;
+      }
+      // Required to receive click events from the buttons on Linux.
+      event.preventDefault();
+    });
+
+    this.addEventListener("mousemove", event => {
+      let target = event.originalTarget;
+
+      // Handle mouseover on the add-engine menu button and its popup items.
+      if ((target.localName == "menuitem" && target.classList.contains("addengine-item")) ||
+          target.classList.contains("addengine-menu-button")) {
+        let menuButton = this.querySelector(".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;
+      }
+
+      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);
+      }
+    });
+
+    this.addEventListener("mouseout", event => {
+      let target = event.originalTarget;
+
+      // Handle mouseout on the add-engine menu button and its popup items.
+      if ((target.localName == "menuitem" && target.classList.contains("addengine-item")) ||
+          target.classList.contains("addengine-menu-button")) {
+        this._updateStateForButton(null);
+        this._addEngineMenuShouldBeOpen = false;
+        this._resetAddEngineMenuTimeout();
+        return;
+      }
+
+      if (target.localName != "button") {
+        return;
+      }
+
+      // Don't update the mouseover state if the context menu is open.
+      if (this._ignoreMouseEvents) {
+        return;
+      }
+
+      this._updateStateForButton(null);
+    });
+
+    this.addEventListener("click", event => {
+      if (event.button == 2) {
+        return; // ignore right clicks.
+      }
 
-    this.container.appendChild(MozXULElement.parseXULToFragment(`
-      <deck class="search-panel-one-offs-header search-panel-header search-panel-current-input">
-        <label class="searchbar-oneoffheader-search" value="&searchWithHeader.label;"/>
-        <hbox class="search-panel-searchforwith search-panel-current-input">
-          <label value="&searchFor.label;"/>
-          <label class="searchbar-oneoffheader-searchtext search-panel-input-value" flex="1" crop="end"/>
-          <label flex="10000" value="&searchWith.label;"/>
-        </hbox>
-        <hbox class="search-panel-searchonengine search-panel-current-input">
-          <label value="&search.label;"/>
-          <label class="searchbar-oneoffheader-engine search-panel-input-value" flex="1" crop="end"/>
-          <label flex="10000" value="&searchAfter.label;"/>
-        </hbox>
-      </deck>
-      <description role="group" class="search-panel-one-offs">
-        <button class="searchbar-engine-one-off-item search-setting-button-compact" tooltiptext="&changeSearchSettings.tooltip;"/>
-      </description>
-      <vbox class="search-add-engines"/>
-      <button class="search-setting-button search-panel-header" label="&changeSearchSettings.button;"/>
-      <menupopup class="search-one-offs-context-menu">
-        <menuitem class="search-one-offs-context-open-in-new-tab" label="&searchInNewTab.label;" accesskey="&searchInNewTab.accesskey;"/>
-        <menuitem class="search-one-offs-context-set-default" label="&searchSetAsDefault.label;" accesskey="&searchSetAsDefault.accesskey;"/>
-      </menupopup>
-      `, ["chrome://browser/locale/browser.dtd"]));
+      let button = event.originalTarget;
+      let engine = button.engine;
+
+      if (!engine) {
+        return;
+      }
+
+      // Select the clicked button so that consumers can easily tell which
+      // button was acted on.
+      this.selectedButton = button;
+      this.handleSearchCommand(event, engine);
+    });
+
+    this.addEventListener("command", event => {
+      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"),
+                                  target.getAttribute("image"), false,
+                                  installCallback);
+      }
+      if (target.classList.contains("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 (target.classList.contains("search-one-offs-context-set-default")) {
+        let currentEngine = Services.search.defaultEngine;
+
+        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.defaultEngine = this._contextEngine;
+      }
+    });
+
+    this.addEventListener("contextmenu", event => {
+      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;
+      }
+      this.querySelector(".search-one-offs-context-set-default")
+          .setAttribute("disabled", target.engine == Services.search.defaultEngine);
+
+      this.contextMenuPopup.openPopupAtScreen(event.screenX, event.screenY, true);
+      event.preventDefault();
+
+      this._contextEngine = target.engine;
+    });
+  }
+
+  connectedCallback() {
+    if (this.delayConnectedCallback()) {
+      return;
+    }
+
+    this.appendChild(getFragment());
 
     this._popup = null;
 
     this._textbox = null;
 
     this._textboxWidth = 0;
 
     /**
@@ -88,83 +272,39 @@ class SearchOneOffs {
      * many engines offered by the current site.
      */
     this._addEngineMenuTimeoutMs = 200;
 
     this._addEngineMenuTimeout = null;
 
     this._addEngineMenuShouldBeOpen = false;
 
-    this.addEventListener("mousedown", this);
-    this.addEventListener("mousemove", this);
-    this.addEventListener("mouseout", this);
-    this.addEventListener("click", this);
-    this.addEventListener("command", this);
-    this.addEventListener("contextmenu", this);
-    this.settingsButton.addEventListener("command", this);
-    this.settingsButtonCompact.addEventListener("command", this);
-
     // Prevent popup events from the context menu from reaching the autocomplete
     // binding (or other listeners).
     let listener = aEvent => aEvent.stopPropagation();
     this.contextMenuPopup.addEventListener("popupshowing", listener);
     this.contextMenuPopup.addEventListener("popuphiding", listener);
     this.contextMenuPopup.addEventListener("popupshown", aEvent => {
       this._ignoreMouseEvents = true;
       aEvent.stopPropagation();
     });
     this.contextMenuPopup.addEventListener("popuphidden", aEvent => {
       this._ignoreMouseEvents = false;
       aEvent.stopPropagation();
     });
 
-    // Invalidate our cached list of engines.
-    Services.prefs.addObserver("browser.search.hiddenOneOffs", this);
-    Services.obs.addObserver(this, "browser-search-engine-modified");
-    Services.obs.addObserver(this, "browser-search-service");
+    // 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);
+    Services.obs.addObserver(this, "browser-search-service", 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");
-
-    window.addEventListener("unload", this);
-  }
-
-  addEventListener(...args) {
-    this.container.addEventListener(...args);
-  }
-
-  removeEventListener(...args) {
-    this.container.removeEventListener(...args);
-  }
-
-  dispatchEvent(...args) {
-    this.container.dispatchEvent(...args);
-  }
-
-  getAttribute(...args) {
-    return this.container.getAttribute(...args);
-  }
-
-  setAttribute(...args) {
-    this.container.setAttribute(...args);
-  }
-
-  querySelector(...args) {
-    return this.container.querySelector(...args);
-  }
-
-  handleEvent(event) {
-    let methodName = "_on_" + event.type;
-    if (methodName in this) {
-      this[methodName](event);
-    } else {
-      throw "Unrecognized search-one-offs event: " + event.type;
-    }
+    Services.obs.addObserver(this, "lightweight-theme-changed", true);
   }
 
   /**
    * 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.
    */
   get buttonWidth() {
@@ -193,48 +333,41 @@ class SearchOneOffs {
     this._popup = val;
 
     // 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;
   }
 
   get popup() {
     return this._popup;
   }
-
   /**
    * 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.
    */
   set textbox(val) {
     if (this._textbox) {
       this._textbox.removeEventListener("input", this);
     }
     if (val) {
       val.addEventListener("input", this);
     }
     return this._textbox = val;
   }
 
-  get style() {
-    return this.container.style;
-  }
-
   get textbox() {
     return this._textbox;
   }
-
   /**
    * The query string currently shown in the one-offs.  If the textbox
    * property is non-null, then this is automatically updated on
    * input.
    */
   set query(val) {
     this._query = val;
     if (this.popup && this.popup.popupOpen) {
@@ -328,21 +461,54 @@ class SearchOneOffs {
       return (!currentEngineNameToIgnore ||
               name != currentEngineNameToIgnore) &&
              !hiddenList.includes(name);
     });
 
     return this._engines;
   }
 
+  /**
+   * This handles events outside the one-off buttons, like on the popup
+   * and textbox.
+   */
+  handleEvent(event) {
+    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;
+    }
+  }
+
   observe(aEngine, aTopic, aData) {
     // Make sure the engine list is refetched next time it's needed.
     this._engines = null;
   }
 
+  showSettings() {
+    openPreferences("paneSearch", { origin: "contentSearch" });
+
+    // If the preference tab was already selected, the panel doesn't
+    // close itself automatically.
+    this.popup.hidePopup();
+  }
+
   /**
    * Updates the parts of the UI that show the query string.
    */
   _updateAfterQueryChanged() {
     let headerSearchText = this.querySelector(".searchbar-oneoffheader-searchtext");
     headerSearchText.setAttribute("value", this.query);
     let groupText;
     let isOneOffSelected =
@@ -1026,224 +1192,14 @@ class SearchOneOffs {
       clearTimeout(this._addEngineMenuTimeout);
     }
     this._addEngineMenuTimeout = setTimeout(() => {
       delete this._addEngineMenuTimeout;
       let button = this.querySelector(".addengine-menu-button");
       button.open = this._addEngineMenuShouldBeOpen;
     }, this._addEngineMenuTimeoutMs);
   }
-
-  // Event handlers below.
-
-  _on_mousedown(event) {
-    let target = event.originalTarget;
-    if (target.classList.contains("addengine-menu-button")) {
-      return;
-    }
-    // Required to receive click events from the buttons on Linux.
-    event.preventDefault();
-  }
-
-  _on_mousemove(event) {
-    let target = event.originalTarget;
-
-    // Handle mouseover on the add-engine menu button and its popup items.
-    if ((target.localName == "menuitem" && target.classList.contains("addengine-item")) ||
-        target.classList.contains("addengine-menu-button")) {
-      let menuButton = this.querySelector(".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;
-    }
-
-    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);
-    }
-  }
-
-  _on_mouseout(event) {
-    let target = event.originalTarget;
-
-    // Handle mouseout on the add-engine menu button and its popup items.
-    if ((target.localName == "menuitem" && target.classList.contains("addengine-item")) ||
-        target.classList.contains("addengine-menu-button")) {
-      this._updateStateForButton(null);
-      this._addEngineMenuShouldBeOpen = false;
-      this._resetAddEngineMenuTimeout();
-      return;
-    }
-
-    if (target.localName != "button") {
-      return;
-    }
-
-    // Don't update the mouseover state if the context menu is open.
-    if (this._ignoreMouseEvents) {
-      return;
-    }
-
-    this._updateStateForButton(null);
-  }
-
-  _on_click(event) {
-    if (event.button == 2) {
-      return; // ignore right clicks.
-    }
-
-    let button = event.originalTarget;
-    let engine = button.engine;
-
-    if (!engine) {
-      return;
-    }
-
-    // Select the clicked button so that consumers can easily tell which
-    // button was acted on.
-    this.selectedButton = button;
-    this.handleSearchCommand(event, engine);
-  }
-
-  _on_command(event) {
-    let target = event.target;
-
-    if (target == this.settingsButton ||
-        target == this.settingsButtonCompact) {
-      openPreferences("paneSearch", { origin: "contentSearch" });
-
-      // If the preference tab was already selected, the panel doesn't
-      // close itself automatically.
-      this.popup.hidePopup();
-      return;
-    }
-
-    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"),
-                                target.getAttribute("image"), false,
-                                installCallback);
-    }
-
-    if (target.classList.contains("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 (target.classList.contains("search-one-offs-context-set-default")) {
-      let currentEngine = Services.search.defaultEngine;
-
-      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.defaultEngine = this._contextEngine;
-    }
-  }
-
-  _on_contextmenu(event) {
-    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;
-    }
-    this.querySelector(".search-one-offs-context-set-default")
-        .setAttribute("disabled", target.engine == Services.search.defaultEngine);
-
-    this.contextMenuPopup.openPopupAtScreen(event.screenX, event.screenY, true);
-    event.preventDefault();
-
-    this._contextEngine = target.engine;
-  }
-
-  _on_input(event) {
-    // 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;
-  }
-
-  _on_popupshowing() {
-    this._rebuild();
-  }
-
-  _on_popuphidden() {
-    Services.tm.dispatchToMainThread(() => {
-      this.selectedButton = null;
-      this._contextEngine = null;
-    });
-  }
-
-  _on_unload() {
-    Services.prefs.removeObserver("browser.search.hiddenOneOffs", this);
-    Services.obs.removeObserver(this, "browser-search-engine-modified");
-    Services.obs.removeObserver(this, "browser-search-service");
-    Services.obs.removeObserver(this, "lightweight-theme-changed");
-  }
 }
 
-window.SearchOneOffs = SearchOneOffs;
+MozXULElement.implementCustomInterface(MozSearchOneOffs, [Ci.nsIObserver, Ci.nsIWeakReference]);
+customElements.define("search-one-offs", MozSearchOneOffs);
 
 }
-
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -372,17 +372,17 @@
     <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"/>
+      <xul:search-one-offs 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
@@ -468,19 +468,18 @@
               this._bundle = Services.strings.createBundle(kBundleURI);
             }
             return this._bundle;
           ]]>
         </getter>
       </property>
 
       <field name="oneOffButtons" readonly="true">
-        new window.SearchOneOffs(
-          document.getAnonymousElementByAttribute(this, "anonid",
-                                                  "search-one-off-buttons"));
+        document.getAnonymousElementByAttribute(this, "anonid",
+                                                "search-one-off-buttons");
       </field>
 
       <method name="updateHeader">
         <body><![CDATA[
           let currentEngine = Services.search.defaultEngine;
           let uri = currentEngine.iconURI;
           if (uri) {
             this.setAttribute("src", uri.spec);
--- a/browser/components/search/test/browser_oneOffContextMenu.js
+++ b/browser/components/search/test/browser_oneOffContextMenu.js
@@ -1,18 +1,20 @@
 "use strict";
 
 const TEST_ENGINE_NAME = "Foo";
 const TEST_ENGINE_BASENAME = "testEngine.xml";
 
 const searchPopup = document.getElementById("PopupSearchAutoComplete");
-const oneOffInstance = searchPopup.oneOffButtons;
-const contextMenu = oneOffInstance.querySelector(".search-one-offs-context-menu");
-const oneOffButtons = oneOffInstance.buttons;
-const searchInNewTabMenuItem = oneOffInstance.querySelector(".search-one-offs-context-open-in-new-tab");
+const oneOffElement = document.getAnonymousElementByAttribute(
+  searchPopup, "anonid", "search-one-off-buttons"
+);
+const contextMenu = oneOffElement.querySelector(".search-one-offs-context-menu");
+const oneOffButtons = oneOffElement.buttons;
+const searchInNewTabMenuItem = oneOffElement.querySelector(".search-one-offs-context-open-in-new-tab");
 
 let searchbar;
 let searchIcon;
 
 add_task(async function init() {
   searchbar = await gCUITestUtils.addSearchBar();
   registerCleanupFunction(() => {
     gCUITestUtils.removeSearchBar();
--- a/browser/components/search/test/browser_oneOffContextMenu_setDefault.js
+++ b/browser/components/search/test/browser_oneOffContextMenu_setDefault.js
@@ -4,18 +4,22 @@ const TEST_ENGINE_NAME = "Foo";
 const TEST_ENGINE_BASENAME = "testEngine.xml";
 const SEARCHBAR_BASE_ID = "searchbar-engine-one-off-item-";
 const URLBAR_BASE_ID = "urlbar-engine-one-off-item-";
 const ONEOFF_URLBAR_PREF = "browser.urlbar.oneOffSearches";
 
 const urlbar = document.getElementById("urlbar");
 const searchPopup = document.getElementById("PopupSearchAutoComplete");
 const urlbarPopup = document.getElementById("PopupAutoCompleteRichResult");
-const searchOneOff = searchPopup.oneOffButtons;
-const urlBarOneOff = urlbarPopup.oneOffSearchButtons;
+const searchOneOffElement = document.getAnonymousElementByAttribute(
+  searchPopup, "anonid", "search-one-off-buttons"
+);
+const urlBarOneOffElement = document.getAnonymousElementByAttribute(
+  urlbarPopup, "anonid", "one-off-search-buttons"
+);
 
 let originalEngine = Services.search.defaultEngine;
 
 function resetEngine() {
   Services.search.defaultEngine = originalEngine;
 }
 
 registerCleanupFunction(resetEngine);
@@ -31,20 +35,20 @@ add_task(async function init() {
 
   await promiseNewEngine(TEST_ENGINE_BASENAME, {
     setAsCurrent: false,
   });
 });
 
 add_task(async function test_searchBarChangeEngine() {
   let oneOffButton = await openPopupAndGetEngineButton(true, searchPopup,
-                                                       searchOneOff,
+                                                       searchOneOffElement,
                                                        SEARCHBAR_BASE_ID);
 
-  const setDefaultEngineMenuItem = searchOneOff.querySelector(
+  const setDefaultEngineMenuItem = searchOneOffElement.querySelector(
     ".search-one-offs-context-set-default"
   );
 
   // Click the set default engine menu item.
   let promise = promiseCurrentEngineChanged();
   EventUtils.synthesizeMouseAtCenter(setDefaultEngineMenuItem, {});
 
   // This also checks the engine correctly changed.
@@ -65,20 +69,20 @@ add_task(async function test_urlBarChang
   registerCleanupFunction(function() {
     Services.prefs.clearUserPref(ONEOFF_URLBAR_PREF);
   });
 
   // Ensure the engine is reset.
   resetEngine();
 
   let oneOffButton = await openPopupAndGetEngineButton(false, urlbarPopup,
-                                                       urlBarOneOff,
+                                                       urlBarOneOffElement,
                                                        URLBAR_BASE_ID);
 
-  const setDefaultEngineMenuItem = urlBarOneOff.querySelector(
+  const setDefaultEngineMenuItem = urlBarOneOffElement.querySelector(
     ".search-one-offs-context-set-default"
   );
 
   // Click the set default engine menu item.
   let promise = promiseCurrentEngineChanged();
   EventUtils.synthesizeMouseAtCenter(setDefaultEngineMenuItem, {});
 
   // This also checks the engine correctly changed.
@@ -119,40 +123,40 @@ function promiseCurrentEngineChanged() {
 
 /**
  * Opens the specified urlbar/search popup and gets the test engine from the
  * one-off buttons.
  *
  * @param {Boolean} isSearch true if the search popup should be opened; false
  *                           for the urlbar popup.
  * @param {Object} popup The expected popup.
- * @param {Object} oneOffInstance The expected one-off instance for the popup.
+ * @param {Object} oneOffElement The expected one-off-element for the popup.
  * @param {String} baseId The expected string for the id of the current
  *                        engine button, without the engine name.
  * @return {Object} Returns an object that represents the one off button for the
  *                          test engine.
  */
-async function openPopupAndGetEngineButton(isSearch, popup, oneOffInstance, baseId) {
+async function openPopupAndGetEngineButton(isSearch, popup, oneOffElement, baseId) {
   // Open the popup.
   let promise = promiseEvent(popup, "popupshown");
   info("Opening panel");
 
   // We have to open the popups in differnt ways.
   if (isSearch) {
     // Use the search icon to avoid hitting the network.
     EventUtils.synthesizeMouseAtCenter(searchIcon, {});
   } else {
     // There's no history at this stage, so we need to press a key.
     urlbar.focus();
     EventUtils.sendString("a");
   }
   await promise;
 
-  const contextMenu = oneOffInstance.contextMenuPopup;
-  const oneOffButtons = oneOffInstance.buttons;
+  const contextMenu = oneOffElement.contextMenuPopup;
+  const oneOffButtons = oneOffElement.buttons;
 
   // Get the one-off button for the test engine.
   let oneOffButton;
   for (let node of oneOffButtons.children) {
     if (node.engine && node.engine.name == TEST_ENGINE_NAME) {
       oneOffButton = node;
       break;
     }
--- a/browser/components/urlbar/UrlbarView.jsm
+++ b/browser/components/urlbar/UrlbarView.jsm
@@ -40,22 +40,16 @@ class UrlbarView {
       if (event.target.classList.contains("urlbarView-row-inner")) {
         event.target.toggleAttribute("overflow", false);
       }
     });
 
     this.controller.addQueryListener(this);
   }
 
-  get oneOffSearchButtons() {
-    return this._oneOffSearchButtons ||
-      (this._oneOffSearchButtons =
-         new this.window.SearchOneOffs(this.panel.querySelector(".search-one-offs")));
-  }
-
   /**
    * Opens the autocomplete results popup.
    */
   open() {
     this.panel.removeAttribute("hidden");
 
     let panelDirection = this.panel.style.direction;
     if (!panelDirection) {
@@ -67,20 +61,16 @@ class UrlbarView {
     let documentRect =
       this._getBoundsWithoutFlushing(this.document.documentElement);
     let width = documentRect.right - documentRect.left;
     this.panel.setAttribute("width", width);
 
     // Subtract two pixels for left and right borders on the panel.
     this._mainContainer.style.maxWidth = (width - 2) + "px";
 
-    // TODO: Search one off buttons are a stub right now.
-    //       We'll need to set them up properly.
-    this.oneOffSearchButtons;
-
     this.panel.openPopup(this.urlbar.textbox.closest("toolbar"), "after_end", 0, -1);
 
     this._rows.firstElementChild.toggleAttribute("selected", true);
   }
 
   /**
    * Closes the autocomplete results popup.
    */