Bug 1490406, radio groups should work in shadow DOM, r=ehsan
authorOlli Pettay <Olli.Pettay@helsinki.fi>
Fri, 21 Sep 2018 03:39:47 +0300
changeset 437589 bfb9062ec18bb386bf3cf851b75fcf2f1de55756
parent 437588 1e53e6af422382ae1ddfee87a7081810b51d8ac5
child 437590 0e28dd35739698fc6794ca4aa218975c512acef1
push id34686
push userrgurzau@mozilla.com
push dateFri, 21 Sep 2018 04:14:16 +0000
treeherdermozilla-central@ebeba937ca2a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan
bugs1490406
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 1490406, radio groups should work in shadow DOM, r=ehsan
dom/base/DocumentOrShadowRoot.cpp
dom/base/DocumentOrShadowRoot.h
dom/base/ShadowRoot.cpp
dom/base/ShadowRoot.h
dom/base/nsDocument.cpp
dom/base/nsDocument.h
dom/html/HTMLInputElement.cpp
testing/web-platform/meta/MANIFEST.json
testing/web-platform/tests/shadow-dom/input-type-radio.html
--- a/dom/base/DocumentOrShadowRoot.cpp
+++ b/dom/base/DocumentOrShadowRoot.cpp
@@ -1,20 +1,23 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #include "DocumentOrShadowRoot.h"
 #include "mozilla/EventStateManager.h"
+#include "mozilla/dom/HTMLInputElement.h"
 #include "mozilla/dom/ShadowRoot.h"
 #include "mozilla/dom/StyleSheetList.h"
 #include "nsDocument.h"
 #include "nsFocusManager.h"
+#include "nsIRadioVisitor.h"
+#include "nsIFormControl.h"
 #include "nsLayoutUtils.h"
 #include "nsSVGUtils.h"
 #include "nsWindowSizes.h"
 
 namespace mozilla {
 namespace dom {
 
 DocumentOrShadowRoot::DocumentOrShadowRoot(mozilla::dom::ShadowRoot& aShadowRoot)
@@ -393,10 +396,210 @@ DocumentOrShadowRoot::LookupImageElement
 }
 
 void
 DocumentOrShadowRoot::ReportEmptyGetElementByIdArg()
 {
   nsContentUtils::ReportEmptyGetElementByIdArg(AsNode().OwnerDoc());
 }
 
+/**
+ * A struct that holds all the information about a radio group.
+ */
+struct nsRadioGroupStruct
+{
+  nsRadioGroupStruct()
+    : mRequiredRadioCount(0)
+    , mGroupSuffersFromValueMissing(false)
+  {}
+
+  /**
+   * A strong pointer to the currently selected radio button.
+   */
+  RefPtr<HTMLInputElement> mSelectedRadioButton;
+  nsCOMArray<nsIFormControl> mRadioButtons;
+  uint32_t mRequiredRadioCount;
+  bool mGroupSuffersFromValueMissing;
+};
+
+nsresult
+DocumentOrShadowRoot::WalkRadioGroup(const nsAString& aName,
+                                     nsIRadioVisitor* aVisitor,
+                                     bool aFlushContent)
+{
+  nsRadioGroupStruct* radioGroup = GetOrCreateRadioGroup(aName);
+
+  for (int i = 0; i < radioGroup->mRadioButtons.Count(); i++) {
+    if (!aVisitor->Visit(radioGroup->mRadioButtons[i])) {
+      return NS_OK;
+    }
+  }
+
+  return NS_OK;
+}
+
+void
+DocumentOrShadowRoot::SetCurrentRadioButton(const nsAString& aName,
+                                            HTMLInputElement* aRadio)
+{
+  nsRadioGroupStruct* radioGroup = GetOrCreateRadioGroup(aName);
+  radioGroup->mSelectedRadioButton = aRadio;
+}
+
+HTMLInputElement*
+DocumentOrShadowRoot::GetCurrentRadioButton(const nsAString& aName)
+{
+  return GetOrCreateRadioGroup(aName)->mSelectedRadioButton;
+}
+
+nsresult
+DocumentOrShadowRoot::GetNextRadioButton(const nsAString& aName,
+                                         const bool aPrevious,
+                                         HTMLInputElement* aFocusedRadio,
+                                         HTMLInputElement** aRadioOut)
+{
+  // XXX Can we combine the HTML radio button method impls of
+  //     nsDocument and nsHTMLFormControl?
+  // XXX Why is HTML radio button stuff in nsDocument, as
+  //     opposed to nsHTMLDocument?
+  *aRadioOut = nullptr;
+
+  nsRadioGroupStruct* radioGroup = GetOrCreateRadioGroup(aName);
+
+  // Return the radio button relative to the focused radio button.
+  // If no radio is focused, get the radio relative to the selected one.
+  RefPtr<HTMLInputElement> currentRadio;
+  if (aFocusedRadio) {
+    currentRadio = aFocusedRadio;
+  } else {
+    currentRadio = radioGroup->mSelectedRadioButton;
+    if (!currentRadio) {
+      return NS_ERROR_FAILURE;
+    }
+  }
+  int32_t index = radioGroup->mRadioButtons.IndexOf(currentRadio);
+  if (index < 0) {
+    return NS_ERROR_FAILURE;
+  }
+
+  int32_t numRadios = radioGroup->mRadioButtons.Count();
+  RefPtr<HTMLInputElement> radio;
+  do {
+    if (aPrevious) {
+      if (--index < 0) {
+        index = numRadios -1;
+      }
+    } else if (++index >= numRadios) {
+      index = 0;
+    }
+    NS_ASSERTION(static_cast<nsGenericHTMLFormElement*>(radioGroup->mRadioButtons[index])->IsHTMLElement(nsGkAtoms::input),
+                 "mRadioButtons holding a non-radio button");
+    radio = static_cast<HTMLInputElement*>(radioGroup->mRadioButtons[index]);
+  } while (radio->Disabled() && radio != currentRadio);
+
+  radio.forget(aRadioOut);
+  return NS_OK;
+}
+
+void
+DocumentOrShadowRoot::AddToRadioGroup(const nsAString& aName,
+                                      HTMLInputElement* aRadio)
+{
+  nsRadioGroupStruct* radioGroup = GetOrCreateRadioGroup(aName);
+  radioGroup->mRadioButtons.AppendObject(aRadio);
+
+  if (aRadio->IsRequired()) {
+    radioGroup->mRequiredRadioCount++;
+  }
+}
+
+void
+DocumentOrShadowRoot::RemoveFromRadioGroup(const nsAString& aName,
+                                           HTMLInputElement* aRadio)
+{
+  nsRadioGroupStruct* radioGroup = GetOrCreateRadioGroup(aName);
+  radioGroup->mRadioButtons.RemoveObject(aRadio);
+
+  if (aRadio->IsRequired()) {
+    NS_ASSERTION(radioGroup->mRequiredRadioCount != 0,
+                 "mRequiredRadioCount about to wrap below 0!");
+    radioGroup->mRequiredRadioCount--;
+  }
+}
+
+uint32_t
+DocumentOrShadowRoot::GetRequiredRadioCount(const nsAString& aName) const
+{
+  nsRadioGroupStruct* radioGroup = GetRadioGroup(aName);
+  return radioGroup ? radioGroup->mRequiredRadioCount : 0;
+}
+
+void
+DocumentOrShadowRoot::RadioRequiredWillChange(const nsAString& aName,
+                                              bool aRequiredAdded)
+{
+  nsRadioGroupStruct* radioGroup = GetOrCreateRadioGroup(aName);
+
+  if (aRequiredAdded) {
+    radioGroup->mRequiredRadioCount++;
+  } else {
+    NS_ASSERTION(radioGroup->mRequiredRadioCount != 0,
+                 "mRequiredRadioCount about to wrap below 0!");
+    radioGroup->mRequiredRadioCount--;
+  }
+}
+
+bool
+DocumentOrShadowRoot::GetValueMissingState(const nsAString& aName) const
+{
+  nsRadioGroupStruct* radioGroup = GetRadioGroup(aName);
+  return radioGroup && radioGroup->mGroupSuffersFromValueMissing;
+}
+
+void
+DocumentOrShadowRoot::SetValueMissingState(const nsAString& aName, bool aValue)
+{
+  nsRadioGroupStruct* radioGroup = GetOrCreateRadioGroup(aName);
+  radioGroup->mGroupSuffersFromValueMissing = aValue;
+}
+
+nsRadioGroupStruct*
+DocumentOrShadowRoot::GetRadioGroup(const nsAString& aName) const
+{
+  nsRadioGroupStruct* radioGroup = nullptr;
+  mRadioGroups.Get(aName, &radioGroup);
+  return radioGroup;
+}
+
+nsRadioGroupStruct*
+DocumentOrShadowRoot::GetOrCreateRadioGroup(const nsAString& aName)
+{
+  return mRadioGroups.LookupForAdd(aName).OrInsert(
+    [] () { return new nsRadioGroupStruct(); });
+}
+
+void
+DocumentOrShadowRoot::Traverse(DocumentOrShadowRoot* tmp,
+                               nsCycleCollectionTraversalCallback &cb)
+{
+  for (auto iter = tmp->mRadioGroups.Iter(); !iter.Done(); iter.Next()) {
+    nsRadioGroupStruct* radioGroup = iter.UserData();
+    NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(
+      cb, "mRadioGroups entry->mSelectedRadioButton");
+    cb.NoteXPCOMChild(ToSupports(radioGroup->mSelectedRadioButton));
+
+    uint32_t i, count = radioGroup->mRadioButtons.Count();
+    for (i = 0; i < count; ++i) {
+      NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(
+        cb, "mRadioGroups entry->mRadioButtons[i]");
+      cb.NoteXPCOMChild(radioGroup->mRadioButtons[i]);
+    }
+  }
+}
+
+void
+DocumentOrShadowRoot::Unlink(DocumentOrShadowRoot* tmp)
+{
+  tmp->mRadioGroups.Clear();
+}
+
 }
 }
--- a/dom/base/DocumentOrShadowRoot.h
+++ b/dom/base/DocumentOrShadowRoot.h
@@ -3,31 +3,37 @@
 /* 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/. */
 
 #ifndef mozilla_dom_DocumentOrShadowRoot_h__
 #define mozilla_dom_DocumentOrShadowRoot_h__
 
 #include "mozilla/dom/NameSpaceConstants.h"
+#include "nsClassHashtable.h"
 #include "nsContentListDeclarations.h"
 #include "nsTArray.h"
 #include "nsIdentifierMapEntry.h"
 
 class nsContentList;
+class nsCycleCollectionTraversalCallback;
 class nsIDocument;
 class nsINode;
+class nsIRadioVisitor;
 class nsWindowSizes;
 
 namespace mozilla {
 class StyleSheet;
 
 namespace dom {
 
 class Element;
+class DocumentOrShadowRoot;
+class HTMLInputElement;
+struct nsRadioGroupStruct;
 class StyleSheetList;
 class ShadowRoot;
 
 /**
  * A class meant to be shared by ShadowRoot and Document, that holds a list of
  * stylesheets.
  *
  * TODO(emilio, bug 1418159): In the future this should hold most of the
@@ -40,16 +46,21 @@ class DocumentOrShadowRoot
     Document,
     ShadowRoot,
   };
 
 public:
   explicit DocumentOrShadowRoot(nsIDocument&);
   explicit DocumentOrShadowRoot(mozilla::dom::ShadowRoot&);
 
+  // Unusual argument naming is because of cycle collection macros.
+  static void Traverse(DocumentOrShadowRoot* tmp,
+                       nsCycleCollectionTraversalCallback &cb);
+  static void Unlink(DocumentOrShadowRoot* tmp);
+
   nsINode& AsNode()
   {
     return mAsNode;
   }
 
   const nsINode& AsNode() const
   {
     return mAsNode;
@@ -181,16 +192,41 @@ public:
       ReportEmptyGetElementByIdArg();
       return false;
     }
     return true;
   }
 
   void ReportEmptyGetElementByIdArg();
 
+  // nsIRadioGroupContainer
+  NS_IMETHOD WalkRadioGroup(const nsAString& aName,
+                            nsIRadioVisitor* aVisitor,
+                            bool aFlushContent);
+  void SetCurrentRadioButton(const nsAString& aName,
+                             HTMLInputElement* aRadio);
+  HTMLInputElement* GetCurrentRadioButton(const nsAString& aName);
+  nsresult GetNextRadioButton(const nsAString& aName,
+                              const bool aPrevious,
+                              HTMLInputElement* aFocusedRadio,
+                              HTMLInputElement** aRadioOut);
+  void AddToRadioGroup(const nsAString& aName,
+                       HTMLInputElement* aRadio);
+  void RemoveFromRadioGroup(const nsAString& aName,
+                            HTMLInputElement* aRadio);
+  uint32_t GetRequiredRadioCount(const nsAString& aName) const;
+  void RadioRequiredWillChange(const nsAString& aName,
+                                       bool aRequiredAdded);
+  bool GetValueMissingState(const nsAString& aName) const;
+  void SetValueMissingState(const nsAString& aName, bool aValue);
+
+  // for radio group
+  nsRadioGroupStruct* GetRadioGroup(const nsAString& aName) const;
+  nsRadioGroupStruct* GetOrCreateRadioGroup(const nsAString& aName);
+
 protected:
   // Returns the reference to the sheet, if found in mStyleSheets.
   already_AddRefed<StyleSheet> RemoveSheet(StyleSheet& aSheet);
   void InsertSheetAt(size_t aIndex, StyleSheet& aSheet);
 
   void AddSizeOfExcludingThis(nsWindowSizes&) const;
   void AddSizeOfOwnedSheetArrayExcludingThis(
       nsWindowSizes&,
@@ -213,16 +249,18 @@ protected:
    * 1) Attribute changes affect the table immediately (removing and adding
    *    entries as needed).
    * 2) Removals from the DOM affect the table immediately
    * 3) Additions to the DOM always update existing entries for names, and add
    *    new ones for IDs.
    */
   nsTHashtable<nsIdentifierMapEntry> mIdentifierMap;
 
+  nsClassHashtable<nsStringHashKey, nsRadioGroupStruct> mRadioGroups;
+
   nsINode& mAsNode;
   const Kind mKind;
 };
 
 inline const nsTArray<Element*>*
 DocumentOrShadowRoot::GetAllElementsForId(const nsAString& aElementId) const
 {
   if (aElementId.IsEmpty()) {
--- a/dom/base/ShadowRoot.cpp
+++ b/dom/base/ShadowRoot.cpp
@@ -36,29 +36,32 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_
       cb.NoteXPCOMChild(sheet);
     }
   }
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDOMStyleSheets)
   for (auto iter = tmp->mIdentifierMap.ConstIter(); !iter.Done();
        iter.Next()) {
     iter.Get()->Traverse(&cb);
   }
+  DocumentOrShadowRoot::Traverse(tmp, cb);
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ShadowRoot)
   if (tmp->GetHost()) {
     tmp->GetHost()->RemoveMutationObserver(tmp);
   }
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mDOMStyleSheets)
   tmp->mIdentifierMap.Clear();
+  DocumentOrShadowRoot::Unlink(tmp);
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END_INHERITED(DocumentFragment)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ShadowRoot)
   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIContent)
   NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
+  NS_INTERFACE_MAP_ENTRY(nsIRadioGroupContainer)
 NS_INTERFACE_MAP_END_INHERITING(DocumentFragment)
 
 NS_IMPL_ADDREF_INHERITED(ShadowRoot, DocumentFragment)
 NS_IMPL_RELEASE_INHERITED(ShadowRoot, DocumentFragment)
 
 ShadowRoot::ShadowRoot(Element* aElement, ShadowRootMode aMode,
                        already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
   : DocumentFragment(aNodeInfo)
--- a/dom/base/ShadowRoot.h
+++ b/dom/base/ShadowRoot.h
@@ -11,16 +11,17 @@
 #include "mozilla/dom/DocumentFragment.h"
 #include "mozilla/dom/DocumentOrShadowRoot.h"
 #include "mozilla/dom/NameSpaceConstants.h"
 #include "mozilla/dom/ShadowRootBinding.h"
 #include "mozilla/ServoBindings.h"
 #include "nsCOMPtr.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsIdentifierMapEntry.h"
+#include "nsIRadioGroupContainer.h"
 #include "nsStubMutationObserver.h"
 #include "nsTHashtable.h"
 
 class nsAtom;
 class nsIContent;
 class nsXBLPrototypeBinding;
 
 namespace mozilla {
@@ -30,20 +31,22 @@ class ServoStyleRuleMap;
 
 namespace css {
 class Rule;
 }
 
 namespace dom {
 
 class Element;
+class HTMLInputElement;
 
 class ShadowRoot final : public DocumentFragment,
                          public DocumentOrShadowRoot,
-                         public nsStubMutationObserver
+                         public nsStubMutationObserver,
+                         public nsIRadioGroupContainer
 {
 public:
   NS_IMPL_FROMNODE_HELPER(ShadowRoot, IsShadowRoot());
 
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ShadowRoot,
                                            DocumentFragment)
   NS_DECL_ISUPPORTS_INHERITED
 
@@ -202,16 +205,71 @@ public:
 
   void SetIsUAWidget()
   {
     mIsUAWidget = true;
   }
 
   void GetEventTargetParent(EventChainPreVisitor& aVisitor) override;
 
+  // nsIRadioGroupContainer
+  NS_IMETHOD WalkRadioGroup(const nsAString& aName,
+                            nsIRadioVisitor* aVisitor,
+                            bool aFlushContent) override
+  {
+    return DocumentOrShadowRoot::WalkRadioGroup(aName, aVisitor, aFlushContent);
+  }
+  virtual void
+  SetCurrentRadioButton(const nsAString& aName,
+                        HTMLInputElement* aRadio) override
+  {
+    DocumentOrShadowRoot::SetCurrentRadioButton(aName, aRadio);
+  }
+  virtual HTMLInputElement*
+  GetCurrentRadioButton(const nsAString& aName) override
+  {
+    return DocumentOrShadowRoot::GetCurrentRadioButton(aName);
+  }
+  NS_IMETHOD
+  GetNextRadioButton(const nsAString& aName,
+                     const bool aPrevious,
+                     HTMLInputElement* aFocusedRadio,
+                     HTMLInputElement** aRadioOut) override
+  {
+    return DocumentOrShadowRoot::GetNextRadioButton(aName, aPrevious,
+                                                    aFocusedRadio, aRadioOut);
+  }
+  virtual void AddToRadioGroup(const nsAString& aName,
+                               HTMLInputElement* aRadio) override
+  {
+    DocumentOrShadowRoot::AddToRadioGroup(aName, aRadio);
+  }
+  virtual void RemoveFromRadioGroup(const nsAString& aName,
+                                    HTMLInputElement* aRadio) override
+  {
+    DocumentOrShadowRoot::RemoveFromRadioGroup(aName, aRadio);
+  }
+  virtual uint32_t GetRequiredRadioCount(const nsAString& aName) const override
+  {
+    return DocumentOrShadowRoot::GetRequiredRadioCount(aName);
+  }
+  virtual void RadioRequiredWillChange(const nsAString& aName,
+                                       bool aRequiredAdded) override
+  {
+    DocumentOrShadowRoot::RadioRequiredWillChange(aName, aRequiredAdded);
+  }
+  virtual bool GetValueMissingState(const nsAString& aName) const override
+  {
+    return DocumentOrShadowRoot::GetValueMissingState(aName);
+  }
+  virtual void SetValueMissingState(const nsAString& aName, bool aValue) override
+  {
+    return DocumentOrShadowRoot::SetValueMissingState(aName, aValue);
+  }
+
 protected:
   // FIXME(emilio): This will need to become more fine-grained.
   void ApplicableRulesChanged();
 
   virtual ~ShadowRoot();
 
   const ShadowRootMode mMode;
 
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -114,20 +114,16 @@
 #include "ExpandedPrincipal.h"
 #include "mozilla/NullPrincipal.h"
 
 #include "nsIDOMWindow.h"
 #include "nsPIDOMWindow.h"
 #include "nsFocusManager.h"
 #include "nsICookieService.h"
 
-// for radio group stuff
-#include "nsIRadioVisitor.h"
-#include "nsIFormControl.h"
-
 #include "nsBidiUtils.h"
 
 #include "nsContentCreatorFunctions.h"
 
 #include "nsIScriptContext.h"
 #include "nsBindingManager.h"
 #include "nsHTMLDocument.h"
 #include "nsIRequest.h"
@@ -719,36 +715,16 @@ nsIdentifierMapEntry::SizeOfExcludingThi
 class SubDocMapEntry : public PLDHashEntryHdr
 {
 public:
   // Both of these are strong references
   Element *mKey; // must be first, to look like PLDHashEntryStub
   nsIDocument *mSubDocument;
 };
 
-
-/**
- * A struct that holds all the information about a radio group.
- */
-struct nsRadioGroupStruct
-{
-  nsRadioGroupStruct()
-    : mRequiredRadioCount(0)
-    , mGroupSuffersFromValueMissing(false)
-  {}
-
-  /**
-   * A strong pointer to the currently selected radio button.
-   */
-  RefPtr<HTMLInputElement> mSelectedRadioButton;
-  nsCOMArray<nsIFormControl> mRadioButtons;
-  uint32_t mRequiredRadioCount;
-  bool mGroupSuffersFromValueMissing;
-};
-
 // nsOnloadBlocker implementation
 NS_IMPL_ISUPPORTS(nsOnloadBlocker, nsIRequest)
 
 NS_IMETHODIMP
 nsOnloadBlocker::GetName(nsACString &aResult)
 {
   aResult.AssignLiteral("about:document-onload-blocker");
   return NS_OK;
@@ -1919,29 +1895,17 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_
   // Traverse all nsDocument nsCOMPtrs.
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParser)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mScriptGlobalObject)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mListenerManager)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDOMStyleSheets)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStyleSheetSetList)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mScriptLoader)
 
-  for (auto iter = tmp->mRadioGroups.Iter(); !iter.Done(); iter.Next()) {
-    nsRadioGroupStruct* radioGroup = iter.UserData();
-    NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(
-      cb, "mRadioGroups entry->mSelectedRadioButton");
-    cb.NoteXPCOMChild(ToSupports(radioGroup->mSelectedRadioButton));
-
-    uint32_t i, count = radioGroup->mRadioButtons.Count();
-    for (i = 0; i < count; ++i) {
-      NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(
-        cb, "mRadioGroups entry->mRadioButtons[i]");
-      cb.NoteXPCOMChild(radioGroup->mRadioButtons[i]);
-    }
-  }
+  DocumentOrShadowRoot::Traverse(tmp, cb);
 
   // The boxobject for an element will only exist as long as it's in the
   // document, so we'll traverse the table here instead of from the element.
   if (tmp->mBoxObjectTable) {
     for (auto iter = tmp->mBoxObjectTable->Iter(); !iter.Done(); iter.Next()) {
       NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mBoxObjectTable entry");
       cb.NoteXPCOMChild(iter.UserData());
     }
@@ -2087,17 +2051,17 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ns
   delete tmp->mSubDocuments;
   tmp->mSubDocuments = nullptr;
 
   tmp->mFrameRequestCallbacks.Clear();
   MOZ_RELEASE_ASSERT(!tmp->mFrameRequestCallbacksScheduled,
                      "How did we get here without our presshell going away "
                      "first?");
 
-  tmp->mRadioGroups.Clear();
+  DocumentOrShadowRoot::Unlink(tmp);
 
   // nsDocument has a pretty complex destructor, so we're going to
   // assume that *most* cycles you actually want to break somewhere
   // else, and not unlink an awful lot here.
 
   tmp->mIdentifierMap.Clear();
   tmp->mExpandoAndGeneration.OwnerUnlinked();
 
@@ -7708,173 +7672,16 @@ nsIDocument::IsScriptEnabled()
   nsCOMPtr<nsIScriptGlobalObject> globalObject = do_QueryInterface(GetInnerWindow());
   if (!globalObject || !globalObject->GetGlobalJSObject()) {
     return false;
   }
 
   return xpc::Scriptability::Get(globalObject->GetGlobalJSObject()).Allowed();
 }
 
-nsRadioGroupStruct*
-nsDocument::GetRadioGroup(const nsAString& aName) const
-{
-  nsRadioGroupStruct* radioGroup = nullptr;
-  mRadioGroups.Get(aName, &radioGroup);
-  return radioGroup;
-}
-
-nsRadioGroupStruct*
-nsDocument::GetOrCreateRadioGroup(const nsAString& aName)
-{
-  return mRadioGroups.LookupForAdd(aName).OrInsert(
-    [] () { return new nsRadioGroupStruct(); });
-}
-
-void
-nsDocument::SetCurrentRadioButton(const nsAString& aName,
-                                  HTMLInputElement* aRadio)
-{
-  nsRadioGroupStruct* radioGroup = GetOrCreateRadioGroup(aName);
-  radioGroup->mSelectedRadioButton = aRadio;
-}
-
-HTMLInputElement*
-nsDocument::GetCurrentRadioButton(const nsAString& aName)
-{
-  return GetOrCreateRadioGroup(aName)->mSelectedRadioButton;
-}
-
-NS_IMETHODIMP
-nsDocument::GetNextRadioButton(const nsAString& aName,
-                               const bool aPrevious,
-                               HTMLInputElement* aFocusedRadio,
-                               HTMLInputElement** aRadioOut)
-{
-  // XXX Can we combine the HTML radio button method impls of
-  //     nsDocument and nsHTMLFormControl?
-  // XXX Why is HTML radio button stuff in nsDocument, as
-  //     opposed to nsHTMLDocument?
-  *aRadioOut = nullptr;
-
-  nsRadioGroupStruct* radioGroup = GetOrCreateRadioGroup(aName);
-
-  // Return the radio button relative to the focused radio button.
-  // If no radio is focused, get the radio relative to the selected one.
-  RefPtr<HTMLInputElement> currentRadio;
-  if (aFocusedRadio) {
-    currentRadio = aFocusedRadio;
-  }
-  else {
-    currentRadio = radioGroup->mSelectedRadioButton;
-    if (!currentRadio) {
-      return NS_ERROR_FAILURE;
-    }
-  }
-  int32_t index = radioGroup->mRadioButtons.IndexOf(currentRadio);
-  if (index < 0) {
-    return NS_ERROR_FAILURE;
-  }
-
-  int32_t numRadios = radioGroup->mRadioButtons.Count();
-  RefPtr<HTMLInputElement> radio;
-  do {
-    if (aPrevious) {
-      if (--index < 0) {
-        index = numRadios -1;
-      }
-    }
-    else if (++index >= numRadios) {
-      index = 0;
-    }
-    NS_ASSERTION(static_cast<nsGenericHTMLFormElement*>(radioGroup->mRadioButtons[index])->IsHTMLElement(nsGkAtoms::input),
-                 "mRadioButtons holding a non-radio button");
-    radio = static_cast<HTMLInputElement*>(radioGroup->mRadioButtons[index]);
-  } while (radio->Disabled() && radio != currentRadio);
-
-  radio.forget(aRadioOut);
-  return NS_OK;
-}
-
-void
-nsDocument::AddToRadioGroup(const nsAString& aName,
-                            HTMLInputElement* aRadio)
-{
-  nsRadioGroupStruct* radioGroup = GetOrCreateRadioGroup(aName);
-  radioGroup->mRadioButtons.AppendObject(aRadio);
-
-  if (aRadio->IsRequired()) {
-    radioGroup->mRequiredRadioCount++;
-  }
-}
-
-void
-nsDocument::RemoveFromRadioGroup(const nsAString& aName,
-                                 HTMLInputElement* aRadio)
-{
-  nsRadioGroupStruct* radioGroup = GetOrCreateRadioGroup(aName);
-  radioGroup->mRadioButtons.RemoveObject(aRadio);
-
-  if (aRadio->IsRequired()) {
-    NS_ASSERTION(radioGroup->mRequiredRadioCount != 0,
-                 "mRequiredRadioCount about to wrap below 0!");
-    radioGroup->mRequiredRadioCount--;
-  }
-}
-
-NS_IMETHODIMP
-nsDocument::WalkRadioGroup(const nsAString& aName,
-                           nsIRadioVisitor* aVisitor,
-                           bool aFlushContent)
-{
-  nsRadioGroupStruct* radioGroup = GetOrCreateRadioGroup(aName);
-
-  for (int i = 0; i < radioGroup->mRadioButtons.Count(); i++) {
-    if (!aVisitor->Visit(radioGroup->mRadioButtons[i])) {
-      return NS_OK;
-    }
-  }
-
-  return NS_OK;
-}
-
-uint32_t
-nsDocument::GetRequiredRadioCount(const nsAString& aName) const
-{
-  nsRadioGroupStruct* radioGroup = GetRadioGroup(aName);
-  return radioGroup ? radioGroup->mRequiredRadioCount : 0;
-}
-
-void
-nsDocument::RadioRequiredWillChange(const nsAString& aName, bool aRequiredAdded)
-{
-  nsRadioGroupStruct* radioGroup = GetOrCreateRadioGroup(aName);
-
-  if (aRequiredAdded) {
-    radioGroup->mRequiredRadioCount++;
-  } else {
-    NS_ASSERTION(radioGroup->mRequiredRadioCount != 0,
-                 "mRequiredRadioCount about to wrap below 0!");
-    radioGroup->mRequiredRadioCount--;
-  }
-}
-
-bool
-nsDocument::GetValueMissingState(const nsAString& aName) const
-{
-  nsRadioGroupStruct* radioGroup = GetRadioGroup(aName);
-  return radioGroup && radioGroup->mGroupSuffersFromValueMissing;
-}
-
-void
-nsDocument::SetValueMissingState(const nsAString& aName, bool aValue)
-{
-  nsRadioGroupStruct* radioGroup = GetOrCreateRadioGroup(aName);
-  radioGroup->mGroupSuffersFromValueMissing = aValue;
-}
-
 void
 nsDocument::RetrieveRelevantHeaders(nsIChannel *aChannel)
 {
   PRTime modDate = 0;
   nsresult rv;
 
   nsCOMPtr<nsIHttpChannel> httpChannel;
   rv = GetHttpChannelHelper(aChannel, getter_AddRefs(httpChannel));
--- a/dom/base/nsDocument.h
+++ b/dom/base/nsDocument.h
@@ -61,19 +61,17 @@
 #define XML_DECLARATION_BITS_DECLARATION_EXISTS   (1 << 0)
 #define XML_DECLARATION_BITS_ENCODING_EXISTS      (1 << 1)
 #define XML_DECLARATION_BITS_STANDALONE_EXISTS    (1 << 2)
 #define XML_DECLARATION_BITS_STANDALONE_YES       (1 << 3)
 
 
 class nsDOMStyleSheetSetList;
 class nsDocument;
-class nsIRadioVisitor;
 class nsIFormControl;
-struct nsRadioGroupStruct;
 class nsOnloadBlocker;
 class nsDOMNavigationTiming;
 class nsWindowSizes;
 class nsHtml5TreeOpExecutor;
 class nsDocumentOnStack;
 class nsISecurityConsoleMessage;
 
 namespace mozilla {
@@ -142,40 +140,67 @@ public:
 
   virtual void EndUpdate() override;
   virtual void BeginLoad() override;
   virtual void EndLoad() override;
 
   // nsIRadioGroupContainer
   NS_IMETHOD WalkRadioGroup(const nsAString& aName,
                             nsIRadioVisitor* aVisitor,
-                            bool aFlushContent) override;
+                            bool aFlushContent) override
+  {
+    return DocumentOrShadowRoot::WalkRadioGroup(aName, aVisitor, aFlushContent);
+  }
   virtual void
     SetCurrentRadioButton(const nsAString& aName,
-                          mozilla::dom::HTMLInputElement* aRadio) override;
+                          mozilla::dom::HTMLInputElement* aRadio) override
+  {
+    DocumentOrShadowRoot::SetCurrentRadioButton(aName, aRadio);
+  }
   virtual mozilla::dom::HTMLInputElement*
-    GetCurrentRadioButton(const nsAString& aName) override;
+    GetCurrentRadioButton(const nsAString& aName) override
+  {
+    return DocumentOrShadowRoot::GetCurrentRadioButton(aName);
+  }
   NS_IMETHOD
     GetNextRadioButton(const nsAString& aName,
                        const bool aPrevious,
-                       mozilla::dom::HTMLInputElement*  aFocusedRadio,
-                       mozilla::dom::HTMLInputElement** aRadioOut) override;
+                       mozilla::dom::HTMLInputElement* aFocusedRadio,
+                       mozilla::dom::HTMLInputElement** aRadioOut) override
+  {
+    return DocumentOrShadowRoot::GetNextRadioButton(aName, aPrevious,
+                                                    aFocusedRadio, aRadioOut);
+  }
   virtual void AddToRadioGroup(const nsAString& aName,
-                               mozilla::dom::HTMLInputElement* aRadio) override;
+                               mozilla::dom::HTMLInputElement* aRadio) override
+  {
+    DocumentOrShadowRoot::AddToRadioGroup(aName, aRadio);
+  }
   virtual void RemoveFromRadioGroup(const nsAString& aName,
-                                    mozilla::dom::HTMLInputElement* aRadio) override;
-  virtual uint32_t GetRequiredRadioCount(const nsAString& aName) const override;
+                                    mozilla::dom::HTMLInputElement* aRadio) override
+  {
+    DocumentOrShadowRoot::RemoveFromRadioGroup(aName, aRadio);
+  }
+  virtual uint32_t GetRequiredRadioCount(const nsAString& aName) const override
+  {
+    return DocumentOrShadowRoot::GetRequiredRadioCount(aName);
+  }
   virtual void RadioRequiredWillChange(const nsAString& aName,
-                                       bool aRequiredAdded) override;
-  virtual bool GetValueMissingState(const nsAString& aName) const override;
-  virtual void SetValueMissingState(const nsAString& aName, bool aValue) override;
-
-  // for radio group
-  nsRadioGroupStruct* GetRadioGroup(const nsAString& aName) const;
-  nsRadioGroupStruct* GetOrCreateRadioGroup(const nsAString& aName);
+                                       bool aRequiredAdded) override
+  {
+    DocumentOrShadowRoot::RadioRequiredWillChange(aName, aRequiredAdded);
+  }
+  virtual bool GetValueMissingState(const nsAString& aName) const override
+  {
+    return DocumentOrShadowRoot::GetValueMissingState(aName);
+  }
+  virtual void SetValueMissingState(const nsAString& aName, bool aValue) override
+  {
+    return DocumentOrShadowRoot::SetValueMissingState(aName, aValue);
+  }
 
   // Check whether shadow DOM is enabled for aGlobal.
   static bool IsShadowDOMEnabled(JSContext* aCx, JSObject* aGlobal);
   // Check whether shadow DOM is enabled for the document this node belongs to.
   // Same as above, but also checks that the caller is either chrome or some addon.
   static bool IsShadowDOMEnabledAndCallerIsChromeOrAddon(JSContext* aCx, JSObject* aObject);
   static bool IsShadowDOMEnabled(const nsINode* aNode);
 
@@ -257,18 +282,16 @@ public:
   // FIXME(emilio): This needs to be here instead of in nsIDocument because Rust
   // can't represent alignas(8) values on 32-bit architectures, which would
   // cause nsIDocument's layout to be wrong in the Rust side.
   //
   // This can be fixed after updating to rust 1.25 and updating bindgen to
   // include https://github.com/rust-lang-nursery/rust-bindgen/pull/1271.
   js::ExpandoAndGeneration mExpandoAndGeneration;
 
-  nsClassHashtable<nsStringHashKey, nsRadioGroupStruct> mRadioGroups;
-
   friend class nsCallRequestFullscreen;
 
   // The application cache that this document is associated with, if
   // any.  This can change during the lifetime of the document.
   nsCOMPtr<nsIApplicationCache> mApplicationCache;
 
   nsCOMPtr<nsIContent> mFirstBaseNodeWithHref;
 private:
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -3047,18 +3047,24 @@ HTMLInputElement::GetRadioGroupContainer
   if (mForm) {
     return mForm;
   }
 
   if (IsInAnonymousSubtree()) {
     return nullptr;
   }
 
-  //XXXsmaug It isn't clear how this should work in Shadow DOM.
-  return static_cast<nsDocument*>(GetUncomposedDoc());
+  DocumentOrShadowRoot* docOrShadow = GetUncomposedDocOrConnectedShadowRoot();
+  if (!docOrShadow) {
+    return nullptr;
+  }
+
+  nsCOMPtr<nsIRadioGroupContainer> container =
+    do_QueryInterface(&(docOrShadow->AsNode()));
+  return container;
 }
 
 HTMLInputElement*
 HTMLInputElement::GetSelectedRadioButton() const
 {
   nsIRadioGroupContainer* container = GetRadioGroupContainer();
   if (!container) {
     return nullptr;
@@ -4627,17 +4633,18 @@ HTMLInputElement::BindToTree(nsIDocument
         NewRunnableMethod("dom::HTMLInputElement::MaybeLoadImage",
                           this,
                           &HTMLInputElement::MaybeLoadImage));
     }
   }
 
   // Add radio to document if we don't have a form already (if we do it's
   // already been added into that group)
-  if (aDocument && !mForm && mType == NS_FORM_INPUT_RADIO) {
+  if (!mForm && mType == NS_FORM_INPUT_RADIO &&
+      GetUncomposedDocOrConnectedShadowRoot()) {
     AddedToRadioGroup();
   }
 
   // Set direction based on value if dir=auto
   if (HasDirAuto()) {
     SetDirectionFromValue(false);
   }
 
@@ -6562,17 +6569,17 @@ HTMLInputElement::AllowDrop()
  * Radio group stuff
  */
 
 void
 HTMLInputElement::AddedToRadioGroup()
 {
   // If the element is neither in a form nor a document, there is no group so we
   // should just stop here.
-  if (!mForm && (!IsInUncomposedDoc() || IsInAnonymousSubtree())) {
+  if (!mForm && (!GetUncomposedDocOrConnectedShadowRoot() || IsInAnonymousSubtree())) {
     return;
   }
 
   // Make sure not to notify if we're still being created
   bool notify = mDoneCreating;
 
   //
   // If the input element is checked, and we add it to the group, it will
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -397861,16 +397861,22 @@
     ]
    ],
    "shadow-dom/input-element-list.html": [
     [
      "/shadow-dom/input-element-list.html",
      {}
     ]
    ],
+   "shadow-dom/input-type-radio.html": [
+    [
+     "/shadow-dom/input-type-radio.html",
+     {}
+    ]
+   ],
    "shadow-dom/leaktests/get-elements.html": [
     [
      "/shadow-dom/leaktests/get-elements.html",
      {}
     ]
    ],
    "shadow-dom/leaktests/html-collection.html": [
     [
@@ -646382,16 +646388,20 @@
   "shadow-dom/historical.html": [
    "2490f05b3e80aa67a0c70a9cb12be282fa0e15b2",
    "testharness"
   ],
   "shadow-dom/input-element-list.html": [
    "b571534eb0d6f3f57cfbec3e706648b19848b6d6",
    "testharness"
   ],
+  "shadow-dom/input-type-radio.html": [
+   "bd5d8e43b0fd9d0c9f1e078ed97a1bbd18b7b0be",
+   "testharness"
+  ],
   "shadow-dom/layout-slot-no-longer-assigned.html": [
    "dfcac99da023ec2bbd94835f71efaef952a62341",
    "reftest"
   ],
   "shadow-dom/layout-slot-no-longer-fallback.html": [
    "7507f11ac18e6590367a147acbc78834b0d19afd",
    "reftest"
   ],
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/input-type-radio.html
@@ -0,0 +1,73 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<input type="radio" name="group" id="lightRadio1">
+<input type="radio" name="group" id="lightRadio2">
+<div id="host"></div>
+<script>
+
+test(() => {
+    var lightRadio1 = document.getElementById("lightRadio1");
+    var lightRadio2 = document.getElementById("lightRadio2");
+
+    var host = document.getElementById("host");
+    var sr = host.attachShadow({mode: "closed"});
+    var shadowRadio1 = document.createElement("input");
+    shadowRadio1.name = "group";
+    shadowRadio1.id = "shadowRadio1";
+    shadowRadio1.type = "radio";
+    sr.appendChild(shadowRadio1);
+    var shadowRadio2 = document.createElement("input");
+    shadowRadio2.name = "group";
+    shadowRadio2.id = "shadowRadio2";
+    shadowRadio2.type = "radio";
+    sr.appendChild(shadowRadio2);
+
+    assert_false(lightRadio1.checked);
+    assert_false(lightRadio2.checked);
+    assert_false(shadowRadio1.checked);
+    assert_false(shadowRadio2.checked);
+
+    lightRadio1.click();
+    assert_true(lightRadio1.checked);
+    assert_false(lightRadio2.checked);
+    assert_false(shadowRadio1.checked);
+    assert_false(shadowRadio2.checked);
+
+    lightRadio2.click();
+    assert_false(lightRadio1.checked);
+    assert_true(lightRadio2.checked);
+    assert_false(shadowRadio1.checked);
+    assert_false(shadowRadio2.checked);
+
+    shadowRadio1.click();
+    assert_false(lightRadio1.checked);
+    assert_true(lightRadio2.checked);
+    assert_true(shadowRadio1.checked);
+    assert_false(shadowRadio2.checked);
+
+    shadowRadio2.click();
+    assert_false(lightRadio1.checked);
+    assert_true(lightRadio2.checked);
+    assert_false(shadowRadio1.checked);
+    assert_true(shadowRadio2.checked);
+
+    // Ensure radio groups work even when modifying shadow DOM.
+    shadowRadio2.remove();
+    sr.appendChild(shadowRadio2);
+    shadowRadio2.click();
+    assert_false(lightRadio1.checked);
+    assert_true(lightRadio2.checked);
+    assert_false(shadowRadio1.checked);
+    assert_true(shadowRadio2.checked);
+
+    shadowRadio1.click();
+    assert_false(lightRadio1.checked);
+    assert_true(lightRadio2.checked);
+    assert_true(shadowRadio1.checked);
+    assert_false(shadowRadio2.checked);
+}, "input type=radio elements should form a group inside shadow DOM.");
+
+</script>