Backed out changeset e859a5aebb5b (bug 1534455) for causing failures at test_bug437844.xul
authorMihai Alexandru Michis <malexandru@mozilla.com>
Tue, 17 Sep 2019 07:51:36 +0300
changeset 493494 17e8e912e185c5c3543e6bc248c78ec5e2401c03
parent 493493 cfdf258c11ae6136fdaa1e0786dba757fb284c48
child 493495 6c6c2a82876fa91f7b38483aec7bfb7632f68bf7
push id95519
push usermalexandru@mozilla.com
push dateTue, 17 Sep 2019 04:52:13 +0000
treeherderautoland@17e8e912e185 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1534455, 437844
milestone71.0a1
backs oute859a5aebb5bcf47e905ce9c3c99dc0931923f23
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 e859a5aebb5b (bug 1534455) for causing failures at test_bug437844.xul
accessible/tests/browser/tree/browser.ini
accessible/tests/browser/tree/browser_searchbar.js
accessible/tests/mochitest/tree/test_txtctrl.xul
browser/base/content/browser.css
browser/base/content/browser.js
browser/base/content/test/about/browser_aboutHome_search_searchbar.js
browser/components/customizableui/test/browser_901207_searchbar_in_panel.js
browser/components/customizableui/test/browser_940307_panel_click_closure_handling.js
browser/components/places/content/bookmarkProperties.xul
browser/components/places/content/editBookmarkPanel.inc.xul
browser/components/places/tests/browser/browser_bookmark_popup.js
browser/components/preferences/in-content/home.inc.xul
browser/components/search/content/searchbar.js
browser/components/search/test/browser/browser_426329.js
browser/components/search/test/browser/browser_searchbar_keyboard_navigation.js
browser/components/search/test/browser/browser_searchbar_openpopup.js
browser/components/search/test/browser/browser_searchbar_smallpanel_keyboard_navigation.js
browser/themes/osx/browser.css
browser/themes/shared/urlbar-searchbar.inc.css
toolkit/content/customElements.js
toolkit/content/jar.mn
toolkit/content/tests/chrome/chrome.ini
toolkit/content/tests/chrome/test_autocomplete2.xul
toolkit/content/tests/chrome/test_autocomplete3.xul
toolkit/content/tests/chrome/test_autocomplete4.xul
toolkit/content/tests/chrome/test_autocomplete5.xul
toolkit/content/tests/chrome/test_autocomplete_emphasis.xul
toolkit/content/tests/chrome/test_autocomplete_mac_caret.xul
toolkit/content/tests/chrome/test_autocomplete_placehold_last_complete.xul
toolkit/content/tests/chrome/test_autocomplete_with_composition_on_textbox.xul
toolkit/content/tests/chrome/test_editor_for_textbox_with_autocomplete.xul
toolkit/content/widgets/autocomplete-input.js
toolkit/content/widgets/autocomplete.xml
toolkit/content/xul.css
toolkit/themes/linux/global/autocomplete.css
toolkit/themes/osx/global/autocomplete.css
toolkit/themes/windows/global/autocomplete.css
--- a/accessible/tests/browser/tree/browser.ini
+++ b/accessible/tests/browser/tree/browser.ini
@@ -1,10 +1,9 @@
 [DEFAULT]
 support-files =
   head.js
   !/accessible/tests/browser/shared-head.js
   !/accessible/tests/mochitest/*.js
 
 [browser_aria_owns.js]
 skip-if = true || (verify && !debug && (os == 'linux')) #Bug 1445513
-[browser_searchbar.js]
 [browser_shadowdom.js]
deleted file mode 100644
--- a/accessible/tests/browser/tree/browser_searchbar.js
+++ /dev/null
@@ -1,52 +0,0 @@
-"use strict";
-
-/* import-globals-from ../../mochitest/role.js */
-loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
-
-// eslint-disable-next-line camelcase
-add_task(async function test_searchbar_a11y_tree() {
-  await SpecialPowers.pushPrefEnv({
-    set: [["browser.search.widget.inNavBar", true]],
-  });
-
-  let searchbar = await TestUtils.waitForCondition(
-    () => document.getElementById("searchbar"),
-    "wait for search bar to appear"
-  );
-
-  // Make sure the popup has been rendered so it shows up in the a11y tree.
-  let popup = document.getElementById("PopupSearchAutoComplete");
-  let promise = BrowserTestUtils.waitForEvent(popup, "popupshown", false);
-  searchbar.textbox.openPopup();
-  await promise;
-
-  promise = BrowserTestUtils.waitForEvent(popup, "popuphidden", false);
-  searchbar.textbox.closePopup();
-  await promise;
-
-  const TREE = {
-    role: ROLE_EDITCOMBOBOX,
-
-    children: [
-      // input element
-      {
-        role: ROLE_ENTRY,
-        children: [],
-      },
-
-      // context menu
-      {
-        role: ROLE_COMBOBOX_LIST,
-        children: [],
-      },
-
-      // result list
-      {
-        role: ROLE_GROUPING,
-        // not testing the structure inside the result list
-      },
-    ],
-  };
-
-  testAccessibleTree(searchbar, TREE);
-});
--- a/accessible/tests/mochitest/tree/test_txtctrl.xul
+++ b/accessible/tests/mochitest/tree/test_txtctrl.xul
@@ -84,17 +84,57 @@
       accTree =
         { SECTION: [
           { PASSWORD_TEXT: [ { TEXT_LEAF: [] } ] },
           { MENUPOPUP: [] }
         ] };
 
       testAccessibleTree("txc_password", accTree);
 
-      SimpleTest.finish();
+      //////////////////////////////////////////////////////////////////////////
+      // autocomplete textbox
+
+      accTree = {
+        // textbox
+        role: ROLE_AUTOCOMPLETE,
+        children: [
+          {
+            // html:input
+            role: ROLE_ENTRY,
+            children: [
+              {
+                // #text
+                role: ROLE_TEXT_LEAF,
+                children: []
+              }
+            ]
+          },
+          {
+            // xul:menupopup
+            role: ROLE_COMBOBOX_LIST,
+            children: []
+          },
+          {
+            // xul:richlistbox
+            role: ROLE_COMBOBOX_LIST,
+            children: []
+          }
+        ]
+      };
+
+      var txc = document.getElementById("txc_autocomplete");
+      SimpleTest.ok(txc, "Testing (New) Toolkit autocomplete widget.");
+
+      // Dumb access to trigger popup lazy creation.
+      dump("Trigget popup lazy creation");
+      waitForEvent(EVENT_REORDER, txc, () => {
+        testAccessibleTree("txc_autocomplete", accTree);
+        SimpleTest.finish();
+      });
+      txc.popup.initialize();
     }
 
     SimpleTest.waitForExplicitFinish();
     addA11yLoadEvent(doTest);
   ]]>
   </script>
 
   <hbox flex="1" style="overflow: auto;">
@@ -112,12 +152,13 @@
     </body>
 
     <vbox flex="1">
       <textbox id="txc" value="hello"/>
       <textbox id="txc_search" is="search-textbox" value="hello"/>
       <textbox id="txc_search_searchbutton" searchbutton="true" is="search-textbox" value="hello"/>
       <textbox id="txc_number" type="number" value="44"/>
       <textbox id="txc_password" type="password" value="hello"/>
+      <textbox id="txc_autocomplete" type="autocomplete" value="hello"/>
     </vbox>
   </hbox>
 
 </window>
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -552,17 +552,17 @@ toolbar:not(#TabsToolbar) > #personal-bo
 /* url bar min-width is defined further down, together with the maximum size
  * of the identity icon block, for different window sizes.
  */
 #search-container {
   min-width: 25ch;
 }
 
 #urlbar,
-#searchbar {
+.searchbar-textbox {
   /* Setting a width and min-width to let the location & search bars maintain
      a constant width in case they haven't be resized manually. (bug 965772) */
   width: 1px;
   min-width: 1px;
 }
 
 /* Display URLs left-to-right but right aligned in RTL mode. */
 #urlbar-input:-moz-locale-dir(rtl) {
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -4730,17 +4730,20 @@ const BrowserSearch = {
           "about:blank"
         );
         Services.obs.addObserver(observer, "browser-delayed-startup-finished");
       }
       return;
     }
 
     let focusUrlBarIfSearchFieldIsNotActive = function(aSearchBar) {
-      if (!aSearchBar || document.activeElement != aSearchBar.textbox) {
+      if (
+        !aSearchBar ||
+        document.activeElement != aSearchBar.textbox.inputField
+      ) {
         // Limit the results to search suggestions, like the search bar.
         gURLBar.search(UrlbarTokenizer.RESTRICT.SEARCH);
       }
     };
 
     let searchBar = this.searchBar;
     let placement = CustomizableUI.getPlacementOfWidget("search-container");
     let focusSearchBar = () => {
--- a/browser/base/content/test/about/browser_aboutHome_search_searchbar.js
+++ b/browser/base/content/test/about/browser_aboutHome_search_searchbar.js
@@ -21,17 +21,17 @@ add_task(async function() {
   info("Cmd+k should focus the search box in the toolbar when it's present");
 
   await BrowserTestUtils.withNewTab(
     { gBrowser, url: "about:home" },
     async function(browser) {
       await BrowserTestUtils.synthesizeMouseAtCenter("#brandLogo", {}, browser);
 
       let doc = window.document;
-      let searchInput = BrowserSearch.searchBar.textbox;
+      let searchInput = BrowserSearch.searchBar.textbox.inputField;
       isnot(
         searchInput,
         doc.activeElement,
         "Search bar should not be the active element."
       );
 
       EventUtils.synthesizeKey("k", { accelKey: true });
       await TestUtils.waitForCondition(() => doc.activeElement === searchInput);
--- a/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js
+++ b/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js
@@ -5,17 +5,17 @@
 "use strict";
 
 logActiveElement();
 
 async function waitForSearchBarFocus() {
   let searchbar = document.getElementById("searchbar");
   await TestUtils.waitForCondition(function() {
     logActiveElement();
-    return document.activeElement === searchbar.textbox;
+    return document.activeElement === searchbar.textbox.inputField;
   });
 }
 
 // Ctrl+K should open the menu panel and focus the search bar if the search bar is in the panel.
 add_task(async function check_shortcut_when_in_closed_overflow_panel_closed() {
   CustomizableUI.addWidgetToArea(
     "search-container",
     CustomizableUI.AREA_FIXED_OVERFLOW_PANEL
--- a/browser/components/customizableui/test/browser_940307_panel_click_closure_handling.js
+++ b/browser/components/customizableui/test/browser_940307_panel_click_closure_handling.js
@@ -48,33 +48,29 @@ add_task(async function searchbar_in_pan
     set: [["browser.search.suggest.enabled", false]],
   });
   let dontShowPopup = e => e.preventDefault();
   let searchbarPopup = searchbar.textbox.popup;
   searchbarPopup.addEventListener("popupshowing", dontShowPopup);
 
   searchbar.value = "foo";
   searchbar.focus();
-
-  // Can't use promisePanelElementShown() here since the search bar
-  // creates its context menu lazily the first time it is opened.
-  let contextMenuShown = new Promise(resolve => {
-    let listener = event => {
-      if (searchbar._menupopup && event.target == searchbar._menupopup) {
-        window.removeEventListener("popupshown", listener);
-        resolve(searchbar._menupopup);
-      }
-    };
-    window.addEventListener("popupshown", listener);
-  });
+  // Reaching into this context menu is pretty evil, but hey... it's a test.
+  let textbox = document.getAnonymousElementByAttribute(
+    searchbar.textbox,
+    "anonid",
+    "moz-input-box"
+  );
+  let contextmenu = textbox.menupopup;
+  let contextMenuShown = promisePanelElementShown(window, contextmenu);
   EventUtils.synthesizeMouseAtCenter(searchbar, {
     type: "contextmenu",
     button: 2,
   });
-  let contextmenu = await contextMenuShown;
+  await contextMenuShown;
 
   ok(isOverflowOpen(), "Panel should still be open");
 
   let selectAll = contextmenu.querySelector("[cmd='cmd_selectAll']");
   let contextMenuHidden = promisePanelElementHidden(window, contextmenu);
   EventUtils.synthesizeMouseAtCenter(selectAll, {});
   await contextMenuHidden;
 
--- a/browser/components/places/content/bookmarkProperties.xul
+++ b/browser/components/places/content/bookmarkProperties.xul
@@ -13,29 +13,27 @@
   <!ENTITY % editBookmarkOverlayDTD SYSTEM "chrome://browser/locale/places/editBookmarkOverlay.dtd">
   %editBookmarkOverlayDTD;
 ]>
 
 <dialog id="bookmarkproperties"
         buttons="accept, cancel"
         buttoniconaccept="save"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-        xmlns:html="http://www.w3.org/1999/xhtml"
         onload="BookmarkPropertiesPanel.onDialogLoad();"
         onunload="BookmarkPropertiesPanel.onDialogUnload();"
         style="min-width: 30em;"
         persist="screenX screenY width">
 
   <stringbundleset id="stringbundleset">
     <stringbundle id="stringBundle"
                   src="chrome://browser/locale/places/bookmarkProperties.properties"/>
   </stringbundleset>
 
   <script src="chrome://browser/content/places/editBookmark.js"/>
   <script src="chrome://browser/content/places/bookmarkProperties.js"/>
   <script src="chrome://global/content/globalOverlay.js"/>
-  <script src="chrome://global/content/editMenuOverlay.js"/>
   <script src="chrome://browser/content/utilityOverlay.js"/>
   <script src="chrome://browser/content/places/places-tree.js"/>
 
 #include editBookmarkPanel.inc.xul
 
 </dialog>
--- a/browser/components/places/content/editBookmarkPanel.inc.xul
+++ b/browser/components/places/content/editBookmarkPanel.inc.xul
@@ -71,44 +71,33 @@
       </hbox>
     </vbox>
 
     <vbox id="editBMPanel_tagsRow"
           collapsed="true">
       <label value="&editBookmarkOverlay.tags.label;"
              accesskey="&editBookmarkOverlay.tags.accesskey;"
              control="editBMPanel_tagsField"/>
-      <hbox flex="1" align="center" role="combobox">
-        <html:input id="editBMPanel_tagsField"
-                 is="autocomplete-input"
-                 style="-moz-box-flex: 1;"
+      <hbox flex="1" align="center">
+        <textbox id="editBMPanel_tagsField"
+                 type="autocomplete"
+                 flex="1"
                  autocompletesearch="places-tag-autocomplete"
-                 autocompletepopup="editBMPanel_tagsAutocomplete"
+                 autocompletepopup="PopupAutoComplete"
                  completedefaultindex="true"
                  completeselectedindex="true"
                  tabscrolling="true"
                  placeholder="&editBookmarkOverlay.tagsEmptyDesc.label;"
                  onchange="gEditItemOverlay.onTagsFieldChange();"/>
         <button id="editBMPanel_tagsSelectorExpander"
                 class="expander-down panel-button"
                 tooltiptext="&editBookmarkOverlay.tagsExpanderDown.tooltip;"
                 tooltiptextdown="&editBookmarkOverlay.tagsExpanderDown.tooltip;"
                 tooltiptextup="&editBookmarkOverlay.expanderUp.tooltip;"
                 oncommand="gEditItemOverlay.toggleTagsSelector().catch(Cu.reportError);"/>
-        <popupset>
-          <panel is="autocomplete-richlistbox-popup"
-                 type="autocomplete-richlistbox"
-                 id="editBMPanel_tagsAutocomplete"
-                 role="group"
-                 noautofocus="true"
-                 hidden="true"
-                 overflowpadding="4"
-                 norolluponanchor="true"
-                 nomaxresults="true"/>
-        </popupset>
       </hbox>
     </vbox>
 
     <vbox id="editBMPanel_tagsSelectorRow"
           collapsed="true">
       <richlistbox id="editBMPanel_tagsSelector"
                    styled="true"
                    height="150"/>
--- a/browser/components/places/tests/browser/browser_bookmark_popup.js
+++ b/browser/components/places/tests/browser/browser_bookmark_popup.js
@@ -573,17 +573,17 @@ add_task(async function enter_during_aut
       tagsField.focus();
 
       // Register a tag into the DB.
       EventUtils.sendString("Abc", window);
       tagsField.blur();
 
       // Start autocomplete with the registered tag.
       tagsField.value = "";
-      let popup = document.getElementById("editBMPanel_tagsAutocomplete");
+      let popup = document.getElementById("PopupAutoComplete");
       let promiseShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
       tagsField.focus();
       EventUtils.sendString("a", window);
       await promiseShown;
       ok(promiseShown, "autocomplete shown");
 
       // Select first candidate.
       EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
@@ -617,17 +617,17 @@ add_task(async function escape_during_au
       tagsField.focus();
 
       // Register a tag into the DB.
       EventUtils.sendString("Abc", window);
       tagsField.blur();
 
       // Start autocomplete with the registered tag.
       tagsField.value = "";
-      let popup = document.getElementById("editBMPanel_tagsAutocomplete");
+      let popup = document.getElementById("PopupAutoComplete");
       let promiseShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
       tagsField.focus();
       EventUtils.sendString("a", window);
       await promiseShown;
       ok(promiseShown, "autocomplete shown");
 
       // Select first candidate.
       EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
--- a/browser/components/preferences/in-content/home.inc.xul
+++ b/browser/components/preferences/in-content/home.inc.xul
@@ -39,33 +39,22 @@
         <menupopup>
             <menuitem value="0" data-l10n-id="home-mode-choice-default" />
             <menuitem value="2" data-l10n-id="home-mode-choice-custom" />
             <menuitem value="1" data-l10n-id="home-mode-choice-blank" />
         </menupopup>
       </menulist>
 
       <vbox id="customSettings" hidden="true">
-        <box role="combobox">
-          <html:input id="homePageUrl"
-                      type="text"
-                      is="autocomplete-input"
-                      class="uri-element check-home-page-controlled"
-                      style="-moz-box-flex: 1;"
-                      data-preference-related="browser.startup.homepage"
-                      data-l10n-id="home-homepage-custom-url"
-                      autocompletepopup="homePageUrlAutocomplete"
-                      autocompletesearch="unifiedcomplete" />
-          <popupset>
-            <panel id="homePageUrlAutocomplete"
-                   is="autocomplete-richlistbox-popup"
-                   type="autocomplete-richlistbox"
-                   noautofocus="true"/>
-          </popupset>
-        </box>
+        <textbox id="homePageUrl"
+                class="uri-element check-home-page-controlled"
+                data-preference-related="browser.startup.homepage"
+                type="autocomplete"
+                data-l10n-id="home-homepage-custom-url"
+                autocompletesearch="unifiedcomplete" />
         <hbox class="homepage-buttons">
           <button id="useCurrentBtn"
                   is="highlightable-button"
                   flex="1"
                   class="homepage-button check-home-page-controlled"
                   data-l10n-id="use-current-pages"
                   data-l10n-args='{"tabCount": 0}'
                   disabled="true"
--- a/browser/components/search/content/searchbar.js
+++ b/browser/components/search/content/searchbar.js
@@ -46,18 +46,17 @@
 
       this.content = MozXULElement.parseXULToFragment(
         `
       <stringbundle src="chrome://browser/locale/search.properties"></stringbundle>
       <hbox class="searchbar-search-button" tooltiptext="&searchIcon.tooltip;">
         <image class="searchbar-search-icon"></image>
         <image class="searchbar-search-icon-overlay"></image>
       </hbox>
-      <html:input class="searchbar-textbox" is="autocomplete-input" type="search" placeholder="&searchInput.placeholder;" autocompletepopup="PopupSearchAutoComplete" autocompletesearch="search-autocomplete" autocompletesearchparam="searchbar-history" maxrows="10" completeselectedindex="true" minresultsforpopup="0"/>
-      <menupopup class="textbox-contextmenu"></menupopup>
+      <textbox class="searchbar-textbox" type="autocomplete" inputtype="search" placeholder="&searchInput.placeholder;" flex="1" autocompletepopup="PopupSearchAutoComplete" autocompletesearch="search-autocomplete" autocompletesearchparam="searchbar-history" maxrows="10" completeselectedindex="true" minresultsforpopup="0"/>
       <hbox class="search-go-container">
         <image class="search-go-button urlbar-icon" hidden="true" onclick="handleSearchCommand(event);" tooltiptext="&contentSearchSubmit.tooltip;"></image>
       </hbox>
     `,
         ["chrome://browser/locale/browser.dtd"]
       );
 
       this._ignoreFocus = false;
@@ -76,20 +75,16 @@
       // Don't go further if in Customize mode.
       if (this.parentNode.parentNode.localName == "toolbarpaletteitem") {
         return;
       }
 
       this._stringBundle = this.querySelector("stringbundle");
       this._textbox = this.querySelector(".searchbar-textbox");
 
-      this._menupopup = null;
-      this._pasteAndSearchMenuItem = null;
-      this._suggestMenuItem = null;
-
       this._setupTextboxEventListeners();
       this._initTextbox();
 
       window.addEventListener("unload", this.destroy);
 
       this.FormHistory = ChromeUtils.import(
         "resource://gre/modules/FormHistory.jsm",
         {}
@@ -199,19 +194,17 @@
       // Make sure to break the cycle from _textbox to us. Otherwise we leak
       // the world. But make sure it's actually pointing to us.
       // Also make sure the textbox has ever been constructed, otherwise the
       // _textbox getter will cause the textbox constructor to run, add an
       // observer, and leak the world too.
       if (
         this._textbox &&
         this._textbox.mController &&
-        this._textbox.mController.input &&
-        this._textbox.mController.input.wrappedJSObject ==
-          this.nsIAutocompleteInput
+        this._textbox.mController.input == this
       ) {
         this._textbox.mController.input = null;
       }
     }
 
     focus() {
       this._textbox.focus();
     }
@@ -434,18 +427,19 @@
     /**
      * Determines if we should select all the text in the searchbar based on the
      * clickSelectsAll pref, searchbar state, and whether the selection is empty.
      */
     _maybeSelectAll() {
       if (
         !this._preventClickSelectsAll &&
         UrlbarPrefs.get("clickSelectsAll") &&
-        document.activeElement == this._textbox &&
-        this._textbox.selectionStart == this._textbox.selectionEnd
+        document.activeElement == this._textbox.inputField &&
+        this._textbox.inputField.selectionStart ==
+          this._textbox.inputField.selectionEnd
       ) {
         this._textbox.editor.selectAll();
       }
     }
 
     _setupEventListeners() {
       this.addEventListener("click", event => {
         this._maybeSelectAll();
@@ -499,17 +493,18 @@
         this.updateGoButtonVisibility();
       });
 
       this.addEventListener(
         "blur",
         event => {
           // If the input field is still focused then a different window has
           // received focus, ignore the next focus event.
-          this._ignoreFocus = document.activeElement == this._textbox;
+          this._ignoreFocus =
+            document.activeElement == this._textbox.inputField;
         },
         true
       );
 
       this.addEventListener(
         "focus",
         event => {
           // Speculatively connect to the current engine's search URI (and
@@ -639,28 +634,16 @@
               event.keyCode == KeyEvent.DOM_VK_UP)
           ) {
             this.textbox.openSearch();
           }
         },
         true
       );
 
-      if (AppConstants.platform == "macosx") {
-        this.textbox.addEventListener(
-          "keypress",
-          event => {
-            if (event.keyCode == KeyEvent.DOM_VK_F4) {
-              this.textbox.openSearch();
-            }
-          },
-          true
-        );
-      }
-
       this.textbox.addEventListener("dragover", event => {
         let types = event.dataTransfer.types;
         if (
           types.includes("text/plain") ||
           types.includes("text/x-moz-text-internal")
         ) {
           event.preventDefault();
         }
@@ -673,50 +656,21 @@
           data = dataTransfer.getData("text/x-moz-text-internal");
         }
         if (data) {
           event.preventDefault();
           this.textbox.value = data;
           this.openSuggestionsPanel();
         }
       });
-
-      this.textbox.addEventListener("contextmenu", event => {
-        if (event.target != this.textbox) {
-          return;
-        }
-
-        if (!this._menupopup) {
-          this._buildContextMenu();
-        }
-
-        BrowserSearch.searchBar._textbox.closePopup();
-        let enabled = Services.prefs.getBoolPref(
-          "browser.search.suggest.enabled"
-        );
-        this._suggestMenuItem.setAttribute("checked", enabled);
-
-        let controller = document.commandDispatcher.getControllerForCommand(
-          "cmd_paste"
-        );
-        enabled = controller.isCommandEnabled("cmd_paste");
-        if (enabled) {
-          this._pasteAndSearchMenuItem.removeAttribute("disabled");
-        } else {
-          this._pasteAndSearchMenuItem.setAttribute("disabled", "true");
-        }
-
-        this._menupopup.openPopupAtScreen(event.screenX, event.screenY, true);
-        event.preventDefault();
-      });
     }
 
     _initTextbox() {
       // nsIController
-      let searchbarController = {
+      this.searchbarController = {
         textbox: this.textbox,
         supportsCommand(command) {
           return (
             command == "cmd_clearhistory" || command == "cmd_togglesuggest"
           );
         },
         isCommandEnabled(command) {
           return true;
@@ -740,24 +694,39 @@
                 !enabled
               );
               break;
             default:
             // do nothing with unrecognized command
           }
         },
       };
-      this.textbox.controllers.appendController(searchbarController);
 
       if (this.parentNode.parentNode.localName == "toolbarpaletteitem") {
         return;
       }
 
-      this.setAttribute("role", "combobox");
-      this.setAttribute("aria-owns", this.textbox.popup.id);
+      let inputBox = document.getAnonymousElementByAttribute(
+        this.textbox,
+        "anonid",
+        "moz-input-box"
+      );
+
+      // Force the Custom Element to upgrade until Bug 1470242 handles this:
+      window.customElements.upgrade(inputBox);
+      let cxmenu = inputBox.menupopup;
+      cxmenu.addEventListener(
+        "popupshowing",
+        () => {
+          this._initContextMenu(cxmenu);
+        },
+        { capture: true, once: true }
+      );
+
+      this.textbox.setAttribute("aria-owns", this.textbox.popup.id);
 
       // This overrides the searchParam property in autocomplete.xml. We're
       // hijacking this property as a vehicle for delivering the privacy
       // information about the window into the guts of nsSearchSuggestions.
       // Note that the setter is the same as the parent. We were not sure whether
       // we can override just the getter. If that proves to be the case, the setter
       // can be removed.
       Object.defineProperty(this.textbox, "searchParam", {
@@ -872,82 +841,101 @@
         if (this.textbox._selectionDetails) {
           BrowserSearch.searchBar.telemetrySearchDetails = this.textbox._selectionDetails;
           this.textbox._selectionDetails = null;
         }
         this.handleSearchCommand(event, engine);
       };
     }
 
-    _buildContextMenu() {
-      const raw = `
-          <menuitem label="&undoCmd.label;" accesskey="&undoCmd.accesskey;" cmd="cmd_undo"/>
-          <menuseparator/>
-          <menuitem label="&cutCmd.label;" accesskey="&cutCmd.accesskey;" cmd="cmd_cut"/>
-          <menuitem label="&copyCmd.label;" accesskey="&copyCmd.accesskey;" cmd="cmd_copy"/>
-          <menuitem label="&pasteCmd.label;" accesskey="&pasteCmd.accesskey;" cmd="cmd_paste"/>
-          <menuitem anonid="paste-and-search" oncommand="BrowserSearch.pasteAndSearch(event)"/>
-          <menuitem label="&deleteCmd.label;" accesskey="&deleteCmd.accesskey;" cmd="cmd_delete"/>
-          <menuseparator/>
-          <menuitem label="&selectAllCmd.label;" accesskey="&selectAllCmd.accesskey;" cmd="cmd_selectAll"/>
-          <menuseparator/>
-          <menuitem cmd="cmd_clearHistory"/>
-          <menuitem cmd="cmd_togglesuggest" type="checkbox" autocheck="false"/>
-      `;
+    _initContextMenu(aMenu) {
+      let stringBundle = this._stringBundle;
+
+      let pasteAndSearch, suggestMenuItem;
+      let element, label, akey;
+
+      element = document.createXULElement("menuseparator");
+      aMenu.appendChild(element);
 
-      this._menupopup = this.querySelector(".textbox-contextmenu");
-
-      let frag = MozXULElement.parseXULToFragment(raw, [
-        "chrome://global/locale/textcontext.dtd",
-      ]);
+      let insertLocation = aMenu.firstElementChild;
+      while (
+        insertLocation.nextElementSibling &&
+        insertLocation.getAttribute("cmd") != "cmd_paste"
+      ) {
+        insertLocation = insertLocation.nextElementSibling;
+      }
+      if (insertLocation) {
+        element = document.createXULElement("menuitem");
+        label = stringBundle.getString("cmd_pasteAndSearch");
+        element.setAttribute("label", label);
+        element.setAttribute("anonid", "paste-and-search");
+        element.setAttribute(
+          "oncommand",
+          "BrowserSearch.pasteAndSearch(event)"
+        );
+        aMenu.insertBefore(element, insertLocation.nextElementSibling);
+        pasteAndSearch = element;
+      }
 
-      // Insert attributes that come from localized properties
-      let el = frag.querySelector('[anonid="paste-and-search"]');
-      el.setAttribute(
-        "label",
-        this._stringBundle.getString("cmd_pasteAndSearch")
-      );
+      element = document.createXULElement("menuitem");
+      label = stringBundle.getString("cmd_clearHistory");
+      akey = stringBundle.getString("cmd_clearHistory_accesskey");
+      element.setAttribute("label", label);
+      element.setAttribute("accesskey", akey);
+      element.setAttribute("cmd", "cmd_clearhistory");
+      aMenu.appendChild(element);
 
-      el = frag.querySelector('[cmd="cmd_clearHistory"]');
-      el.setAttribute(
-        "label",
-        this._stringBundle.getString("cmd_clearHistory")
-      );
-      el.setAttribute(
-        "accesskey",
-        this._stringBundle.getString("cmd_clearHistory_accesskey")
-      );
+      element = document.createXULElement("menuitem");
+      label = stringBundle.getString("cmd_showSuggestions");
+      akey = stringBundle.getString("cmd_showSuggestions_accesskey");
+      element.setAttribute("anonid", "toggle-suggest-item");
+      element.setAttribute("label", label);
+      element.setAttribute("accesskey", akey);
+      element.setAttribute("cmd", "cmd_togglesuggest");
+      element.setAttribute("type", "checkbox");
+      element.setAttribute("autocheck", "false");
+      suggestMenuItem = element;
+      aMenu.appendChild(element);
 
-      el = frag.querySelector('[cmd="cmd_togglesuggest"]');
-      el.setAttribute(
-        "label",
-        this._stringBundle.getString("cmd_showSuggestions")
-      );
-      el.setAttribute(
-        "accesskey",
-        this._stringBundle.getString("cmd_showSuggestions_accesskey")
-      );
+      if (AppConstants.platform == "macosx") {
+        this.textbox.addEventListener(
+          "keypress",
+          event => {
+            if (event.keyCode == KeyEvent.DOM_VK_F4) {
+              this.textbox.openSearch();
+            }
+          },
+          true
+        );
+      }
+
+      this.textbox.controllers.appendController(this.searchbarController);
 
-      this._menupopup.appendChild(frag);
+      let onpopupshowing = function() {
+        BrowserSearch.searchBar._textbox.closePopup();
+        if (suggestMenuItem) {
+          let enabled = Services.prefs.getBoolPref(
+            "browser.search.suggest.enabled"
+          );
+          suggestMenuItem.setAttribute("checked", enabled);
+        }
 
-      this._pasteAndSearchMenuItem = this._menupopup.querySelector(
-        '[anonid="paste-and-search"]'
-      );
-      this._suggestMenuItem = this._menupopup.querySelector(
-        '[cmd="cmd_togglesuggest"]'
-      );
+        if (!pasteAndSearch) {
+          return;
+        }
 
-      this._menupopup.addEventListener("command", cmdEvt => {
-        let cmd = cmdEvt.originalTarget.getAttribute("cmd");
-        if (cmd) {
-          let controller = document.commandDispatcher.getControllerForCommand(
-            cmd
-          );
-          controller.doCommand(cmd);
-          cmdEvt.stopPropagation();
-          cmdEvt.preventDefault();
+        let controller = document.commandDispatcher.getControllerForCommand(
+          "cmd_paste"
+        );
+        let enabled = controller.isCommandEnabled("cmd_paste");
+        if (enabled) {
+          pasteAndSearch.removeAttribute("disabled");
+        } else {
+          pasteAndSearch.setAttribute("disabled", "true");
         }
-      });
+      };
+      aMenu.addEventListener("popupshowing", onpopupshowing);
+      onpopupshowing();
     }
   }
 
   customElements.define("searchbar", MozSearchbar);
 }
--- a/browser/components/search/test/browser/browser_426329.js
+++ b/browser/components/search/test/browser/browser_426329.js
@@ -283,28 +283,22 @@ add_task(async function testSearchHistor
 });
 
 add_task(async function testAutocomplete() {
   let popup = searchBar.textbox.popup;
   let popupShownPromise = BrowserTestUtils.waitForEvent(popup, "popupshown");
   searchBar.textbox.showHistoryPopup();
   await popupShownPromise;
   checkMenuEntries(searchEntries);
-  searchBar.textbox.closePopup();
 });
 
 add_task(async function testClearHistory() {
   // Open the textbox context menu to trigger controller attachment.
   let textbox = searchBar.textbox;
-  let popupShownPromise = BrowserTestUtils.waitForEvent(
-    window,
-    "popupshown",
-    false,
-    event => event.target.classList.contains("textbox-contextmenu")
-  );
+  let popupShownPromise = BrowserTestUtils.waitForEvent(textbox, "popupshown");
   EventUtils.synthesizeMouseAtCenter(textbox, {
     type: "contextmenu",
     button: 2,
   });
   await popupShownPromise;
   // Close the context menu.
   EventUtils.synthesizeKey("KEY_Escape");
 
--- a/browser/components/search/test/browser/browser_searchbar_keyboard_navigation.js
+++ b/browser/components/search/test/browser/browser_searchbar_keyboard_navigation.js
@@ -169,17 +169,17 @@ add_task(async function test_arrows() {
     kUserValue,
     "the textfield value should be back to initial value"
   );
 });
 
 add_task(async function test_typing_clears_button_selection() {
   is(
     Services.focus.focusedElement,
-    textbox,
+    textbox.inputField,
     "the search bar should be focused"
   ); // from the previous test.
   ok(!textbox.selectedButton, "no button should be selected");
 
   EventUtils.synthesizeKey("KEY_ArrowUp");
   ok(
     textbox.selectedButton.classList.contains("search-setting-button"),
     "the settings item should be selected"
@@ -191,17 +191,17 @@ add_task(async function test_typing_clea
 
   // Remove the character.
   EventUtils.synthesizeKey("KEY_Backspace");
 });
 
 add_task(async function test_tab() {
   is(
     Services.focus.focusedElement,
-    textbox,
+    textbox.inputField,
     "the search bar should be focused"
   ); // from the previous test.
 
   let oneOffs = getOneOffs();
   ok(!textbox.selectedButton, "no one-off button should be selected");
 
   // Pressing tab should select the first one-off without selecting suggestions.
   // now cycle through the one-off items, the first one is already selected.
--- a/browser/components/search/test/browser/browser_searchbar_openpopup.js
+++ b/browser/components/search/test/browser/browser_searchbar_openpopup.js
@@ -171,17 +171,17 @@ add_task(async function open_empty() {
 
 // With no text in the search box left clicking it should not open the popup.
 add_no_popup_task(function click_doesnt_open_popup() {
   gURLBar.focus();
 
   EventUtils.synthesizeMouseAtCenter(textbox, {});
   is(
     Services.focus.focusedElement,
-    textbox,
+    textbox.inputField,
     "Should have focused the search bar"
   );
   is(textbox.selectionStart, 0, "Should have selected all of the text");
   is(textbox.selectionEnd, 0, "Should have selected all of the text");
 });
 
 // Left clicking in a non-empty search box when unfocused should focus it and open the popup.
 add_task(async function click_opens_popup() {
@@ -194,17 +194,17 @@ add_task(async function click_opens_popu
   isnot(
     searchPopup.getAttribute("showonlysettings"),
     "true",
     "Should show the full popup"
   );
 
   is(
     Services.focus.focusedElement,
-    textbox,
+    textbox.inputField,
     "Should have focused the search bar"
   );
   is(textbox.selectionStart, 0, "Should have selected all of the text");
   is(textbox.selectionEnd, 3, "Should have selected all of the text");
 
   promise = promiseEvent(searchPopup, "popuphidden");
   searchPopup.hidePopup();
   await promise;
@@ -212,32 +212,24 @@ add_task(async function click_opens_popu
   textbox.value = "";
 });
 
 // Right clicking in a non-empty search box when unfocused should open the edit context menu.
 add_no_popup_task(async function right_click_doesnt_open_popup() {
   gURLBar.focus();
   textbox.value = "foo";
 
-  // Can't wait for an event on the actual menu since it is created
-  // lazily the first time it is displayed.
-  let promise = new Promise(resolve => {
-    let listener = event => {
-      if (searchbar._menupopup && event.target == searchbar._menupopup) {
-        resolve(searchbar._menupopup);
-      }
-    };
-    window.addEventListener("popupshown", listener);
-  });
+  let contextPopup = textbox.inputField.parentNode.menupopup;
+  let promise = promiseEvent(contextPopup, "popupshown");
   context_click(textbox);
-  let contextPopup = await promise;
+  await promise;
 
   is(
     Services.focus.focusedElement,
-    textbox,
+    textbox.inputField,
     "Should have focused the search bar"
   );
   is(textbox.selectionStart, 0, "Should have selected all of the text");
   is(textbox.selectionEnd, 3, "Should have selected all of the text");
 
   promise = promiseEvent(contextPopup, "popuphidden");
   contextPopup.hidePopup();
   await promise;
@@ -256,17 +248,17 @@ add_task(async function focus_change_clo
   isnot(
     searchPopup.getAttribute("showonlysettings"),
     "true",
     "Should show the full popup"
   );
 
   is(
     Services.focus.focusedElement,
-    textbox,
+    textbox.inputField,
     "Should have focused the search bar"
   );
   is(textbox.selectionStart, 0, "Should have selected all of the text");
   is(textbox.selectionEnd, 3, "Should have selected all of the text");
 
   promise = promiseEvent(searchPopup, "popuphidden");
   let promise2 = promiseEvent(searchbar.textbox, "blur");
   EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
@@ -289,17 +281,17 @@ add_task(async function focus_change_clo
   is(
     searchPopup.getAttribute("showonlysettings"),
     "true",
     "Should show the small popup"
   );
 
   is(
     Services.focus.focusedElement,
-    textbox,
+    textbox.inputField,
     "Should have focused the search bar"
   );
 
   promise = promiseEvent(searchPopup, "popuphidden");
   let promise2 = promiseEvent(searchbar.textbox, "blur");
   EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
   await promise;
   await promise2;
@@ -316,17 +308,17 @@ add_task(async function escape_closes_po
   isnot(
     searchPopup.getAttribute("showonlysettings"),
     "true",
     "Should show the full popup"
   );
 
   is(
     Services.focus.focusedElement,
-    textbox,
+    textbox.inputField,
     "Should have focused the search bar"
   );
   is(textbox.selectionStart, 0, "Should have selected all of the text");
   is(textbox.selectionEnd, 3, "Should have selected all of the text");
 
   promise = promiseEvent(searchPopup, "popuphidden");
   EventUtils.synthesizeKey("KEY_Escape");
   await promise;
@@ -345,27 +337,33 @@ add_task(async function contextmenu_clos
   isnot(
     searchPopup.getAttribute("showonlysettings"),
     "true",
     "Should show the full popup"
   );
 
   is(
     Services.focus.focusedElement,
-    textbox,
+    textbox.inputField,
     "Should have focused the search bar"
   );
   is(textbox.selectionStart, 0, "Should have selected all of the text");
   is(textbox.selectionEnd, 3, "Should have selected all of the text");
 
   promise = promiseEvent(searchPopup, "popuphidden");
-  context_click(textbox);
+
+  // synthesizeKey does not work with VK_CONTEXT_MENU (bug 1127368)
+  EventUtils.synthesizeMouseAtCenter(textbox, {
+    type: "contextmenu",
+    button: null,
+  });
+
   await promise;
 
-  let contextPopup = searchbar._menupopup;
+  let contextPopup = textbox.inputField.parentNode.menupopup;
   promise = promiseEvent(contextPopup, "popuphidden");
   contextPopup.hidePopup();
   await promise;
 
   textbox.value = "";
 });
 
 // Tabbing to the search box should open the popup if it contains text.
@@ -381,17 +379,17 @@ add_task(async function tab_opens_popup(
   isnot(
     searchPopup.getAttribute("showonlysettings"),
     "true",
     "Should show the full popup"
   );
 
   is(
     Services.focus.focusedElement,
-    textbox,
+    textbox.inputField,
     "Should have focused the search bar"
   );
   is(textbox.selectionStart, 0, "Should have selected all of the text");
   is(textbox.selectionEnd, 3, "Should have selected all of the text");
 
   promise = promiseEvent(searchPopup, "popuphidden");
   searchPopup.hidePopup();
   await promise;
@@ -405,17 +403,17 @@ add_no_popup_task(function tab_doesnt_op
   textbox.value = "foo";
 
   // Extra tab stop for url buttons.
   EventUtils.synthesizeKey("KEY_Tab");
   EventUtils.synthesizeKey("KEY_Tab");
 
   is(
     Services.focus.focusedElement,
-    textbox,
+    textbox.inputField,
     "Should have focused the search bar"
   );
   is(textbox.selectionStart, 0, "Should have selected all of the text");
   is(textbox.selectionEnd, 3, "Should have selected all of the text");
 
   textbox.value = "";
 });
 
@@ -430,17 +428,17 @@ add_task(async function refocus_window_d
   isnot(
     searchPopup.getAttribute("showonlysettings"),
     "true",
     "Should show the full popup"
   );
 
   is(
     Services.focus.focusedElement,
-    textbox,
+    textbox.inputField,
     "Should have focused the search bar"
   );
   is(textbox.selectionStart, 0, "Should have selected all of the text");
   is(textbox.selectionEnd, 3, "Should have selected all of the text");
 
   promise = promiseEvent(searchPopup, "popuphidden");
   let newWin = OpenBrowserWindow();
   await new Promise(resolve => waitForFocus(resolve, newWin));
@@ -477,17 +475,17 @@ add_task(async function refocus_window_d
   isnot(
     searchPopup.getAttribute("showonlysettings"),
     "true",
     "Should show the full popup"
   );
 
   is(
     Services.focus.focusedElement,
-    textbox,
+    textbox.inputField,
     "Should have focused the search bar"
   );
   is(textbox.selectionStart, 0, "Should have selected all of the text");
   is(textbox.selectionEnd, 3, "Should have selected all of the text");
 
   promise = promiseEvent(searchPopup, "popuphidden");
   let newWin = OpenBrowserWindow();
   await new Promise(resolve => waitForFocus(resolve, newWin));
@@ -538,17 +536,17 @@ add_task(async function dont_consume_cli
   isnot(
     searchPopup.getAttribute("showonlysettings"),
     "true",
     "Should show the full popup"
   );
 
   is(
     Services.focus.focusedElement,
-    textbox,
+    textbox.inputField,
     "Should have focused the search bar"
   );
   is(textbox.selectionStart, 0, "Should have selected all of the text");
   is(textbox.selectionEnd, 3, "Should have selected all of the text");
 
   promise = promiseEvent(searchPopup, "popuphidden");
   await synthesizeNativeMouseClick(gURLBar.inputField);
   await promise;
@@ -576,31 +574,31 @@ add_task(async function drop_opens_popup
   await focusEventPromise;
 
   let promise = promiseEvent(searchPopup, "popupshown");
   // Use a source for the drop that is outside of the search bar area, to avoid
   // it receiving a mousedown and causing the popup to sometimes open.
   let homeButton = document.getElementById("home-button");
   EventUtils.synthesizeDrop(
     homeButton,
-    textbox,
+    textbox.inputField,
     [[{ type: "text/plain", data: "foo" }]],
     "move",
     window
   );
   await promise;
 
   isnot(
     searchPopup.getAttribute("showonlysettings"),
     "true",
     "Should show the full popup"
   );
   is(
     Services.focus.focusedElement,
-    textbox,
+    textbox.inputField,
     "Should have focused the search bar"
   );
   promise = promiseEvent(searchPopup, "popuphidden");
   searchPopup.hidePopup();
   await promise;
 
   textbox.value = "";
 });
--- a/browser/components/search/test/browser/browser_searchbar_smallpanel_keyboard_navigation.js
+++ b/browser/components/search/test/browser/browser_searchbar_smallpanel_keyboard_navigation.js
@@ -131,17 +131,17 @@ add_task(async function test_arrows() {
   ok(!textbox.selectedButton, "no one-off button should be selected");
   is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
   is(textbox.value, "", "the textfield value should be unmodified");
 });
 
 add_task(async function test_tab() {
   is(
     Services.focus.focusedElement,
-    textbox,
+    textbox.inputField,
     "the search bar should be focused"
   ); // from the previous test.
 
   let oneOffs = getOneOffs();
   ok(!textbox.selectedButton, "no one-off button should be selected");
 
   // Pressing tab should select the first one-off without selecting suggestions.
   // now cycle through the one-off items, the first one is already selected.
@@ -166,17 +166,17 @@ add_task(async function test_tab() {
   // Pressing tab again should close the panel...
   let promise = promiseEvent(searchPopup, "popuphidden");
   EventUtils.synthesizeKey("KEY_Tab");
   await promise;
 
   // ... and move the focus out of the searchbox.
   isnot(
     Services.focus.focusedElement,
-    textbox,
+    textbox.inputField,
     "the search bar no longer be focused"
   );
 });
 
 add_task(async function test_shift_tab() {
   // First reopen the panel.
   let promise = promiseEvent(searchPopup, "popupshown");
   info("Opening search panel");
@@ -220,17 +220,17 @@ add_task(async function test_shift_tab()
   // Pressing shift+tab again should close the panel...
   promise = promiseEvent(searchPopup, "popuphidden");
   EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
   await promise;
 
   // ... and move the focus out of the searchbox.
   isnot(
     Services.focus.focusedElement,
-    textbox,
+    textbox.inputField,
     "the search bar no longer be focused"
   );
 });
 
 add_task(async function test_alt_down() {
   // First reopen the panel.
   let promise = promiseEvent(searchPopup, "popupshown");
   info("Opening search panel");
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -309,28 +309,28 @@
   margin-bottom: -26px;
 }
 
 /* Bookmarking panel */
 
 %include ../shared/places/editBookmarkPanel.inc.css
 
 #editBookmarkPanelRows > vbox > textbox,
-#editBookmarkPanelRows > vbox > hbox > html|input {
+#editBookmarkPanelRows > vbox > hbox > textbox {
   -moz-appearance: none;
   background-color: var(--arrowpanel-field-background);
   color: inherit;
   border-radius: 2px;
   border: 1px solid var(--panel-separator-color) !important;
   margin: 0;
   padding: 3px 6px;
 }
 
 #editBookmarkPanelRows > vbox > textbox[focused="true"],
-#editBookmarkPanelRows > vbox > hbox > html|input:focus {
+#editBookmarkPanelRows > vbox > hbox > textbox[focused="true"] {
   border-color: -moz-mac-focusring !important;
   box-shadow: var(--focus-ring-box-shadow);
 }
 
 /* The following elements come from editBookmarkPanel.inc.xul. Styling that's
    specific to the editBookmarkPanel should be in browser.css. Styling that
    should be shared by all editBookmarkPanel.inc.xul consumers should be in
    editBookmark.css. */
--- a/browser/themes/shared/urlbar-searchbar.inc.css
+++ b/browser/themes/shared/urlbar-searchbar.inc.css
@@ -42,22 +42,19 @@
   box-shadow: 0 1px 4px rgba(0,0,0,.05);
   min-height: 30px;
   overflow: -moz-hidden-unscrollable;
   text-shadow: none;
 }
 
 .searchbar-textbox {
   -moz-appearance: none;
-  -moz-box-flex: 1;
   background: none;
   color: inherit;
-  border: none;
-  font: inherit;
-  margin: 0 !important; /* override autocomplete.css */
+  margin: 0;
 }
 
 #urlbar:hover,
 #searchbar:hover {
   border-color: @fieldHoverBorderColor@;
   box-shadow: 0 1px 6px rgba(0,0,0,.1);
 }
 
--- a/toolkit/content/customElements.js
+++ b/toolkit/content/customElements.js
@@ -1,15 +1,15 @@
 /* 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/. */
 
 // This file defines these globals on the window object.
 // Define them here so that ESLint can find them:
-/* globals MozXULElement, MozHTMLElement, MozElements */
+/* globals MozXULElement, MozElements */
 
 "use strict";
 
 // This is loaded into chrome windows with the subscript loader. Wrap in
 // a block to prevent accidentally leaking globals onto `window`.
 (() => {
   // Handle customElements.js being loaded as a script in addition to the subscriptLoader
   // from MainProcessSingleton, to handle pages that can open both before and after
@@ -611,17 +611,16 @@
     Object.defineProperty(MozElementBase, "name", { value: `Moz${Base.name}` });
     if (instrumentedBaseClasses) {
       instrumentedBaseClasses.add(MozElementBase);
     }
     return MozElementBase;
   };
 
   const MozXULElement = MozElements.MozElementMixin(XULElement);
-  const MozHTMLElement = MozElements.MozElementMixin(HTMLElement);
 
   /**
    * Given an object, add a proxy that reflects interface implementations
    * onto the object itself.
    */
   function getInterfaceProxy(obj) {
     /* globals MozQueryInterface */
     if (!obj._customInterfaceProxy) {
@@ -733,17 +732,16 @@
           : this.getAttribute("accesskey");
       }
     };
   MozElements.BaseTextMixin = BaseTextMixin;
   MozElements.BaseText = BaseTextMixin(MozXULElement);
 
   // Attach the base class to the window so other scripts can use it:
   window.MozXULElement = MozXULElement;
-  window.MozHTMLElement = MozHTMLElement;
 
   customElements.setElementCreationCallback("browser", () => {
     Services.scriptloader.loadSubScript(
       "chrome://global/content/elements/browser-custom-element.js",
       window
     );
   });
 
@@ -774,20 +772,16 @@
     ]) {
       Services.scriptloader.loadSubScript(script, window);
     }
 
     for (let [tag, script] of [
       ["findbar", "chrome://global/content/elements/findbar.js"],
       ["menulist", "chrome://global/content/elements/menulist.js"],
       ["search-textbox", "chrome://global/content/elements/search-textbox.js"],
-      [
-        "autocomplete-input",
-        "chrome://global/content/elements/autocomplete-input.js",
-      ],
       ["stringbundle", "chrome://global/content/elements/stringbundle.js"],
       [
         "printpreview-toolbar",
         "chrome://global/content/printPreviewToolbar.js",
       ],
       ["editor", "chrome://global/content/elements/editor.js"],
     ]) {
       customElements.setElementCreationCallback(tag, () => {
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -56,28 +56,28 @@ toolkit.jar:
    content/global/resetProfileProgress.xul
    content/global/TopLevelVideoDocument.js
    content/global/timepicker.xhtml
    content/global/treeUtils.js
 #ifndef MOZ_FENNEC
    content/global/viewZoomOverlay.js
 #endif
    content/global/widgets.css
+   content/global/bindings/autocomplete.xml    (widgets/autocomplete.xml)
    content/global/bindings/calendar.js         (widgets/calendar.js)
    content/global/bindings/datekeeper.js       (widgets/datekeeper.js)
    content/global/bindings/datepicker.js       (widgets/datepicker.js)
    content/global/bindings/datetimebox.css     (widgets/datetimebox.css)
    content/global/bindings/general.xml         (widgets/general.xml)
    content/global/bindings/popup.xml           (widgets/popup.xml)
    content/global/bindings/scrollbox.xml       (widgets/scrollbox.xml)
    content/global/bindings/spinner.js          (widgets/spinner.js)
    content/global/bindings/textbox.xml         (widgets/textbox.xml)
    content/global/bindings/timekeeper.js       (widgets/timekeeper.js)
    content/global/bindings/timepicker.js       (widgets/timepicker.js)
-   content/global/elements/autocomplete-input.js              (widgets/autocomplete-input.js)
    content/global/elements/autocomplete-popup.js              (widgets/autocomplete-popup.js)
    content/global/elements/autocomplete-richlistitem.js       (widgets/autocomplete-richlistitem.js)
    content/global/elements/browser-custom-element.js          (widgets/browser-custom-element.js)
    content/global/elements/button.js           (widgets/button.js)
    content/global/elements/checkbox.js         (widgets/checkbox.js)
    content/global/elements/datetimebox.js      (widgets/datetimebox.js)
    content/global/elements/dialog.js           (widgets/dialog.js)
    content/global/elements/findbar.js          (widgets/findbar.js)
--- a/toolkit/content/tests/chrome/chrome.ini
+++ b/toolkit/content/tests/chrome/chrome.ini
@@ -59,16 +59,17 @@ support-files =
 [test_arrowpanel.xul]
 skip-if = (verify && (os == 'win')) || (debug && (os == 'win') && (bits == 32)) # Bug 1546256
 [test_autocomplete2.xul]
 [test_autocomplete3.xul]
 [test_autocomplete4.xul]
 [test_autocomplete5.xul]
 [test_autocomplete_emphasis.xul]
 [test_autocomplete_with_composition_on_input.html]
+[test_autocomplete_with_composition_on_textbox.xul]
 [test_autocomplete_placehold_last_complete.xul]
 [test_browser_drop.xul]
 [test_bug253481.xul]
 tags = clipboard
 [test_bug263683.xul]
 skip-if = debug && (os == 'win' || os == 'linux')
 [test_bug304188.xul]
 skip-if = webrender
@@ -103,16 +104,17 @@ skip-if = toolkit == "cocoa"
 [test_contextmenu_list.xul]
 [test_custom_element_base.xul]
 [test_custom_element_delay_connection.xul]
 [test_custom_element_parts.html]
 [test_deck.xul]
 [test_dialogfocus.xul]
 [test_edit_contextmenu.html]
 [test_editor_for_input_with_autocomplete.html]
+[test_editor_for_textbox_with_autocomplete.xul]
 [test_findbar.xul]
 tags = clipboard
 skip-if = os == 'mac' && os_version == '10.14' # macosx1014 due to 1550078
 [test_findbar_entireword.xul]
 [test_findbar_events.xul]
 [test_focus_anons.xul]
 [test_frames.xul]
 [test_hiddenitems.xul]
--- a/toolkit/content/tests/chrome/test_autocomplete2.xul
+++ b/toolkit/content/tests/chrome/test_autocomplete2.xul
@@ -1,22 +1,20 @@
 <?xml version="1.0"?>
 <?xml-stylesheet href="chrome://global/skin" type="text/css"?>
 <?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
 
 <window title="Autocomplete Widget Test 2"
-        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-        xmlns:html="http://www.w3.org/1999/xhtml">
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
-<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
-<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+  <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
 
-<html:input id="autocomplete"
-            is="autocomplete-input"
-            autocompletesearch="simple"/>
+<textbox id="autocomplete" type="autocomplete"
+         autocompletesearch="simple"/>
 
 <script class="testbody" type="application/javascript">
 <![CDATA[
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 // Set to indicate whether or not we want autoCompleteSimple to return a result
 var returnResult = false;
--- a/toolkit/content/tests/chrome/test_autocomplete3.xul
+++ b/toolkit/content/tests/chrome/test_autocomplete3.xul
@@ -1,22 +1,20 @@
 <?xml version="1.0"?>
 <?xml-stylesheet href="chrome://global/skin" type="text/css"?>
 <?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
 
 <window title="Autocomplete Widget Test 3"
-        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-        xmlns:html="http://www.w3.org/1999/xhtml">
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
   <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
 
-<html:input id="autocomplete"
-            is="autocomplete-input"
-            autocompletesearch="simple"/>
+<textbox id="autocomplete" type="autocomplete"
+         autocompletesearch="simple"/>
 
 <script class="testbody" type="application/javascript">
 <![CDATA[
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 // Set to indicate whether or not we want autoCompleteSimple to return a result
 var returnResult = true;
--- a/toolkit/content/tests/chrome/test_autocomplete4.xul
+++ b/toolkit/content/tests/chrome/test_autocomplete4.xul
@@ -1,23 +1,22 @@
 <?xml version="1.0"?>
 <?xml-stylesheet href="chrome://global/skin" type="text/css"?>
 <?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
 
 <window title="Autocomplete Widget Test 4"
-        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-        xmlns:html="http://www.w3.org/1999/xhtml">
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
   <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
 
-<html:input id="autocomplete"
-            is="autocomplete-input"
-            completedefaultindex="true"
-            autocompletesearch="simple"/>
+<textbox id="autocomplete"
+         type="autocomplete"
+         completedefaultindex="true"
+         autocompletesearch="simple"/>
 
 <script class="testbody" type="application/javascript">
 <![CDATA[
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 // Set to indicate whether or not we want autoCompleteSimple to return a result
 var returnResult = true;
--- a/toolkit/content/tests/chrome/test_autocomplete5.xul
+++ b/toolkit/content/tests/chrome/test_autocomplete5.xul
@@ -1,23 +1,21 @@
 <?xml version="1.0"?>
 <?xml-stylesheet href="chrome://global/skin" type="text/css"?>
 <?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
 
 <window title="Autocomplete Widget Test 5"
-        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-        xmlns:html="http://www.w3.org/1999/xhtml">
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
   <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
 
-<html:input id="autocomplete"
-            is="autocomplete-input"
-            autocompletesearch="simple"
-            notifylegacyevents="true"/>
+<textbox id="autocomplete" type="autocomplete"
+         autocompletesearch="simple"
+         notifylegacyevents="true"/>
 
 <script class="testbody" type="application/javascript">
 <![CDATA[
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 const ACR = Ci.nsIAutoCompleteResult;
 
--- a/toolkit/content/tests/chrome/test_autocomplete_emphasis.xul
+++ b/toolkit/content/tests/chrome/test_autocomplete_emphasis.xul
@@ -1,23 +1,21 @@
 <?xml version="1.0"?>
 <?xml-stylesheet href="chrome://global/skin" type="text/css"?>
 <?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
 
 <window title="Autocomplete emphasis test"
-        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-        xmlns:html="http://www.w3.org/1999/xhtml">
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
   <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
 
-<html:input id="richautocomplete"
-            is="autocomplete-input"
-            autocompletesearch="simple"
-            autocompletepopup="richpopup"/>
+<textbox id="richautocomplete" type="autocomplete"
+         autocompletesearch="simple"
+         autocompletepopup="richpopup"/>
 <panel is="autocomplete-richlistbox-popup"
        id="richpopup"
        type="autocomplete-richlistbox"
        noautofocus="true"/>
 
 <script class="testbody" type="application/javascript">
 <![CDATA[
 
--- a/toolkit/content/tests/chrome/test_autocomplete_mac_caret.xul
+++ b/toolkit/content/tests/chrome/test_autocomplete_mac_caret.xul
@@ -1,21 +1,20 @@
 <?xml version="1.0"?>
 <?xml-stylesheet href="chrome://global/skin" type="text/css"?>
 <?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
 
 <window title="Autocomplete Widget Test"
         onload="setTimeout(keyCaretTest, 0);"
-        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-        xmlns:html="http://www.w3.org/1999/xhtml">
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
   <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
 
-<html:input id="autocomplete" is="autocomplete-input"/>
+<textbox id="autocomplete" type="autocomplete"/>
 
 <script class="testbody" type="application/javascript">
 <![CDATA[
 
 SimpleTest.waitForExplicitFinish();
 
 function keyCaretTest()
 {
@@ -47,24 +46,20 @@ function keyCaretTest()
 
   SimpleTest.finish();
 }
 
 function checkKeyCaretTest(key, expectedStart, expectedEnd, result, testid)
 {
   var autocomplete = $("autocomplete");
   var keypressFired = false;
-  function listener(event) {
-    if (event.target == autocomplete) {
-      keypressFired = true;
-    }
-  }
-  SpecialPowers.addSystemEventListener(window, "keypress", listener, false);
+  SpecialPowers.addSystemEventListener(autocomplete.inputField, "keypress", () => {
+    keypressFired = true;
+  }, {once: true});
   synthesizeKey(key, {});
-  SpecialPowers.removeSystemEventListener(window, "keypress", listener, false);
   is(keypressFired, result, `${testid} keypress event should${result ? "" : " not"} be fired`);
   is(autocomplete.selectionStart, expectedStart, testid + " selectionStart");
   is(autocomplete.selectionEnd, expectedEnd, testid + " selectionEnd");
 }
 
 ]]>
 </script>
 
--- a/toolkit/content/tests/chrome/test_autocomplete_placehold_last_complete.xul
+++ b/toolkit/content/tests/chrome/test_autocomplete_placehold_last_complete.xul
@@ -1,27 +1,26 @@
 <?xml version="1.0"?>
 <?xml-stylesheet href="chrome://global/skin" type="text/css"?>
 <?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
 
 <window title="Autocomplete Widget Test"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-        xmlns:html="http://www.w3.org/1999/xhtml"
         onload="runTest();">
 
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
   <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
   <script type="application/javascript"
           src="chrome://global/content/globalOverlay.js"/>
 
-<html:input id="autocomplete"
-            is="autocomplete-input"
-            completedefaultindex="true"
-            timeout="0"
-            autocompletesearch="simple"/>
+<textbox id="autocomplete"
+         type="autocomplete"
+         completedefaultindex="true"
+         timeout="0"
+         autocompletesearch="simple"/>
 
 <script class="testbody" type="application/javascript">
 <![CDATA[
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 function autoCompleteSimpleResult(aString, searchId) {
new file mode 100644
--- /dev/null
+++ b/toolkit/content/tests/chrome/test_autocomplete_with_composition_on_textbox.xul
@@ -0,0 +1,122 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+                 type="text/css"?>
+<window title="Testing autocomplete with composition"
+  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+  <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" />
+  <script type="text/javascript"
+          src="file_autocomplete_with_composition.js" />
+
+  <textbox id="textbox" type="autocomplete"
+           autocompletesearch="simpleForComposition"/>
+
+<body  xmlns="http://www.w3.org/1999/xhtml">
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+
+const nsIAutoCompleteResult = Ci.nsIAutoCompleteResult;
+
+// This result can't be constructed in-line, because otherwise we leak memory.
+function nsAutoCompleteSimpleResult(aString)
+{
+  this.searchString = aString;
+  if (aString == "" || aString == "Mozilla".substr(0, aString.length)) {
+    this.searchResult = nsIAutoCompleteResult.RESULT_SUCCESS;
+    this.matchCount = 1;
+    this._value = "Mozilla";
+  } else {
+    this.searchResult = nsIAutoCompleteResult.RESULT_NOMATCH;
+    this.matchCount = 0;
+    this._value = "";
+  }
+}
+
+nsAutoCompleteSimpleResult.prototype = {
+ _value: "",
+ searchString: null,
+ searchResult: nsIAutoCompleteResult.RESULT_FAILURE,
+ defaultIndex: 0,
+ errorDescription: null,
+ matchCount: 0,
+ getValueAt: function(aIndex) { return aIndex == 0 ? this._value : null; },
+ getCommentAt: function() { return null; },
+ getStyleAt: function() { return null; },
+ getImageAt: function() { return null; },
+ getFinalCompleteValueAt: function(aIndex) { return this.getValueAt(aIndex); },
+ getLabelAt: function() { return null; },
+ removeValueAt: function() {}
+};
+
+// A basic autocomplete implementation that either returns one result or none
+var autoCompleteSimpleID =
+  Components.ID("0a2afbdb-f30e-47d1-9cb1-0cd160240aca");
+var autoCompleteSimpleName =
+  "@mozilla.org/autocomplete/search;1?name=simpleForComposition"
+var autoCompleteSimple = {
+  QueryInterface: function(iid) {
+    if (iid.equals(Ci.nsISupports) ||
+        iid.equals(Ci.nsIFactory) ||
+        iid.equals(Ci.nsIAutoCompleteSearch))
+      return this;
+
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  },
+
+  createInstance: function(outer, iid) {
+    return this.QueryInterface(iid);
+  },
+
+  startSearch: function(aString, aParam, aResult, aListener) {
+    var result = new nsAutoCompleteSimpleResult(aString);
+    aListener.onSearchResult(this, result);
+  },
+
+  stopSearch: function() {}
+};
+
+var componentManager =
+  Components.manager
+            .QueryInterface(Ci.nsIComponentRegistrar);
+componentManager.registerFactory(autoCompleteSimpleID,
+                                 "Test Simple Autocomplete for composition",
+                                 autoCompleteSimpleName, autoCompleteSimple);
+
+function runTests()
+{
+  var target = document.getElementById("textbox");
+  target.setAttribute("timeout", 1);
+  var test1 = new nsDoTestsForAutoCompleteWithComposition(
+    "Testing on XUL textbox (asynchronously search)",
+    window, target, target.controller, is,
+    function () { return target.value; },
+    function () {
+      target.setAttribute("timeout", 0);
+      var test2 = new nsDoTestsForAutoCompleteWithComposition(
+        "Testing on XUL textbox (synchronously search)",
+        window, target, target.controller, is,
+        function () { return target.value; },
+        function () {
+          // Unregister the factory so that we don't get in the way of other
+          // tests
+          componentManager.unregisterFactory(autoCompleteSimpleID,
+                                             autoCompleteSimple);
+          SimpleTest.finish();
+        });
+    });
+}
+
+SimpleTest.waitForFocus(runTests);
+]]>
+</script>
+</window>
new file mode 100644
--- /dev/null
+++ b/toolkit/content/tests/chrome/test_editor_for_textbox_with_autocomplete.xul
@@ -0,0 +1,124 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+                 type="text/css"?>
+<window title="Basic editor behavior for XUL textbox element with autocomplete"
+  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+  <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" />
+  <script type="text/javascript"
+          src="file_editor_with_autocomplete.js" />
+
+  <textbox id="textbox" type="autocomplete"
+           autocompletesearch="simpleForComposition"/>
+
+<body  xmlns="http://www.w3.org/1999/xhtml">
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+
+const nsIAutoCompleteResult = Ci.nsIAutoCompleteResult;
+
+// This result can't be constructed in-line, because otherwise we leak memory.
+function nsAutoCompleteSimpleResult(aString)
+{
+  this.searchString = aString;
+  if (aString == "" ||
+      aString.toLowerCase() == "mozilla".substr(0, aString.length)) {
+    this.searchResult = nsIAutoCompleteResult.RESULT_SUCCESS;
+    this.matchCount = 1;
+    this._value = "Mozilla";
+  } else {
+    this.searchResult = nsIAutoCompleteResult.RESULT_NOMATCH;
+    this.matchCount = 0;
+    this._value = "";
+  }
+}
+
+nsAutoCompleteSimpleResult.prototype = {
+ _value: "",
+ searchString: null,
+ searchResult: nsIAutoCompleteResult.RESULT_FAILURE,
+ defaultIndex: 0,
+ errorDescription: null,
+ matchCount: 0,
+ getValueAt: function(aIndex) { return aIndex == 0 ? this._value : null; },
+ getCommentAt: function() { return null; },
+ getStyleAt: function() { return null; },
+ getImageAt: function() { return null; },
+ getFinalCompleteValueAt: function(aIndex) { return this.getValueAt(aIndex); },
+ getLabelAt: function() { return null; },
+ removeValueAt: function() {}
+};
+
+// A basic autocomplete implementation that either returns one result or none
+var autoCompleteSimpleID =
+  Components.ID("0a2afbdb-f30e-47d1-9cb1-0cd160240aca");
+var autoCompleteSimpleName =
+  "@mozilla.org/autocomplete/search;1?name=simpleForComposition"
+var autoCompleteSimple = {
+  QueryInterface: function(iid) {
+    if (iid.equals(Ci.nsISupports) ||
+        iid.equals(Ci.nsIFactory) ||
+        iid.equals(Ci.nsIAutoCompleteSearch))
+      return this;
+
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  },
+
+  createInstance: function(outer, iid) {
+    return this.QueryInterface(iid);
+  },
+
+  startSearch: function(aString, aParam, aResult, aListener) {
+    var result = new nsAutoCompleteSimpleResult(aString);
+    aListener.onSearchResult(this, result);
+  },
+
+  stopSearch: function() {}
+};
+
+var componentManager =
+  Components.manager
+            .QueryInterface(Ci.nsIComponentRegistrar);
+componentManager.registerFactory(autoCompleteSimpleID,
+                                 "Test Simple Autocomplete for composition",
+                                 autoCompleteSimpleName, autoCompleteSimple);
+
+async function runTests()
+{
+  var target = document.getElementById("textbox");
+
+  target.setAttribute("timeout", 1);
+  let tests1 = new nsDoTestsForEditorWithAutoComplete(
+    "Testing on XUL textbox (asynchronously search)",
+    window, target, target.controller, is, todo_is,
+    function() { return target.value; });
+  await tests1.run();
+
+  target.setAttribute("timeout", 0);
+  let tests2 = new nsDoTestsForEditorWithAutoComplete(
+        "Testing on XUL textbox (synchronously search)",
+        window, target, target.controller, is, todo_is,
+        function() { return target.value; });
+  await tests2.run();
+
+  // Unregister the factory so that we don't get in the way of other
+  // tests
+  componentManager.unregisterFactory(autoCompleteSimpleID,
+                                     autoCompleteSimple);
+  SimpleTest.finish();
+}
+
+SimpleTest.waitForFocus(runTests);
+]]>
+</script>
+</window>
rename from toolkit/content/widgets/autocomplete-input.js
rename to toolkit/content/widgets/autocomplete.xml
--- a/toolkit/content/widgets/autocomplete-input.js
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -1,683 +1,595 @@
-/* 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/. */
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
-"use strict";
+<bindings id="autocompleteBindings"
+          xmlns="http://www.mozilla.org/xbl"
+          xmlns:html="http://www.w3.org/1999/xhtml"
+          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+          xmlns:xbl="http://www.mozilla.org/xbl">
 
-// This is loaded into all XUL windows. Wrap in a block to prevent
-// leaking to window scope.
-{
-  const { AppConstants } = ChromeUtils.import(
-    "resource://gre/modules/AppConstants.jsm"
-  );
+  <binding id="autocomplete"
+           extends="chrome://global/content/bindings/textbox.xml#textbox">
+    <content sizetopopup="pref">
+      <xul:moz-input-box anonid="moz-input-box" flex="1">
+        <html:input anonid="input" class="textbox-input"
+                    allowevents="true"
+                    autocomplete="off"
+                    xbl:inherits="value,type=inputtype,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,mozactionhint"/>
+      </xul:moz-input-box>
+      <xul:popupset anonid="popupset" class="autocomplete-result-popupset"/>
+    </content>
 
-  class AutocompleteInput extends HTMLInputElement {
-    constructor() {
-      super();
+    <implementation implements="nsIAutoCompleteInput, nsIDOMXULMenuListElement">
+      <field name="mController">null</field>
+      <field name="mSearchNames">null</field>
+      <field name="mIgnoreInput">false</field>
+      <field name="noRollupOnEmptySearch">false</field>
+
+      <constructor><![CDATA[
+        this.mController = Cc["@mozilla.org/autocomplete/controller;1"].
+          getService(Ci.nsIAutoCompleteController);
+      ]]></constructor>
+
+      <!-- =================== nsIAutoCompleteInput =================== -->
 
-      ChromeUtils.defineModuleGetter(
-        this,
-        "PrivateBrowsingUtils",
-        "resource://gre/modules/PrivateBrowsingUtils.jsm"
-      );
+      <field name="_popup">null</field>
+      <property name="popup" readonly="true">
+        <getter><![CDATA[
+          // Memoize the result in a field rather than replacing this property,
+          // so that it can be reset along with the binding.
+          if (this._popup) {
+            return this._popup;
+          }
 
-      this.addEventListener("input", event => {
-        this.onInput(event);
-      });
+          let popup = null;
+          let popupId = this.getAttribute("autocompletepopup");
+          if (popupId) {
+            popup = document.getElementById(popupId);
+          }
+          if (!popup) {
+            popup = document.createXULElement("panel", { is: "autocomplete-richlistbox-popup" });
+            popup.setAttribute("type", "autocomplete-richlistbox");
+            popup.setAttribute("noautofocus", "true");
 
-      this.addEventListener("keypress", event => this.handleKeyPress(event), {
-        capture: true,
-        mozSystemGroup: true,
-      });
+            let popupset = document.getAnonymousElementByAttribute(this, "anonid", "popupset");
+            popupset.appendChild(popup);
+          }
+          popup.mInput = this;
 
-      this.addEventListener(
-        "compositionstart",
-        event => {
-          if (
-            this.mController.input.wrappedJSObject == this.nsIAutocompleteInput
-          ) {
-            this.mController.handleStartComposition();
-          }
-        },
-        true
-      );
+          return this._popup = popup;
+        ]]></getter>
+      </property>
+      <property name="popupElement" readonly="true"
+                onget="return this.popup;"/>
+
+      <property name="controller" onget="return this.mController;" readonly="true"/>
+
+      <property name="popupOpen"
+                onget="return this.popup.popupOpen;"
+                onset="if (val) this.openPopup(); else this.closePopup();"/>
+
+      <property name="disableAutoComplete"
+                onset="this.setAttribute('disableautocomplete', val); return val;"
+                onget="return this.getAttribute('disableautocomplete') == 'true';"/>
 
-      this.addEventListener(
-        "compositionend",
-        event => {
-          if (
-            this.mController.input.wrappedJSObject == this.nsIAutocompleteInput
-          ) {
-            this.mController.handleEndComposition();
-          }
-        },
-        true
-      );
+      <property name="completeDefaultIndex"
+                onset="this.setAttribute('completedefaultindex', val); return val;"
+                onget="return this.getAttribute('completedefaultindex') == 'true';"/>
+
+      <property name="completeSelectedIndex"
+                onset="this.setAttribute('completeselectedindex', val); return val;"
+                onget="return this.getAttribute('completeselectedindex') == 'true';"/>
+
+      <property name="forceComplete"
+                onset="this.setAttribute('forcecomplete', val); return val;"
+                onget="return this.getAttribute('forcecomplete') == 'true';"/>
+
+      <property name="minResultsForPopup"
+                onset="this.setAttribute('minresultsforpopup', val); return val;"
+                onget="var m = parseInt(this.getAttribute('minresultsforpopup')); return isNaN(m) ? 1 : m;"/>
+
+      <property name="timeout"
+                onset="this.setAttribute('timeout', val); return val;"
+                onget="var t = parseInt(this.getAttribute('timeout')); return isNaN(t) ? 50 : t;"/>
+
+      <property name="searchParam"
+                onget="return this.getAttribute('autocompletesearchparam') || '';"
+                onset="this.setAttribute('autocompletesearchparam', val); return val;"/>
+
+      <property name="searchCount" readonly="true"
+                onget="this.initSearchNames(); return this.mSearchNames.length;"/>
 
-      this.addEventListener(
-        "focus",
-        event => {
-          this.attachController();
-          if (
-            window.gBrowser &&
-            window.gBrowser.selectedBrowser.hasAttribute("usercontextid")
-          ) {
-            this.userContextId = parseInt(
-              window.gBrowser.selectedBrowser.getAttribute("usercontextid")
-            );
-          } else {
-            this.userContextId = 0;
+      <property name="PrivateBrowsingUtils" readonly="true">
+        <getter><![CDATA[
+          let module = {};
+          ChromeUtils.import("resource://gre/modules/PrivateBrowsingUtils.jsm", module);
+          Object.defineProperty(this, "PrivateBrowsingUtils", {
+            configurable: true,
+            enumerable: true,
+            writable: true,
+            value: module.PrivateBrowsingUtils,
+          });
+          return module.PrivateBrowsingUtils;
+        ]]></getter>
+      </property>
+
+      <property name="inPrivateContext" readonly="true"
+                onget="return this.PrivateBrowsingUtils.isWindowPrivate(window);"/>
+
+      <property name="noRollupOnCaretMove" readonly="true"
+                onget="return this.popup.getAttribute('norolluponanchor') == 'true'"/>
+
+      <!-- This is the maximum number of drop-down rows we get when we
+            hit the drop marker beside fields that have it (like the URLbar).-->
+      <field name="maxDropMarkerRows" readonly="true">14</field>
+
+      <method name="getSearchAt">
+        <parameter name="aIndex"/>
+        <body><![CDATA[
+          this.initSearchNames();
+          return this.mSearchNames[aIndex];
+        ]]></body>
+      </method>
+
+      <method name="setTextValueWithReason">
+        <parameter name="aValue"/>
+        <parameter name="aReason"/>
+        <body><![CDATA[
+          if (aReason == Ci.nsIAutoCompleteInput
+                           .TEXTVALUE_REASON_COMPLETEDEFAULT) {
+            this._textValueSetByCompleteDefault = true;
           }
-        },
-        true
-      );
+          this.textValue = aValue;
+          this._textValueSetByCompleteDefault = false;
+        ]]></body>
+      </method>
 
-      this.addEventListener(
-        "blur",
-        event => {
-          if (!this._dontBlur) {
-            if (this.forceComplete && this.mController.matchCount >= 1) {
-              // If forceComplete is requested, we need to call the enter processing
-              // on blur so the input will be forced to the closest match.
-              // Thunderbird is the only consumer of forceComplete and this is used
-              // to force an recipient's email to the exact address book entry.
-              this.mController.handleEnter(true);
-            }
-            if (!this.ignoreBlurWhileSearching) {
-              this.detachController();
+      <property name="textValue">
+        <getter><![CDATA[
+          if (typeof this.onBeforeTextValueGet == "function") {
+            let result = this.onBeforeTextValueGet();
+            if (result) {
+              return result.value;
             }
           }
-        },
-        true
-      );
-    }
-
-    connectedCallback() {
-      this.setAttribute("is", "autocomplete-input");
-      this.setAttribute("autocomplete", "off");
-
-      this.mController = Cc[
-        "@mozilla.org/autocomplete/controller;1"
-      ].getService(Ci.nsIAutoCompleteController);
-      this.mSearchNames = null;
-      this.mIgnoreInput = false;
-      this.noRollupOnEmptySearch = false;
-
-      this._popup = null;
-
-      /**
-       * This is the maximum number of drop-down rows we get when we
-       * hit the drop marker beside fields that have it (like the URLbar).
-       */
-      this.maxDropMarkerRows = 14;
-
-      this.nsIAutocompleteInput = this.getCustomInterfaceCallback(
-        Ci.nsIAutoCompleteInput
-      );
+          return this.value;
+        ]]></getter>
+        <setter><![CDATA[
+          if (typeof this.onBeforeTextValueSet == "function" &&
+              !this._textValueSetByCompleteDefault) {
+            val = this.onBeforeTextValueSet(val);
+          }
 
-      this.valueIsTyped = false;
-      this._textValueSetByCompleteDefault = false;
-
-      this._selectionDetails = null;
-    }
-
-    get popup() {
-      // Memoize the result in a field rather than replacing this property,
-      // so that it can be reset along with the binding.
-      if (this._popup) {
-        return this._popup;
-      }
+          // "input" event is automatically dispatched by the editor if
+          // necessary.
+          this._setValueInternal(val, true);
 
-      let popup = null;
-      let popupId = this.getAttribute("autocompletepopup");
-      if (popupId) {
-        popup = document.getElementById(popupId);
-      }
-
-      /* This path is only used in tests, we have the <popupset> and <panel>
-         in document for other usages */
-      if (!popup) {
-        popup = document.createXULElement("panel", {
-          is: "autocomplete-richlistbox-popup",
-        });
-        popup.setAttribute("type", "autocomplete-richlistbox");
-        popup.setAttribute("noautofocus", "true");
+          return this.value;
+        ]]></setter>
+      </property>
 
-        if (!this._popupset) {
-          this._popupset = document.createXULElement("popupset");
-          document.documentElement.appendChild(this._popupset);
-        }
-
-        this._popupset.appendChild(popup);
-      }
-      popup.mInput = this;
-
-      return (this._popup = popup);
-    }
-
-    get popupElement() {
-      return this.popup;
-    }
-
-    get controller() {
-      return this.mController;
-    }
-
-    set popupOpen(val) {
-      if (val) {
-        this.openPopup();
-      } else {
-        this.closePopup();
-      }
-    }
+      <method name="selectTextRange">
+        <parameter name="aStartIndex"/>
+        <parameter name="aEndIndex"/>
+        <body><![CDATA[
+          this.inputField.setSelectionRange(aStartIndex, aEndIndex);
+        ]]></body>
+      </method>
 
-    get popupOpen() {
-      return this.popup.popupOpen;
-    }
-
-    set disableAutoComplete(val) {
-      this.setAttribute("disableautocomplete", val);
-      return val;
-    }
-
-    get disableAutoComplete() {
-      return this.getAttribute("disableautocomplete") == "true";
-    }
+      <method name="onSearchBegin">
+        <body><![CDATA[
+          if (this.popup && typeof this.popup.onSearchBegin == "function")
+            this.popup.onSearchBegin();
+        ]]></body>
+      </method>
 
-    set completeDefaultIndex(val) {
-      this.setAttribute("completedefaultindex", val);
-      return val;
-    }
-
-    get completeDefaultIndex() {
-      return this.getAttribute("completedefaultindex") == "true";
-    }
+      <method name="onSearchComplete">
+        <body><![CDATA[
+          if (this.mController.matchCount == 0)
+            this.setAttribute("nomatch", "true");
+          else
+            this.removeAttribute("nomatch");
 
-    set completeSelectedIndex(val) {
-      this.setAttribute("completeselectedindex", val);
-      return val;
-    }
-
-    get completeSelectedIndex() {
-      return this.getAttribute("completeselectedindex") == "true";
-    }
+          if (this.ignoreBlurWhileSearching && !this.focused) {
+            this.handleEnter();
+            this.detachController();
+          }
+        ]]></body>
+      </method>
 
-    set forceComplete(val) {
-      this.setAttribute("forcecomplete", val);
-      return val;
-    }
-
-    get forceComplete() {
-      return this.getAttribute("forcecomplete") == "true";
-    }
-
-    set minResultsForPopup(val) {
-      this.setAttribute("minresultsforpopup", val);
-      return val;
-    }
-
-    get minResultsForPopup() {
-      var m = parseInt(this.getAttribute("minresultsforpopup"));
-      return isNaN(m) ? 1 : m;
-    }
-
-    set timeout(val) {
-      this.setAttribute("timeout", val);
-      return val;
-    }
+      <method name="onTextEntered">
+        <parameter name="event"/>
+        <body><![CDATA[
+          if (this.getAttribute("notifylegacyevents") === "true") {
+            let e = new CustomEvent("textEntered", {
+              bubbles: false,
+              cancelable: true,
+              detail: { rootEvent: event }
+            });
+            return !this.dispatchEvent(e);
+          }
+        ]]></body>
+      </method>
 
-    get timeout() {
-      var t = parseInt(this.getAttribute("timeout"));
-      return isNaN(t) ? 50 : t;
-    }
-
-    set searchParam(val) {
-      this.setAttribute("autocompletesearchparam", val);
-      return val;
-    }
+      <method name="onTextReverted">
+        <body><![CDATA[
+          if (this.getAttribute("notifylegacyevents") === "true") {
+            let e = new CustomEvent("textReverted", {
+              bubbles: false,
+              cancelable: true
+            });
+            return !this.dispatchEvent(e);
+          }
+        ]]></body>
+      </method>
 
-    get searchParam() {
-      return this.getAttribute("autocompletesearchparam") || "";
-    }
+      <!-- =================== nsIDOMXULMenuListElement =================== -->
 
-    get searchCount() {
-      this.initSearchNames();
-      return this.mSearchNames.length;
-    }
-
-    get inPrivateContext() {
-      return this.PrivateBrowsingUtils.isWindowPrivate(window);
-    }
-
-    get noRollupOnCaretMove() {
-      return this.popup.getAttribute("norolluponanchor") == "true";
-    }
+      <property name="editable" readonly="true"
+                onget="return true;" />
 
-    set textValue(val) {
-      if (
-        typeof this.onBeforeTextValueSet == "function" &&
-        !this._textValueSetByCompleteDefault
-      ) {
-        val = this.onBeforeTextValueSet(val);
-      }
-
-      // "input" event is automatically dispatched by the editor if
-      // necessary.
-      this._setValueInternal(val, true);
-
-      return this.value;
-    }
+      <property name="crop"
+                onset="this.setAttribute('crop',val); return val;"
+                onget="return this.getAttribute('crop');"/>
 
-    get textValue() {
-      if (typeof this.onBeforeTextValueGet == "function") {
-        let result = this.onBeforeTextValueGet();
-        if (result) {
-          return result.value;
-        }
-      }
-      return this.value;
-    }
-    /**
-     * =================== nsIDOMXULMenuListElement ===================
-     */
-    get editable() {
-      return true;
-    }
+      <property name="open"
+                onget="return this.getAttribute('open') == 'true';">
+        <setter><![CDATA[
+          if (val)
+            this.showHistoryPopup();
+          else
+            this.closePopup();
+        ]]></setter>
+      </property>
+
+      <!-- =================== PUBLIC MEMBERS =================== -->
 
-    set crop(val) {
-      return false;
-    }
-
-    get crop() {
-      return false;
-    }
-
-    set open(val) {
-      if (val) {
-        this.showHistoryPopup();
-      } else {
-        this.closePopup();
-      }
-    }
+      <field name="valueIsTyped">false</field>
+      <field name="_textValueSetByCompleteDefault">false</field>
+      <property name="value"
+                onset="return this._setValueInternal(val, false);">
+        <getter><![CDATA[
+          if (typeof this.onBeforeValueGet == "function") {
+            var result = this.onBeforeValueGet();
+            if (result)
+              return result.value;
+          }
+          return this.inputField.value;
+        ]]></getter>
+      </property>
 
-    get open() {
-      return this.getAttribute("open") == "true";
-    }
-
-    set value(val) {
-      return this._setValueInternal(val, false);
-    }
+      <property name="focused" readonly="true"
+                onget="return this.getAttribute('focused') == 'true';"/>
 
-    get value() {
-      if (typeof this.onBeforeValueGet == "function") {
-        var result = this.onBeforeValueGet();
-        if (result) {
-          return result.value;
-        }
-      }
-      return super.value;
-    }
+      <!-- maximum number of rows to display at a time -->
+      <property name="maxRows"
+                onset="this.setAttribute('maxrows', val); return val;"
+                onget="return parseInt(this.getAttribute('maxrows')) || 0;"/>
 
-    get focused() {
-      return this === document.activeElement;
-    }
-    /**
-     * maximum number of rows to display at a time
-     */
-    set maxRows(val) {
-      this.setAttribute("maxrows", val);
-      return val;
-    }
+      <!-- option to allow scrolling through the list via the tab key, rather than
+           tab moving focus out of the textbox -->
+      <property name="tabScrolling"
+                onset="this.setAttribute('tabscrolling', val); return val;"
+                onget="return this.getAttribute('tabscrolling') == 'true';"/>
 
-    get maxRows() {
-      return parseInt(this.getAttribute("maxrows")) || 0;
-    }
-    /**
-     * option to allow scrolling through the list via the tab key, rather than
-     * tab moving focus out of the textbox
-     */
-    set tabScrolling(val) {
-      this.setAttribute("tabscrolling", val);
-      return val;
-    }
+      <!-- option to completely ignore any blur events while searches are
+           still going on. -->
+      <property name="ignoreBlurWhileSearching"
+                onset="this.setAttribute('ignoreblurwhilesearching', val); return val;"
+                onget="return this.getAttribute('ignoreblurwhilesearching') == 'true';"/>
 
-    get tabScrolling() {
-      return this.getAttribute("tabscrolling") == "true";
-    }
-    /**
-     * option to completely ignore any blur events while searches are
-     * still going on.
-     */
-    set ignoreBlurWhileSearching(val) {
-      this.setAttribute("ignoreblurwhilesearching", val);
-      return val;
-    }
+      <!-- option to highlight entries that don't have any matches -->
+      <property name="highlightNonMatches"
+                onset="this.setAttribute('highlightnonmatches', val); return val;"
+                onget="return this.getAttribute('highlightnonmatches') == 'true';"/>
+
+      <!-- =================== PRIVATE MEMBERS =================== -->
 
-    get ignoreBlurWhileSearching() {
-      return this.getAttribute("ignoreblurwhilesearching") == "true";
-    }
-    /**
-     * option to highlight entries that don't have any matches
-     */
-    set highlightNonMatches(val) {
-      this.setAttribute("highlightnonmatches", val);
-      return val;
-    }
+      <!-- ::::::::::::: autocomplete controller ::::::::::::: -->
 
-    get highlightNonMatches() {
-      return this.getAttribute("highlightnonmatches") == "true";
-    }
-
-    getSearchAt(aIndex) {
-      this.initSearchNames();
-      return this.mSearchNames[aIndex];
-    }
+      <method name="attachController">
+        <body><![CDATA[
+          this.mController.input = this;
+        ]]></body>
+      </method>
 
-    setTextValueWithReason(aValue, aReason) {
-      if (aReason == Ci.nsIAutoCompleteInput.TEXTVALUE_REASON_COMPLETEDEFAULT) {
-        this._textValueSetByCompleteDefault = true;
-      }
-      this.textValue = aValue;
-      this._textValueSetByCompleteDefault = false;
-    }
-
-    selectTextRange(aStartIndex, aEndIndex) {
-      super.setSelectionRange(aStartIndex, aEndIndex);
-    }
+      <method name="detachController">
+        <body><![CDATA[
+          if (this.mController.input == this)
+            this.mController.input = null;
+        ]]></body>
+      </method>
 
-    onSearchBegin() {
-      if (this.popup && typeof this.popup.onSearchBegin == "function") {
-        this.popup.onSearchBegin();
-      }
-    }
-
-    onSearchComplete() {
-      if (this.mController.matchCount == 0) {
-        this.setAttribute("nomatch", "true");
-      } else {
-        this.removeAttribute("nomatch");
-      }
+      <!-- ::::::::::::: popup opening ::::::::::::: -->
 
-      if (this.ignoreBlurWhileSearching && !this.focused) {
-        this.handleEnter();
-        this.detachController();
-      }
-    }
+      <method name="openPopup">
+        <body><![CDATA[
+          if (this.focused)
+            this.popup.openAutocompletePopup(this, this);
+        ]]></body>
+      </method>
 
-    onTextEntered(event) {
-      if (this.getAttribute("notifylegacyevents") === "true") {
-        let e = new CustomEvent("textEntered", {
-          bubbles: false,
-          cancelable: true,
-          detail: { rootEvent: event },
-        });
-        return !this.dispatchEvent(e);
-      }
-      return false;
-    }
+      <method name="closePopup">
+        <body><![CDATA[
+          this.popup.closePopup();
+        ]]></body>
+      </method>
 
-    onTextReverted(event) {
-      if (this.getAttribute("notifylegacyevents") === "true") {
-        let e = new CustomEvent("textReverted", {
-          bubbles: false,
-          cancelable: true,
-          detail: { rootEvent: event },
-        });
-        return !this.dispatchEvent(e);
-      }
-      return false;
-    }
-
-    /**
-     * =================== PRIVATE MEMBERS ===================
-     */
+      <method name="showHistoryPopup">
+        <body><![CDATA[
+          // Store our "normal" maxRows on the popup, so that it can reset the
+          // value when the popup is hidden.
+          this.popup._normalMaxRows = this.maxRows;
 
-    /*
-     * ::::::::::::: autocomplete controller :::::::::::::
-     */
-
-    attachController() {
-      this.mController.input = this.nsIAutocompleteInput;
-    }
-
-    detachController() {
-      if (this.mController.input.wrappedJSObject == this.nsIAutocompleteInput) {
-        this.mController.input = null;
-      }
-    }
-
-    /**
-     * ::::::::::::: popup opening :::::::::::::
-     */
-    openPopup() {
-      if (this.focused) {
-        this.popup.openAutocompletePopup(this.nsIAutocompleteInput, this);
-      }
-    }
-
-    closePopup() {
-      this.popup.closePopup();
-    }
+          // Increase our maxRows temporarily, since we want the dropdown to
+          // be bigger in this case. The popup's popupshowing/popuphiding
+          // handlers will take care of resetting this.
+          this.maxRows = this.maxDropMarkerRows;
 
-    showHistoryPopup() {
-      // Store our "normal" maxRows on the popup, so that it can reset the
-      // value when the popup is hidden.
-      this.popup._normalMaxRows = this.maxRows;
-
-      // Increase our maxRows temporarily, since we want the dropdown to
-      // be bigger in this case. The popup's popupshowing/popuphiding
-      // handlers will take care of resetting this.
-      this.maxRows = this.maxDropMarkerRows;
+          // Ensure that we have focus.
+          if (!this.focused)
+            this.focus();
+          this.attachController();
+          this.mController.startSearch("");
+        ]]></body>
+      </method>
 
-      // Ensure that we have focus.
-      if (!this.focused) {
-        this.focus();
-      }
-      this.attachController();
-      this.mController.startSearch("");
-    }
+      <method name="toggleHistoryPopup">
+        <body><![CDATA[
+          if (!this.popup.popupOpen)
+            this.showHistoryPopup();
+          else
+            this.closePopup();
+        ]]></body>
+      </method>
 
-    toggleHistoryPopup() {
-      if (!this.popup.popupOpen) {
-        this.showHistoryPopup();
-      } else {
-        this.closePopup();
-      }
-    }
-
-    handleKeyPress(aEvent) {
-      // Re: urlbarDeferred, see the comment in urlbarBindings.xml.
-      if (aEvent.defaultPrevented && !aEvent.urlbarDeferred) {
-        return false;
-      }
+      <!-- ::::::::::::: key handling ::::::::::::: -->
 
-      const isMac = AppConstants.platform == "macosx";
-      var cancel = false;
+      <field name="_selectionDetails">null</field>
+      <method name="onKeyPress">
+        <parameter name="aEvent"/>
+        <body><![CDATA[
+          return this.handleKeyPress(aEvent);
+        ]]></body>
+      </method>
 
-      // Catch any keys that could potentially move the caret. Ctrl can be
-      // used in combination with these keys on Windows and Linux; and Alt
-      // can be used on OS X, so make sure the unused one isn't used.
-      let metaKey = isMac ? aEvent.ctrlKey : aEvent.altKey;
-      if (!metaKey) {
-        switch (aEvent.keyCode) {
-          case KeyEvent.DOM_VK_LEFT:
-          case KeyEvent.DOM_VK_RIGHT:
-          case KeyEvent.DOM_VK_HOME:
-            cancel = this.mController.handleKeyNavigation(aEvent.keyCode);
-            break;
-        }
-      }
+      <method name="handleKeyPress">
+        <parameter name="aEvent"/>
+        <parameter name="aOptions"/>
+        <body><![CDATA[
+          if (aEvent.target.localName != "textbox")
+            return true; // Let child buttons of autocomplete take input
 
-      // Handle keys that are not part of a keyboard shortcut (no Ctrl or Alt)
-      if (!aEvent.ctrlKey && !aEvent.altKey) {
-        switch (aEvent.keyCode) {
-          case KeyEvent.DOM_VK_TAB:
-            if (this.tabScrolling && this.popup.popupOpen) {
-              cancel = this.mController.handleKeyNavigation(
-                aEvent.shiftKey ? KeyEvent.DOM_VK_UP : KeyEvent.DOM_VK_DOWN
-              );
-            } else if (this.forceComplete && this.mController.matchCount >= 1) {
-              this.mController.handleTab();
-            }
-            break;
-          case KeyEvent.DOM_VK_UP:
-          case KeyEvent.DOM_VK_DOWN:
-          case KeyEvent.DOM_VK_PAGE_UP:
-          case KeyEvent.DOM_VK_PAGE_DOWN:
-            cancel = this.mController.handleKeyNavigation(aEvent.keyCode);
-            break;
-        }
-      }
+          // Re: urlbarDeferred, see the comment in urlbarBindings.xml.
+          if (aEvent.defaultPrevented && !aEvent.urlbarDeferred) {
+            return false;
+          }
+
+          const isMac = /Mac/.test(navigator.platform);
+          var cancel = false;
 
-      // Handle readline/emacs-style navigation bindings on Mac.
-      if (
-        isMac &&
-        this.popup.popupOpen &&
-        aEvent.ctrlKey &&
-        (aEvent.key === "n" || aEvent.key === "p")
-      ) {
-        const effectiveKey =
-          aEvent.key === "p" ? KeyEvent.DOM_VK_UP : KeyEvent.DOM_VK_DOWN;
-        cancel = this.mController.handleKeyNavigation(effectiveKey);
-      }
-
-      // Handle keys we know aren't part of a shortcut, even with Alt or
-      // Ctrl.
-      switch (aEvent.keyCode) {
-        case KeyEvent.DOM_VK_ESCAPE:
-          cancel = this.mController.handleEscape();
-          break;
-        case KeyEvent.DOM_VK_RETURN:
-          if (isMac) {
-            // Prevent the default action, since it will beep on Mac
-            if (aEvent.metaKey) {
-              aEvent.preventDefault();
+          // Catch any keys that could potentially move the caret. Ctrl can be
+          // used in combination with these keys on Windows and Linux; and Alt
+          // can be used on OS X, so make sure the unused one isn't used.
+          let metaKey = isMac ? aEvent.ctrlKey : aEvent.altKey;
+          if (!metaKey) {
+            switch (aEvent.keyCode) {
+              case KeyEvent.DOM_VK_LEFT:
+              case KeyEvent.DOM_VK_RIGHT:
+              case KeyEvent.DOM_VK_HOME:
+                cancel = this.mController.handleKeyNavigation(aEvent.keyCode);
+                break;
             }
           }
-          if (this.popup.selectedIndex >= 0) {
-            this._selectionDetails = {
-              index: this.popup.selectedIndex,
-              kind: "key",
-            };
-          }
-          cancel = this.handleEnter(aEvent);
-          break;
-        case KeyEvent.DOM_VK_DELETE:
-          if (isMac && !aEvent.shiftKey) {
-            break;
+
+          // Handle keys that are not part of a keyboard shortcut (no Ctrl or Alt)
+          if (!aEvent.ctrlKey && !aEvent.altKey) {
+            switch (aEvent.keyCode) {
+              case KeyEvent.DOM_VK_TAB:
+                if (this.tabScrolling && this.popup.popupOpen)
+                  cancel = this.mController.handleKeyNavigation(aEvent.shiftKey ?
+                                                                KeyEvent.DOM_VK_UP :
+                                                                KeyEvent.DOM_VK_DOWN);
+                else if (this.forceComplete && this.mController.matchCount >= 1)
+                  this.mController.handleTab();
+                break;
+              case KeyEvent.DOM_VK_UP:
+              case KeyEvent.DOM_VK_DOWN:
+              case KeyEvent.DOM_VK_PAGE_UP:
+              case KeyEvent.DOM_VK_PAGE_DOWN:
+                cancel = this.mController.handleKeyNavigation(aEvent.keyCode);
+                break;
+            }
           }
-          cancel = this.handleDelete();
-          break;
-        case KeyEvent.DOM_VK_BACK_SPACE:
-          if (isMac && aEvent.shiftKey) {
-            cancel = this.handleDelete();
-          }
-          break;
-        case KeyEvent.DOM_VK_DOWN:
-        case KeyEvent.DOM_VK_UP:
-          if (aEvent.altKey) {
-            this.toggleHistoryPopup();
-          }
-          break;
-        case KeyEvent.DOM_VK_F4:
-          if (!isMac) {
-            this.toggleHistoryPopup();
+
+          // Handle readline/emacs-style navigation bindings on Mac.
+          if (isMac &&
+              this.popup.popupOpen &&
+              aEvent.ctrlKey &&
+              (aEvent.key === "n" || aEvent.key === "p")) {
+            const effectiveKey = (aEvent.key === "p") ?
+                                 KeyEvent.DOM_VK_UP :
+                                 KeyEvent.DOM_VK_DOWN;
+            cancel = this.mController.handleKeyNavigation(effectiveKey);
           }
-          break;
-      }
-
-      if (cancel) {
-        aEvent.stopPropagation();
-        aEvent.preventDefault();
-      }
-
-      return true;
-    }
-
-    handleEnter(event) {
-      return this.mController.handleEnter(false, event || null);
-    }
 
-    handleDelete() {
-      return this.mController.handleDelete();
-    }
+          // Handle keys we know aren't part of a shortcut, even with Alt or
+          // Ctrl.
+          switch (aEvent.keyCode) {
+            case KeyEvent.DOM_VK_ESCAPE:
+              cancel = this.mController.handleEscape();
+              break;
+            case KeyEvent.DOM_VK_RETURN:
+              if (isMac) {
+                // Prevent the default action, since it will beep on Mac
+                if (aEvent.metaKey)
+                  aEvent.preventDefault();
+              }
+              if (this.popup.selectedIndex >= 0) {
+                this._selectionDetails = {
+                  index: this.popup.selectedIndex,
+                  kind: "key",
+                };
+              }
+              cancel = this.handleEnter(aEvent, aOptions);
+              break;
+            case KeyEvent.DOM_VK_DELETE:
+              if (isMac && !aEvent.shiftKey) {
+                break;
+              }
+              cancel = this.handleDelete();
+              break;
+            case KeyEvent.DOM_VK_BACK_SPACE:
+              if (isMac && aEvent.shiftKey) {
+                cancel = this.handleDelete();
+              }
+              break;
+            case KeyEvent.DOM_VK_DOWN:
+            case KeyEvent.DOM_VK_UP:
+              if (aEvent.altKey)
+                this.toggleHistoryPopup();
+              break;
+            case KeyEvent.DOM_VK_F4:
+              if (!isMac) {
+                this.toggleHistoryPopup();
+              }
+              break;
+          }
 
-    /**
-     * ::::::::::::: miscellaneous :::::::::::::
-     */
-    initSearchNames() {
-      if (!this.mSearchNames) {
-        var names = this.getAttribute("autocompletesearch");
-        if (!names) {
-          this.mSearchNames = [];
-        } else {
-          this.mSearchNames = names.split(" ");
-        }
-      }
-    }
+          if (cancel) {
+            aEvent.stopPropagation();
+            aEvent.preventDefault();
+          }
+
+          return true;
+        ]]></body>
+      </method>
+
+      <method name="handleEnter">
+        <parameter name="event"/>
+        <body><![CDATA[
+          return this.mController.handleEnter(false, event || null);
+        ]]></body>
+      </method>
+
+      <method name="handleDelete">
+        <body><![CDATA[
+          return this.mController.handleDelete();
+        ]]></body>
+      </method>
+
+      <!-- ::::::::::::: miscellaneous ::::::::::::: -->
 
-    _focus() {
-      this._dontBlur = true;
-      this.focus();
-      this._dontBlur = false;
-    }
+      <method name="initSearchNames">
+        <body><![CDATA[
+          if (!this.mSearchNames) {
+            var names = this.getAttribute("autocompletesearch");
+            if (!names)
+              this.mSearchNames = [];
+            else
+              this.mSearchNames = names.split(" ");
+          }
+        ]]></body>
+      </method>
 
-    resetActionType() {
-      if (this.mIgnoreInput) {
-        return;
-      }
-      this.removeAttribute("actiontype");
-    }
+      <method name="_focus">
+        <!-- doesn't reset this.mController -->
+        <body><![CDATA[
+          this._dontBlur = true;
+          this.focus();
+          this._dontBlur = false;
+        ]]></body>
+      </method>
 
-    _setValueInternal(value, isUserInput) {
-      this.mIgnoreInput = true;
+      <method name="resetActionType">
+        <body><![CDATA[
+          if (this.mIgnoreInput)
+            return;
+          this.removeAttribute("actiontype");
+        ]]></body>
+      </method>
 
-      if (typeof this.onBeforeValueSet == "function") {
-        value = this.onBeforeValueSet(value);
-      }
+      <method name="_setValueInternal">
+        <parameter name="aValue"/>
+        <parameter name="aIsUserInput"/>
+        <body><![CDATA[
+          this.mIgnoreInput = true;
 
-      if (
-        typeof this.trimValue == "function" &&
-        !this._textValueSetByCompleteDefault
-      ) {
-        value = this.trimValue(value);
-      }
+          if (typeof this.onBeforeValueSet == "function")
+            aValue = this.onBeforeValueSet(aValue);
+
+          if (typeof this.trimValue == "function" &&
+              !this._textValueSetByCompleteDefault)
+            aValue = this.trimValue(aValue);
+
+          this.valueIsTyped = false;
+          if (aIsUserInput) {
+            this.inputField.setUserInput(aValue);
+          } else {
+            this.inputField.value = aValue;
+          }
+
+          if (typeof this.formatValue == "function")
+            this.formatValue();
 
-      this.valueIsTyped = false;
-      if (isUserInput) {
-        super.setUserInput(value);
-      } else {
-        super.value = value;
-      }
+          this.mIgnoreInput = false;
+          var event = document.createEvent("Events");
+          event.initEvent("ValueChange", true, true);
+          this.inputField.dispatchEvent(event);
+          return aValue;
+        ]]></body>
+      </method>
 
-      if (typeof this.formatValue == "function") {
-        this.formatValue();
-      }
+      <method name="onInput">
+        <parameter name="aEvent"/>
+        <body><![CDATA[
+          if (!this.mIgnoreInput && this.mController.input == this) {
+            this.valueIsTyped = true;
+            this.mController.handleText();
+          }
+          this.resetActionType();
+        ]]></body>
+      </method>
+    </implementation>
 
-      this.mIgnoreInput = false;
-      var event = document.createEvent("Events");
-      event.initEvent("ValueChange", true, true);
-      super.dispatchEvent(event);
-      return value;
-    }
+    <handlers>
+      <handler event="input"><![CDATA[
+        this.onInput(event);
+      ]]></handler>
+
+      <handler event="keypress" phase="capturing" group="system"
+               action="return this.onKeyPress(event);"/>
 
-    onInput(aEvent) {
-      if (
-        !this.mIgnoreInput &&
-        this.mController.input.wrappedJSObject == this.nsIAutocompleteInput
-      ) {
-        this.valueIsTyped = true;
-        this.mController.handleText();
-      }
-      this.resetActionType();
-    }
-  }
+      <handler event="compositionstart" phase="capturing"
+               action="if (this.mController.input == this) this.mController.handleStartComposition();"/>
+
+      <handler event="compositionend" phase="capturing"
+               action="if (this.mController.input == this) this.mController.handleEndComposition();"/>
+
+      <handler event="focus" phase="capturing"><![CDATA[
+        this.attachController();
+        if (window.gBrowser && window.gBrowser.selectedBrowser.hasAttribute("usercontextid")) {
+          this.userContextId = parseInt(window.gBrowser.selectedBrowser.getAttribute("usercontextid"));
+        } else {
+          this.userContextId = 0;
+        }
+      ]]></handler>
 
-  MozHTMLElement.implementCustomInterface(AutocompleteInput, [
-    Ci.nsIAutoCompleteInput,
-    Ci.nsIDOMXULMenuListElement,
-  ]);
-  customElements.define("autocomplete-input", AutocompleteInput, {
-    extends: "input",
-  });
-}
+      <handler event="blur" phase="capturing"><![CDATA[
+        if (!this._dontBlur) {
+          if (this.forceComplete && this.mController.matchCount >= 1) {
+            // If forceComplete is requested, we need to call the enter processing
+            // on blur so the input will be forced to the closest match.
+            // Thunderbird is the only consumer of forceComplete and this is used
+            // to force an recipient's email to the exact address book entry.
+            this.mController.handleEnter(true);
+          }
+          if (!this.ignoreBlurWhileSearching)
+            this.detachController();
+        }
+      ]]></handler>
+    </handlers>
+  </binding>
+</bindings>
--- a/toolkit/content/xul.css
+++ b/toolkit/content/xul.css
@@ -470,16 +470,20 @@ textbox[is="search-textbox"] {
   direction: rtl;
 }
 
 /********** autocomplete textbox **********/
 
 /* SeaMonkey uses its own autocomplete and the toolkit's autocomplete widget */
 %ifndef MOZ_SUITE
 
+textbox[type="autocomplete"] {
+  -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#autocomplete");
+}
+
 panel[type="autocomplete-richlistbox"] {
   -moz-binding: none;
 }
 
 .autocomplete-richlistbox {
   -moz-user-focus: ignore;
   overflow-x: hidden !important;
 }
--- a/toolkit/themes/linux/global/autocomplete.css
+++ b/toolkit/themes/linux/global/autocomplete.css
@@ -6,21 +6,17 @@
   == Styles used by the autocomplete widget.
   ======================================================================= */
 
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 @namespace html url("http://www.w3.org/1999/xhtml");
 
 /* ::::: autocomplete ::::: */
 
-html|input[is="autocomplete-input"] {
-  margin: 2px 4px; /* matches xul <textbox> default margin */
-}
-
-html|input[nomatch="true"][highlightnonmatches="true"] {
+textbox[nomatch="true"][highlightnonmatches="true"] {
   color: red;
 }
 
 /* ::::: autocomplete popups ::::: */
 
 panel[type="autocomplete-richlistbox"] {
   border: 1px solid ThreeDShadow;
   padding: 0;
--- a/toolkit/themes/osx/global/autocomplete.css
+++ b/toolkit/themes/osx/global/autocomplete.css
@@ -1,20 +1,16 @@
 /* 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/. */
 
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 @namespace html url("http://www.w3.org/1999/xhtml");
 
-html|input[is="autocomplete-input"] {
-  margin: 4px; /* matches xul <textbox> default margin */
-}
-
-html|input[nomatch="true"][highlightnonmatches="true"] {
+textbox[nomatch="true"][highlightnonmatches="true"] {
   color: red;
 }
 
 /* ::::: autocomplete popups ::::: */
 
 panel[type="autocomplete-richlistbox"] {
   padding: 0px !important;
   color: -moz-FieldText;
--- a/toolkit/themes/windows/global/autocomplete.css
+++ b/toolkit/themes/windows/global/autocomplete.css
@@ -6,21 +6,17 @@
   == Styles used by the autocomplete widget.
   ======================================================================= */
 
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 @namespace html url("http://www.w3.org/1999/xhtml");
 
 /* ::::: autocomplete ::::: */
 
-html|input[is="autocomplete-input"] {
-  margin: 2px 4px; /* matches xul <textbox> default margin */
-}
-
-html|input[nomatch="true"][highlightnonmatches="true"] {
+textbox[nomatch="true"][highlightnonmatches="true"] {
   color: red;
 }
 
 /* ::::: autocomplete popups ::::: */
 
 panel[type="autocomplete-richlistbox"] {
   -moz-appearance: none;
   padding: 0;