Bug 1525101 - Convert autocomplete-rich-result-popup into a Custom Element, r=MattN
authorAlexander Surkov <surkov.alexander@gmail.com>
Tue, 05 Mar 2019 17:45:57 +0000
changeset 520377 a83c218cd961857e02be7bac0ff9c971b026c4ce
parent 520376 87dab0b128cba85fa5ec0d9047b53838b3584ce1
child 520378 cb4c115a9f5914abf07a620880c197864ef8001f
child 520406 daf32259f33e227db36ba3982653ec3d7e416185
push id10862
push userffxbld-merge
push dateMon, 11 Mar 2019 13:01:11 +0000
treeherdermozilla-beta@a2e7f5c935da [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1525101
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1525101 - Convert autocomplete-rich-result-popup into a Custom Element, r=MattN Differential Revision: https://phabricator.services.mozilla.com/D20506
accessible/tests/mochitest/events/test_focus_autocomplete.xul
accessible/tests/mochitest/tree/test_txtctrl.xul
browser/base/content/browser.xul
browser/base/content/webext-panels.xul
toolkit/components/passwordmgr/test/browser/browser_autocomplete_footer.js
toolkit/components/passwordmgr/test/browser/browser_autocomplete_insecure_warning.js
toolkit/components/passwordmgr/test/browser/browser_focus_before_first_DOMContentLoaded.js
toolkit/content/customElements.js
toolkit/content/jar.mn
toolkit/content/tests/chrome/test_autocomplete_emphasis.xul
toolkit/content/widgets/autocomplete-popup.js
toolkit/content/widgets/autocomplete.xml
toolkit/content/xul.css
toolkit/mozapps/extensions/content/extensions.xul
--- a/accessible/tests/mochitest/events/test_focus_autocomplete.xul
+++ b/accessible/tests/mochitest/events/test_focus_autocomplete.xul
@@ -492,17 +492,20 @@
 
     <vbox flex="1">
       <textbox id="autocomplete" type="autocomplete"
                autocompletesearch="test-a11y-search"/>
 
       <textbox id="richautocomplete" type="autocomplete"
                autocompletesearch="test-a11y-search"
                autocompletepopup="richpopup"/>
-      <panel id="richpopup" type="autocomplete-richlistbox" noautofocus="true"/>
+      <panel is="autocomplete-richlistbox-popup"
+             id="richpopup"
+             type="autocomplete-richlistbox"
+             noautofocus="true"/>
 
       <iframe id="iframe"/>
 
       <iframe id="iframe2"/>
 
       <searchbar id="searchbar"/>
 
       <vbox id="eventdump"/>
--- a/accessible/tests/mochitest/tree/test_txtctrl.xul
+++ b/accessible/tests/mochitest/tree/test_txtctrl.xul
@@ -128,17 +128,17 @@
       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;
+      txc.popup.initialize();
     }
 
     SimpleTest.waitForExplicitFinish();
     addA11yLoadEvent(doTest);
   ]]>
   </script>
 
   <hbox flex="1" style="overflow: auto;">
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -231,17 +231,18 @@ xmlns="http://www.w3.org/1999/xhtml"
                onpopupshowing="return FillHistoryMenu(event.target);"
                oncommand="gotoHistoryIndex(event); event.stopPropagation();"
                onclick="checkForMiddleClick(this, event);"/>
     <tooltip id="aHTMLTooltip" page="true"/>
     <tooltip id="remoteBrowserTooltip"/>
 
     <!-- for search and content formfill/pw manager -->
 
-    <panel type="autocomplete-richlistbox"
+    <panel is="autocomplete-richlistbox-popup"
+           type="autocomplete-richlistbox"
            id="PopupAutoComplete"
            role="group"
            noautofocus="true"
            hidden="true"
            overflowpadding="4"
            norolluponanchor="true"
            nomaxresults="true" />
 
--- a/browser/base/content/webext-panels.xul
+++ b/browser/base/content/webext-panels.xul
@@ -35,17 +35,18 @@
              disabled="true"/>
     <command id="Browser:Stop" oncommand="PanelBrowserStop();"/>
     <command id="Browser:Reload" oncommand="PanelBrowserReload();"/>
   </commandset>
 
   <popupset id="mainPopupSet">
     <tooltip id="aHTMLTooltip" page="true"/>
 
-    <panel type="autocomplete-richlistbox"
+    <panel is="autocomplete-richlistbox-popup"
+           type="autocomplete-richlistbox"
            id="PopupAutoComplete"
            noautofocus="true"
            hidden="true"
            overflowpadding="4"
            norolluponanchor="true" />
 
     <menupopup id="contentAreaContextMenu" pagemenu="start"
                onpopupshowing="if (event.target != this)
--- a/toolkit/components/passwordmgr/test/browser/browser_autocomplete_footer.js
+++ b/toolkit/components/passwordmgr/test/browser/browser_autocomplete_footer.js
@@ -52,17 +52,17 @@ add_task(async function test_autocomplet
     // Focus the username field to open the popup.
     await ContentTask.spawn(browser, null, function openAutocomplete() {
       content.document.getElementById("form-basic-username").focus();
     });
 
     await promiseShown;
     ok(promiseShown, "autocomplete shown");
 
-    let footer = document.getAnonymousElementByAttribute(popup, "originaltype", "loginsFooter");
+    let footer = popup.querySelector(`[originaltype="loginsFooter"]`);
     ok(footer, "Got footer richlistitem");
 
     await TestUtils.waitForCondition(() => {
       return !EventUtils.isHidden(footer);
     }, "Waiting for footer to become visible");
 
     EventUtils.synthesizeMouseAtCenter(footer, {});
     await TestUtils.waitForCondition(() => {
--- a/toolkit/components/passwordmgr/test/browser/browser_autocomplete_insecure_warning.js
+++ b/toolkit/components/passwordmgr/test/browser/browser_autocomplete_insecure_warning.js
@@ -21,17 +21,17 @@ add_task(async function test_clickInsecu
     // Focus the username field to open the popup.
     await ContentTask.spawn(browser, null, function openAutocomplete() {
       content.document.getElementById("form-basic-username").focus();
     });
 
     await promiseShown;
     ok(promiseShown, "autocomplete shown");
 
-    let warningItem = document.getAnonymousElementByAttribute(popup, "type", "insecureWarning");
+    let warningItem = popup.querySelector(`[type="insecureWarning"]`);
     ok(warningItem, "Got warning richlistitem");
 
     await BrowserTestUtils.waitForCondition(() => !warningItem.collapsed, "Wait for warning to show");
 
     info("Clicking on warning");
     let supportTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, EXPECTED_SUPPORT_URL);
     EventUtils.synthesizeMouseAtCenter(warningItem, {});
     let supportTab = await supportTabPromise;
--- a/toolkit/components/passwordmgr/test/browser/browser_focus_before_first_DOMContentLoaded.js
+++ b/toolkit/components/passwordmgr/test/browser/browser_focus_before_first_DOMContentLoaded.js
@@ -44,17 +44,17 @@ add_task(async function test_autocomplet
     is(doc.activeElement, uname, "#uname element should be focused");
     is(uname.value, "", "Checking username is empty");
     is(pword.value, "", "Checking password is empty");
   });
 
   await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, newTab.linkedBrowser);
   await autocompletePopupShown;
 
-  let richlistbox = document.getAnonymousNodes(autocompletePopup)[0];
+  let richlistbox = autocompletePopup.richlistbox;
   is(richlistbox.localName, "richlistbox", "The richlistbox should be the first anonymous node");
   for (let i = 0; i < autocompletePopup.view.matchCount; i++) {
     if (richlistbox.selectedItem &&
         richlistbox.selectedItem.textContent.includes("tempuser1")) {
       break;
     }
     await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, newTab.linkedBrowser);
   }
--- a/toolkit/content/customElements.js
+++ b/toolkit/content/customElements.js
@@ -505,16 +505,17 @@ if (!isDummyDocument) {
   for (let script of [
     "chrome://global/content/elements/general.js",
     "chrome://global/content/elements/checkbox.js",
     "chrome://global/content/elements/menu.js",
     "chrome://global/content/elements/notificationbox.js",
     "chrome://global/content/elements/popupnotification.js",
     "chrome://global/content/elements/radio.js",
     "chrome://global/content/elements/richlistbox.js",
+    "chrome://global/content/elements/autocomplete-popup.js",
     "chrome://global/content/elements/autocomplete-richlistitem.js",
     "chrome://global/content/elements/textbox.js",
     "chrome://global/content/elements/tabbox.js",
     "chrome://global/content/elements/tree.js",
   ]) {
     Services.scriptloader.loadSubScript(script, window);
   }
 
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -78,16 +78,17 @@ toolkit.jar:
    content/global/bindings/tabbox.xml          (widgets/tabbox.xml)
    content/global/bindings/text.xml            (widgets/text.xml)
    content/global/elements/text.js             (widgets/text.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/bindings/toolbarbutton.xml   (widgets/toolbarbutton.xml)
 *  content/global/bindings/wizard.xml          (widgets/wizard.xml)
+   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/checkbox.js         (widgets/checkbox.js)
    content/global/elements/datetimebox.js      (widgets/datetimebox.js)
    content/global/elements/findbar.js          (widgets/findbar.js)
    content/global/elements/editor.js           (widgets/editor.js)
    content/global/elements/general.js          (widgets/general.js)
    content/global/elements/menu.js             (widgets/menu.js)
--- a/toolkit/content/tests/chrome/test_autocomplete_emphasis.xul
+++ b/toolkit/content/tests/chrome/test_autocomplete_emphasis.xul
@@ -9,17 +9,20 @@
           src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
   <script type="application/javascript"
           src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
 
 <textbox id="richautocomplete" type="autocomplete"
          autocompletesearch="simple"
          onsearchcomplete="checkSearchCompleted();"
          autocompletepopup="richpopup"/>
-<panel id="richpopup" type="autocomplete-richlistbox" noautofocus="true"/>
+<panel is="autocomplete-richlistbox-popup"
+       id="richpopup"
+       type="autocomplete-richlistbox"
+       noautofocus="true"/>
 
 <script class="testbody" type="application/javascript">
 <![CDATA[
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 const ACR = Ci.nsIAutoCompleteResult;
 
--- a/toolkit/content/widgets/autocomplete-popup.js
+++ b/toolkit/content/widgets/autocomplete-popup.js
@@ -1,600 +1,575 @@
-  <binding id="autocomplete-rich-result-popup">
-    <content ignorekeys="true" level="top" consumeoutsideclicks="never">
-      <xul:richlistbox anonid="richlistbox" class="autocomplete-richlistbox" flex="1"/>
-      <xul:hbox>
-        <children/>
-      </xul:hbox>
-    </content>
+/* 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";
+
+// This is loaded into all XUL windows. Wrap in a block to prevent
+// leaking to window scope.
+{
+const MozPopupElement = MozElementMixin(XULPopupElement);
+MozElements.MozAutocompleteRichlistboxPopup = class MozAutocompleteRichlistboxPopup extends MozPopupElement {
+  constructor() {
+    super();
+
+    this.mInput = null;
+    this.mPopupOpen = false;
+    this._currentIndex = 0;
+
+    this.setListeners();
+  }
+
+  initialize() {
+    this.setAttribute("ignorekeys", "true");
+    this.setAttribute("level", "top");
+    this.setAttribute("consumeoutsideclicks", "never");
+
+    this.textContent = "";
+    this.appendChild(MozXULElement.parseXULToFragment(this._markup));
+
+    /**
+     * This is the default number of rows that we give the autocomplete
+     * popup when the textbox doesn't have a "maxrows" attribute
+     * for us to use.
+     */
+    this.defaultMaxRows = 6;
 
-    <implementation implements="nsIAutoCompletePopup">
-      <field name="mInput">null</field>
-      <field name="mPopupOpen">false</field>
-      <field name="_currentIndex">0</field>
+    /**
+     * In some cases (e.g. when the input's dropmarker button is clicked),
+     * the input wants to display a popup with more rows. In that case, it
+     * should increase its maxRows property and store the "normal" maxRows
+     * in this field. When the popup is hidden, we restore the input's
+     * maxRows to the value stored in this field.
+     *
+     * This field is set to -1 between uses so that we can tell when it's
+     * been set by the input and when we need to set it in the popupshowing
+     * handler.
+     */
+    this._normalMaxRows = -1;
+    this._previousSelectedIndex = -1;
+    this.mLastMoveTime = Date.now();
+    this.mousedOverIndex = -1;
+    this._richlistbox = this.querySelector(".autocomplete-richlistbox");
 
-      <constructor><![CDATA[
-        if (!this.listEvents) {
-          this.listEvents = {
-            handleEvent: event => {
-              if (!this.parentNode) {
+    if (!this.listEvents) {
+      this.listEvents = {
+        handleEvent: event => {
+          if (!this.parentNode) {
+            return;
+          }
+
+          switch (event.type) {
+            case "mouseup":
+              // Don't call onPopupClick for the scrollbar buttons, thumb,
+              // slider, etc. If we hit the richlistbox and not a
+              // richlistitem, we ignore the event.
+              if (event.target.closest("richlistbox,richlistitem")
+                .localName == "richlistitem") {
+                this.onPopupClick(event);
+              }
+              break;
+            case "mousemove":
+              if (Date.now() - this.mLastMoveTime <= 30) {
+                return;
+              }
+
+              let item = event.target.closest("richlistbox,richlistitem");
+
+              // If we hit the richlistbox and not a richlistitem, we ignore
+              // the event.
+              if (item.localName == "richlistbox") {
                 return;
               }
 
-              switch (event.type) {
-                case "mouseup":
-                  // Don't call onPopupClick for the scrollbar buttons, thumb,
-                  // slider, etc. If we hit the richlistbox and not a
-                  // richlistitem, we ignore the event.
-                  if (event.target.closest("richlistbox,richlistitem")
-                                  .localName == "richlistitem") {
-                    this.onPopupClick(event);
-                  }
-                  break;
-                case "mousemove":
-                  if (Date.now() - this.mLastMoveTime <= 30) {
-                    return;
-                  }
+              let index = this.richlistbox.getIndexOfItem(item);
+
+              this.mousedOverIndex = index;
+
+              if (item.selectedByMouseOver) {
+                this.richlistbox.selectedIndex = index;
+              }
 
-                  let item = event.target.closest("richlistbox,richlistitem");
+              this.mLastMoveTime = Date.now();
+              break;
+          }
+        },
+      };
+      this.richlistbox.addEventListener("mouseup", this.listEvents);
+      this.richlistbox.addEventListener("mousemove", this.listEvents);
+    }
+  }
 
-                  // If we hit the richlistbox and not a richlistitem, we ignore
-                  // the event.
-                  if (item.localName == "richlistbox") {
-                    return;
-                  }
-
-                  let index = this.richlistbox.getIndexOfItem(item);
-
-                  this.mousedOverIndex = index;
+  get richlistbox() {
+    if (!this._richlistbox) {
+      this.initialize();
+    }
+    return this._richlistbox;
+  }
 
-                  if (item.selectedByMouseOver) {
-                    this.richlistbox.selectedIndex = index;
-                  }
-
-                  this.mLastMoveTime = Date.now();
-                  break;
-              }
-            },
-          };
-          this.richlistbox.addEventListener("mouseup", this.listEvents);
-          this.richlistbox.addEventListener("mousemove", this.listEvents);
-        }
-      ]]></constructor>
+  get _markup() {
+    return `
+      <richlistbox class="autocomplete-richlistbox" flex="1"></richlistbox>
+    `;
+  }
 
-      <destructor><![CDATA[
-        if (this.listEvents) {
-          this.richlistbox.removeEventListener("mouseup", this.listEvents);
-          this.richlistbox.removeEventListener("mousemove", this.listEvents);
-          delete this.listEvents;
-        }
-      ]]></destructor>
+  /**
+   * nsIAutoCompletePopup
+   */
+  get input() {
+    return this.mInput;
+  }
 
-      <!-- =================== nsIAutoCompletePopup =================== -->
-
-      <property name="input" readonly="true"
-                onget="return this.mInput"/>
+  get overrideValue() {
+    return null;
+  }
 
-      <property name="overrideValue" readonly="true"
-                onget="return null;"/>
+  get popupOpen() {
+    return this.mPopupOpen;
+  }
 
-      <property name="popupOpen" readonly="true"
-                onget="return this.mPopupOpen;"/>
+  get maxRows() {
+    return (this.mInput && this.mInput.maxRows) || this.defaultMaxRows;
+  }
 
-      <method name="closePopup">
-        <body>
-          <![CDATA[
-          if (this.mPopupOpen) {
-            this.hidePopup();
-            this.removeAttribute("width");
-          }
-        ]]>
-        </body>
-      </method>
+  set selectedIndex(val) {
+    if (val != this.richlistbox.selectedIndex) {
+      this._previousSelectedIndex = this.richlistbox.selectedIndex;
+    }
+    this.richlistbox.selectedIndex = val;
+    // Since ensureElementIsVisible may cause an expensive Layout flush,
+    // invoke it only if there may be a scrollbar, so if we could fetch
+    // more results than we can show at once.
+    // maxResults is the maximum number of fetched results, maxRows is the
+    // maximum number of rows we show at once, without a scrollbar.
+    if (this.mPopupOpen && this.maxResults > this.maxRows) {
+      // when clearing the selection (val == -1, so selectedItem will be
+      // null), we want to scroll back to the top.  see bug #406194
+      this.richlistbox.ensureElementIsVisible(
+        this.richlistbox.selectedItem || this.richlistbox.firstElementChild);
+    }
+    return val;
+  }
 
-      <!-- This is the default number of rows that we give the autocomplete
-           popup when the textbox doesn't have a "maxrows" attribute
-           for us to use. -->
-      <field name="defaultMaxRows" readonly="true">6</field>
-
-      <!-- In some cases (e.g. when the input's dropmarker button is clicked),
-           the input wants to display a popup with more rows. In that case, it
-           should increase its maxRows property and store the "normal" maxRows
-           in this field. When the popup is hidden, we restore the input's
-           maxRows to the value stored in this field.
-
-           This field is set to -1 between uses so that we can tell when it's
-           been set by the input and when we need to set it in the popupshowing
-           handler. -->
-      <field name="_normalMaxRows">-1</field>
+  get selectedIndex() {
+    return this.richlistbox.selectedIndex;
+  }
 
-      <property name="maxRows" readonly="true">
-        <getter>
-          <![CDATA[
-          return (this.mInput && this.mInput.maxRows) || this.defaultMaxRows;
-        ]]>
-        </getter>
-      </property>
+  get maxResults() {
+    // This is how many richlistitems will be kept around.
+    // Note, this getter may be overridden, or instances
+    // can have the nomaxresults attribute set to have no
+    // limit.
+    if (this.getAttribute("nomaxresults") == "true") {
+      return Infinity;
+    }
+    return 20;
+  }
 
-      <method name="getNextIndex">
-        <parameter name="aReverse"/>
-        <parameter name="aAmount"/>
-        <parameter name="aIndex"/>
-        <parameter name="aMaxRow"/>
-        <body><![CDATA[
-          if (aMaxRow < 0)
-            return -1;
+  get matchCount() {
+    return Math.min(this.mInput.controller.matchCount, this.maxResults);
+  }
 
-          var newIdx = aIndex + (aReverse ? -1 : 1) * aAmount;
-          if (aReverse && aIndex == -1 || newIdx > aMaxRow && aIndex != aMaxRow)
-            newIdx = aMaxRow;
-          else if (!aReverse && aIndex == -1 || newIdx < 0 && aIndex != 0)
-            newIdx = 0;
+  get overflowPadding() {
+    return Number(this.getAttribute("overflowpadding"));
+  }
 
-          if (newIdx < 0 && aIndex == 0 || newIdx > aMaxRow && aIndex == aMaxRow)
-            aIndex = -1;
-          else
-            aIndex = newIdx;
+  set view(val) {
+    return val;
+  }
 
-          return aIndex;
-        ]]></body>
-      </method>
+  get view() {
+    return this.mInput.controller;
+  }
 
-      <method name="onPopupClick">
-        <parameter name="aEvent"/>
-        <body><![CDATA[
-          this.input.controller.handleEnter(true, aEvent);
-        ]]></body>
-      </method>
+  closePopup() {
+    if (this.mPopupOpen) {
+      this.hidePopup();
+      this.removeAttribute("width");
+    }
+  }
 
-      <property name="selectedIndex"
-                onget="return this.richlistbox.selectedIndex;">
-        <setter>
-          <![CDATA[
-          if (val != this.richlistbox.selectedIndex) {
-            this._previousSelectedIndex = this.richlistbox.selectedIndex;
-          }
-          this.richlistbox.selectedIndex = val;
-          // Since ensureElementIsVisible may cause an expensive Layout flush,
-          // invoke it only if there may be a scrollbar, so if we could fetch
-          // more results than we can show at once.
-          // maxResults is the maximum number of fetched results, maxRows is the
-          // maximum number of rows we show at once, without a scrollbar.
-          if (this.mPopupOpen && this.maxResults > this.maxRows) {
-            // when clearing the selection (val == -1, so selectedItem will be
-            // null), we want to scroll back to the top.  see bug #406194
-            this.richlistbox.ensureElementIsVisible(
-              this.richlistbox.selectedItem || this.richlistbox.firstElementChild);
-          }
-          return val;
-        ]]>
-        </setter>
-      </property>
+  getNextIndex(aReverse, aAmount, aIndex, aMaxRow) {
+    if (aMaxRow < 0)
+      return -1;
+
+    var newIdx = aIndex + (aReverse ? -1 : 1) * aAmount;
+    if (aReverse && aIndex == -1 || newIdx > aMaxRow && aIndex != aMaxRow)
+      newIdx = aMaxRow;
+    else if (!aReverse && aIndex == -1 || newIdx < 0 && aIndex != 0)
+      newIdx = 0;
 
-      <field name="_previousSelectedIndex">-1</field>
-      <field name="mLastMoveTime">Date.now()</field>
-      <field name="mousedOverIndex">-1</field>
+    if (newIdx < 0 && aIndex == 0 || newIdx > aMaxRow && aIndex == aMaxRow)
+      aIndex = -1;
+    else
+      aIndex = newIdx;
 
-      <method name="onSearchBegin">
-        <body><![CDATA[
-          this.mousedOverIndex = -1;
+    return aIndex;
+  }
+
+  onPopupClick(aEvent) {
+    this.input.controller.handleEnter(true, aEvent);
+  }
+
+  onSearchBegin() {
+    this.mousedOverIndex = -1;
 
-          if (typeof this._onSearchBegin == "function") {
-            this._onSearchBegin();
-          }
-        ]]></body>
-      </method>
+    if (typeof this._onSearchBegin == "function") {
+      this._onSearchBegin();
+    }
+  }
+
+  openAutocompletePopup(aInput, aElement) {
+    // until we have "baseBinding", (see bug #373652) this allows
+    // us to override openAutocompletePopup(), but still call
+    // the method on the base class
+    this._openAutocompletePopup(aInput, aElement);
+  }
 
-      <method name="openAutocompletePopup">
-        <parameter name="aInput"/>
-        <parameter name="aElement"/>
-        <body>
-          <![CDATA[
-          // until we have "baseBinding", (see bug #373652) this allows
-          // us to override openAutocompletePopup(), but still call
-          // the method on the base class
-          this._openAutocompletePopup(aInput, aElement);
-        ]]>
-        </body>
-      </method>
+  _openAutocompletePopup(aInput, aElement) {
+    if (!this._initialized) {
+      this.initialize();
+      this._initialized = true;
+    }
 
-      <method name="_openAutocompletePopup">
-        <parameter name="aInput"/>
-        <parameter name="aElement"/>
-        <body>
-          <![CDATA[
-          if (!this.mPopupOpen) {
-            // It's possible that the panel is hidden initially
-            // to avoid impacting startup / new window performance
-            aInput.popup.hidden = false;
+    if (!this.mPopupOpen) {
+      // It's possible that the panel is hidden initially
+      // to avoid impacting startup / new window performance
+      aInput.popup.hidden = false;
 
-            this.mInput = aInput;
-            // clear any previous selection, see bugs 400671 and 488357
-            this.selectedIndex = -1;
+      this.mInput = aInput;
+      // clear any previous selection, see bugs 400671 and 488357
+      this.selectedIndex = -1;
 
-            var width = aElement.getBoundingClientRect().width;
-            this.setAttribute("width", width > 100 ? width : 100);
-            // invalidate() depends on the width attribute
-            this._invalidate();
+      var width = aElement.getBoundingClientRect().width;
+      this.setAttribute("width", width > 100 ? width : 100);
+      // invalidate() depends on the width attribute
+      this._invalidate();
 
-            this.openPopup(aElement, "after_start", 0, 0, false, false);
-          }
-        ]]>
-        </body>
-      </method>
+      this.openPopup(aElement, "after_start", 0, 0, false, false);
+    }
+  }
 
-      <method name="invalidate">
-        <parameter name="reason"/>
-        <body>
-          <![CDATA[
-          // Don't bother doing work if we're not even showing
-          if (!this.mPopupOpen)
-            return;
+  invalidate(reason) {
+    // Don't bother doing work if we're not even showing
+    if (!this.mPopupOpen)
+      return;
+
+    this._invalidate(reason);
+  }
 
-          this._invalidate(reason);
-          ]]>
-        </body>
-      </method>
+  _invalidate(reason) {
+    // collapsed if no matches
+    this.richlistbox.collapsed = (this.matchCount == 0);
 
-      <method name="_invalidate">
-        <parameter name="reason"/>
-        <body>
-          <![CDATA[
-          // collapsed if no matches
-          this.richlistbox.collapsed = (this.matchCount == 0);
+    // Update the richlistbox height.
+    if (this._adjustHeightRAFToken) {
+      cancelAnimationFrame(this._adjustHeightRAFToken);
+      this._adjustHeightRAFToken = null;
+    }
 
-          // Update the richlistbox height.
-          if (this._adjustHeightRAFToken) {
-            cancelAnimationFrame(this._adjustHeightRAFToken);
-            this._adjustHeightRAFToken = null;
-          }
+    if (this.mPopupOpen) {
+      delete this._adjustHeightOnPopupShown;
+      this._adjustHeightRAFToken = requestAnimationFrame(() => this.adjustHeight());
+    } else {
+      this._adjustHeightOnPopupShown = true;
+    }
 
-          if (this.mPopupOpen) {
-            delete this._adjustHeightOnPopupShown;
-            this._adjustHeightRAFToken = requestAnimationFrame(() => this.adjustHeight());
-          } else {
-            this._adjustHeightOnPopupShown = true;
-          }
+    this._currentIndex = 0;
+    if (this._appendResultTimeout) {
+      clearTimeout(this._appendResultTimeout);
+    }
+    this._appendCurrentResult(reason);
+  }
 
-          this._currentIndex = 0;
-          if (this._appendResultTimeout) {
-            clearTimeout(this._appendResultTimeout);
-          }
-          this._appendCurrentResult(reason);
-        ]]>
-        </body>
-      </method>
+  _collapseUnusedItems() {
+    let existingItemsCount = this.richlistbox.children.length;
+    for (let i = this.matchCount; i < existingItemsCount; ++i) {
+      let item = this.richlistbox.children[i];
 
-      <property name="maxResults" readonly="true">
-        <getter>
-          <![CDATA[
-            // This is how many richlistitems will be kept around.
-            // Note, this getter may be overridden, or instances
-            // can have the nomaxresults attribute set to have no
-            // limit.
-            if (this.getAttribute("nomaxresults") == "true") {
-              return Infinity;
-            }
+      item.collapsed = true;
+      if (typeof item._onCollapse == "function") {
+        item._onCollapse();
+      }
+    }
+  }
 
-            return 20;
-          ]]>
-        </getter>
-      </property>
+  adjustHeight() {
+    // Figure out how many rows to show
+    let rows = this.richlistbox.children;
+    let numRows = Math.min(this.matchCount, this.maxRows, rows.length);
 
-      <property name="matchCount" readonly="true">
-        <getter>
-          <![CDATA[
-          return Math.min(this.mInput.controller.matchCount, this.maxResults);
-          ]]>
-        </getter>
-      </property>
+    // Default the height to 0 if we have no rows to show
+    let height = 0;
+    if (numRows) {
+      let firstRowRect = rows[0].getBoundingClientRect();
+      if (this._rlbPadding == undefined) {
+        let style = window.getComputedStyle(this.richlistbox);
+        let paddingTop = parseInt(style.paddingTop) || 0;
+        let paddingBottom = parseInt(style.paddingBottom) || 0;
+        this._rlbPadding = paddingTop + paddingBottom;
+      }
 
-      <method name="_collapseUnusedItems">
-        <body>
-          <![CDATA[
-            let existingItemsCount = this.richlistbox.children.length;
-            for (let i = this.matchCount; i < existingItemsCount; ++i) {
-              let item = this.richlistbox.children[i];
-
-              item.collapsed = true;
-              if (typeof item._onCollapse == "function") {
-                item._onCollapse();
-              }
-            }
-          ]]>
-        </body>
-      </method>
+      // The class `forceHandleUnderflow` is for the item might need to
+      // handle OverUnderflow or Overflow when the height of an item will
+      // be changed dynamically.
+      for (let i = 0; i < numRows; i++) {
+        if (rows[i].classList.contains("forceHandleUnderflow")) {
+          rows[i].handleOverUnderflow();
+        }
+      }
 
-      <method name="adjustHeight">
-        <body>
-          <![CDATA[
-          // Figure out how many rows to show
-          let rows = this.richlistbox.children;
-          let numRows = Math.min(this.matchCount, this.maxRows, rows.length);
+      let lastRowRect = rows[numRows - 1].getBoundingClientRect();
+      // Calculate the height to have the first row to last row shown
+      height = lastRowRect.bottom - firstRowRect.top +
+        this._rlbPadding;
+    }
 
-          // Default the height to 0 if we have no rows to show
-          let height = 0;
-          if (numRows) {
-            let firstRowRect = rows[0].getBoundingClientRect();
-            if (this._rlbPadding == undefined) {
-              let style = window.getComputedStyle(this.richlistbox);
-              let paddingTop = parseInt(style.paddingTop) || 0;
-              let paddingBottom = parseInt(style.paddingBottom) || 0;
-              this._rlbPadding = paddingTop + paddingBottom;
-            }
+    let currentHeight = this.richlistbox.getBoundingClientRect().height;
+    if (height <= currentHeight) {
+      this._collapseUnusedItems();
+    }
+    this.richlistbox.style.removeProperty("height");
+    // We need to get the ceiling of the calculated value to ensure that the box fully contains
+    // all of its contents and doesn't cause a scrollbar since nsIBoxObject only expects a
+    // `long`. e.g. if `height` is 99.5 the richlistbox would render at height 99px with a
+    // scrollbar for the extra 0.5px.
+    this.richlistbox.height = Math.ceil(height);
+  }
 
-            // The class `forceHandleUnderflow` is for the item might need to
-            // handle OverUnderflow or Overflow when the height of an item will
-            // be changed dynamically.
-            for (let i = 0; i < numRows; i++) {
-              if (rows[i].classList.contains("forceHandleUnderflow")) {
-                rows[i].handleOverUnderflow();
-              }
-            }
-
-            let lastRowRect = rows[numRows - 1].getBoundingClientRect();
-            // Calculate the height to have the first row to last row shown
-            height = lastRowRect.bottom - firstRowRect.top +
-                     this._rlbPadding;
-          }
+  _appendCurrentResult(invalidateReason) {
+    var controller = this.mInput.controller;
+    var matchCount = this.matchCount;
+    var existingItemsCount = this.richlistbox.children.length;
 
-          let currentHeight = this.richlistbox.getBoundingClientRect().height;
-          if (height <= currentHeight) {
-            this._collapseUnusedItems();
-          }
-          this.richlistbox.style.removeProperty("height");
-          // We need to get the ceiling of the calculated value to ensure that the box fully contains
-          // all of its contents and doesn't cause a scrollbar since nsIBoxObject only expects a
-          // `long`. e.g. if `height` is 99.5 the richlistbox would render at height 99px with a
-          // scrollbar for the extra 0.5px.
-          this.richlistbox.height = Math.ceil(height);
-          ]]>
-        </body>
-      </method>
+    // Process maxRows per chunk to improve performance and user experience
+    for (let i = 0; i < this.maxRows; i++) {
+      if (this._currentIndex >= matchCount) {
+        break;
+      }
+      let item;
+      let itemExists = this._currentIndex < existingItemsCount;
 
-      <method name="_appendCurrentResult">
-        <parameter name="invalidateReason"/>
-        <body>
-          <![CDATA[
-          var controller = this.mInput.controller;
-          var matchCount = this.matchCount;
-          var existingItemsCount = this.richlistbox.children.length;
-
-          // Process maxRows per chunk to improve performance and user experience
-          for (let i = 0; i < this.maxRows; i++) {
-            if (this._currentIndex >= matchCount) {
-              break;
-            }
-            let item;
-            let itemExists = this._currentIndex < existingItemsCount;
+      let originalValue, originalText, originalType;
+      let style = controller.getStyleAt(this._currentIndex);
+      let value =
+        style && style.includes("autofill") ?
+        controller.getFinalCompleteValueAt(this._currentIndex) :
+        controller.getValueAt(this._currentIndex);
+      let label = controller.getLabelAt(this._currentIndex);
+      let comment = controller.getCommentAt(this._currentIndex);
+      let image = controller.getImageAt(this._currentIndex);
+      // trim the leading/trailing whitespace
+      let trimmedSearchString = controller.searchString.replace(/^\s+/, "").replace(/\s+$/, "");
 
-            let originalValue, originalText, originalType;
-            let style = controller.getStyleAt(this._currentIndex);
-            let value =
-              style && style.includes("autofill") ?
-              controller.getFinalCompleteValueAt(this._currentIndex) :
-              controller.getValueAt(this._currentIndex);
-            let label = controller.getLabelAt(this._currentIndex);
-            let comment = controller.getCommentAt(this._currentIndex);
-            let image = controller.getImageAt(this._currentIndex);
-            // trim the leading/trailing whitespace
-            let trimmedSearchString = controller.searchString.replace(/^\s+/, "").replace(/\s+$/, "");
+      let reusable = false;
+      if (itemExists) {
+        item = this.richlistbox.children[this._currentIndex];
 
-            let reusable = false;
-            if (itemExists) {
-              item = this.richlistbox.children[this._currentIndex];
+        // Url may be a modified version of value, see _adjustAcItem().
+        originalValue = item.getAttribute("url") || item.getAttribute("ac-value");
+        originalText = item.getAttribute("ac-text");
+        originalType = item.getAttribute("originaltype");
 
-              // Url may be a modified version of value, see _adjustAcItem().
-              originalValue = item.getAttribute("url") || item.getAttribute("ac-value");
-              originalText = item.getAttribute("ac-text");
-              originalType = item.getAttribute("originaltype");
-
-              // The styles on the list which have different <content> structure and overrided
-              // _adjustAcItem() are unreusable.
-              const UNREUSEABLE_STYLES = [
-                "autofill-profile",
-                "autofill-footer",
-                "autofill-clear-button",
-                "autofill-insecureWarning",
-                "insecureWarning",
-                "loginsFooter",
-              ];
-              // Reuse the item when its style is exactly equal to the previous style or
-              // neither of their style are in the UNREUSEABLE_STYLES.
-              reusable = originalType === style ||
-                !(UNREUSEABLE_STYLES.includes(style) || UNREUSEABLE_STYLES.includes(originalType));
-            }
+        // The styles on the list which have different <content> structure and overrided
+        // _adjustAcItem() are unreusable.
+        const UNREUSEABLE_STYLES = [
+          "autofill-profile",
+          "autofill-footer",
+          "autofill-clear-button",
+          "autofill-insecureWarning",
+          "insecureWarning",
+          "loginsFooter",
+        ];
+        // Reuse the item when its style is exactly equal to the previous style or
+        // neither of their style are in the UNREUSEABLE_STYLES.
+        reusable = originalType === style ||
+          !(UNREUSEABLE_STYLES.includes(style) || UNREUSEABLE_STYLES.includes(originalType));
+      }
 
-            // If no reusable item available, then create a new item.
-            if (!reusable) {
-              let options = null;
-              switch (style) {
-                case "autofill-profile":
-                  options = { is: "autocomplete-profile-listitem" };
-                  break;
-                case "autofill-footer":
-                  options = { is: "autocomplete-profile-listitem-footer" };
-                  break;
-                case "autofill-clear-button":
-                  options = { is: "autocomplete-profile-listitem-clear-button" };
-                  break;
-                case "autofill-insecureWarning":
-                  options = { is: "autocomplete-creditcard-insecure-field" };
-                  break;
-                case "insecureWarning":
-                  options = { is: "autocomplete-richlistitem-insecure-warning" };
-                  break;
-                case "loginsFooter":
-                  options = { is: "autocomplete-richlistitem-logins-footer" };
-                  break;
-                default:
-                  options = { is: "autocomplete-richlistitem" };
-              }
-              item = document.createXULElement("richlistitem", options);
-              item.className = "autocomplete-richlistitem";
-            }
-
-            item.setAttribute("dir", this.style.direction);
-            item.setAttribute("ac-image", image);
-            item.setAttribute("ac-value", value);
-            item.setAttribute("ac-label", label);
-            item.setAttribute("ac-comment", comment);
-            item.setAttribute("ac-text", trimmedSearchString);
+      // If no reusable item available, then create a new item.
+      if (!reusable) {
+        let options = null;
+        switch (style) {
+          case "autofill-profile":
+            options = { is: "autocomplete-profile-listitem" };
+            break;
+          case "autofill-footer":
+            options = { is: "autocomplete-profile-listitem-footer" };
+            break;
+          case "autofill-clear-button":
+            options = { is: "autocomplete-profile-listitem-clear-button" };
+            break;
+          case "autofill-insecureWarning":
+            options = { is: "autocomplete-creditcard-insecure-field" };
+            break;
+          case "insecureWarning":
+            options = { is: "autocomplete-richlistitem-insecure-warning" };
+            break;
+          case "loginsFooter":
+            options = { is: "autocomplete-richlistitem-logins-footer" };
+            break;
+          default:
+            options = { is: "autocomplete-richlistitem" };
+        }
+        item = document.createXULElement("richlistitem", options);
+        item.className = "autocomplete-richlistitem";
+      }
 
-            // Completely reuse the existing richlistitem for invalidation
-            // due to new results, but only when: the item is the same, *OR*
-            // we are about to replace the currently moused-over item, to
-            // avoid surprising the user.
-            let iface = Ci.nsIAutoCompletePopup;
-            if (reusable &&
-                originalText == trimmedSearchString &&
-                invalidateReason == iface.INVALIDATE_REASON_NEW_RESULT &&
-                (originalValue == value ||
-                 this.mousedOverIndex === this._currentIndex)) {
-              // try to re-use the existing item
-              let reused = item._reuseAcItem();
-              if (reused) {
-                this._currentIndex++;
-                continue;
-              }
-            } else {
-              if (typeof item._cleanup == "function") {
-                item._cleanup();
-              }
-              item.setAttribute("originaltype", style);
-            }
+      item.setAttribute("dir", this.style.direction);
+      item.setAttribute("ac-image", image);
+      item.setAttribute("ac-value", value);
+      item.setAttribute("ac-label", label);
+      item.setAttribute("ac-comment", comment);
+      item.setAttribute("ac-text", trimmedSearchString);
 
-            if (reusable) {
-              // Adjust only when the result's type is reusable for existing
-              // item's. Otherwise, we might insensibly call old _adjustAcItem()
-              // as new binding has not been attached yet.
-              // We don't need to worry about switching to new binding, since
-              // _adjustAcItem() will fired by its own constructor accordingly.
-              item._adjustAcItem();
-              item.collapsed = false;
-            } else if (itemExists) {
-              let oldItem = this.richlistbox.children[this._currentIndex];
-              this.richlistbox.replaceChild(item, oldItem);
-            } else {
-              this.richlistbox.appendChild(item);
-            }
+      // Completely reuse the existing richlistitem for invalidation
+      // due to new results, but only when: the item is the same, *OR*
+      // we are about to replace the currently moused-over item, to
+      // avoid surprising the user.
+      let iface = Ci.nsIAutoCompletePopup;
+      if (reusable &&
+        originalText == trimmedSearchString &&
+        invalidateReason == iface.INVALIDATE_REASON_NEW_RESULT &&
+        (originalValue == value ||
+          this.mousedOverIndex === this._currentIndex)) {
+        // try to re-use the existing item
+        let reused = item._reuseAcItem();
+        if (reused) {
+          this._currentIndex++;
+          continue;
+        }
+      } else {
+        if (typeof item._cleanup == "function") {
+          item._cleanup();
+        }
+        item.setAttribute("originaltype", style);
+      }
 
-            this._currentIndex++;
-          }
+      if (reusable) {
+        // Adjust only when the result's type is reusable for existing
+        // item's. Otherwise, we might insensibly call old _adjustAcItem()
+        // as new binding has not been attached yet.
+        // We don't need to worry about switching to new binding, since
+        // _adjustAcItem() will fired by its own constructor accordingly.
+        item._adjustAcItem();
+        item.collapsed = false;
+      } else if (itemExists) {
+        let oldItem = this.richlistbox.children[this._currentIndex];
+        this.richlistbox.replaceChild(item, oldItem);
+      } else {
+        this.richlistbox.appendChild(item);
+      }
 
-          if (typeof this.onResultsAdded == "function") {
-            // The items bindings may not be attached yet, so we must delay this
-            // before we can properly handle items properly without breaking
-            // the richlistbox.
-            Services.tm.dispatchToMainThread(() => this.onResultsAdded());
-          }
+      this._currentIndex++;
+    }
 
-          if (this._currentIndex < matchCount) {
-            // yield after each batch of items so that typing the url bar is
-            // responsive
-            this._appendResultTimeout = setTimeout(() => this._appendCurrentResult(), 0);
-          }
-        ]]>
-        </body>
-      </method>
+    if (typeof this.onResultsAdded == "function") {
+      // The items bindings may not be attached yet, so we must delay this
+      // before we can properly handle items properly without breaking
+      // the richlistbox.
+      Services.tm.dispatchToMainThread(() => this.onResultsAdded());
+    }
 
-      <property name="overflowPadding"
-                onget="return Number(this.getAttribute('overflowpadding'))"
-                readonly="true" />
+    if (this._currentIndex < matchCount) {
+      // yield after each batch of items so that typing the url bar is
+      // responsive
+      this._appendResultTimeout = setTimeout(() => this._appendCurrentResult(), 0);
+    }
+  }
 
-      <method name="selectBy">
-        <parameter name="aReverse"/>
-        <parameter name="aPage"/>
-        <body>
-          <![CDATA[
-          try {
-            var amount = aPage ? 5 : 1;
+  selectBy(aReverse, aPage) {
+    try {
+      var amount = aPage ? 5 : 1;
 
-            // because we collapsed unused items, we can't use this.richlistbox.getRowCount(), we need to use the matchCount
-            this.selectedIndex = this.getNextIndex(aReverse, amount, this.selectedIndex, this.matchCount - 1);
-            if (this.selectedIndex == -1) {
-              this.input._focus();
-            }
-          } catch (ex) {
-            // do nothing - occasionally timer-related js errors happen here
-            // e.g. "this.selectedIndex has no properties", when you type fast and hit a
-            // navigation key before this popup has opened
-          }
-            ]]>
-        </body>
-      </method>
+      // because we collapsed unused items, we can't use this.richlistbox.getRowCount(), we need to use the matchCount
+      this.selectedIndex = this.getNextIndex(aReverse, amount, this.selectedIndex, this.matchCount - 1);
+      if (this.selectedIndex == -1) {
+        this.input._focus();
+      }
+    } catch (ex) {
+      // do nothing - occasionally timer-related js errors happen here
+      // e.g. "this.selectedIndex has no properties", when you type fast and hit a
+      // navigation key before this popup has opened
+    }
+  }
 
-      <field name="richlistbox">
-        document.getAnonymousElementByAttribute(this, "anonid", "richlistbox");
-      </field>
-
-      <property name="view"
-                onget="return this.mInput.controller;"
-                onset="return val;"/>
+  disconnectedCallback() {
+    if (this.listEvents) {
+      this.richlistbox.removeEventListener("mouseup", this.listEvents);
+      this.richlistbox.removeEventListener("mousemove", this.listEvents);
+      delete this.listEvents;
+    }
+  }
 
-    </implementation>
-    <handlers>
-      <handler event="popupshowing"><![CDATA[
-        // If normalMaxRows wasn't already set by the input, then set it here
-        // so that we restore the correct number when the popup is hidden.
+  setListeners() {
+    this.addEventListener("popupshowing", (event) => {
+      // If normalMaxRows wasn't already set by the input, then set it here
+      // so that we restore the correct number when the popup is hidden.
 
-        // Null-check this.mInput; see bug 1017914
-        if (this._normalMaxRows < 0 && this.mInput) {
-          this._normalMaxRows = this.mInput.maxRows;
-        }
+      // Null-check this.mInput; see bug 1017914
+      if (this._normalMaxRows < 0 && this.mInput) {
+        this._normalMaxRows = this.mInput.maxRows;
+      }
 
-        // Set an attribute for styling the popup based on the input.
-        let inputID = "";
-        if (this.mInput && this.mInput.ownerDocument &&
-            this.mInput.ownerDocument.documentURIObject.schemeIs("chrome")) {
-          inputID = this.mInput.id;
-          // Take care of elements with no id that are inside xbl bindings
-          if (!inputID) {
-            let bindingParent = this.mInput.ownerDocument.getBindingParent(this.mInput);
-            if (bindingParent) {
-              inputID = bindingParent.id;
-            }
+      // Set an attribute for styling the popup based on the input.
+      let inputID = "";
+      if (this.mInput && this.mInput.ownerDocument &&
+        this.mInput.ownerDocument.documentURIObject.schemeIs("chrome")) {
+        inputID = this.mInput.id;
+        // Take care of elements with no id that are inside xbl bindings
+        if (!inputID) {
+          let bindingParent = this.mInput.ownerDocument.getBindingParent(this.mInput);
+          if (bindingParent) {
+            inputID = bindingParent.id;
           }
         }
-        this.setAttribute("autocompleteinput", inputID);
+      }
+      this.setAttribute("autocompleteinput", inputID);
 
-        this.mPopupOpen = true;
-      ]]></handler>
+      this.mPopupOpen = true;
+    });
 
-      <handler event="popupshown">
-        <![CDATA[
-          if (this._adjustHeightOnPopupShown) {
-            delete this._adjustHeightOnPopupShown;
-            this.adjustHeight();
-          }
-      ]]>
-      </handler>
+    this.addEventListener("popupshown", (event) => {
+       if (this._adjustHeightOnPopupShown) {
+        delete this._adjustHeightOnPopupShown;
+        this.adjustHeight();
+      }
+    });
 
-      <handler event="popuphiding"><![CDATA[
-        var isListActive = true;
-        if (this.selectedIndex == -1)
-          isListActive = false;
-        this.input.controller.stopSearch();
+    this.addEventListener("popuphiding", (event) => {
+      var isListActive = true;
+      if (this.selectedIndex == -1)
+        isListActive = false;
+      this.input.controller.stopSearch();
+
+      this.removeAttribute("autocompleteinput");
+      this.mPopupOpen = false;
 
-        this.removeAttribute("autocompleteinput");
-        this.mPopupOpen = false;
+      // Reset the maxRows property to the cached "normal" value (if there's
+      // any), and reset normalMaxRows so that we can detect whether it was set
+      // by the input when the popupshowing handler runs.
 
-        // Reset the maxRows property to the cached "normal" value (if there's
-        // any), and reset normalMaxRows so that we can detect whether it was set
-        // by the input when the popupshowing handler runs.
+      // Null-check this.mInput; see bug 1017914
+      if (this.mInput && this._normalMaxRows > 0) {
+        this.mInput.maxRows = this._normalMaxRows;
+      }
+      this._normalMaxRows = -1;
+      // If the list was being navigated and then closed, make sure
+      // we fire accessible focus event back to textbox
 
-        // Null-check this.mInput; see bug 1017914
-        if (this.mInput && this._normalMaxRows > 0) {
-          this.mInput.maxRows = this._normalMaxRows;
-        }
-        this._normalMaxRows = -1;
-        // If the list was being navigated and then closed, make sure
-        // we fire accessible focus event back to textbox
+      // Null-check this.mInput; see bug 1017914
+      if (isListActive && this.mInput) {
+        this.mInput.mIgnoreFocus = true;
+        this.mInput._focus();
+        this.mInput.mIgnoreFocus = false;
+      }
+    });
+  }
+};
 
-        // Null-check this.mInput; see bug 1017914
-        if (isListActive && this.mInput) {
-          this.mInput.mIgnoreFocus = true;
-          this.mInput._focus();
-          this.mInput.mIgnoreFocus = false;
-        }
-      ]]></handler>
-    </handlers>
-  </binding>
+MozPopupElement.implementCustomInterface(MozElements.MozAutocompleteRichlistboxPopup, [Ci.nsIAutoCompletePopup]);
+
+customElements.define("autocomplete-richlistbox-popup", MozElements.MozAutocompleteRichlistboxPopup, {
+  extends: "panel",
+});
+}
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -65,17 +65,17 @@
           }
 
           let popup = null;
           let popupId = this.getAttribute("autocompletepopup");
           if (popupId) {
             popup = document.getElementById(popupId);
           }
           if (!popup) {
-            popup = document.createXULElement("panel");
+            popup = document.createXULElement("panel", { is: "autocomplete-richlistbox-popup" });
             popup.setAttribute("type", "autocomplete-richlistbox");
             popup.setAttribute("noautofocus", "true");
 
             let popupset = document.getAnonymousElementByAttribute(this, "anonid", "popupset");
             popupset.appendChild(popup);
           }
           popup.mInput = this;
 
--- a/toolkit/content/xul.css
+++ b/toolkit/content/xul.css
@@ -545,17 +545,17 @@ textbox[type="search"] {
 /* 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: url("chrome://global/content/bindings/autocomplete.xml#autocomplete-rich-result-popup");
+  -moz-binding: none;
 }
 
 .autocomplete-richlistbox {
   -moz-user-focus: ignore;
   overflow-x: hidden !important;
 }
 
 .autocomplete-richlistitem {
--- a/toolkit/mozapps/extensions/content/extensions.xul
+++ b/toolkit/mozapps/extensions/content/extensions.xul
@@ -63,17 +63,18 @@
                  activateontab="true" position="after_start"
                  level="parent"
 #ifdef XP_WIN
                  consumeoutsideclicks="false" ignorekeys="shortcuts"
 #endif
         />
     </menulist>
 
-    <panel type="autocomplete-richlistbox"
+    <panel is="autocomplete-richlistbox-popup"
+           type="autocomplete-richlistbox"
            id="PopupAutoComplete"
            noautofocus="true"
            hidden="true"
            norolluponanchor="true"
            nomaxresults="true" />
 
     <tooltip id="addonitem-tooltip"/>