Bug 1705141 - Implement imperative slotting API r=smaug,emilio
authorSean Feng <sefeng@mozilla.com>
Tue, 20 Jul 2021 18:06:25 +0000
changeset 586211 0a87a26cc1b1f2d32cbbee74147a8273f93eb564
parent 586210 48efa42e544ea8e65a452192fcd20a78c97f0d12
child 586212 0c51eaf636b089097500fc721b31d99dcd92b0a3
push id146587
push usersefeng@mozilla.com
push dateTue, 20 Jul 2021 18:09:18 +0000
treeherderautoland@0a87a26cc1b1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug, emilio
bugs1705141
milestone92.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 1705141 - Implement imperative slotting API r=smaug,emilio Differential Revision: https://phabricator.services.mozilla.com/D119444
dom/base/Element.cpp
dom/base/Element.h
dom/base/FragmentOrElement.cpp
dom/base/ShadowRoot.cpp
dom/base/ShadowRoot.h
dom/base/nsIContent.h
dom/base/nsINode.cpp
dom/html/HTMLSlotElement.cpp
dom/html/HTMLSlotElement.h
dom/webidl/Element.webidl
dom/webidl/HTMLSlotElement.webidl
dom/webidl/ShadowRoot.webidl
modules/libpref/init/StaticPrefList.yaml
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
@@ -1181,21 +1181,21 @@ already_AddRefed<ShadowRoot> Element::At
     aError.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
     return nullptr;
   }
 
   if (StaticPrefs::dom_webcomponents_shadowdom_report_usage()) {
     OwnerDoc()->ReportShadowDOMUsage();
   }
 
-  return AttachShadowWithoutNameChecks(aInit.mMode);
+  return AttachShadowWithoutNameChecks(aInit.mMode, aInit.mSlotAssignment);
 }
 
 already_AddRefed<ShadowRoot> Element::AttachShadowWithoutNameChecks(
-    ShadowRootMode aMode) {
+    ShadowRootMode aMode, SlotAssignmentMode aSlotAssignment) {
   nsAutoScriptBlocker scriptBlocker;
 
   RefPtr<mozilla::dom::NodeInfo> nodeInfo =
       mNodeInfo->NodeInfoManager()->GetNodeInfo(
           nsGkAtoms::documentFragmentNodeName, nullptr, kNameSpaceID_None,
           DOCUMENT_FRAGMENT_NODE);
 
   // If there are no children, the flat tree is not changing due to the presence
@@ -1214,17 +1214,17 @@ already_AddRefed<ShadowRoot> Element::At
 
   /**
    * 4. Let shadow be a new shadow root whose node document is
    *    context object's node document, host is context object,
    *    and mode is init's mode.
    */
   auto* nim = nodeInfo->NodeInfoManager();
   RefPtr<ShadowRoot> shadowRoot =
-      new (nim) ShadowRoot(this, aMode, nodeInfo.forget());
+      new (nim) ShadowRoot(this, aMode, aSlotAssignment, nodeInfo.forget());
 
   if (NodeOrAncestorHasDirAuto()) {
     shadowRoot->SetAncestorHasDirAuto();
   }
 
   /**
    * 5. Set context object's shadow root to shadow.
    */
@@ -2623,17 +2623,17 @@ nsresult Element::AfterSetAttr(int32_t a
           } else {
             shadow->PartRemoved(*this);
           }
         }
       }
       MOZ_ASSERT(HasPartAttribute() == isPart);
     } else if (aName == nsGkAtoms::slot && GetParent()) {
       if (ShadowRoot* shadow = GetParent()->GetShadowRoot()) {
-        shadow->MaybeReassignElement(*this);
+        shadow->MaybeReassignContent(*this);
       }
     }
   }
   return NS_OK;
 }
 
 void Element::PreIdMaybeChange(int32_t aNamespaceID, nsAtom* aName,
                                const nsAttrValueOrString* aValue) {
--- a/dom/base/Element.h
+++ b/dom/base/Element.h
@@ -1261,17 +1261,18 @@ class Element : public FragmentOrElement
   MOZ_CAN_RUN_SCRIPT already_AddRefed<DOMRect> GetBoundingClientRect();
 
   // Shadow DOM v1
   already_AddRefed<ShadowRoot> AttachShadow(const ShadowRootInit& aInit,
                                             ErrorResult& aError);
   bool CanAttachShadowDOM() const;
 
   already_AddRefed<ShadowRoot> AttachShadowWithoutNameChecks(
-      ShadowRootMode aMode);
+      ShadowRootMode aMode,
+      SlotAssignmentMode aSlotAssignmentMode = SlotAssignmentMode::Named);
 
   // Attach UA Shadow Root if it is not attached.
   enum class NotifyUAWidgetSetup : bool { No, Yes };
   void AttachAndSetUAShadowRoot(NotifyUAWidgetSetup = NotifyUAWidgetSetup::Yes);
 
   // Dispatch an event to UAWidgetsChild, triggering construction
   // or onchange callback on the existing widget.
   void NotifyUAWidgetSetupOrChange();
--- a/dom/base/FragmentOrElement.cpp
+++ b/dom/base/FragmentOrElement.cpp
@@ -532,17 +532,19 @@ void nsIContent::nsExtendedContentSlots:
   aCb.NoteXPCOMChild(NS_ISUPPORTS_CAST(nsIContent*, mContainingShadow));
 
   NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(aCb, "mExtendedSlots->mAssignedSlot");
   aCb.NoteXPCOMChild(NS_ISUPPORTS_CAST(nsIContent*, mAssignedSlot.get()));
 }
 
 nsIContent::nsExtendedContentSlots::nsExtendedContentSlots() = default;
 
-nsIContent::nsExtendedContentSlots::~nsExtendedContentSlots() = default;
+nsIContent::nsExtendedContentSlots::~nsExtendedContentSlots() {
+  MOZ_ASSERT(!mManualSlotAssignment);
+}
 
 size_t nsIContent::nsExtendedContentSlots::SizeOfExcludingThis(
     MallocSizeOf aMallocSizeOf) const {
   // For now, nothing to measure here.  We don't actually own any of our
   // members.
   return 0;
 }
 
--- a/dom/base/ShadowRoot.cpp
+++ b/dom/base/ShadowRoot.cpp
@@ -9,16 +9,17 @@
 #include "mozilla/dom/ShadowRoot.h"
 #include "mozilla/dom/DocumentFragment.h"
 #include "ChildIterator.h"
 #include "nsContentUtils.h"
 #include "nsWindowSizes.h"
 #include "mozilla/dom/DirectionalityUtils.h"
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/HTMLSlotElement.h"
+#include "mozilla/dom/Text.h"
 #include "mozilla/dom/TreeOrderedArrayInlines.h"
 #include "mozilla/EventDispatcher.h"
 #include "mozilla/IdentifierMapEntry.h"
 #include "mozilla/PresShell.h"
 #include "mozilla/PresShellInlines.h"
 #include "mozilla/ScopeExit.h"
 #include "mozilla/ServoStyleRuleMap.h"
 #include "mozilla/StyleSheet.h"
@@ -42,20 +43,22 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(
   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIContent)
   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,
+                       SlotAssignmentMode aSlotAssignment,
                        already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
     : DocumentFragment(std::move(aNodeInfo)),
       DocumentOrShadowRoot(this),
       mMode(aMode),
+      mSlotAssignment(aSlotAssignment),
       mIsUAWidget(false) {
   SetHost(aElement);
 
   // Nodes in a shadow tree should never store a value
   // in the subtree root pointer, nodes in the shadow tree
   // track the subtree root using GetContainingShadow().
   ClearSubtreeRootPointer();
 
@@ -192,58 +195,82 @@ void ShadowRoot::AddSlot(HTMLSlotElement
 
   // Note that if name attribute missing, the slot is a default slot.
   nsAutoString name;
   aSlot->GetName(name);
 
   SlotArray& currentSlots = *mSlotMap.GetOrInsertNew(name);
 
   size_t index = currentSlots.Insert(*aSlot);
-  if (index != 0) {
+
+  // For Named slots, slottables are inserted into the other slot
+  // which has the same name already, however it's not the case
+  // for manual slots
+  if (index != 0 && SlotAssignment() == SlotAssignmentMode::Named) {
     return;
   }
 
   HTMLSlotElement* oldSlot = currentSlots->SafeElementAt(1);
-  if (oldSlot) {
-    MOZ_DIAGNOSTIC_ASSERT(oldSlot != aSlot);
+  if (SlotAssignment() == SlotAssignmentMode::Named) {
+    if (oldSlot) {
+      MOZ_DIAGNOSTIC_ASSERT(oldSlot != aSlot);
 
-    // Move assigned nodes from old slot to new slot.
-    InvalidateStyleAndLayoutOnSubtree(oldSlot);
-    const nsTArray<RefPtr<nsINode>>& assignedNodes = oldSlot->AssignedNodes();
-    bool doEnqueueSlotChange = false;
-    while (assignedNodes.Length() > 0) {
-      nsINode* assignedNode = assignedNodes[0];
+      // Move assigned nodes from old slot to new slot.
+      InvalidateStyleAndLayoutOnSubtree(oldSlot);
+      const nsTArray<RefPtr<nsINode>>& assignedNodes = oldSlot->AssignedNodes();
+      bool doEnqueueSlotChange = false;
+      while (assignedNodes.Length() > 0) {
+        nsINode* assignedNode = assignedNodes[0];
+
+        oldSlot->RemoveAssignedNode(*assignedNode->AsContent());
+        aSlot->AppendAssignedNode(*assignedNode->AsContent());
+        doEnqueueSlotChange = true;
+      }
 
-      oldSlot->RemoveAssignedNode(*assignedNode->AsContent());
-      aSlot->AppendAssignedNode(*assignedNode->AsContent());
-      doEnqueueSlotChange = true;
-    }
+      if (doEnqueueSlotChange) {
+        oldSlot->EnqueueSlotChangeEvent();
+        aSlot->EnqueueSlotChangeEvent();
+        SlotStateChanged(oldSlot);
+        SlotStateChanged(aSlot);
+      }
+    } else {
+      bool doEnqueueSlotChange = false;
+      // Otherwise add appropriate nodes to this slot from the host.
+      for (nsIContent* child = GetHost()->GetFirstChild(); child;
+           child = child->GetNextSibling()) {
+        nsAutoString slotName;
+        if (auto* element = Element::FromNode(*child)) {
+          element->GetAttr(nsGkAtoms::slot, slotName);
+        }
+        if (!child->IsSlotable() || !slotName.Equals(name)) {
+          continue;
+        }
+        doEnqueueSlotChange = true;
+        aSlot->AppendAssignedNode(*child);
+      }
 
-    if (doEnqueueSlotChange) {
-      oldSlot->EnqueueSlotChangeEvent();
-      aSlot->EnqueueSlotChangeEvent();
-      SlotStateChanged(oldSlot);
-      SlotStateChanged(aSlot);
+      if (doEnqueueSlotChange) {
+        aSlot->EnqueueSlotChangeEvent();
+        SlotStateChanged(aSlot);
+      }
     }
   } else {
     bool doEnqueueSlotChange = false;
-    // Otherwise add appropriate nodes to this slot from the host.
-    for (nsIContent* child = GetHost()->GetFirstChild(); child;
-         child = child->GetNextSibling()) {
-      nsAutoString slotName;
-      if (auto* element = Element::FromNode(*child)) {
-        element->GetAttr(nsGkAtoms::slot, slotName);
-      }
-      if (!child->IsSlotable() || !slotName.Equals(name)) {
+    for (const auto& node : aSlot->ManuallyAssignedNodes()) {
+      if (GetHost() != node->GetParent()) {
         continue;
       }
+
+      MOZ_ASSERT(node->IsContent(),
+                 "Manually assigned nodes should be an element or a text");
+      nsIContent* content = node->AsContent();
+
+      aSlot->AppendAssignedNode(*content);
       doEnqueueSlotChange = true;
-      aSlot->AppendAssignedNode(*child);
     }
-
     if (doEnqueueSlotChange) {
       aSlot->EnqueueSlotChangeEvent();
       SlotStateChanged(aSlot);
     }
   }
 }
 
 void ShadowRoot::RemoveSlot(HTMLSlotElement* aSlot) {
@@ -253,32 +280,40 @@ void ShadowRoot::RemoveSlot(HTMLSlotElem
   aSlot->GetName(name);
 
   MOZ_ASSERT(mSlotMap.Get(name));
 
   SlotArray& currentSlots = *mSlotMap.Get(name);
   MOZ_DIAGNOSTIC_ASSERT(currentSlots->Contains(aSlot),
                         "Slot to de-register wasn't found?");
   if (currentSlots->Length() == 1) {
-    MOZ_ASSERT(currentSlots->ElementAt(0) == aSlot);
+    MOZ_ASSERT_IF(SlotAssignment() == SlotAssignmentMode::Named,
+                  currentSlots->ElementAt(0) == aSlot);
 
     InvalidateStyleAndLayoutOnSubtree(aSlot);
 
     mSlotMap.Remove(name);
     if (!aSlot->AssignedNodes().IsEmpty()) {
       aSlot->ClearAssignedNodes();
       aSlot->EnqueueSlotChangeEvent();
     }
 
     return;
   }
+  if (SlotAssignment() == SlotAssignmentMode::Manual) {
+    InvalidateStyleAndLayoutOnSubtree(aSlot);
+    if (!aSlot->AssignedNodes().IsEmpty()) {
+      aSlot->ClearAssignedNodes();
+      aSlot->EnqueueSlotChangeEvent();
+    }
+  }
 
   const bool wasFirstSlot = currentSlots->ElementAt(0) == aSlot;
   currentSlots.RemoveElement(*aSlot);
-  if (!wasFirstSlot) {
+  if (!wasFirstSlot || SlotAssignment() == SlotAssignmentMode::Manual) {
     return;
   }
 
   // Move assigned nodes from removed slot to the next slot in
   // tree order with the same name.
   InvalidateStyleAndLayoutOnSubtree(aSlot);
   HTMLSlotElement* replacementSlot = currentSlots->ElementAt(0);
   const nsTArray<RefPtr<nsINode>>& assignedNodes = aSlot->AssignedNodes();
@@ -526,86 +561,133 @@ void ShadowRoot::GetEventTargetParent(Ev
   aVisitor.SetParentTarget(shadowHost, false);
 
   nsCOMPtr<nsIContent> content(do_QueryInterface(aVisitor.mEvent->mTarget));
   if (content && content->GetContainingShadow() == this) {
     aVisitor.mEventTargetAtParent = shadowHost;
   }
 }
 
-ShadowRoot::SlotAssignment ShadowRoot::SlotAssignmentFor(nsIContent& aContent) {
-  nsAutoString slotName;
-  // Note that if slot attribute is missing, assign it to the first default
-  // slot, if exists.
-  if (Element* element = Element::FromNode(aContent)) {
-    element->GetAttr(nsGkAtoms::slot, slotName);
+ShadowRoot::SlotInsertionPoint ShadowRoot::SlotInsertionPointFor(
+    nsIContent& aContent) {
+  HTMLSlotElement* slot = nullptr;
+
+  if (SlotAssignment() == SlotAssignmentMode::Manual) {
+    slot = aContent.GetManualSlotAssignment();
+    if (!slot || slot->GetContainingShadow() != this) {
+      return {};
+    }
+  } else {
+    nsAutoString slotName;
+    // Note that if slot attribute is missing, assign it to the first default
+    // slot, if exists.
+    if (Element* element = Element::FromNode(aContent)) {
+      element->GetAttr(nsGkAtoms::slot, slotName);
+    }
+
+    SlotArray* slots = mSlotMap.Get(slotName);
+    if (!slots) {
+      return {};
+    }
+    slot = (*slots)->ElementAt(0);
   }
 
-  SlotArray* slots = mSlotMap.Get(slotName);
-  if (!slots) {
-    return {};
-  }
-
-  HTMLSlotElement* slot = (*slots)->ElementAt(0);
   MOZ_ASSERT(slot);
 
-  if (!aContent.GetNextSibling()) {
-    // aContent is the last child, no need to loop through the assigned nodes,
-    // we're necessarily the last one.
-    //
-    // This prevents multiple appends into the host from getting quadratic.
-    return {slot, Nothing()};
+  if (SlotAssignment() == SlotAssignmentMode::Named) {
+    if (!aContent.GetNextSibling()) {
+      // aContent is the last child, no need to loop through the assigned nodes,
+      // we're necessarily the last one.
+      //
+      // This prevents multiple appends into the host from getting quadratic.
+      return {slot, Nothing()};
+    }
+  } else {
+    // For manual slots, if aContent is the last element, we return Nothing
+    // because we just need to append the element to the assigned nodes. No need
+    // to return an index.
+    if (slot->ManuallyAssignedNodes().SafeLastElement(nullptr) == &aContent) {
+      return {slot, Nothing()};
+    }
   }
 
   // Find the appropriate position in the assigned node list for the newly
   // assigned content.
-  const nsTArray<RefPtr<nsINode>>& assignedNodes = slot->AssignedNodes();
-  nsIContent* currentContent = GetHost()->GetFirstChild();
-  for (uint32_t i = 0; i < assignedNodes.Length(); i++) {
-    // Seek through the host's explicit children until the
-    // assigned content is found.
-    while (currentContent && currentContent != assignedNodes[i]) {
-      if (currentContent == &aContent) {
-        return {slot, Some(i)};
+  if (SlotAssignment() == SlotAssignmentMode::Manual) {
+    const nsTArray<nsINode*>& manuallyAssignedNodes =
+        slot->ManuallyAssignedNodes();
+    auto index = manuallyAssignedNodes.IndexOf(&aContent);
+    if (index != manuallyAssignedNodes.NoIndex) {
+      return {slot, Some(index)};
+    }
+  } else {
+    const nsTArray<RefPtr<nsINode>>& assignedNodes = slot->AssignedNodes();
+    nsIContent* currentContent = GetHost()->GetFirstChild();
+    for (uint32_t i = 0; i < assignedNodes.Length(); i++) {
+      // Seek through the host's explicit children until the
+      // assigned content is found.
+      while (currentContent && currentContent != assignedNodes[i]) {
+        if (currentContent == &aContent) {
+          return {slot, Some(i)};
+        }
+        currentContent = currentContent->GetNextSibling();
       }
-      currentContent = currentContent->GetNextSibling();
     }
   }
 
   return {slot, Nothing()};
 }
 
-void ShadowRoot::MaybeReassignElement(Element& aElement) {
-  MOZ_ASSERT(aElement.GetParent() == GetHost());
-  HTMLSlotElement* oldSlot = aElement.GetAssignedSlot();
-  SlotAssignment assignment = SlotAssignmentFor(aElement);
+void ShadowRoot::MaybeReassignContent(nsIContent& aElementOrText) {
+  MOZ_ASSERT(aElementOrText.GetParent() == GetHost());
+  MOZ_ASSERT(aElementOrText.IsElement() || aElementOrText.IsText());
+  HTMLSlotElement* oldSlot = aElementOrText.GetAssignedSlot();
+
+  SlotInsertionPoint assignment = SlotInsertionPointFor(aElementOrText);
 
   if (assignment.mSlot == oldSlot) {
     // Nothing to do here.
     return;
   }
 
-  if (Document* doc = GetComposedDoc()) {
-    if (RefPtr<PresShell> presShell = doc->GetPresShell()) {
-      presShell->SlotAssignmentWillChange(aElement, oldSlot, assignment.mSlot);
+  // The layout invalidation piece for Manual slots is handled in
+  // HTMLSlotElement::Assign
+  if (aElementOrText.IsElement() &&
+      SlotAssignment() == SlotAssignmentMode::Named) {
+    if (Document* doc = GetComposedDoc()) {
+      if (RefPtr<PresShell> presShell = doc->GetPresShell()) {
+        presShell->SlotAssignmentWillChange(*aElementOrText.AsElement(),
+                                            oldSlot, assignment.mSlot);
+      }
     }
   }
 
   if (oldSlot) {
-    oldSlot->RemoveAssignedNode(aElement);
-    oldSlot->EnqueueSlotChangeEvent();
+    if (SlotAssignment() == SlotAssignmentMode::Named) {
+      oldSlot->RemoveAssignedNode(aElementOrText);
+      // Don't need to EnqueueSlotChangeEvent for Manual slots because it
+      // needs to be done in tree order, so
+      // HTMLSlotElement::Assign will handle it explicitly.
+      oldSlot->EnqueueSlotChangeEvent();
+    } else {
+      oldSlot->RemoveManuallyAssignedNode(aElementOrText);
+    }
   }
 
   if (assignment.mSlot) {
     if (assignment.mIndex) {
-      assignment.mSlot->InsertAssignedNode(*assignment.mIndex, aElement);
+      assignment.mSlot->InsertAssignedNode(*assignment.mIndex, aElementOrText);
     } else {
-      assignment.mSlot->AppendAssignedNode(aElement);
+      assignment.mSlot->AppendAssignedNode(aElementOrText);
     }
-    assignment.mSlot->EnqueueSlotChangeEvent();
+    // Similar as above, HTMLSlotElement::Assign handles enqueuing
+    // slotchange event.
+    if (SlotAssignment() == SlotAssignmentMode::Named) {
+      assignment.mSlot->EnqueueSlotChangeEvent();
+    }
   }
 }
 
 Element* ShadowRoot::GetActiveElement() {
   return GetRetargetedFocusedElement();
 }
 
 nsINode* ShadowRoot::ImportNodeAndAppendChildAt(nsINode& aParentNode,
@@ -675,17 +757,17 @@ void ShadowRoot::MaybeSlotHostChild(nsIC
   if (aChild.IsRootOfNativeAnonymousSubtree()) {
     return;
   }
 
   if (!aChild.IsSlotable()) {
     return;
   }
 
-  SlotAssignment assignment = SlotAssignmentFor(aChild);
+  SlotInsertionPoint assignment = SlotInsertionPointFor(aChild);
   if (!assignment.mSlot) {
     return;
   }
 
   // Fallback content will go away, let layout know.
   if (assignment.mSlot->AssignedNodes().IsEmpty() &&
       assignment.mSlot->HasChildren()) {
     InvalidateStyleAndLayoutOnSubtree(assignment.mSlot);
--- a/dom/base/ShadowRoot.h
+++ b/dom/base/ShadowRoot.h
@@ -47,38 +47,40 @@ class ShadowRoot final : public Document
 
  public:
   NS_IMPL_FROMNODE_HELPER(ShadowRoot, IsShadowRoot());
 
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ShadowRoot, DocumentFragment)
   NS_DECL_ISUPPORTS_INHERITED
 
   ShadowRoot(Element* aElement, ShadowRootMode aMode,
+             SlotAssignmentMode aSlotAssignment,
              already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
 
   void AddSizeOfExcludingThis(nsWindowSizes&, size_t* aNodeSize) const final;
 
-  // Try to reassign an element to a slot.
-  void MaybeReassignElement(Element&);
+  // Try to reassign an element or text to a slot.
+  void MaybeReassignContent(nsIContent& aElementOrText);
   // Called when an element is inserted as a direct child of our host. Tries to
   // slot the child in one of our slots.
   void MaybeSlotHostChild(nsIContent&);
   // Called when a direct child of our host is removed. Tries to un-slot the
   // child from the currently-assigned slot, if any.
   void MaybeUnslotHostChild(nsIContent&);
 
   // Shadow DOM v1
   Element* Host() const {
     MOZ_ASSERT(GetHost(),
                "ShadowRoot always has a host, how did we create "
                "this ShadowRoot?");
     return GetHost();
   }
 
   ShadowRootMode Mode() const { return mMode; }
+  SlotAssignmentMode SlotAssignment() const { return mSlotAssignment; }
   bool IsClosed() const { return mMode == ShadowRootMode::Closed; }
 
   void RemoveSheetFromStyles(StyleSheet&);
   void RuleAdded(StyleSheet&, css::Rule&);
   void RuleRemoved(StyleSheet&, css::Rule&);
   void RuleChanged(StyleSheet&, css::Rule*, StyleRuleChangeKind);
   void ImportRuleLoaded(CSSImportRule&, StyleSheet&);
   void SheetCloned(StyleSheet&);
@@ -99,61 +101,61 @@ class ShadowRoot final : public Document
   //
   // Forgets our shadow host and unbinds all our kids.
   void Unattach();
 
   // Calls BindToTree on each of our kids, and also maybe flags us as being
   // connected.
   nsresult Bind();
 
- private:
-  void InsertSheetIntoAuthorData(size_t aIndex, StyleSheet&,
-                                 const nsTArray<RefPtr<StyleSheet>>&);
-
-  void AppendStyleSheet(StyleSheet& aSheet) {
-    InsertSheetAt(SheetCount(), aSheet);
-  }
-
-  /**
-   * Represents the insertion point in a slot for a given node.
-   */
-  struct SlotAssignment {
-    HTMLSlotElement* mSlot = nullptr;
-    Maybe<uint32_t> mIndex;
-
-    SlotAssignment() = default;
-    SlotAssignment(HTMLSlotElement* aSlot, const Maybe<uint32_t>& aIndex)
-        : mSlot(aSlot), mIndex(aIndex) {}
-  };
-
-  /**
-   * Return the assignment corresponding to the content node at this particular
-   * point in time.
-   *
-   * It's the caller's responsibility to actually call InsertAssignedNode /
-   * AppendAssignedNode in the slot as needed.
-   */
-  SlotAssignment SlotAssignmentFor(nsIContent&);
-
   /**
    * Explicitly invalidates the style and layout of the flattened-tree subtree
    * rooted at the element.
    *
    * You need to use this whenever the flat tree is going to be shuffled in a
    * way that layout doesn't understand via the usual ContentInserted /
    * ContentAppended / ContentRemoved notifications. For example, if removing an
    * element will cause a change in the flat tree such that other element will
    * start showing up (like fallback content), this method needs to be called on
    * an ancestor of that element.
    *
    * It is important that this runs _before_ actually shuffling the flat tree
    * around, so that layout knows the actual tree that it needs to invalidate.
    */
   void InvalidateStyleAndLayoutOnSubtree(Element*);
 
+ private:
+  void InsertSheetIntoAuthorData(size_t aIndex, StyleSheet&,
+                                 const nsTArray<RefPtr<StyleSheet>>&);
+
+  void AppendStyleSheet(StyleSheet& aSheet) {
+    InsertSheetAt(SheetCount(), aSheet);
+  }
+
+  /**
+   * Represents the insertion point in a slot for a given node.
+   */
+  struct SlotInsertionPoint {
+    HTMLSlotElement* mSlot = nullptr;
+    Maybe<uint32_t> mIndex;
+
+    SlotInsertionPoint() = default;
+    SlotInsertionPoint(HTMLSlotElement* aSlot, const Maybe<uint32_t>& aIndex)
+        : mSlot(aSlot), mIndex(aIndex) {}
+  };
+
+  /**
+   * Return the assignment corresponding to the content node at this particular
+   * point in time.
+   *
+   * It's the caller's responsibility to actually call InsertAssignedNode /
+   * AppendAssignedNode in the slot as needed.
+   */
+  SlotInsertionPoint SlotInsertionPointFor(nsIContent&);
+
  public:
   void AddSlot(HTMLSlotElement* aSlot);
   void RemoveSlot(HTMLSlotElement* aSlot);
   bool HasSlots() const { return !mSlotMap.IsEmpty(); };
   HTMLSlotElement* GetDefaultSlot() const {
     SlotArray* list = mSlotMap.Get(u""_ns);
     return list ? (*list)->ElementAt(0) : nullptr;
   }
@@ -255,16 +257,18 @@ class ShadowRoot final : public Document
  protected:
   // FIXME(emilio): This will need to become more fine-grained.
   void ApplicableRulesChanged();
 
   virtual ~ShadowRoot();
 
   const ShadowRootMode mMode;
 
+  const SlotAssignmentMode mSlotAssignment;
+
   // The computed data from the style sheets.
   UniquePtr<RawServoAuthorStyles> mServoStyles;
   UniquePtr<mozilla::ServoStyleRuleMap> mStyleRuleMap;
 
   using SlotArray = TreeOrderedArray<HTMLSlotElement>;
   // Map from name of slot to an array of all slots in the shadow DOM with with
   // the given name. The slots are stored as a weak pointer because the elements
   // are in the shadow tree and should be kept alive by its parent.
--- a/dom/base/nsIContent.h
+++ b/dom/base/nsIContent.h
@@ -371,16 +371,26 @@ class nsIContent : public nsINode {
    * Gets the assigned slot associated with this content based on parent's
    * shadow root mode. Returns null if parent's shadow root is "closed".
    * https://dom.spec.whatwg.org/#dom-slotable-assignedslot
    *
    * @return The assigned slot element or null.
    */
   mozilla::dom::HTMLSlotElement* GetAssignedSlotByMode() const;
 
+  mozilla::dom::HTMLSlotElement* GetManualSlotAssignment() const {
+    const nsExtendedContentSlots* slots = GetExistingExtendedContentSlots();
+    return slots ? slots->mManualSlotAssignment : nullptr;
+  }
+
+  void SetManualSlotAssignment(mozilla::dom::HTMLSlotElement* aSlot) {
+    MOZ_ASSERT(aSlot || GetExistingExtendedContentSlots());
+    ExtendedContentSlots()->mManualSlotAssignment = aSlot;
+  }
+
   /**
    * Same as GetFlattenedTreeParentNode, but returns null if the parent is
    * non-nsIContent.
    */
   inline nsIContent* GetFlattenedTreeParent() const;
 
  protected:
   // Handles getting inserted or removed directly under a <slot> element.
@@ -669,16 +679,18 @@ class nsIContent : public nsINode {
      * @see nsIContent::GetContainingShadow
      */
     RefPtr<mozilla::dom::ShadowRoot> mContainingShadow;
 
     /**
      * @see nsIContent::GetAssignedSlot
      */
     RefPtr<mozilla::dom::HTMLSlotElement> mAssignedSlot;
+
+    mozilla::dom::HTMLSlotElement* mManualSlotAssignment = nullptr;
   };
 
   class nsContentSlots : public nsINode::nsSlots {
    public:
     nsContentSlots() : nsINode::nsSlots(), mExtendedSlots(0) {}
 
     ~nsContentSlots() {
       if (!(mExtendedSlots & sNonOwningExtendedSlotsFlag)) {
--- a/dom/base/nsINode.cpp
+++ b/dom/base/nsINode.cpp
@@ -647,16 +647,24 @@ DocumentOrShadowRoot* nsINode::GetUncomp
 void nsINode::LastRelease() {
   nsINode::nsSlots* slots = GetExistingSlots();
   if (slots) {
     if (!slots->mMutationObservers.IsEmpty()) {
       NS_OBSERVER_ARRAY_NOTIFY_OBSERVERS(slots->mMutationObservers,
                                          NodeWillBeDestroyed, (this));
     }
 
+    if (IsContent()) {
+      nsIContent* content = AsContent();
+      if (HTMLSlotElement* slot = content->GetManualSlotAssignment()) {
+        content->SetManualSlotAssignment(nullptr);
+        slot->RemoveManuallyAssignedNode(*content);
+      }
+    }
+
     delete slots;
     mSlots = nullptr;
   }
 
   // Kill properties first since that may run external code, so we want to
   // be in as complete state as possible at that time.
   if (IsDocument()) {
     // Delete all properties before tearing down the document. Some of the
--- a/dom/html/HTMLSlotElement.cpp
+++ b/dom/html/HTMLSlotElement.cpp
@@ -1,20 +1,21 @@
 /* -*- 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 "mozilla/PresShell.h"
 #include "mozilla/dom/DocGroup.h"
 #include "mozilla/dom/Document.h"
 #include "mozilla/dom/HTMLSlotElement.h"
-#include "mozilla/dom/HTMLSlotElementBinding.h"
 #include "mozilla/dom/HTMLUnknownElement.h"
 #include "mozilla/dom/ShadowRoot.h"
+#include "mozilla/dom/Text.h"
 #include "nsContentUtils.h"
 #include "nsGkAtoms.h"
 
 nsGenericHTMLElement* NS_NewHTMLSlotElement(
     already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
     mozilla::dom::FromParser aFromParser) {
   RefPtr<mozilla::dom::NodeInfo> nodeInfo(std::move(aNodeInfo));
   auto* nim = nodeInfo->NodeInfoManager();
@@ -22,17 +23,22 @@ nsGenericHTMLElement* NS_NewHTMLSlotElem
 }
 
 namespace mozilla::dom {
 
 HTMLSlotElement::HTMLSlotElement(
     already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
     : nsGenericHTMLElement(std::move(aNodeInfo)) {}
 
-HTMLSlotElement::~HTMLSlotElement() = default;
+HTMLSlotElement::~HTMLSlotElement() {
+  for (const auto& node : mManuallyAssignedNodes) {
+    MOZ_ASSERT(node->AsContent()->GetManualSlotAssignment() == this);
+    node->AsContent()->SetManualSlotAssignment(nullptr);
+  }
+}
 
 NS_IMPL_ADDREF_INHERITED(HTMLSlotElement, nsGenericHTMLElement)
 NS_IMPL_RELEASE_INHERITED(HTMLSlotElement, nsGenericHTMLElement)
 
 NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLSlotElement, nsGenericHTMLElement,
                                    mAssignedNodes)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HTMLSlotElement)
@@ -42,16 +48,19 @@ NS_IMPL_ELEMENT_CLONE(HTMLSlotElement)
 
 nsresult HTMLSlotElement::BindToTree(BindContext& aContext, nsINode& aParent) {
   RefPtr<ShadowRoot> oldContainingShadow = GetContainingShadow();
 
   nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent);
   NS_ENSURE_SUCCESS(rv, rv);
 
   ShadowRoot* containingShadow = GetContainingShadow();
+  mInManualShadowRoot =
+      containingShadow &&
+      containingShadow->SlotAssignment() == SlotAssignmentMode::Manual;
   if (containingShadow && !oldContainingShadow) {
     containingShadow->AddSlot(this);
   }
 
   return NS_OK;
 }
 
 void HTMLSlotElement::UnbindFromTree(bool aNullParent) {
@@ -150,16 +159,157 @@ void HTMLSlotElement::AssignedElements(c
     }
   }
 }
 
 const nsTArray<RefPtr<nsINode>>& HTMLSlotElement::AssignedNodes() const {
   return mAssignedNodes;
 }
 
+const nsTArray<nsINode*>& HTMLSlotElement::ManuallyAssignedNodes() const {
+  return mManuallyAssignedNodes;
+}
+
+void HTMLSlotElement::Assign(const Sequence<OwningElementOrText>& aNodes) {
+  MOZ_ASSERT(StaticPrefs::dom_shadowdom_slot_assign_enabled());
+  nsAutoScriptBlocker scriptBlocker;
+
+  // no-op if the input nodes and the assigned nodes are identical
+  // This also works if the two 'assign' calls are like
+  //   > slot.assign(node1, node2);
+  //   > slot.assign(node1, node2, node1, node2);
+  if (!mAssignedNodes.IsEmpty() && aNodes.Length() >= mAssignedNodes.Length()) {
+    nsTHashMap<nsPtrHashKey<nsIContent>, size_t> nodeIndexMap;
+    for (size_t i = 0; i < aNodes.Length(); ++i) {
+      nsIContent* content;
+      if (aNodes[i].IsElement()) {
+        content = aNodes[i].GetAsElement();
+      } else {
+        content = aNodes[i].GetAsText();
+      }
+      MOZ_ASSERT(content);
+      // We only care about the first index this content appears
+      // in the array
+      nodeIndexMap.LookupOrInsert(content, i);
+    }
+
+    if (nodeIndexMap.Count() == mAssignedNodes.Length()) {
+      bool isIdentical = true;
+      for (size_t i = 0; i < mAssignedNodes.Length(); ++i) {
+        size_t indexInInputNodes;
+        if (!nodeIndexMap.Get(mAssignedNodes[i]->AsContent(),
+                              &indexInInputNodes) ||
+            indexInInputNodes != i) {
+          isIdentical = false;
+          break;
+        }
+      }
+      if (isIdentical) {
+        return;
+      }
+    }
+  }
+
+  // 1. For each node of this's manually assigned nodes, set node's manual slot
+  // assignment to null.
+  for (nsINode* node : mManuallyAssignedNodes) {
+    MOZ_ASSERT(node->AsContent()->GetManualSlotAssignment() == this);
+    node->AsContent()->SetManualSlotAssignment(nullptr);
+  }
+
+  // 2. Let nodesSet be a new ordered set.
+  mManuallyAssignedNodes.Clear();
+
+  nsIContent* host = nullptr;
+  ShadowRoot* root = GetContainingShadow();
+
+  // An optimization to keep track which slots need to enqueue
+  // slotchange event, such that they can be enqueued later in
+  // tree order.
+  nsTHashSet<RefPtr<HTMLSlotElement>> changedSlots;
+
+  // Clear out existing assigned nodes
+  if (mInManualShadowRoot) {
+    nsTArray<RefPtr<nsINode>> assignedNodes(std::move(mAssignedNodes));
+    for (RefPtr<nsINode>& node : assignedNodes) {
+      nsIContent* content = node->AsContent();
+      HTMLSlotElement* oldSlot = content->GetAssignedSlot();
+      MOZ_RELEASE_ASSERT(oldSlot == this);
+      if (changedSlots.EnsureInserted(oldSlot)) {
+        if (root) {
+          MOZ_ASSERT(oldSlot->GetContainingShadow() == root);
+          root->InvalidateStyleAndLayoutOnSubtree(oldSlot);
+        }
+      }
+      oldSlot->RemoveAssignedNode(*content);
+    }
+
+    MOZ_ASSERT(mAssignedNodes.IsEmpty());
+    host = GetContainingShadowHost();
+  }
+
+  for (const OwningElementOrText& elementOrText : aNodes) {
+    nsIContent* content;
+    if (elementOrText.IsElement()) {
+      content = elementOrText.GetAsElement();
+    } else {
+      content = elementOrText.GetAsText();
+    }
+
+    MOZ_ASSERT(content);
+    // XXXsmaug Should we have a helper for
+    //         https://infra.spec.whatwg.org/#ordered-set?
+    if (content->GetManualSlotAssignment() != this) {
+      if (HTMLSlotElement* oldSlot = content->GetAssignedSlot()) {
+        if (changedSlots.EnsureInserted(oldSlot)) {
+          if (root) {
+            MOZ_ASSERT(oldSlot->GetContainingShadow() == root);
+            root->InvalidateStyleAndLayoutOnSubtree(oldSlot);
+          }
+        }
+      }
+
+      if (changedSlots.EnsureInserted(this)) {
+        if (root) {
+          root->InvalidateStyleAndLayoutOnSubtree(this);
+        }
+      }
+      // 3.1 (HTML Spec) If content's manual slot assignment refers to a slot,
+      // then remove node from that slot's manually assigned nodes. 3.2 (HTML
+      // Spec) Set content's manual slot assignment to this.
+      if (HTMLSlotElement* oldSlot = content->GetManualSlotAssignment()) {
+        oldSlot->RemoveManuallyAssignedNode(*content);
+      }
+      content->SetManualSlotAssignment(this);
+      mManuallyAssignedNodes.AppendElement(content);
+
+      if (root && host && content->GetParent() == host) {
+        // Equivalent to 4.2.2.4.3 (DOM Spec) `Set slot's assigned nodes to
+        // slottables`
+        root->MaybeReassignContent(*content);
+      }
+    }
+  }
+
+  // The `assign slottables` step is completed already at this point,
+  // however we haven't fired the `slotchange` event yet because this
+  // needs to be done in tree order.
+  if (root) {
+    for (nsIContent* child = root->GetFirstChild(); child;
+         child = child->GetNextNode()) {
+      if (HTMLSlotElement* slot = HTMLSlotElement::FromNode(child)) {
+        if (changedSlots.EnsureRemoved(slot)) {
+          slot->EnqueueSlotChangeEvent();
+        }
+      }
+    }
+    MOZ_ASSERT(changedSlots.IsEmpty());
+  }
+}
+
 void HTMLSlotElement::InsertAssignedNode(uint32_t aIndex, nsIContent& aNode) {
   MOZ_ASSERT(!aNode.GetAssignedSlot(), "Losing track of a slot");
   mAssignedNodes.InsertElementAt(aIndex, &aNode);
   aNode.SetAssignedSlot(this);
   SlotAssignedNodeChanged(this, aNode);
 }
 
 void HTMLSlotElement::AppendAssignedNode(nsIContent& aNode) {
@@ -211,14 +361,19 @@ void HTMLSlotElement::EnqueueSlotChangeE
 }
 
 void HTMLSlotElement::FireSlotChangeEvent() {
   nsContentUtils::DispatchTrustedEvent(
       OwnerDoc(), static_cast<nsIContent*>(this), u"slotchange"_ns,
       CanBubble::eYes, Cancelable::eNo);
 }
 
+void HTMLSlotElement::RemoveManuallyAssignedNode(nsIContent& aNode) {
+  mManuallyAssignedNodes.RemoveElement(&aNode);
+  RemoveAssignedNode(aNode);
+}
+
 JSObject* HTMLSlotElement::WrapNode(JSContext* aCx,
                                     JS::Handle<JSObject*> aGivenProto) {
   return HTMLSlotElement_Binding::Wrap(aCx, this, aGivenProto);
 }
 
 }  // namespace mozilla::dom
--- a/dom/html/HTMLSlotElement.h
+++ b/dom/html/HTMLSlotElement.h
@@ -4,22 +4,21 @@
  * 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_HTMLSlotElement_h
 #define mozilla_dom_HTMLSlotElement_h
 
 #include "nsGenericHTMLElement.h"
 #include "nsTArray.h"
+#include "mozilla/dom/HTMLSlotElementBinding.h"
 
 namespace mozilla {
 namespace dom {
 
-struct AssignedNodesOptions;
-
 class HTMLSlotElement final : public nsGenericHTMLElement {
  public:
   explicit HTMLSlotElement(
       already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
   NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLSlotElement, slot)
 
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLSlotElement,
@@ -47,40 +46,48 @@ class HTMLSlotElement final : public nsG
   void GetName(nsAString& aName) { GetHTMLAttr(nsGkAtoms::name, aName); }
 
   void AssignedNodes(const AssignedNodesOptions& aOptions,
                      nsTArray<RefPtr<nsINode>>& aNodes);
 
   void AssignedElements(const AssignedNodesOptions& aOptions,
                         nsTArray<RefPtr<Element>>& aNodes);
 
+  void Assign(const Sequence<OwningElementOrText>& aNodes);
+
   // Helper methods
   const nsTArray<RefPtr<nsINode>>& AssignedNodes() const;
+  const nsTArray<nsINode*>& ManuallyAssignedNodes() const;
   void InsertAssignedNode(uint32_t aIndex, nsIContent&);
   void AppendAssignedNode(nsIContent&);
   void RemoveAssignedNode(nsIContent&);
   void ClearAssignedNodes();
 
   void EnqueueSlotChangeEvent();
   void RemovedFromSignalSlotList() {
     MOZ_ASSERT(mInSignalSlotList);
     mInSignalSlotList = false;
   }
 
   void FireSlotChangeEvent();
 
+  void RemoveManuallyAssignedNode(nsIContent&);
+
  protected:
   virtual ~HTMLSlotElement();
   JSObject* WrapNode(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) final;
 
   nsTArray<RefPtr<nsINode>> mAssignedNodes;
+  nsTArray<nsINode*> mManuallyAssignedNodes;
 
   // Whether we're in the signal slot list of our unit of related similar-origin
   // browsing contexts.
   //
   // https://dom.spec.whatwg.org/#signal-slot-list
   bool mInSignalSlotList = false;
+
+  bool mInManualShadowRoot = false;
 };
 
 }  // namespace dom
 }  // namespace mozilla
 
 #endif  // mozilla_dom_HTMLSlotElement_h
--- a/dom/webidl/Element.webidl
+++ b/dom/webidl/Element.webidl
@@ -255,16 +255,18 @@ partial interface Element {
   Element?  querySelector(UTF8String selectors);
   [Throws, Pure]
   NodeList  querySelectorAll(UTF8String selectors);
 };
 
 // https://dom.spec.whatwg.org/#dictdef-shadowrootinit
 dictionary ShadowRootInit {
   required ShadowRootMode mode;
+  [Pref="dom.shadowdom.slot.assign.enabled"]
+  SlotAssignmentMode slotAssignment = "named";
 };
 
 // https://dom.spec.whatwg.org/#element
 partial interface Element {
   // Shadow DOM v1
   [Throws, UseCounter]
   ShadowRoot attachShadow(ShadowRootInit shadowRootInitDict);
   [BinaryName="shadowRootByMode"]
--- a/dom/webidl/HTMLSlotElement.webidl
+++ b/dom/webidl/HTMLSlotElement.webidl
@@ -13,13 +13,15 @@
 
 [Exposed=Window]
 interface HTMLSlotElement : HTMLElement {
   [HTMLConstructor] constructor();
 
   [CEReactions, SetterThrows] attribute DOMString name;
   sequence<Node> assignedNodes(optional AssignedNodesOptions options = {});
   sequence<Element> assignedElements(optional AssignedNodesOptions options = {});
+  [Pref="dom.shadowdom.slot.assign.enabled"]
+  void assign((Element or Text)... nodes);
 };
 
 dictionary AssignedNodesOptions {
   boolean flatten = false;
 };
--- a/dom/webidl/ShadowRoot.webidl
+++ b/dom/webidl/ShadowRoot.webidl
@@ -11,22 +11,26 @@
  */
 
 // https://dom.spec.whatwg.org/#enumdef-shadowrootmode
 enum ShadowRootMode {
   "open",
   "closed"
 };
 
+enum SlotAssignmentMode { "manual", "named" };
+
 // https://dom.spec.whatwg.org/#shadowroot
 [Exposed=Window]
 interface ShadowRoot : DocumentFragment
 {
   // Shadow DOM v1
   readonly attribute ShadowRootMode mode;
+  [Pref="dom.shadowdom.slot.assign.enabled"]
+  readonly attribute SlotAssignmentMode slotAssignment;
   readonly attribute Element host;
 
   Element? getElementById(DOMString elementId);
 
   // https://w3c.github.io/DOM-Parsing/#the-innerhtml-mixin
   [CEReactions, SetterThrows]
   attribute [LegacyNullToEmptyString] DOMString innerHTML;
 
--- a/modules/libpref/init/StaticPrefList.yaml
+++ b/modules/libpref/init/StaticPrefList.yaml
@@ -3662,16 +3662,22 @@
   value: true
   mirror: always
 
 - name: dom.webkitBlink.dirPicker.enabled
   type: RelaxedAtomicBool
   value: @IS_NOT_ANDROID@
   mirror: always
 
+# Is the 'assign' API for slot element enabled?
+- name: dom.shadowdom.slot.assign.enabled
+  type: bool
+  value: true
+  mirror: always
+
 # NOTE: This preference is used in unit tests. If it is removed or its default
 # value changes, please update test_sharedMap_static_prefs.js accordingly.
 - name: dom.webcomponents.shadowdom.report_usage
   type: bool
   value: false
   mirror: always
 
 # Is support for form-associated custom element enabled?