Merge autoland to mozilla-central a=merge
authorCoroiu Cristina <ccoroiu@mozilla.com>
Sat, 17 Nov 2018 23:34:16 +0200
changeset 446917 a92c330bac8bce0c51099d630349aecbb71d7c35
parent 446913 94debc6ca20b5f9e8d2e617430673dd94139c4e9 (current diff)
parent 446916 5322e59f933edee69fa4a848e1709d7c21a4552b (diff)
child 446922 77223bb2fac278373dfcdde11fcda74b4c80aa61
push id35056
push userccoroiu@mozilla.com
push dateSat, 17 Nov 2018 21:35:03 +0000
treeherdermozilla-central@a92c330bac8b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone65.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
Merge autoland to mozilla-central a=merge
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -84,16 +84,17 @@
 
 <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
@@ -255,24 +256,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>
-      <search-one-offs class="search-one-offs"
-                       compact="true"
-                       includecurrentengine="true"
-                       disabletab="true"/>
+      <hbox 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,17 +12,16 @@
 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).display,
+    window.getComputedStyle(gURLBar.popup.oneOffSearchButtons.container).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).display,
+    window.getComputedStyle(gURLBar.popup.oneOffSearchButtons.container).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).display,
+    window.getComputedStyle(gURLBar.popup.oneOffSearchButtons.container).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:search-one-offs anonid="one-off-search-buttons"
-                             class="search-one-offs"
-                             compact="true"
-                             includecurrentengine="true"
-                             disabletab="true"
-                             flex="1"/>
+        <xul:hbox 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,18 +1907,19 @@ 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">
-        document.getAnonymousElementByAttribute(this, "anonid",
-                                                "one-off-search-buttons");
+        new window.SearchOneOffs(
+          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
@@ -3,228 +3,44 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 /* eslint-env mozilla/browser-window */
 
 {
 
-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.
-      }
+class SearchOneOffs {
+  constructor(container) {
+    this.container = container;
 
-      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.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"]));
 
     this._popup = null;
 
     this._textbox = null;
 
     this._textboxWidth = 0;
 
     /**
@@ -272,41 +88,82 @@ class MozSearchOneOffs extends MozXULEle
      * 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);
+
     // 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();
     });
 
     // Add weak referenced observers to invalidate our cached list of engines.
+    this.QueryInterface = ChromeUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]);
     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", true);
   }
 
+  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 new Error("Unrecognized search-one-offs event: " + event.type);
+    }
+  }
+
   /**
    * 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() {
     return 49;
   }
@@ -333,41 +190,48 @@ class MozSearchOneOffs extends MozXULEle
     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) {
@@ -461,54 +325,21 @@ class MozSearchOneOffs extends MozXULEle
       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 =
@@ -1192,14 +1023,217 @@ class MozSearchOneOffs extends MozXULEle
       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;
+    });
+  }
 }
 
-MozXULElement.implementCustomInterface(MozSearchOneOffs, [Ci.nsIObserver, Ci.nsIWeakReference]);
-customElements.define("search-one-offs", MozSearchOneOffs);
+window.SearchOneOffs = SearchOneOffs;
 
 }
+
--- 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:search-one-offs anonid="search-one-off-buttons" class="search-one-offs"/>
+      <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
@@ -468,18 +468,19 @@
               this._bundle = Services.strings.createBundle(kBundleURI);
             }
             return this._bundle;
           ]]>
         </getter>
       </property>
 
       <field name="oneOffButtons" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid",
-                                                "search-one-off-buttons");
+        new window.SearchOneOffs(
+          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/browser_oneOffContextMenu.js
+++ b/browser/components/search/test/browser/browser_oneOffContextMenu.js
@@ -1,20 +1,18 @@
 "use strict";
 
 const TEST_ENGINE_NAME = "Foo";
 const TEST_ENGINE_BASENAME = "testEngine.xml";
 
 const searchPopup = document.getElementById("PopupSearchAutoComplete");
-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");
+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");
 
 let searchbar;
 let searchIcon;
 
 add_task(async function init() {
   searchbar = await gCUITestUtils.addSearchBar();
   registerCleanupFunction(() => {
     gCUITestUtils.removeSearchBar();
--- a/browser/components/search/test/browser/browser_oneOffContextMenu_setDefault.js
+++ b/browser/components/search/test/browser/browser_oneOffContextMenu_setDefault.js
@@ -4,22 +4,18 @@ 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 searchOneOffElement = document.getAnonymousElementByAttribute(
-  searchPopup, "anonid", "search-one-off-buttons"
-);
-const urlBarOneOffElement = document.getAnonymousElementByAttribute(
-  urlbarPopup, "anonid", "one-off-search-buttons"
-);
+const searchOneOff = searchPopup.oneOffButtons;
+const urlBarOneOff = urlbarPopup.oneOffSearchButtons;
 
 let originalEngine = Services.search.defaultEngine;
 
 function resetEngine() {
   Services.search.defaultEngine = originalEngine;
 }
 
 registerCleanupFunction(resetEngine);
@@ -35,20 +31,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,
-                                                       searchOneOffElement,
+                                                       searchOneOff,
                                                        SEARCHBAR_BASE_ID);
 
-  const setDefaultEngineMenuItem = searchOneOffElement.querySelector(
+  const setDefaultEngineMenuItem = searchOneOff.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.
@@ -69,20 +65,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,
-                                                       urlBarOneOffElement,
+                                                       urlBarOneOff,
                                                        URLBAR_BASE_ID);
 
-  const setDefaultEngineMenuItem = urlBarOneOffElement.querySelector(
+  const setDefaultEngineMenuItem = urlBarOneOff.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.
@@ -123,40 +119,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} oneOffElement The expected one-off-element for the popup.
+ * @param {Object} oneOffInstance The expected one-off instance 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, oneOffElement, baseId) {
+async function openPopupAndGetEngineButton(isSearch, popup, oneOffInstance, 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 = oneOffElement.contextMenuPopup;
-  const oneOffButtons = oneOffElement.buttons;
+  const contextMenu = oneOffInstance.contextMenuPopup;
+  const oneOffButtons = oneOffInstance.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,16 +40,22 @@ 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) {
@@ -61,16 +67,20 @@ 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.
    */
--- a/dom/base/nsFocusManager.cpp
+++ b/dom/base/nsFocusManager.cpp
@@ -3095,17 +3095,17 @@ private:
 void
 ScopedContentTraversal::Next()
 {
   MOZ_ASSERT(mCurrent);
 
   // Get mCurrent's first child if it's in the same scope.
   if (!(mCurrent->GetShadowRoot() || mCurrent->IsHTMLElement(nsGkAtoms::slot)) ||
       mCurrent == mOwner) {
-    FlattenedChildIterator iter(mCurrent);
+    StyleChildrenIterator iter(mCurrent);
     nsIContent* child = iter.GetNextChild();
     if (child) {
       SetCurrent(child);
       return;
     }
   }
 
   // If mOwner has no children, END traversal
@@ -3113,22 +3113,21 @@ ScopedContentTraversal::Next()
     SetCurrent(nullptr);
     return;
   }
 
   nsIContent* current = mCurrent;
   while (1) {
     // Create parent's iterator and move to current
     nsIContent* parent = current->GetFlattenedTreeParent();
-    FlattenedChildIterator parentIter(parent);
+    StyleChildrenIterator parentIter(parent);
     parentIter.Seek(current);
 
     // Get next sibling of current
-    nsIContent* next = parentIter.GetNextChild();
-    if (next) {
+    if (nsIContent* next = parentIter.GetNextChild()) {
       SetCurrent(next);
       return;
     }
 
     // If no next sibling and parent is mOwner, END traversal
     if (parent == mOwner) {
       SetCurrent(nullptr);
       return;
@@ -3142,40 +3141,40 @@ void
 ScopedContentTraversal::Prev()
 {
   MOZ_ASSERT(mCurrent);
 
   nsIContent* parent;
   nsIContent* last;
   if (mCurrent == mOwner) {
     // Get last child of mOwner
-    FlattenedChildIterator ownerIter(mOwner, false /* aStartAtBeginning */);
+    StyleChildrenIterator ownerIter(mOwner, false /* aStartAtBeginning */);
     last = ownerIter.GetPreviousChild();
 
     parent = last;
   } else {
     // Create parent's iterator and move to mCurrent
     parent = mCurrent->GetFlattenedTreeParent();
-    FlattenedChildIterator parentIter(parent);
+    StyleChildrenIterator parentIter(parent);
     parentIter.Seek(mCurrent);
 
     // Get previous sibling
     last = parentIter.GetPreviousChild();
   }
 
   while (last) {
     parent = last;
     if (parent->GetShadowRoot() ||
         parent->IsHTMLElement(nsGkAtoms::slot)) {
       // Skip contents in other scopes
       break;
     }
 
     // Find last child
-    FlattenedChildIterator iter(parent, false /* aStartAtBeginning */);
+    StyleChildrenIterator iter(parent, false /* aStartAtBeginning */);
     last = iter.GetPreviousChild();
   }
 
   // If parent is mOwner and no previous sibling remains, END traversal
   SetCurrent(parent == mOwner ? nullptr : parent);
 }
 
 nsIContent*
@@ -3303,82 +3302,16 @@ nsFocusManager::GetNextTabbableContentIn
       } else {
         nsIFrame* frame = iterContent->GetPrimaryFrame();
         if (!frame) {
           continue;
         }
         frame->IsFocusable(&tabIndex, 0);
       }
       if (tabIndex < 0 || !(aIgnoreTabIndex || tabIndex == aCurrentTabIndex)) {
-        // If the element has native anonymous content, we may need to
-        // focus some NAC element, even if the element itself isn't focusable.
-        // This happens for example with <input type="date">.
-        // So, try to find NAC and then traverse the frame tree to find elements
-        // to focus.
-        // Yet, even if the frame is a nsIAnonymousContentCreator, don't
-        // traverse into the element again when the element is in a UA Widget,
-        // because there isn't any NAC to focus.
-        nsIFrame* possibleAnonOwnerFrame = iterContent->GetPrimaryFrame();
-        nsIAnonymousContentCreator* anonCreator =
-          do_QueryFrame(possibleAnonOwnerFrame);
-        bool isIterContentInUAWidgetShadow =
-          iterContent->GetContainingShadow() &&
-          iterContent->GetContainingShadow()->IsUAWidget();
-        if (anonCreator &&
-            !isIterContentInUAWidgetShadow &&
-            !iterContent->IsInNativeAnonymousSubtree()) {
-          nsIFrame* frame = nullptr;
-          // Find the first or last frame in tree order so that
-          // we can scope frame traversing to NAC.
-          if (aForward) {
-            frame = possibleAnonOwnerFrame->PrincipalChildList().FirstChild();
-          } else {
-            frame = possibleAnonOwnerFrame->PrincipalChildList().LastChild();
-            nsIFrame* last = frame;
-            while (last) {
-              frame = last;
-              last = frame->PrincipalChildList().LastChild();
-            }
-          };
-
-          nsCOMPtr<nsIFrameEnumerator> frameTraversal;
-          nsresult rv = NS_NewFrameTraversal(getter_AddRefs(frameTraversal),
-                                             iterContent->OwnerDoc()->
-                                               GetShell()->GetPresContext(),
-                                             frame,
-                                             ePreOrder,
-                                             false, // aVisual
-                                             false, // aLockInScrollView
-                                             true, // aFollowOOFs
-                                             true  // aSkipPopupChecks
-                                             );
-          if (NS_SUCCEEDED(rv)) {
-            nsIFrame* frame =
-              static_cast<nsIFrame*>(frameTraversal->CurrentItem());
-            while (frame) {
-              int32_t tabIndex;
-              frame->IsFocusable(&tabIndex, 0);
-              if (tabIndex >= 0 &&
-                  (aIgnoreTabIndex || aCurrentTabIndex == tabIndex)) {
-                return frame->GetContent();
-              }
-
-              if (aForward) {
-                frameTraversal->Next();
-              } else {
-                frameTraversal->Prev();
-              }
-              frame = static_cast<nsIFrame*>(frameTraversal->CurrentItem());
-              if (frame == possibleAnonOwnerFrame) {
-                break;
-              }
-            }
-          }
-        }
-
         continue;
       }
 
       if (!IsHostOrSlot(iterContent)) {
         nsCOMPtr<nsIContent> elementInFrame;
         bool checkSubDocument = true;
         if (aForDocumentNavigation &&
             TryDocumentNavigation(iterContent, &checkSubDocument,
@@ -3976,17 +3909,17 @@ nsFocusManager::GetNextTabbableMapArea(b
 }
 
 int32_t
 nsFocusManager::GetNextTabIndex(nsIContent* aParent,
                                 int32_t aCurrentTabIndex,
                                 bool aForward)
 {
   int32_t tabIndex, childTabIndex;
-  FlattenedChildIterator iter(aParent);
+  StyleChildrenIterator iter(aParent);
 
   if (aForward) {
     tabIndex = 0;
     for (nsIContent* child = iter.GetNextChild();
          child;
          child = iter.GetNextChild()) {
       // Skip child's descendants if child is a shadow host or slot, as they are
       // in the focus navigation scope owned by child's shadow root
--- a/dom/base/test/file_bug1453693.html
+++ b/dom/base/test/file_bug1453693.html
@@ -375,15 +375,17 @@
 
       function init() {
         SimpleTest.waitForFocus(runTest);
       }
     </script>
     <style>
     </style>
     <template id="template">
-      <p tabindex="0" id="p1">component</p>
-      <p tabindex="0" id="p2">/component</p>
+      <div style="overflow: hidden">
+        <p tabindex="0" id="p1">component</p>
+        <p tabindex="0" id="p2">/component</p>
+      </div>
     </template>
   </head>
   <body onload="init()">
   </body>
 </html>