Bug 1531870 - [de-xbl] convert popup binding to custom element; r=bgrins
authorPaul Morris <paul@paulwmorris.com>
Sat, 30 Mar 2019 00:05:08 +0000
changeset 525726 666dcd68b0234b4f8ef2259c5ef71aa6756a7fd0
parent 525725 b9654a90f60adb2b8667cfa1e6ef7545579357b7
child 525727 d42c60ccf0d05a8b1e6098c1ab62d26e6edd2267
child 525741 0ea914bf03b719cd0821564b551dbb384ba95ff5
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1531870
milestone68.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 1531870 - [de-xbl] convert popup binding to custom element; r=bgrins For now, only add the MozMenuPopup base class to MozElements, and don't define a custom element for it with `customElements.define`. This is to help avoid conflicts in de-xbl work. (See the bug for details.) Includes a function to do 'manual slotting', moving child elements into place. Dynamically adding, modifying, or removing child nodes after the element is connected needs to be handled manually as well. Differential Revision: https://phabricator.services.mozilla.com/D25467
layout/base/tests/chrome/test_bug420499.xul
layout/xul/nsMenuPopupFrame.cpp
layout/xul/reftest/popup-explicit-size.xul
toolkit/content/customElements.js
toolkit/content/jar.mn
toolkit/content/widgets/menupopup.js
toolkit/content/xul.css
--- a/layout/base/tests/chrome/test_bug420499.xul
+++ b/layout/base/tests/chrome/test_bug420499.xul
@@ -23,21 +23,21 @@ https://bugzilla.mozilla.org/show_bug.cg
           </menupopup>
         </menu>
         <menuitem label="Item3"/>
         <menuitem label="Item4"/>
       </menupopup>
     </menu>
 
     <popupset>
-      <popup id="contextmenu">
+      <menupopup id="contextmenu">
         <menuitem label="Cut"/>
         <menuitem label="Copy"/>
         <menuitem label="Paste"/>
-      </popup>
+      </menupopup>
       <tooltip id="tooltip" orient="vertical">
         <description value="This is a tooltip"/>
       </tooltip>
     </popupset>
   
   <!-- test results are displayed in the html:body -->
   <body xmlns="http://www.w3.org/1999/xhtml" bgcolor="white">
   
--- a/layout/xul/nsMenuPopupFrame.cpp
+++ b/layout/xul/nsMenuPopupFrame.cpp
@@ -682,17 +682,17 @@ void nsMenuPopupFrame::InitializePopup(n
   mVFlip = false;
   mHFlip = false;
   mAlignmentOffset = 0;
   mPositionedOffset = 0;
 
   mAnchorType = aAnchorType;
 
   // if aAttributesOverride is true, then the popupanchor, popupalign and
-  // position attributes on the <popup> override those values passed in.
+  // position attributes on the <menupopup> override those values passed in.
   // If false, those attributes are only used if the values passed in are empty
   if (aAnchorContent || aAnchorType == MenuPopupAnchorType_Rect) {
     nsAutoString anchor, align, position, flip;
     mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::popupanchor,
                                    anchor);
     mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::popupalign,
                                    align);
     mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::position,
--- a/layout/xul/reftest/popup-explicit-size.xul
+++ b/layout/xul/reftest/popup-explicit-size.xul
@@ -1,7 +1,7 @@
 <?xml version="1.0"?>
 <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
 <window align="start" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
   <label value="One"/>
-  <popup height="40"/>
+  <menupopup height="40"/>
   <label value="Two"/>
 </window>
--- a/toolkit/content/customElements.js
+++ b/toolkit/content/customElements.js
@@ -500,16 +500,17 @@ customElements.setElementCreationCallbac
 // For now, don't load any elements in the extension dummy document.
 // We will want to load <browser> when that's migrated (bug 1441935).
 const isDummyDocument = document.documentURI == "chrome://extensions/content/dummy.xul";
 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/menupopup.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",
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -87,16 +87,17 @@ toolkit.jar:
    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)
+   content/global/elements/menupopup.js        (widgets/menupopup.js)
    content/global/elements/notificationbox.js  (widgets/notificationbox.js)
    content/global/elements/pluginProblem.js    (widgets/pluginProblem.js)
    content/global/elements/radio.js            (widgets/radio.js)
    content/global/elements/richlistbox.js      (widgets/richlistbox.js)
    content/global/elements/marquee.css         (widgets/marquee.css)
    content/global/elements/marquee.js          (widgets/marquee.js)
    content/global/elements/menulist.js         (widgets/menulist.js)
    content/global/elements/popupnotification.js  (widgets/popupnotification.js)
copy from toolkit/content/widgets/popup.xml
copy to toolkit/content/widgets/menupopup.js
--- a/toolkit/content/widgets/popup.xml
+++ b/toolkit/content/widgets/menupopup.js
@@ -1,391 +1,182 @@
-<?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/. -->
-
-<bindings id="popupBindings"
-   xmlns="http://www.mozilla.org/xbl"
-   xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-   xmlns:xbl="http://www.mozilla.org/xbl">
+/* 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/. */
 
-  <binding id="popup">
-    <content>
-      <xul:arrowscrollbox class="popup-internal-box" flex="1" orient="vertical"
-                          smoothscroll="false">
-        <children/>
-      </xul:arrowscrollbox>
-    </content>
-
-    <implementation>
-      <field name="scrollBox" readonly="true">
-        document.getAnonymousElementByAttribute(this, "class", "popup-internal-box");
-      </field>
-
-      <field name="AUTOSCROLL_INTERVAL">25</field>
-      <field name="NOT_DRAGGING">0</field>
-      <field name="DRAG_OVER_BUTTON">-1</field>
-      <field name="DRAG_OVER_POPUP">1</field>
-
-      <field name="_draggingState">this.NOT_DRAGGING</field>
-      <field name="_scrollTimer">0</field>
+"use strict";
 
-      <method name="_enableDragScrolling">
-        <!-- when overItem is true, drag started over menuitem; when false, drag
-             started while the popup was opening.
-          -->
-        <parameter name="overItem"/>
-        <body>
-        <![CDATA[
-          if (!this._draggingState) {
-            this.setCaptureAlways();
-            this._draggingState = overItem ? this.DRAG_OVER_POPUP : this.DRAG_OVER_BUTTON;
-          }
-        ]]>
-        </body>
-      </method>
-
-      <method name="_clearScrollTimer">
-        <body>
-        <![CDATA[
-          if (this._scrollTimer) {
-            this.ownerGlobal.clearInterval(this._scrollTimer);
-            this._scrollTimer = 0;
-          }
-        ]]>
-        </body>
-      </method>
-
-      <constructor><![CDATA[
-        // Enable the drag-to-scroll events only in menulist popups.
-        if (!this.parentNode || this.parentNode.localName != "menulist") {
-          return;
-        }
-
-        // XBL bindings might be constructed more than once.
-        if (this.eventListenersAdded) {
-          return;
-        }
-        this.eventListenersAdded = true;
-
-        this.addEventListener("popupshown", () => {
-          // Enable drag scrolling even when the mouse wasn't used. The
-          // mousemove handler will remove it if the mouse isn't down.
-          this._enableDragScrolling(false);
-        });
-
-        this.addEventListener("popuphidden", () => {
-          this._draggingState = this.NOT_DRAGGING;
-          this._clearScrollTimer();
-          this.releaseCapture();
-        });
+// This is loaded into all XUL windows. Wrap in a block to prevent
+// leaking to window scope.
+{
+  class MozMenuPopup extends MozElements.MozElementMixin(XULPopupElement) {
+    constructor() {
+      super();
 
-        this.addEventListener("mousedown", event => {
-          if (event.button != 0) {
-            return;
-          }
-
-          if (this.state == "open" &&
-            (event.target.localName == "menuitem" ||
-              event.target.localName == "menu" ||
-              event.target.localName == "menucaption")) {
-            this._enableDragScrolling(true);
-          }
-        });
-
-        this.addEventListener("mouseup", event => {
-          if (event.button != 0) {
-            return;
-          }
-
-          this._draggingState = this.NOT_DRAGGING;
-          this._clearScrollTimer();
-        });
-
-        this.addEventListener("mousemove", event => {
-          if (!this._draggingState) {
-            return;
-          }
-
-          this._clearScrollTimer();
-
-          // If the user released the mouse before the popup opens, we will
-          // still be capturing, so check that the button is still pressed. If
-          // not, release the capture and do nothing else. This also handles if
-          // the dropdown was opened via the keyboard.
-          if (!(event.buttons & 1)) {
-            this._draggingState = this.NOT_DRAGGING;
-            this.releaseCapture();
-            return;
-          }
+      this.AUTOSCROLL_INTERVAL = 25;
+      this.NOT_DRAGGING = 0;
+      this.DRAG_OVER_BUTTON = -1;
+      this.DRAG_OVER_POPUP = 1;
+      this._draggingState = this.NOT_DRAGGING;
+      this._scrollTimer = 0;
 
-          // If dragging outside the top or bottom edge of the popup, but within
-          // the popup area horizontally, scroll the list in that direction. The
-          // _draggingState flag is used to ensure that scrolling does not start
-          // until the mouse has moved over the popup first, preventing
-          // scrolling while over the dropdown button.
-          let popupRect = this.getOuterScreenRect();
-          if (event.screenX >= popupRect.left &&
-              event.screenX <= popupRect.right) {
-            if (this._draggingState == this.DRAG_OVER_BUTTON) {
-              if (event.screenY > popupRect.top &&
-                  event.screenY < popupRect.bottom) {
-                this._draggingState = this.DRAG_OVER_POPUP;
-              }
-            }
-
-            if (this._draggingState == this.DRAG_OVER_POPUP &&
-                (event.screenY <= popupRect.top ||
-                 event.screenY >= popupRect.bottom)) {
-              let scrollAmount = event.screenY <= popupRect.top ? -1 : 1;
-              this.scrollBox.scrollByIndex(scrollAmount, true);
-
-              let win = this.ownerGlobal;
-              this._scrollTimer = win.setInterval(() => {
-                this.scrollBox.scrollByIndex(scrollAmount, true);
-              }, this.AUTOSCROLL_INTERVAL);
-            }
-          }
-        });
-      ]]></constructor>
-    </implementation>
-
-    <handlers>
-      <handler event="popupshowing" phase="target">
-        <![CDATA[
-          var array = [];
-          var width = 0;
-          for (var menuitem = this.firstElementChild; menuitem; menuitem = menuitem.nextElementSibling) {
-            if (menuitem.localName == "menuitem" && menuitem.hasAttribute("acceltext")) {
-              var accel = document.getAnonymousElementByAttribute(menuitem, "anonid", "accel");
-              if (accel) {
-                array.push(accel);
-                let accelWidth = accel.getBoundingClientRect().width;
-                if (accelWidth > width) {
-                  width = accelWidth;
-                }
+      this.addEventListener("popupshowing", event => {
+        let array = [];
+        let width = 0;
+        for (let menuitem = this.firstElementChild; menuitem; menuitem = menuitem.nextElementSibling) {
+          if (menuitem.localName == "menuitem" && menuitem.hasAttribute("acceltext")) {
+            let accel = menuitem.querySelector(".menu-accel-container");
+            if (accel && accel.boxObject) {
+              array.push(accel);
+              if (accel.boxObject.width > width) {
+                width = accel.boxObject.width;
               }
             }
           }
-          for (var i = 0; i < array.length; i++)
-            array[i].width = width;
-        ]]>
-      </handler>
-    </handlers>
-  </binding>
-
-  <binding id="panel">
-    <implementation>
-      <field name="_prevFocus">0</field>
-    </implementation>
-
-    <handlers>
-      <handler event="popupshowing"><![CDATA[
-        // Capture the previous focus before has a chance to get set inside the panel
-        try {
-          this._prevFocus = Cu
-                            .getWeakReference(document.commandDispatcher.focusedElement);
-          if (this._prevFocus.get())
-            return;
-        } catch (ex) { }
+        }
+        array.forEach(accel => accel.width = width);
+      });
+    }
 
-        this._prevFocus = Cu.getWeakReference(document.activeElement);
-      ]]></handler>
-      <handler event="popupshown"><![CDATA[
-        // Fire event for accessibility APIs
-        var alertEvent = document.createEvent("Events");
-        alertEvent.initEvent("AlertActive", true, true);
-        this.dispatchEvent(alertEvent);
-       ]]></handler>
-      <handler event="popuphiding"><![CDATA[
-        try {
-          this._currentFocus = document.commandDispatcher.focusedElement;
-        } catch (e) {
-          this._currentFocus = document.activeElement;
-        }
-      ]]></handler>
-      <handler event="popuphidden"><![CDATA[
-        function doFocus() {
-          // Focus was set on an element inside this panel,
-          // so we need to move it back to where it was previously
-          try {
-            let fm = Cc["@mozilla.org/focus-manager;1"]
-                       .getService(Ci.nsIFocusManager);
-            fm.setFocus(prevFocus, fm.FLAG_NOSCROLL);
-          } catch (e) {
-            prevFocus.focus();
-          }
-        }
-        var currentFocus = this._currentFocus;
-        var prevFocus = this._prevFocus ? this._prevFocus.get() : null;
-        this._currentFocus = null;
-        this._prevFocus = null;
+    connectedCallback() {
+      if (this.delayConnectedCallback() || this.hasConnected) {
+        return;
+      }
+      this.hasConnected = true;
+      this.appendChild(MozXULElement.parseXULToFragment(`
+        <arrowscrollbox class="popup-internal-box"
+                        flex="1"
+                        orient="vertical"
+                        smoothscroll="false"/>
+      `));
+      this.scrollBox = this.querySelector(".popup-internal-box");
+
+      if (this.parentNode && this.parentNode.localName == "menulist") {
+        this._setUpMenulistPopup();
+      }
+    }
 
-        // Avoid changing focus if focus changed while we hide the popup
-        // (This can happen e.g. if the popup is hiding as a result of a
-        // click/keypress that focused something)
-        let nowFocus;
-        try {
-          nowFocus = document.commandDispatcher.focusedElement;
-        } catch (e) {
-          nowFocus = document.activeElement;
-        }
-        if (nowFocus && nowFocus != currentFocus)
-          return;
-
-        if (prevFocus && this.getAttribute("norestorefocus") != "true") {
-          // Try to restore focus
-          try {
-            if (document.commandDispatcher.focusedWindow != window)
-              return; // Focus has already been set to a window outside of this panel
-          } catch (ex) {}
+    /**
+     * When a custom element (CE) class extends this MozMenuPopup class,
+     * and child nodes are present inside that CE in the XUL files
+     * where it is used, then this method should be called in that CE's
+     * connectedCallback. It will slot those child nodes into place
+     * inside the CE's internal scroll box element.
+     *
+     * This "manual slotting" is done with this method, and not by default
+     * in the connectedCallback of this base class, to support cases where
+     * children are dynamically added, etc. (Which also requires "manual
+     * slotting".) See bug 1531870.
+     */
+    _setUpChildElements() {
+      while (this.childElementCount > 1) {
+        this.scrollBox.appendChild(this.firstElementChild);
+      }
+    }
 
-          if (!currentFocus) {
-            doFocus();
-            return;
-          }
-          while (currentFocus) {
-            if (currentFocus == this) {
-              doFocus();
-              return;
-            }
-            currentFocus = currentFocus.parentNode;
-          }
-        }
-      ]]></handler>
-    </handlers>
-  </binding>
+    /**
+     * Adds event listeners for a MozMenuPopup inside a menulist element.
+     */
+    _setUpMenulistPopup() {
+      this.addEventListener("popupshown", () => {
+        // Enable drag scrolling even when the mouse wasn't used. The
+        // mousemove handler will remove it if the mouse isn't down.
+        this._enableDragScrolling(false);
+      });
+
+      this.addEventListener("popuphidden", () => {
+        this._draggingState = this.NOT_DRAGGING;
+        this._clearScrollTimer();
+        this.releaseCapture();
+      });
 
-  <binding id="arrowpanel" extends="chrome://global/content/bindings/popup.xml#panel">
-    <resources>
-      <!-- Fixes an issue with the "test_arrowpanel.xul" animation on Mac, see bug 1470880. -->
-      <stylesheet src="data:text/css,"/>
-    </resources>
+      this.addEventListener("mousedown", event => {
+        if (event.button != 0) {
+          return;
+        }
 
-    <content flip="both" side="top" position="bottomcenter topleft" consumeoutsideclicks="false">
-      <xul:vbox anonid="container" class="panel-arrowcontainer" flex="1"
-               xbl:inherits="side,panelopen">
-        <xul:box anonid="arrowbox" class="panel-arrowbox">
-          <xul:image anonid="arrow" class="panel-arrow" xbl:inherits="side"/>
-        </xul:box>
-        <xul:box class="panel-arrowcontent" xbl:inherits="side,align,dir,orient,pack" flex="1">
-          <children/>
-        </xul:box>
-      </xul:vbox>
-    </content>
-    <implementation>
-      <field name="_fadeTimer">null</field>
-      <method name="adjustArrowPosition">
-        <body>
-        <![CDATA[
-        var anchor = this.anchorNode;
-        if (!anchor) {
+        if (this.state == "open" &&
+          (event.target.localName == "menuitem" ||
+            event.target.localName == "menu" ||
+            event.target.localName == "menucaption")) {
+          this._enableDragScrolling(true);
+        }
+      });
+
+      this.addEventListener("mouseup", event => {
+        if (event.button != 0) {
           return;
         }
 
-        var container = document.getAnonymousElementByAttribute(this, "anonid", "container");
-        var arrowbox = document.getAnonymousElementByAttribute(this, "anonid", "arrowbox");
-
-        var position = this.alignmentPosition;
-        var offset = this.alignmentOffset;
-
-        this.setAttribute("arrowposition", position);
-
-        if (position.indexOf("start_") == 0 || position.indexOf("end_") == 0) {
-          container.orient = "horizontal";
-          arrowbox.orient = "vertical";
-          if (position.indexOf("_after") > 0) {
-            arrowbox.pack = "end";
-          } else {
-            arrowbox.pack = "start";
-          }
-          arrowbox.style.transform = "translate(0, " + -offset + "px)";
-
-          // The assigned side stays the same regardless of direction.
-          var isRTL = (window.getComputedStyle(this).direction == "rtl");
+        this._draggingState = this.NOT_DRAGGING;
+        this._clearScrollTimer();
+      });
 
-          if (position.indexOf("start_") == 0) {
-            container.dir = "reverse";
-            this.setAttribute("side", isRTL ? "left" : "right");
-          } else {
-            container.dir = "";
-            this.setAttribute("side", isRTL ? "right" : "left");
-          }
-        } else if (position.indexOf("before_") == 0 || position.indexOf("after_") == 0) {
-          container.orient = "";
-          arrowbox.orient = "";
-          if (position.indexOf("_end") > 0) {
-            arrowbox.pack = "end";
-          } else {
-            arrowbox.pack = "start";
-          }
-          arrowbox.style.transform = "translate(" + -offset + "px, 0)";
-
-          if (position.indexOf("before_") == 0) {
-            container.dir = "reverse";
-            this.setAttribute("side", "bottom");
-          } else {
-            container.dir = "";
-            this.setAttribute("side", "top");
-          }
-        }
-        ]]>
-        </body>
-      </method>
-    </implementation>
-    <handlers>
-      <handler event="popupshowing" phase="target">
-      <![CDATA[
-        var arrow = document.getAnonymousElementByAttribute(this, "anonid", "arrow");
-        arrow.hidden = this.anchorNode == null;
-        document.getAnonymousElementByAttribute(this, "anonid", "arrowbox")
-                .style.removeProperty("transform");
-
-        if (this.getAttribute("animate") != "false") {
-          this.setAttribute("animate", "open");
-          // the animating attribute prevents user interaction during transition
-          // it is removed when popupshown fires
-          this.setAttribute("animating", "true");
+      this.addEventListener("mousemove", event => {
+        if (!this._draggingState) {
+          return;
         }
 
-        // set fading
-        var fade = this.getAttribute("fade");
-        var fadeDelay = 0;
-        if (fade == "fast") {
-          fadeDelay = 1;
-        } else if (fade == "slow") {
-          fadeDelay = 4000;
-        } else {
+        this._clearScrollTimer();
+
+        // If the user released the mouse before the menupopup opens, we will
+        // still be capturing, so check that the button is still pressed. If
+        // not, release the capture and do nothing else. This also handles if
+        // the dropdown was opened via the keyboard.
+        if (!(event.buttons & 1)) {
+          this._draggingState = this.NOT_DRAGGING;
+          this.releaseCapture();
           return;
         }
 
-        this._fadeTimer = setTimeout(() => this.hidePopup(true), fadeDelay, this);
-      ]]>
-      </handler>
-      <handler event="popuphiding" phase="target">
-        let animate = (this.getAttribute("animate") != "false");
+        // If dragging outside the top or bottom edge of the menupopup, but
+        // within the menupopup area horizontally, scroll the list in that
+        // direction. The _draggingState flag is used to ensure that scrolling
+        // does not start until the mouse has moved over the menupopup first,
+        // preventing scrolling while over the dropdown button.
+        let popupRect = this.getOuterScreenRect();
+        if (event.screenX >= popupRect.left &&
+          event.screenX <= popupRect.right) {
+          if (this._draggingState == this.DRAG_OVER_BUTTON) {
+            if (event.screenY > popupRect.top &&
+              event.screenY < popupRect.bottom) {
+              this._draggingState = this.DRAG_OVER_POPUP;
+            }
+          }
 
-        if (this._fadeTimer) {
-          clearTimeout(this._fadeTimer);
-          if (animate) {
-            this.setAttribute("animate", "fade");
+          if (this._draggingState == this.DRAG_OVER_POPUP &&
+            (event.screenY <= popupRect.top ||
+              event.screenY >= popupRect.bottom)) {
+            let scrollAmount = event.screenY <= popupRect.top ? -1 : 1;
+            this.scrollBox.scrollByIndex(scrollAmount, true);
+
+            let win = this.ownerGlobal;
+            this._scrollTimer = win.setInterval(() => {
+              this.scrollBox.scrollByIndex(scrollAmount, true);
+            }, this.AUTOSCROLL_INTERVAL);
           }
-        } else if (animate) {
-          this.setAttribute("animate", "cancel");
         }
-      </handler>
-      <handler event="popupshown" phase="target">
-        this.removeAttribute("animating");
-        this.setAttribute("panelopen", "true");
-      </handler>
-      <handler event="popuphidden" phase="target">
-        this.removeAttribute("panelopen");
-        if (this.getAttribute("animate") != "false") {
-          this.removeAttribute("animate");
-        }
-      </handler>
-      <handler event="popuppositioned" phase="target">
-        this.adjustArrowPosition();
-      </handler>
-    </handlers>
-  </binding>
-</bindings>
+      });
+
+      this._menulistPopupIsSetUp = true;
+    }
+
+    _enableDragScrolling(overItem) {
+      if (!this._draggingState) {
+        this.setCaptureAlways();
+        this._draggingState = overItem
+          ? this.DRAG_OVER_POPUP
+          : this.DRAG_OVER_BUTTON;
+      }
+    }
+
+    _clearScrollTimer() {
+      if (this._scrollTimer) {
+        this.ownerGlobal.clearInterval(this._scrollTimer);
+        this._scrollTimer = 0;
+      }
+    }
+  }
+
+  // Add this MozMenuPopup base class to MozElements, but don't define a custom
+  // element for it with `customElements.define` (for now, see bug 1531870).
+  MozElements.MozMenuPopup = MozMenuPopup;
+}
--- a/toolkit/content/xul.css
+++ b/toolkit/content/xul.css
@@ -283,32 +283,28 @@ menuitem[type="checkbox"],
 menuitem[type="radio"] {
   -moz-binding: url("chrome://global/content/bindings/menu.xml#menuitem-iconic");
 }
 
 .menu-text {
   -moz-box-flex: 1;
 }
 
-/********* popup & menupopup ***********/
+/********* menupopup, panel, & tooltip ***********/
 
-/* <popup> is deprecated.  Only <menupopup> and <tooltip> are still valid. */
-
-popup,
 menupopup {
   -moz-binding: url("chrome://global/content/bindings/popup.xml#popup");
   -moz-box-orient: vertical;
 }
 
 panel {
   -moz-binding: url("chrome://global/content/bindings/popup.xml#panel");
   -moz-box-orient: vertical;
 }
 
-popup,
 menupopup,
 panel,
 tooltip {
   display: -moz-popup;
   z-index: 2147483647;
   text-shadow: none;
 }