Bug 1481949 - Migrate <radiogroup> to a Custom Element;r=timdream
authorBrian Grinstead <bgrinstead@mozilla.com>
Mon, 01 Oct 2018 18:01:02 +0000
changeset 494787 b82060ff3299da0ac4bd497251c0f995ef857f12
parent 494786 96cb7281e630428a8d2b6128813615ae694b868d
child 494788 05e791fb2c6b2fe7f967adf86e55517231bcf59d
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstimdream
bugs1481949
milestone64.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 1481949 - Migrate <radiogroup> to a Custom Element;r=timdream Differential Revision: https://phabricator.services.mozilla.com/D6326
toolkit/content/customElements.js
toolkit/content/jar.mn
toolkit/content/tests/chrome/test_custom_element_base.xul
toolkit/content/widgets/radio.js
toolkit/content/widgets/radio.xml
toolkit/content/xul.css
--- a/toolkit/content/customElements.js
+++ b/toolkit/content/customElements.js
@@ -117,18 +117,25 @@ class MozXULElement extends XULElement {
    * all of the interfaces that are specified.
    *
    * @param cls
    *        The class that implements the interface.
    * @param names
    *        Array of interface names.
    */
   static implementCustomInterface(cls, ifaces) {
+    const numbers = new Set(ifaces.map(i => i.number));
+    if (cls.prototype.customInterfaceNumbers) {
+      // Base class already implemented some interfaces. Inherit:
+      cls.prototype.customInterfaceNumbers.forEach(number => numbers.add(number));
+    }
+
+    cls.prototype.customInterfaceNumbers = numbers;
     cls.prototype.getCustomInterfaceCallback = function getCustomInterfaceCallback(iface) {
-      if (ifaces.includes(Ci[Components.interfacesByID[iface.number]])) {
+      if (numbers.has(iface.number)) {
         return getInterfaceProxy(this);
       }
       return null;
     };
   }
 }
 
 /**
@@ -151,38 +158,65 @@ function getInterfaceProxy(obj) {
         return propOrMethod;
       },
     });
   }
 
   return obj._customInterfaceProxy;
 }
 
+class MozBaseControl extends MozXULElement {
+  get disabled() {
+    return this.getAttribute("disabled") == "true";
+  }
+
+  set disabled(val) {
+    if (val) {
+      this.setAttribute("disabled", "true");
+    } else {
+      this.removeAttribute("disabled");
+    }
+  }
+
+  get tabIndex() {
+    return parseInt(this.getAttribute("tabindex")) || 0;
+  }
+
+  set tabIndex(val) {
+    if (val) {
+      this.setAttribute("tabindex", val);
+    } else {
+      this.removeAttribute("tabindex");
+    }
+  }
+}
+
+MozXULElement.implementCustomInterface(MozBaseControl, [Ci.nsIDOMXULControlElement]);
+
 // Attach the base class to the window so other scripts can use it:
 window.MozXULElement = MozXULElement;
+window.MozBaseControl = MozBaseControl;
 
 // 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/radio.js",
     "chrome://global/content/elements/textbox.js",
     "chrome://global/content/elements/tabbox.js",
   ]) {
     Services.scriptloader.loadSubScript(script, window);
   }
 
   for (let [tag, script] of [
     ["findbar", "chrome://global/content/elements/findbar.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, () => {
       Services.scriptloader.loadSubScript(script, window);
     });
   }
-
 }
-
 }
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -95,16 +95,17 @@ toolkit.jar:
    content/global/bindings/toolbar.xml         (widgets/toolbar.xml)
    content/global/bindings/toolbarbutton.xml   (widgets/toolbarbutton.xml)
    content/global/bindings/tree.xml            (widgets/tree.xml)
    content/global/bindings/videocontrols.xml   (widgets/videocontrols.xml)
 *  content/global/bindings/wizard.xml          (widgets/wizard.xml)
    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/radio.js            (widgets/radio.js)
    content/global/elements/stringbundle.js     (widgets/stringbundle.js)
    content/global/elements/tabbox.js           (widgets/tabbox.js)
    content/global/elements/textbox.js          (widgets/textbox.js)
    content/global/elements/videocontrols.js    (widgets/videocontrols.js)
 #ifdef XP_MACOSX
    content/global/macWindowMenu.js
 #endif
    content/global/gmp-sources/openh264.json    (gmp-sources/openh264.json)
--- a/toolkit/content/tests/chrome/test_custom_element_base.xul
+++ b/toolkit/content/tests/chrome/test_custom_element_base.xul
@@ -12,16 +12,17 @@
   <!-- test results are displayed in the html:body -->
   <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/>
 
   <button id="one"/>
   <simpleelement id="two" style="-moz-user-focus: normal;"/>
   <simpleelement id="three" disabled="true" style="-moz-user-focus: normal;"/>
   <button id="four"/>
 
+
   <!-- test code goes here -->
   <script type="application/javascript"><![CDATA[
 
   SimpleTest.waitForExplicitFinish();
 
   async function runTests() {
     ok(MozXULElement, "MozXULElement defined on the window");
     testParseXULToFragment();
@@ -108,27 +109,35 @@
     // Enable Full Keyboard Access emulation on Mac
     await SpecialPowers.pushPrefEnv({"set": [["accessibility.tabfocus", 7]]});
 
     ok(!twoElement.disabled, "two is enabled");
     ok(document.getElementById("three").disabled, "three is disabled");
 
     await SimpleTest.promiseFocus();
     ok(document.hasFocus(), "has focus");
-    
+
     // This should skip the disabled simpleelement.
     synthesizeKey("VK_TAB");
     is(document.activeElement.id, "one", "Tab 1");
     synthesizeKey("VK_TAB");
     is(document.activeElement.id, "two", "Tab 2");
     synthesizeKey("VK_TAB");
     is(document.activeElement.id, "four", "Tab 3");
 
     twoElement.disabled = true;
     is(twoElement.getAttribute("disabled"), "true", "two disabled after change");
 
     synthesizeKey("VK_TAB", { shiftKey: true });
     is(document.activeElement.id, "one", "Tab 1");
+
+    info("Checking that interfaces get inherited automatically with implementCustomInterface");
+    class ExtendedElement extends SimpleElement { }
+    MozXULElement.implementCustomInterface(ExtendedElement, [Ci.nsIDOMXULSelectControlElement]);
+    customElements.define("extendedelement", ExtendedElement);
+    const extendedInstance = document.createXULElement("extendedelement");
+    ok(extendedInstance instanceof Ci.nsIDOMXULSelectControlElement, "interface applied");
+    ok(extendedInstance instanceof Ci.nsIDOMXULControlElement, "inherited interface applied");
   }
   ]]>
   </script>
 </window>
 
copy from toolkit/content/widgets/radio.xml
copy to toolkit/content/widgets/radio.js
--- a/toolkit/content/widgets/radio.xml
+++ b/toolkit/content/widgets/radio.js
@@ -1,481 +1,377 @@
-<?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/. -->
+/* 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.
+{
 
-<bindings id="radioBindings"
-   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">
+class MozRadiogroup extends MozBaseControl {
+  constructor() {
+    super();
 
-  <binding id="radiogroup"
-           extends="chrome://global/content/bindings/general.xml#basecontrol">
-    <implementation implements="nsIDOMXULSelectControlElement">
-      <constructor>
-        <![CDATA[
-          if (this.getAttribute("disabled") == "true")
-            this.disabled = true;
+    this.addEventListener("mousedown", (event) => {
+      if (this.disabled)
+        event.preventDefault();
+    });
 
-          var children = this._getRadioChildren();
-          var length = children.length;
-          for (var i = 0; i < length; i++) {
-            if (children[i].getAttribute("selected") == "true") {
-              this.selectedIndex = i;
-              return;
-            }
-          }
-
-          var value = this.value;
-          if (value)
-            this.value = value;
-          else
-            this.selectedIndex = 0;
-        ]]>
-      </constructor>
+    /**
+     * keyboard navigation  Here's how keyboard navigation works in radio groups on Windows:
+     * The group takes 'focus'
+     * The user is then free to navigate around inside the group
+     * using the arrow keys. Accessing previous or following radio buttons
+     * is done solely through the arrow keys and not the tab button. Tab
+     * takes you to the next widget in the tab order
+     */
+    this.addEventListener("keypress", (event) => {
+      if (event.key != " " || event.originalTarget != this) {
+        return;
+      }
+      this.selectedItem = this.focusedItem;
+      this.selectedItem.doCommand();
+      // Prevent page from scrolling on the space key.
+      event.preventDefault();
+    });
 
-      <property name="value" onget="return this.getAttribute('value');">
-        <setter>
-          <![CDATA[
-            this.setAttribute("value", val);
-            var children = this._getRadioChildren();
-            for (var i = 0; i < children.length; i++) {
-              if (String(children[i].value) == String(val)) {
-                this.selectedItem = children[i];
-                break;
-              }
-            }
-            return val;
-          ]]>
-        </setter>
-      </property>
-      <property name="disabled">
-        <getter>
-        <![CDATA[
-          if (this.getAttribute("disabled") == "true")
-            return true;
-          var children = this._getRadioChildren();
-          for (var i = 0; i < children.length; ++i) {
-            if (!children[i].hidden && !children[i].collapsed && !children[i].disabled)
-              return false;
-          }
-          return true;
-        ]]>
-        </getter>
-        <setter>
-        <![CDATA[
-          if (val)
-            this.setAttribute("disabled", "true");
-          else
-            this.removeAttribute("disabled");
-          var children = this._getRadioChildren();
-          for (var i = 0; i < children.length; ++i) {
-            children[i].disabled = val;
-          }
-          return val;
-        ]]>
-        </setter>
-      </property>
+    this.addEventListener("keypress", (event) => {
+      if (event.keyCode != KeyEvent.DOM_VK_UP || event.originalTarget != this) {
+        return;
+      }
+      this.checkAdjacentElement(false);
+      event.stopPropagation();
+      event.preventDefault();
+    });
+
+    this.addEventListener("keypress", (event) => {
+      if (event.keyCode != KeyEvent.DOM_VK_LEFT || event.originalTarget != this) {
+        return;
+      }
+      // left arrow goes back when we are ltr, forward when we are rtl
+      this.checkAdjacentElement(document.defaultView.getComputedStyle(
+        this).direction == "rtl");
+      event.stopPropagation();
+      event.preventDefault();
+    });
 
-      <property name="itemCount" readonly="true"
-                onget="return this._getRadioChildren().length"/>
+    this.addEventListener("keypress", (event) => {
+      if (event.keyCode != KeyEvent.DOM_VK_DOWN || event.originalTarget != this) {
+        return;
+      }
+      this.checkAdjacentElement(true);
+      event.stopPropagation();
+      event.preventDefault();
+    });
 
-      <property name="selectedIndex">
-        <getter>
-        <![CDATA[
-          var children = this._getRadioChildren();
-          for (var i = 0; i < children.length; ++i) {
-            if (children[i].selected)
-              return i;
-          }
-          return -1;
-        ]]>
-        </getter>
-        <setter>
-        <![CDATA[
-          this.selectedItem = this._getRadioChildren()[val];
-          return val;
-        ]]>
-        </setter>
-      </property>
+    this.addEventListener("keypress", (event) => {
+      if (event.keyCode != KeyEvent.DOM_VK_RIGHT || event.originalTarget != this) {
+        return;
+      }
+      // right arrow goes forward when we are ltr, back when we are rtl
+      this.checkAdjacentElement(document.defaultView.getComputedStyle(
+        this).direction == "ltr");
+      event.stopPropagation();
+      event.preventDefault();
+    });
 
-      <property name="selectedItem">
-        <getter>
-        <![CDATA[
-          var children = this._getRadioChildren();
-          for (var i = 0; i < children.length; ++i) {
-            if (children[i].selected)
-              return children[i];
-          }
-          return null;
-        ]]>
-        </getter>
-        <setter>
-        <![CDATA[
-          var focused = this.getAttribute("focused") == "true";
-          var alreadySelected = false;
+    /**
+     * set a focused attribute on the selected item when the group
+     * receives focus so that we can style it as if it were focused even though
+     * it is not (Windows platform behaviour is for the group to receive focus,
+     * not the item
+     */
+    this.addEventListener("focus", (event) => {
+      if (event.originalTarget != this) {
+        return;
+      }
+      this.setAttribute("focused", "true");
+      if (this.focusedItem)
+        return;
 
-          if (val) {
-            alreadySelected = val.getAttribute("selected") == "true";
-            val.setAttribute("focused", focused);
-            val.setAttribute("selected", "true");
-            this.setAttribute("value", val.value);
-          } else {
-            this.removeAttribute("value");
+      var val = this.selectedItem;
+      if (!val || val.disabled || val.hidden || val.collapsed) {
+        var children = this._getRadioChildren();
+        for (var i = 0; i < children.length; ++i) {
+          if (!children[i].hidden && !children[i].collapsed && !children[i].disabled) {
+            val = children[i];
+            break;
           }
+        }
+      }
+      this.focusedItem = val;
+    });
 
-          // uncheck all other group nodes
-          var children = this._getRadioChildren();
-          var previousItem = null;
-          for (var i = 0; i < children.length; ++i) {
-            if (children[i] != val) {
-              if (children[i].getAttribute("selected") == "true")
-                previousItem = children[i];
-
-              children[i].removeAttribute("selected");
-              children[i].removeAttribute("focused");
-            }
-          }
-
-          var event = document.createEvent("Events");
-          event.initEvent("select", false, true);
-          this.dispatchEvent(event);
+    this.addEventListener("blur", (event) => {
+      if (event.originalTarget != this) {
+        return;
+      }
+      this.removeAttribute("focused");
+      this.focusedItem = null;
+    });
+  }
 
-          if (!alreadySelected && focused) {
-            // Only report if actual change
-            var myEvent;
-            if (val) {
-              myEvent = document.createEvent("Events");
-              myEvent.initEvent("RadioStateChange", true, true);
-              val.dispatchEvent(myEvent);
-            }
+  connectedCallback() {
+    this._radioChildren = null;
 
-            if (previousItem) {
-              myEvent = document.createEvent("Events");
-              myEvent.initEvent("RadioStateChange", true, true);
-              previousItem.dispatchEvent(myEvent);
-            }
-          }
+    if (this.getAttribute("disabled") == "true")
+      this.disabled = true;
 
-          return val;
-        ]]>
-        </setter>
-      </property>
+    var children = this._getRadioChildren();
+    var length = children.length;
+    for (var i = 0; i < length; i++) {
+      if (children[i].getAttribute("selected") == "true") {
+        this.selectedIndex = i;
+        return;
+      }
+    }
 
-      <property name="focusedItem">
-        <getter>
-        <![CDATA[
-          var children = this._getRadioChildren();
-          for (var i = 0; i < children.length; ++i) {
-            if (children[i].getAttribute("focused") == "true")
-              return children[i];
-          }
-          return null;
-        ]]>
-        </getter>
-        <setter>
-        <![CDATA[
-          if (val) val.setAttribute("focused", "true");
+    var value = this.value;
+    if (value)
+      this.value = value;
+    else
+      this.selectedIndex = 0;
+  }
 
-          // unfocus all other group nodes
-          var children = this._getRadioChildren();
-          for (var i = 0; i < children.length; ++i) {
-            if (children[i] != val)
-              children[i].removeAttribute("focused");
-          }
-          return val;
-        ]]>
-        </setter>
-      </property>
+  set value(val) {
+    this.setAttribute("value", val);
+    var children = this._getRadioChildren();
+    for (var i = 0; i < children.length; i++) {
+      if (String(children[i].value) == String(val)) {
+        this.selectedItem = children[i];
+        break;
+      }
+    }
+    return val;
+  }
 
-      <method name="checkAdjacentElement">
-        <parameter name="aNextFlag"/>
-        <body>
-        <![CDATA[
-          var currentElement = this.focusedItem || this.selectedItem;
-          var i;
-          var children = this._getRadioChildren();
-          for (i = 0; i < children.length; ++i ) {
-            if (children[i] == currentElement)
-              break;
-          }
-          var index = i;
+  get value() {
+    return this.getAttribute("value");
+  }
 
-          if (aNextFlag) {
-            do {
-              if (++i == children.length)
-                i = 0;
-              if (i == index)
-                break;
-            }
-            while (children[i].hidden || children[i].collapsed || children[i].disabled);
-            // XXX check for display/visibility props too
+  set disabled(val) {
+    if (val)
+      this.setAttribute("disabled", "true");
+    else
+      this.removeAttribute("disabled");
+    var children = this._getRadioChildren();
+    for (var i = 0; i < children.length; ++i) {
+      children[i].disabled = val;
+    }
+    return val;
+  }
 
-            this.selectedItem = children[i];
-            children[i].doCommand();
-          } else {
-            do {
-              if (i == 0)
-                i = children.length;
-              if (--i == index)
-                break;
-            }
-            while (children[i].hidden || children[i].collapsed || children[i].disabled);
-            // XXX check for display/visibility props too
+  get disabled() {
+    if (this.getAttribute("disabled") == "true")
+      return true;
+    var children = this._getRadioChildren();
+    for (var i = 0; i < children.length; ++i) {
+      if (!children[i].hidden && !children[i].collapsed && !children[i].disabled)
+        return false;
+    }
+    return true;
+  }
 
-            this.selectedItem = children[i];
-            children[i].doCommand();
-          }
-        ]]>
-        </body>
-      </method>
-      <field name="_radioChildren">null</field>
-      <method name="_getRadioChildren">
-        <body>
-        <![CDATA[
-          if (this._radioChildren)
-            return this._radioChildren;
+  get itemCount() {
+    return this._getRadioChildren().length;
+  }
 
-          var radioChildren = [];
-          var doc = this.ownerDocument;
+  set selectedIndex(val) {
+    this.selectedItem = this._getRadioChildren()[val];
+    return val;
+  }
 
-          if (this.hasChildNodes()) {
-            // Don't store the collected child nodes immediately,
-            // collecting the child nodes could trigger constructors
-            // which would blow away our list.
+  get selectedIndex() {
+    var children = this._getRadioChildren();
+    for (var i = 0; i < children.length; ++i) {
+      if (children[i].selected)
+        return i;
+    }
+    return -1;
+  }
 
-            var iterator = doc.createTreeWalker(this,
-                                                NodeFilter.SHOW_ELEMENT,
-                                                this._filterRadioGroup);
-            while (iterator.nextNode())
-              radioChildren.push(iterator.currentNode);
-            return this._radioChildren = radioChildren;
-          }
+  set selectedItem(val) {
+    var focused = this.getAttribute("focused") == "true";
+    var alreadySelected = false;
 
-          // We don't have child nodes.
-          const XUL_NS = "http://www.mozilla.org/keymaster/"
-                       + "gatekeeper/there.is.only.xul";
-          var elems = doc.getElementsByAttribute("group", this.id);
-          for (var i = 0; i < elems.length; i++) {
-            if ((elems[i].namespaceURI == XUL_NS) &&
-                (elems[i].localName == "radio")) {
-              radioChildren.push(elems[i]);
-            }
-          }
-          return this._radioChildren = radioChildren;
-        ]]>
-        </body>
-      </method>
-      <method name="_filterRadioGroup">
-        <parameter name="node"/>
-        <body>
-        <![CDATA[
-          switch (node.localName) {
-            case "radio": return NodeFilter.FILTER_ACCEPT;
-            case "template":
-            case "radiogroup": return NodeFilter.FILTER_REJECT;
-            default: return NodeFilter.FILTER_SKIP;
-          }
-        ]]>
-        </body>
-      </method>
+    if (val) {
+      alreadySelected = val.getAttribute("selected") == "true";
+      val.setAttribute("focused", focused);
+      val.setAttribute("selected", "true");
+      this.setAttribute("value", val.value);
+    } else {
+      this.removeAttribute("value");
+    }
 
-      <method name="getIndexOfItem">
-        <parameter name="item"/>
-        <body>
-          return this._getRadioChildren().indexOf(item);
-        </body>
-      </method>
+    // uncheck all other group nodes
+    var children = this._getRadioChildren();
+    var previousItem = null;
+    for (var i = 0; i < children.length; ++i) {
+      if (children[i] != val) {
+        if (children[i].getAttribute("selected") == "true")
+          previousItem = children[i];
 
-      <method name="getItemAtIndex">
-        <parameter name="index"/>
-        <body>
-        <![CDATA[
-          var children = this._getRadioChildren();
-          return (index >= 0 && index < children.length) ? children[index] : null;
-        ]]>
-        </body>
-      </method>
+        children[i].removeAttribute("selected");
+        children[i].removeAttribute("focused");
+      }
+    }
+
+    var event = document.createEvent("Events");
+    event.initEvent("select", false, true);
+    this.dispatchEvent(event);
 
-      <method name="appendItem">
-        <parameter name="label"/>
-        <parameter name="value"/>
-        <body>
-        <![CDATA[
-          var radio = document.createXULElement("radio");
-          radio.setAttribute("label", label);
-          radio.setAttribute("value", value);
-          this.appendChild(radio);
-          this._radioChildren = null;
-          return radio;
-        ]]>
-        </body>
-      </method>
-    </implementation>
+    if (!alreadySelected && focused) {
+      // Only report if actual change
+      var myEvent;
+      if (val) {
+        myEvent = document.createEvent("Events");
+        myEvent.initEvent("RadioStateChange", true, true);
+        val.dispatchEvent(myEvent);
+      }
 
-    <handlers>
-      <handler event="mousedown">
-        if (this.disabled)
-          event.preventDefault();
-       </handler>
+      if (previousItem) {
+        myEvent = document.createEvent("Events");
+        myEvent.initEvent("RadioStateChange", true, true);
+        previousItem.dispatchEvent(myEvent);
+      }
+    }
+
+    return val;
+  }
 
-      <!-- keyboard navigation -->
-      <!-- Here's how keyboard navigation works in radio groups on Windows:
-           The group takes 'focus'
-           The user is then free to navigate around inside the group
-           using the arrow keys. Accessing previous or following radio buttons
-           is done solely through the arrow keys and not the tab button. Tab
-           takes you to the next widget in the tab order -->
-      <handler event="keypress" key=" " phase="target">
-        this.selectedItem = this.focusedItem;
-        this.selectedItem.doCommand();
-        // Prevent page from scrolling on the space key.
-        event.preventDefault();
-      </handler>
-      <handler event="keypress" keycode="VK_UP" phase="target">
-        this.checkAdjacentElement(false);
-        event.stopPropagation();
-        event.preventDefault();
-      </handler>
-      <handler event="keypress" keycode="VK_LEFT" phase="target">
-        // left arrow goes back when we are ltr, forward when we are rtl
-        this.checkAdjacentElement(document.defaultView.getComputedStyle(
-                                    this).direction == "rtl");
-        event.stopPropagation();
-        event.preventDefault();
-      </handler>
-      <handler event="keypress" keycode="VK_DOWN" phase="target">
-        this.checkAdjacentElement(true);
-        event.stopPropagation();
-        event.preventDefault();
-      </handler>
-      <handler event="keypress" keycode="VK_RIGHT" phase="target">
-        // right arrow goes forward when we are ltr, back when we are rtl
-        this.checkAdjacentElement(document.defaultView.getComputedStyle(
-                                    this).direction == "ltr");
-        event.stopPropagation();
-        event.preventDefault();
-      </handler>
+  get selectedItem() {
+    var children = this._getRadioChildren();
+    for (var i = 0; i < children.length; ++i) {
+      if (children[i].selected)
+        return children[i];
+    }
+    return null;
+  }
+
+  set focusedItem(val) {
+    if (val) val.setAttribute("focused", "true");
+
+    // unfocus all other group nodes
+    var children = this._getRadioChildren();
+    for (var i = 0; i < children.length; ++i) {
+      if (children[i] != val)
+        children[i].removeAttribute("focused");
+    }
+    return val;
+  }
+
+  get focusedItem() {
+    var children = this._getRadioChildren();
+    for (var i = 0; i < children.length; ++i) {
+      if (children[i].getAttribute("focused") == "true")
+        return children[i];
+    }
+    return null;
+  }
 
-      <!-- set a focused attribute on the selected item when the group
-           receives focus so that we can style it as if it were focused even though
-           it is not (Windows platform behaviour is for the group to receive focus,
-           not the item -->
-      <handler event="focus" phase="target">
-        <![CDATA[
-          this.setAttribute("focused", "true");
-          if (this.focusedItem)
-            return;
+  checkAdjacentElement(aNextFlag) {
+    var currentElement = this.focusedItem || this.selectedItem;
+    var i;
+    var children = this._getRadioChildren();
+    for (i = 0; i < children.length; ++i) {
+      if (children[i] == currentElement)
+        break;
+    }
+    var index = i;
+
+    if (aNextFlag) {
+      do {
+        if (++i == children.length)
+          i = 0;
+        if (i == index)
+          break;
+      }
+      while (children[i].hidden || children[i].collapsed || children[i].disabled);
+      // XXX check for display/visibility props too
 
-          var val = this.selectedItem;
-          if (!val || val.disabled || val.hidden || val.collapsed) {
-            var children = this._getRadioChildren();
-            for (var i = 0; i < children.length; ++i) {
-              if (!children[i].hidden && !children[i].collapsed && !children[i].disabled) {
-                val = children[i];
-                break;
-              }
-            }
-          }
-          this.focusedItem = val;
-        ]]>
-      </handler>
-      <handler event="blur" phase="target">
-        this.removeAttribute("focused");
-        this.focusedItem = null;
-      </handler>
-    </handlers>
-  </binding>
+      this.selectedItem = children[i];
+      children[i].doCommand();
+    } else {
+      do {
+        if (i == 0)
+          i = children.length;
+        if (--i == index)
+          break;
+      }
+      while (children[i].hidden || children[i].collapsed || children[i].disabled);
+      // XXX check for display/visibility props too
 
-  <binding id="radio"
-    extends="chrome://global/content/bindings/general.xml#basetext">
-    <content>
-      <xul:image class="radio-check" xbl:inherits="disabled,selected"/>
-      <xul:hbox class="radio-label-box" align="center" flex="1">
-        <xul:image class="radio-icon" xbl:inherits="src"/>
-        <xul:label class="radio-label" xbl:inherits="xbl:text=label,accesskey,crop" flex="1"/>
-      </xul:hbox>
-    </content>
+      this.selectedItem = children[i];
+      children[i].doCommand();
+    }
+  }
 
-    <implementation implements="nsIDOMXULSelectControlItemElement">
-      <constructor>
-        <![CDATA[
-          // Just clear out the parent's cached list of radio children
-          var control = this.control;
-          if (control)
-            control._radioChildren = null;
-        ]]>
-      </constructor>
-      <destructor>
-        <![CDATA[
-          if (!this.control)
-            return;
+  _getRadioChildren() {
+    if (this._radioChildren)
+      return this._radioChildren;
+
+    var radioChildren = [];
+    var doc = this.ownerDocument;
+
+    if (this.hasChildNodes()) {
+      // Don't store the collected child nodes immediately,
+      // collecting the child nodes could trigger constructors
+      // which would blow away our list.
 
-          var radioList = this.control._radioChildren;
-          if (!radioList)
-            return;
-          for (var i = 0; i < radioList.length; ++i) {
-            if (radioList[i] == this) {
-              radioList.splice(i, 1);
-              return;
-            }
-          }
-        ]]>
-      </destructor>
-      <property name="value" onset="this.setAttribute('value', val); return val;"
-                             onget="return this.getAttribute('value');"/>
-      <property name="selected" readonly="true">
-        <getter>
-          <![CDATA[
-            return this.hasAttribute("selected");
-          ]]>
-        </getter>
-      </property>
-      <property name="radioGroup" readonly="true" onget="return this.control"/>
-      <property name="control" readonly="true">
-        <getter>
-        <![CDATA[
-          const XUL_NS = "http://www.mozilla.org/keymaster/"
-                       + "gatekeeper/there.is.only.xul";
-          var parent = this.parentNode;
-          while (parent) {
-            if ((parent.namespaceURI == XUL_NS) &&
-                (parent.localName == "radiogroup")) {
-              return parent;
-            }
-            parent = parent.parentNode;
-          }
+      var iterator = doc.createTreeWalker(this,
+        NodeFilter.SHOW_ELEMENT,
+        this._filterRadioGroup);
+      while (iterator.nextNode())
+        radioChildren.push(iterator.currentNode);
+      return this._radioChildren = radioChildren;
+    }
+
+    // We don't have child nodes.
+    const XUL_NS = "http://www.mozilla.org/keymaster/" +
+      "gatekeeper/there.is.only.xul";
+    var elems = doc.getElementsByAttribute("group", this.id);
+    for (var i = 0; i < elems.length; i++) {
+      if ((elems[i].namespaceURI == XUL_NS) &&
+        (elems[i].localName == "radio")) {
+        radioChildren.push(elems[i]);
+      }
+    }
+    return this._radioChildren = radioChildren;
+  }
 
-          var group = this.getAttribute("group");
-          if (!group) {
-            return null;
-          }
+  _filterRadioGroup(node) {
+    switch (node.localName) {
+      case "radio":
+        return NodeFilter.FILTER_ACCEPT;
+      case "template":
+      case "radiogroup":
+        return NodeFilter.FILTER_REJECT;
+      default:
+        return NodeFilter.FILTER_SKIP;
+    }
+  }
+
+  getIndexOfItem(item) {
+    return this._getRadioChildren().indexOf(item);
+  }
 
-          parent = this.ownerDocument.getElementById(group);
-          if (!parent ||
-              (parent.namespaceURI != XUL_NS) ||
-              (parent.localName != "radiogroup")) {
-            parent = null;
-          }
-          return parent;
-        ]]>
-        </getter>
-      </property>
-    </implementation>
-    <handlers>
-      <handler event="click" button="0">
-        <![CDATA[
-          if (!this.disabled)
-            this.control.selectedItem = this;
-         ]]>
-      </handler>
+  getItemAtIndex(index) {
+    var children = this._getRadioChildren();
+    return (index >= 0 && index < children.length) ? children[index] : null;
+  }
 
-      <handler event="mousedown" button="0">
-        <![CDATA[
-          if (!this.disabled)
-            this.control.focusedItem = this;
-         ]]>
-      </handler>
-    </handlers>
-  </binding>
-</bindings>
+  appendItem(label, value) {
+    var radio = document.createXULElement("radio");
+    radio.setAttribute("label", label);
+    radio.setAttribute("value", value);
+    this.appendChild(radio);
+    this._radioChildren = null;
+    return radio;
+  }
+}
+
+MozXULElement.implementCustomInterface(MozRadiogroup, [Ci.nsIDOMXULSelectControlElement]);
+
+customElements.define("radiogroup", MozRadiogroup);
+
+}
--- a/toolkit/content/widgets/radio.xml
+++ b/toolkit/content/widgets/radio.xml
@@ -4,394 +4,16 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 
 <bindings id="radioBindings"
    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">
 
-  <binding id="radiogroup"
-           extends="chrome://global/content/bindings/general.xml#basecontrol">
-    <implementation implements="nsIDOMXULSelectControlElement">
-      <constructor>
-        <![CDATA[
-          if (this.getAttribute("disabled") == "true")
-            this.disabled = true;
-
-          var children = this._getRadioChildren();
-          var length = children.length;
-          for (var i = 0; i < length; i++) {
-            if (children[i].getAttribute("selected") == "true") {
-              this.selectedIndex = i;
-              return;
-            }
-          }
-
-          var value = this.value;
-          if (value)
-            this.value = value;
-          else
-            this.selectedIndex = 0;
-        ]]>
-      </constructor>
-
-      <property name="value" onget="return this.getAttribute('value');">
-        <setter>
-          <![CDATA[
-            this.setAttribute("value", val);
-            var children = this._getRadioChildren();
-            for (var i = 0; i < children.length; i++) {
-              if (String(children[i].value) == String(val)) {
-                this.selectedItem = children[i];
-                break;
-              }
-            }
-            return val;
-          ]]>
-        </setter>
-      </property>
-      <property name="disabled">
-        <getter>
-        <![CDATA[
-          if (this.getAttribute("disabled") == "true")
-            return true;
-          var children = this._getRadioChildren();
-          for (var i = 0; i < children.length; ++i) {
-            if (!children[i].hidden && !children[i].collapsed && !children[i].disabled)
-              return false;
-          }
-          return true;
-        ]]>
-        </getter>
-        <setter>
-        <![CDATA[
-          if (val)
-            this.setAttribute("disabled", "true");
-          else
-            this.removeAttribute("disabled");
-          var children = this._getRadioChildren();
-          for (var i = 0; i < children.length; ++i) {
-            children[i].disabled = val;
-          }
-          return val;
-        ]]>
-        </setter>
-      </property>
-
-      <property name="itemCount" readonly="true"
-                onget="return this._getRadioChildren().length"/>
-
-      <property name="selectedIndex">
-        <getter>
-        <![CDATA[
-          var children = this._getRadioChildren();
-          for (var i = 0; i < children.length; ++i) {
-            if (children[i].selected)
-              return i;
-          }
-          return -1;
-        ]]>
-        </getter>
-        <setter>
-        <![CDATA[
-          this.selectedItem = this._getRadioChildren()[val];
-          return val;
-        ]]>
-        </setter>
-      </property>
-
-      <property name="selectedItem">
-        <getter>
-        <![CDATA[
-          var children = this._getRadioChildren();
-          for (var i = 0; i < children.length; ++i) {
-            if (children[i].selected)
-              return children[i];
-          }
-          return null;
-        ]]>
-        </getter>
-        <setter>
-        <![CDATA[
-          var focused = this.getAttribute("focused") == "true";
-          var alreadySelected = false;
-
-          if (val) {
-            alreadySelected = val.getAttribute("selected") == "true";
-            val.setAttribute("focused", focused);
-            val.setAttribute("selected", "true");
-            this.setAttribute("value", val.value);
-          } else {
-            this.removeAttribute("value");
-          }
-
-          // uncheck all other group nodes
-          var children = this._getRadioChildren();
-          var previousItem = null;
-          for (var i = 0; i < children.length; ++i) {
-            if (children[i] != val) {
-              if (children[i].getAttribute("selected") == "true")
-                previousItem = children[i];
-
-              children[i].removeAttribute("selected");
-              children[i].removeAttribute("focused");
-            }
-          }
-
-          var event = document.createEvent("Events");
-          event.initEvent("select", false, true);
-          this.dispatchEvent(event);
-
-          if (!alreadySelected && focused) {
-            // Only report if actual change
-            var myEvent;
-            if (val) {
-              myEvent = document.createEvent("Events");
-              myEvent.initEvent("RadioStateChange", true, true);
-              val.dispatchEvent(myEvent);
-            }
-
-            if (previousItem) {
-              myEvent = document.createEvent("Events");
-              myEvent.initEvent("RadioStateChange", true, true);
-              previousItem.dispatchEvent(myEvent);
-            }
-          }
-
-          return val;
-        ]]>
-        </setter>
-      </property>
-
-      <property name="focusedItem">
-        <getter>
-        <![CDATA[
-          var children = this._getRadioChildren();
-          for (var i = 0; i < children.length; ++i) {
-            if (children[i].getAttribute("focused") == "true")
-              return children[i];
-          }
-          return null;
-        ]]>
-        </getter>
-        <setter>
-        <![CDATA[
-          if (val) val.setAttribute("focused", "true");
-
-          // unfocus all other group nodes
-          var children = this._getRadioChildren();
-          for (var i = 0; i < children.length; ++i) {
-            if (children[i] != val)
-              children[i].removeAttribute("focused");
-          }
-          return val;
-        ]]>
-        </setter>
-      </property>
-
-      <method name="checkAdjacentElement">
-        <parameter name="aNextFlag"/>
-        <body>
-        <![CDATA[
-          var currentElement = this.focusedItem || this.selectedItem;
-          var i;
-          var children = this._getRadioChildren();
-          for (i = 0; i < children.length; ++i ) {
-            if (children[i] == currentElement)
-              break;
-          }
-          var index = i;
-
-          if (aNextFlag) {
-            do {
-              if (++i == children.length)
-                i = 0;
-              if (i == index)
-                break;
-            }
-            while (children[i].hidden || children[i].collapsed || children[i].disabled);
-            // XXX check for display/visibility props too
-
-            this.selectedItem = children[i];
-            children[i].doCommand();
-          } else {
-            do {
-              if (i == 0)
-                i = children.length;
-              if (--i == index)
-                break;
-            }
-            while (children[i].hidden || children[i].collapsed || children[i].disabled);
-            // XXX check for display/visibility props too
-
-            this.selectedItem = children[i];
-            children[i].doCommand();
-          }
-        ]]>
-        </body>
-      </method>
-      <field name="_radioChildren">null</field>
-      <method name="_getRadioChildren">
-        <body>
-        <![CDATA[
-          if (this._radioChildren)
-            return this._radioChildren;
-
-          var radioChildren = [];
-          var doc = this.ownerDocument;
-
-          if (this.hasChildNodes()) {
-            // Don't store the collected child nodes immediately,
-            // collecting the child nodes could trigger constructors
-            // which would blow away our list.
-
-            var iterator = doc.createTreeWalker(this,
-                                                NodeFilter.SHOW_ELEMENT,
-                                                this._filterRadioGroup);
-            while (iterator.nextNode())
-              radioChildren.push(iterator.currentNode);
-            return this._radioChildren = radioChildren;
-          }
-
-          // We don't have child nodes.
-          const XUL_NS = "http://www.mozilla.org/keymaster/"
-                       + "gatekeeper/there.is.only.xul";
-          var elems = doc.getElementsByAttribute("group", this.id);
-          for (var i = 0; i < elems.length; i++) {
-            if ((elems[i].namespaceURI == XUL_NS) &&
-                (elems[i].localName == "radio")) {
-              radioChildren.push(elems[i]);
-            }
-          }
-          return this._radioChildren = radioChildren;
-        ]]>
-        </body>
-      </method>
-      <method name="_filterRadioGroup">
-        <parameter name="node"/>
-        <body>
-        <![CDATA[
-          switch (node.localName) {
-            case "radio": return NodeFilter.FILTER_ACCEPT;
-            case "template":
-            case "radiogroup": return NodeFilter.FILTER_REJECT;
-            default: return NodeFilter.FILTER_SKIP;
-          }
-        ]]>
-        </body>
-      </method>
-
-      <method name="getIndexOfItem">
-        <parameter name="item"/>
-        <body>
-          return this._getRadioChildren().indexOf(item);
-        </body>
-      </method>
-
-      <method name="getItemAtIndex">
-        <parameter name="index"/>
-        <body>
-        <![CDATA[
-          var children = this._getRadioChildren();
-          return (index >= 0 && index < children.length) ? children[index] : null;
-        ]]>
-        </body>
-      </method>
-
-      <method name="appendItem">
-        <parameter name="label"/>
-        <parameter name="value"/>
-        <body>
-        <![CDATA[
-          var radio = document.createXULElement("radio");
-          radio.setAttribute("label", label);
-          radio.setAttribute("value", value);
-          this.appendChild(radio);
-          this._radioChildren = null;
-          return radio;
-        ]]>
-        </body>
-      </method>
-    </implementation>
-
-    <handlers>
-      <handler event="mousedown">
-        if (this.disabled)
-          event.preventDefault();
-       </handler>
-
-      <!-- keyboard navigation -->
-      <!-- Here's how keyboard navigation works in radio groups on Windows:
-           The group takes 'focus'
-           The user is then free to navigate around inside the group
-           using the arrow keys. Accessing previous or following radio buttons
-           is done solely through the arrow keys and not the tab button. Tab
-           takes you to the next widget in the tab order -->
-      <handler event="keypress" key=" " phase="target">
-        this.selectedItem = this.focusedItem;
-        this.selectedItem.doCommand();
-        // Prevent page from scrolling on the space key.
-        event.preventDefault();
-      </handler>
-      <handler event="keypress" keycode="VK_UP" phase="target">
-        this.checkAdjacentElement(false);
-        event.stopPropagation();
-        event.preventDefault();
-      </handler>
-      <handler event="keypress" keycode="VK_LEFT" phase="target">
-        // left arrow goes back when we are ltr, forward when we are rtl
-        this.checkAdjacentElement(document.defaultView.getComputedStyle(
-                                    this).direction == "rtl");
-        event.stopPropagation();
-        event.preventDefault();
-      </handler>
-      <handler event="keypress" keycode="VK_DOWN" phase="target">
-        this.checkAdjacentElement(true);
-        event.stopPropagation();
-        event.preventDefault();
-      </handler>
-      <handler event="keypress" keycode="VK_RIGHT" phase="target">
-        // right arrow goes forward when we are ltr, back when we are rtl
-        this.checkAdjacentElement(document.defaultView.getComputedStyle(
-                                    this).direction == "ltr");
-        event.stopPropagation();
-        event.preventDefault();
-      </handler>
-
-      <!-- set a focused attribute on the selected item when the group
-           receives focus so that we can style it as if it were focused even though
-           it is not (Windows platform behaviour is for the group to receive focus,
-           not the item -->
-      <handler event="focus" phase="target">
-        <![CDATA[
-          this.setAttribute("focused", "true");
-          if (this.focusedItem)
-            return;
-
-          var val = this.selectedItem;
-          if (!val || val.disabled || val.hidden || val.collapsed) {
-            var children = this._getRadioChildren();
-            for (var i = 0; i < children.length; ++i) {
-              if (!children[i].hidden && !children[i].collapsed && !children[i].disabled) {
-                val = children[i];
-                break;
-              }
-            }
-          }
-          this.focusedItem = val;
-        ]]>
-      </handler>
-      <handler event="blur" phase="target">
-        this.removeAttribute("focused");
-        this.focusedItem = null;
-      </handler>
-    </handlers>
-  </binding>
-
   <binding id="radio"
     extends="chrome://global/content/bindings/general.xml#basetext">
     <content>
       <xul:image class="radio-check" xbl:inherits="disabled,selected"/>
       <xul:hbox class="radio-label-box" align="center" flex="1">
         <xul:image class="radio-icon" xbl:inherits="src"/>
         <xul:label class="radio-label" xbl:inherits="xbl:text=label,accesskey,crop" flex="1"/>
       </xul:hbox>
--- a/toolkit/content/xul.css
+++ b/toolkit/content/xul.css
@@ -213,17 +213,16 @@ popupnotification {
 
 checkbox {
   -moz-binding: url("chrome://global/content/bindings/checkbox.xml#checkbox");
 }
 
 /********** radio **********/
 
 radiogroup {
-  -moz-binding: url("chrome://global/content/bindings/radio.xml#radiogroup");
   -moz-box-orient: vertical;
 }
 
 radio {
   -moz-binding: url("chrome://global/content/bindings/radio.xml#radio");
 }
 
 /******** groupbox *********/