Bug 1460962 - Support customized built-in element in XUL draft
authorTimothy Guan-tin Chien <timdream@gmail.com>
Fri, 11 May 2018 12:44:46 -0700
changeset 797293 16058bc5d33bc171a270298a1f15a921a33ab127
parent 793238 76aad17f5b50a1b969eec8cbb5aa9875555640bc
child 797425 fe09f7ec7909e936aec60eec8a0a7341a993d491
push id110447
push usertimdream@gmail.com
push dateFri, 18 May 2018 20:58:00 +0000
bugs1460962, 1446247
milestone62.0a1
Bug 1460962 - Support customized built-in element in XUL This patch enables us to specify a custom element type with |is| attribute or property when creating a XUL element. Because non-dashed names are valid custom element names in XUL (bug 1446247), other checks has to modified accordingly. The checks I am settling with are 1) Forbids the custom built-in element names to be a non-dashed name. 2) Forbids the custom built-in element to extend a dashed built-in element name. This also ensures the custom built-in element types don't take on the same name as the element name it extends. MozReview-Commit-ID: GCQ9RnfvvrC
dom/base/CustomElementRegistry.cpp
dom/base/Element.cpp
dom/base/nsContentCreatorFunctions.h
dom/base/nsContentUtils.cpp
dom/base/nsContentUtils.h
dom/base/nsNameSpaceManager.cpp
dom/bindings/BindingUtils.cpp
dom/tests/mochitest/webcomponents/test_xul_custom_element.xul
dom/xul/nsXULElement.cpp
dom/xul/nsXULElement.h
--- a/dom/base/CustomElementRegistry.cpp
+++ b/dom/base/CustomElementRegistry.cpp
@@ -730,32 +730,52 @@ CustomElementRegistry::Define(const nsAS
    * 7. If extends is not null, then:
    *    1. If extends is a valid custom element name, then throw a
    *       "NotSupportedError" DOMException.
    *    2. If the element interface for extends and the HTML namespace is
    *       HTMLUnknownElement (e.g., if extends does not indicate an element
    *       definition in this specification), then throw a "NotSupportedError"
    *       DOMException.
    *    3. Set localName to extends.
+   *
+   * Special note for XUL elements:
+   *
+   * For step 7.1, we'll subject XUL to the same rules as HTML, so that a
+   * custom built-in element will not be extending from a dashed name.
+   * Step 7.2 is disregarded. But, we do check if the name is a dashed name
+   * (i.e. step 2) given that there is no reason for a custom built-in element
+   * type to take on a non-dashed name.
+   * This also ensures the name of the built-in custom element type can never
+   * be the same as the built-in element name, so we don't break the assumption
+   * elsewhere.
    */
   nsAutoString localName(aName);
   if (aOptions.mExtends.WasPassed()) {
     RefPtr<nsAtom> extendsAtom(NS_Atomize(aOptions.mExtends.Value()));
-    if (nsContentUtils::IsCustomElementName(extendsAtom, nameSpaceID)) {
+    if (nsContentUtils::IsCustomElementName(extendsAtom, kNameSpaceID_XHTML)) {
       aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
       return;
     }
 
-    // bgsound and multicol are unknown html element.
-    int32_t tag = nsHTMLTags::CaseSensitiveAtomTagToId(extendsAtom);
-    if (tag == eHTMLTag_userdefined ||
-        tag == eHTMLTag_bgsound ||
-        tag == eHTMLTag_multicol) {
-      aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
-      return;
+    if (nameSpaceID == kNameSpaceID_XHTML) {
+      // bgsound and multicol are unknown html element.
+      int32_t tag = nsHTMLTags::CaseSensitiveAtomTagToId(extendsAtom);
+      if (tag == eHTMLTag_userdefined ||
+          tag == eHTMLTag_bgsound ||
+          tag == eHTMLTag_multicol) {
+        aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+        return;
+      }
+    } else { // kNameSpaceID_XUL
+      // As stated above, ensure the name of the customized built-in element
+      // (the one that goes to the |is| attribute) is a dashed name.
+      if (!nsContentUtils::IsNameWithDash(nameAtom)) {
+        aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+        return;
+      }
     }
 
     localName.Assign(aOptions.mExtends.Value());
   }
 
   /**
    * 8. If this CustomElementRegistry's element definition is running flag is set,
    *    then throw a "NotSupportedError" DOMException and abort these steps.
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
@@ -4269,20 +4269,37 @@ Element::ClearServoData(nsIDocument* aDo
 void
 Element::SetCustomElementData(CustomElementData* aData)
 {
   nsExtendedDOMSlots *slots = ExtendedDOMSlots();
   MOZ_ASSERT(!slots->mCustomElementData, "Custom element data may not be changed once set.");
   #if DEBUG
     nsAtom* name = NodeInfo()->NameAtom();
     nsAtom* type = aData->GetCustomElementType();
-    if (nsContentUtils::IsCustomElementName(name, NodeInfo()->NamespaceID())) {
-      MOZ_ASSERT(type == name);
-    } else {
-      MOZ_ASSERT(type != name);
+    if (NodeInfo()->NamespaceID() == kNameSpaceID_XHTML) {
+      if (nsContentUtils::IsCustomElementName(name, kNameSpaceID_XHTML)) {
+        MOZ_ASSERT(type == name);
+      } else {
+        MOZ_ASSERT(type != name);
+      }
+    } else { // kNameSpaceID_XUL
+      // Check to see if the tag name is a dashed name.
+      if (nsContentUtils::IsNameWithDash(name)) {
+        // Assert that a tag name with dashes is always an autonomous custom
+        // element.
+        MOZ_ASSERT(type == name);
+      } else {
+        // Could still be an autonomous custom element with a non-dashed tag name.
+        // Need the check below for sure.
+        if (type != name) {
+          // Assert that the name of the built-in custom element type is always
+          // a dashed name.
+          MOZ_ASSERT(nsContentUtils::IsNameWithDash(type));
+        }
+      }
     }
   #endif
   slots->mCustomElementData = aData;
 }
 
 CustomElementDefinition*
 Element::GetCustomElementDefinition() const
 {
--- a/dom/base/nsContentCreatorFunctions.h
+++ b/dom/base/nsContentCreatorFunctions.h
@@ -57,17 +57,19 @@ CreateHTMLElement(uint32_t aNodeType,
 nsresult
 NS_NewMathMLElement(mozilla::dom::Element** aResult,
                     already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
 
 #ifdef MOZ_XUL
 nsresult
 NS_NewXULElement(mozilla::dom::Element** aResult,
                  already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
-                 mozilla::dom::FromParser aFromParser);
+                 mozilla::dom::FromParser aFromParser,
+                 nsAtom* aIsAtom = nullptr,
+                 mozilla::dom::CustomElementDefinition* aDefinition = nullptr);
 
 void
 NS_TrustedNewXULElement(mozilla::dom::Element** aResult,
                         already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
 #endif
 
 nsresult
 NS_NewSVGElement(mozilla::dom::Element** aResult,
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -3179,23 +3179,18 @@ nsContentUtils::NewURIWithDocumentCharse
                      aDocument->GetDocumentCharacterSet(),
                      aBaseURI, sIOService);
   }
   return NS_NewURI(aResult, aSpec, nullptr, aBaseURI, sIOService);
 }
 
 // static
 bool
-nsContentUtils::IsCustomElementName(nsAtom* aName, uint32_t aNameSpaceID)
-{
-  // Allow non-dashed names in XUL for XBL to Custom Element migrations.
-  if (aNameSpaceID == kNameSpaceID_XUL) {
-    return true;
-  }
-
+nsContentUtils::IsNameWithDash(nsAtom* aName)
+{
   // A valid custom element name is a sequence of characters name which
   // must match the PotentialCustomElementName production:
   // PotentialCustomElementName ::= [a-z] (PCENChar)* '-' (PCENChar)*
   const char16_t* name = aName->GetUTF16String();
   uint32_t len = aName->GetLength();
   bool hasDash = false;
 
   if (!len || name[0] < 'a' || name[0] > 'z') {
@@ -3235,16 +3230,29 @@ nsContentUtils::IsCustomElementName(nsAt
          (name[i] < 0xFDF0 || name[i] > 0xFFFD)) {
         return false;
       }
 
       i++;
     }
   }
 
+  return hasDash;
+}
+
+// static
+bool
+nsContentUtils::IsCustomElementName(nsAtom* aName, uint32_t aNameSpaceID)
+{
+  // Allow non-dashed names in XUL for XBL to Custom Element migrations.
+  if (aNameSpaceID == kNameSpaceID_XUL) {
+    return true;
+  }
+
+  bool hasDash = IsNameWithDash(aName);
   if (!hasDash) {
     return false;
   }
 
   // The custom element name must not be one of the following values:
   //  annotation-xml
   //  color-profile
   //  font-face
@@ -9890,18 +9898,32 @@ nsContentUtils::NewXULOrHTMLElement(Elem
 
   nsAtom *name = nodeInfo->NameAtom();
   int32_t tag = eHTMLTag_unknown;
   bool isCustomElementName = false;
   if (nodeInfo->NamespaceEquals(kNameSpaceID_XHTML)) {
     tag = nsHTMLTags::CaseSensitiveAtomTagToId(name);
     isCustomElementName = (tag == eHTMLTag_userdefined &&
                            nsContentUtils::IsCustomElementName(name, kNameSpaceID_XHTML));
-  } else {
-    isCustomElementName = nsContentUtils::IsCustomElementName(name, kNameSpaceID_XUL);
+  } else { // kNameSpaceID_XUL
+    if (aIsAtom) {
+      // Make sure the customized built-in element to be constructed confirms
+      // to our naming requirement, i.e. [is] must be a dashed name and
+      // the tag name must not.
+      // if so, set isCustomElementName to false to kick off all the logics
+      // that pick up aIsAtom.
+      if (nsContentUtils::IsNameWithDash(aIsAtom) &&
+          !nsContentUtils::IsNameWithDash(name)) {
+        isCustomElementName = false;
+      } else {
+        isCustomElementName = nsContentUtils::IsCustomElementName(name, kNameSpaceID_XUL);
+      }
+    } else {
+      isCustomElementName = nsContentUtils::IsCustomElementName(name, kNameSpaceID_XUL);
+    }
   }
 
   RefPtr<nsAtom> tagAtom = nodeInfo->NameAtom();
   RefPtr<nsAtom> typeAtom;
   bool isCustomElement = isCustomElementName || aIsAtom;
   if (isCustomElement) {
     typeAtom = isCustomElementName ? tagAtom.get() : aIsAtom;
   }
@@ -9960,19 +9982,21 @@ nsContentUtils::NewXULOrHTMLElement(Elem
     ErrorResult rv;
 
     // Step 5.
     if (definition->IsCustomBuiltIn()) {
       // SetupCustomElement() should be called with an element that don't have
       // CustomElementData setup, if not we will hit the assertion in
       // SetCustomElementData().
       // Built-in element
-      MOZ_ASSERT(nodeInfo->NamespaceEquals(kNameSpaceID_XHTML),
-                 "Custom built-in XUL elements are not supported yet.");
-      *aResult = CreateHTMLElement(tag, nodeInfo.forget(), aFromParser).take();
+      if (nodeInfo->NamespaceEquals(kNameSpaceID_XHTML)) {
+        *aResult = CreateHTMLElement(tag, nodeInfo.forget(), aFromParser).take();
+      } else {
+        NS_IF_ADDREF(*aResult = nsXULElement::Construct(nodeInfo.forget()));
+      }
       (*aResult)->SetCustomElementData(new CustomElementData(typeAtom));
       if (synchronousCustomElements) {
         CustomElementRegistry::Upgrade(*aResult, definition, rv);
         if (rv.MaybeSetPendingException(cx)) {
           aes.ReportException();
         }
       } else {
         nsContentUtils::EnqueueUpgradeReaction(*aResult, definition);
--- a/dom/base/nsContentUtils.h
+++ b/dom/base/nsContentUtils.h
@@ -700,16 +700,21 @@ public:
    * aDocument.
    */
   static nsresult NewURIWithDocumentCharset(nsIURI** aResult,
                                             const nsAString& aSpec,
                                             nsIDocument* aDocument,
                                             nsIURI* aBaseURI);
 
   /**
+   * Returns true if |aName| is a name with dashes.
+   */
+  static bool IsNameWithDash(nsAtom* aName);
+
+  /**
    * Returns true if |aName| is a valid name to be registered via
    * customElements.define.
    */
   static bool IsCustomElementName(nsAtom* aName, uint32_t aNameSpaceID);
 
   static nsresult CheckQName(const nsAString& aQualifiedName,
                              bool aNamespaceAware = true,
                              const char16_t** aColon = nullptr);
--- a/dom/base/nsNameSpaceManager.cpp
+++ b/dom/base/nsNameSpaceManager.cpp
@@ -181,23 +181,23 @@ nsNameSpaceManager::GetNameSpaceID(nsAto
 nsresult
 NS_NewElement(Element** aResult,
               already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
               FromParser aFromParser,
               const nsAString* aIs)
 {
   RefPtr<mozilla::dom::NodeInfo> ni = aNodeInfo;
   int32_t ns = ni->NamespaceID();
+  RefPtr<nsAtom> isAtom = aIs ? NS_Atomize(*aIs) : nullptr;
   if (ns == kNameSpaceID_XHTML) {
-    RefPtr<nsAtom> isAtom = aIs ? NS_Atomize(*aIs) : nullptr;
     return NS_NewHTMLElement(aResult, ni.forget(), aFromParser, isAtom);
   }
 #ifdef MOZ_XUL
   if (ns == kNameSpaceID_XUL) {
-    return NS_NewXULElement(aResult, ni.forget(), aFromParser);
+    return NS_NewXULElement(aResult, ni.forget(), aFromParser, isAtom);
   }
 #endif
   if (ns == kNameSpaceID_MathML) {
     // If the mathml.disabled pref. is true, convert all MathML nodes into
     // disabled MathML nodes by swapping the namespace.
     if (ni->NodeInfoManager()->MathMLEnabled()) {
       return NS_NewMathMLElement(aResult, ni.forget());
     }
--- a/dom/bindings/BindingUtils.cpp
+++ b/dom/bindings/BindingUtils.cpp
@@ -43,16 +43,17 @@
 #include "mozilla/dom/DOMException.h"
 #include "mozilla/dom/ElementBinding.h"
 #include "mozilla/dom/HTMLObjectElement.h"
 #include "mozilla/dom/HTMLObjectElementBinding.h"
 #include "mozilla/dom/HTMLEmbedElement.h"
 #include "mozilla/dom/HTMLElementBinding.h"
 #include "mozilla/dom/HTMLEmbedElementBinding.h"
 #include "mozilla/dom/XULElementBinding.h"
+#include "mozilla/dom/XULPopupElementBinding.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/ResolveSystemBinding.h"
 #include "mozilla/dom/WebIDLGlobalNameHash.h"
 #include "mozilla/dom/WorkerPrivate.h"
 #include "mozilla/dom/WorkerScope.h"
 #include "mozilla/dom/XrayExpandoClass.h"
 #include "mozilla/jsipc/CrossProcessObjectWrappers.h"
 #include "ipc/ErrorIPCUtils.h"
@@ -3783,35 +3784,42 @@ HTMLConstructor(JSContext* aCx, unsigned
     if (!constructor) {
       return false;
     }
 
     if (constructor != js::CheckedUnwrap(callee)) {
       return ThrowErrorMessage(aCx, MSG_ILLEGAL_CONSTRUCTOR);
     }
   } else {
-    // Step 5.
-    // If the definition is for a customized built-in element, the localName
-    // should be one of the ones defined in the specification for this interface.
-
-    // Customized built-in elements are not supported for XUL yet.
-    if (ns == kNameSpaceID_XUL) {
-      return Throw(aCx, NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+    constructorGetterCallback cb;
+    if (ns == kNameSpaceID_XHTML) {
+      // Step 5.
+      // If the definition is for a customized built-in element, the localName
+      // should be one of the ones defined in the specification for this interface.
+      tag = nsHTMLTags::CaseSensitiveAtomTagToId(definition->mLocalName);
+      if (tag == eHTMLTag_userdefined) {
+        return ThrowErrorMessage(aCx, MSG_ILLEGAL_CONSTRUCTOR);
+      }
+
+      MOZ_ASSERT(tag <= NS_HTML_TAG_MAX, "tag is out of bounds");
+
+      // If the definition is for a customized built-in element, the active
+      // function should be the localname's element interface.
+      cb = sConstructorGetterCallback[tag];
+    } else { // kNameSpaceID_XUL
+      if (definition->mLocalName == nsGkAtoms::menupopup ||
+          definition->mLocalName == nsGkAtoms::popup ||
+          definition->mLocalName == nsGkAtoms::panel ||
+          definition->mLocalName == nsGkAtoms::tooltip) {
+        cb = XULPopupElementBinding::GetConstructorObject;
+      } else {
+        cb = XULElementBinding::GetConstructorObject;
+      }
     }
 
-    tag = nsHTMLTags::CaseSensitiveAtomTagToId(definition->mLocalName);
-    if (tag == eHTMLTag_userdefined) {
-      return ThrowErrorMessage(aCx, MSG_ILLEGAL_CONSTRUCTOR);
-    }
-
-    MOZ_ASSERT(tag <= NS_HTML_TAG_MAX, "tag is out of bounds");
-
-    // If the definition is for a customized built-in element, the active
-    // function should be the localname's element interface.
-    constructorGetterCallback cb = sConstructorGetterCallback[tag];
     if (!cb) {
       return ThrowErrorMessage(aCx, MSG_ILLEGAL_CONSTRUCTOR);
     }
 
     // We want to get the constructor from our global's compartment, not the
     // caller compartment.
     JSAutoCompartment ac(aCx, global.Get());
     JS::Rooted<JSObject*> constructor(aCx, cb(aCx));
--- a/dom/tests/mochitest/webcomponents/test_xul_custom_element.xul
+++ b/dom/tests/mochitest/webcomponents/test_xul_custom_element.xul
@@ -24,19 +24,54 @@
       }
     }
 
     customElements.define("test-custom-element", TestCustomElement);
 
     class TestWithoutDash extends XULElement { }
     customElements.define("testwithoutdash", TestWithoutDash);
 
-    function runTest() {
-      const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+    class TestWithoutDashExtended extends TestWithoutDash {
+      constructor() {
+        super();
+      }
+
+      connectedCallback() {
+        this.textContent = "quux";
+      }
+    }
+    customElements.define("testwithoutdash-extended", TestWithoutDashExtended, { extends: "testwithoutdash" });
+
+    class TestCustomBuiltInElement extends XULElement {
+      constructor() {
+        super();
+      }
 
+      connectedCallback() {
+        this.textContent = "baz";
+      }
+    }
+    customElements.define("test-built-in-element",
+      TestCustomBuiltInElement, { extends: "axulelement" });
+
+    class TestPopupExtendElement extends XULPopupElement {
+      constructor() {
+        super();
+      }
+
+      connectedCallback() {
+        this.textContent = "quuz";
+      }
+    }
+    customElements.define("test-popup-extend",
+      TestPopupExtendElement, { extends: "popup" });
+
+    const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+    function basicCustomElementCreate() {
       let element = document.createElementNS(XUL_NS, "test-custom-element");
       document.querySelector("#content").appendChild(element);
       is(element.textContent, "foo", "Should have set the textContent");
       ok(element instanceof TestCustomElement, "Should be an instance of TestCustomElement");
 
       let element2 = element.cloneNode(false);
       is(element2.textContent, "", "Shouldn't have cloned the textContent");
       document.querySelector("#content").appendChild(element2);
@@ -45,31 +80,228 @@
 
       let element3 = new TestCustomElement();
       is(element3.localName, "test-custom-element", "Should see the right tag");
       is(element3.textContent, "", "Shouldn't have been inserted yet");
       is(element3.namespaceURI, XUL_NS, "Should have set the right namespace");
       document.querySelector("#content").appendChild(element3);
       is(element3.textContent, "foo", "Should have set the textContent");
       ok(element3 instanceof TestCustomElement, "Should be an instance of TestCustomElement");
+    }
 
+    function parserBasicElementUpgrade() {
       let element4 = document.getElementById("element4");
       is(element4.textContent, "foo",
          "Parser should have instantiated the custom element.");
       ok(element4 instanceof TestCustomElement, "Should be an instance of TestCustomElement");
+    }
 
+    function tagNameWithoutDash() {
       let element5 = document.getElementById("element5");
       ok(element5 instanceof TestWithoutDash, "Should be an instance of TestWithoutDash");
+    }
+
+    function upgradeAfterDefine() {
+      class TestCustomElement1 extends XULElement {
+        constructor() {
+          super();
+        }
+
+        connectedCallback() {
+          this.textContent = "bar";
+        }
+      }
+
+      let element = document.createElementNS(XUL_NS, "test-custom-element-1");
+      ok(!(element instanceof TestCustomElement1), "Should not be an instance of TestCustomElement1");
+      customElements.define("test-custom-element-1", TestCustomElement1);
+      ok(!(element instanceof TestCustomElement1), "Should not be an instance of TestCustomElement1");
+      document.querySelector("#content").appendChild(element);
+      ok(element instanceof TestCustomElement1, "Should be upgraded to an instance of TestCustomElement1");
+      is(element.textContent, "bar", "Should have set the textContent");
+    }
+
+    function basicElementCreateBuiltIn() {
+      let element = document.createElementNS(XUL_NS, "axulelement", { is: "test-built-in-element" });
+      ok(element instanceof TestCustomBuiltInElement, "Should be an instance of TestCustomBuiltInElement");
+      is(element.getAttribute("is"), "test-built-in-element", "The |is| attribute of the created element should be the extended type.");
+      document.querySelector("#content").appendChild(element);
+      is(element.textContent, "baz", "Should have set the textContent");
+
+      let element2 = element.cloneNode(false);
+      is(element2.localName, "axulelement", "Should see the right tag");
+      is(element2.getAttribute("is"), "test-built-in-element", "The |is| attribute of the created element should be the extended type.");
+      is(element2.textContent, "", "Shouldn't have cloned the textContent");
+      document.querySelector("#content").appendChild(element2);
+      is(element2.textContent, "baz", "Should have set the textContent");
+      ok(element2 instanceof TestCustomBuiltInElement, "Should be an instance of TestCustomBuiltInElement");
+
+      let element3 = new TestCustomBuiltInElement();
+      is(element3.localName, "axulelement", "Should see the right tag");
+      is(element3.textContent, "", "Shouldn't have been inserted yet");
+      is(element3.namespaceURI, XUL_NS, "Should have set the right namespace");
+      document.querySelector("#content").appendChild(element3);
+      is(element3.textContent, "baz", "Should have set the textContent");
+      ok(element3 instanceof TestCustomBuiltInElement, "Should be an instance of TestCustomBuiltInElement");
+    }
+
+    function parserBasicElementUpgradeBuiltIn() {
+      let element = document.getElementById("element6");
+      is(element.textContent, "baz",
+         "Parser should have instantiated the custom element.");
+      ok(element instanceof TestCustomBuiltInElement, "Should be an instance of TestCustomBuiltInElement");
+    }
+
+    function subclassElementCreateBuiltIn() {
+      let element = document.createElementNS(XUL_NS, "popup", { is: "test-popup-extend" });
+      ok(element instanceof TestPopupExtendElement, "Should be an instance of TestPopupExtendElement");
+      is(element.getAttribute("is"), "test-popup-extend", "The |is| attribute of the created element should be the extended type.");
+      document.querySelector("#content").appendChild(element);
+      is(element.textContent, "quuz", "Should have set the textContent");
+
+      let element2 = element.cloneNode(false);
+      is(element2.localName, "popup", "Should see the right tag");
+      is(element2.getAttribute("is"), "test-popup-extend", "The |is| attribute of the created element should be the extended type.");
+      is(element2.textContent, "", "Shouldn't have cloned the textContent");
+      document.querySelector("#content").appendChild(element2);
+      is(element2.textContent, "quuz", "Should have set the textContent");
+      ok(element2 instanceof TestPopupExtendElement, "Should be an instance of TestPopupExtendElement");
+
+      let element3 = new TestPopupExtendElement();
+      is(element3.localName, "popup", "Should see the right tag");
+      is(element3.textContent, "", "Shouldn't have been inserted yet");
+      is(element3.namespaceURI, XUL_NS, "Should have set the right namespace");
+      document.querySelector("#content").appendChild(element3);
+      is(element3.textContent, "quuz", "Should have set the textContent");
+      ok(element3 instanceof TestPopupExtendElement, "Should be an instance of TestPopupExtendElement");
+    }
+
+    function parserSubclassElementUpgradeBuiltIn() {
+      let element = document.getElementById("element7");
+      is(element.textContent, "quuz",
+         "Parser should have instantiated the custom element.");
+      ok(element instanceof TestPopupExtendElement, "Should be an instance of TestPopupExtendElement");
+    }
+
+    function upgradeAfterDefineBuiltIn() {
+      class TestCustomBuiltInElement1 extends XULElement {
+        constructor() {
+          super();
+        }
+
+        connectedCallback() {
+          this.textContent = "qux";
+        }
+      }
+      let element = document.createElementNS(XUL_NS, "axulelement", { is: "test-built-in-element-1" });
+      ok(!(element instanceof TestCustomBuiltInElement1), "Should not be an instance of TestCustomBuiltInElement1");
+      customElements.define("test-built-in-element-1",
+        TestCustomBuiltInElement1, { extends: "axulelement" });
+      ok(!(element instanceof TestCustomBuiltInElement1), "Should not be an instance of TestCustomBuiltInElement1");
+      document.querySelector("#content").appendChild(element);
+      ok(element instanceof TestCustomBuiltInElement1, "Should be upgraded to an instance of TestCustomBuiltInElement1");
+      is(element.textContent, "qux", "Should have set the textContent");
+    }
+
+    function throwForInvalidBuiltInName() {
+      try {
+        // <axulelement is="testwithoutdashbuiltin" /> is not allowed;
+        // built-in type names need dashes.
+        customElements.define(
+          "testwithoutdashbuiltin", class extends XULElement {}, { extends: "axulelement" });
+        ok(false, "Built-in type name without dash should be rejected.");
+      } catch (e) {
+        ok(true, "Built-in type name without dash is rejected.");
+      }
+      try {
+        // <test-built-in-element-2 is="test-custom-element-2" /> is not allowed;
+        // built-in type tag names forbid dashes
+        customElements.define(
+          "test-built-in-element-2", class extends XULElement {}, { extends: "test-custom-element-2" });
+        ok(false, "Extending from a name with dash should be rejected.");
+      } catch (e) {
+        ok(true, "Extending from a name with dash is rejected.");
+      }
+    }
+
+    function extendingWithoutDashCustomElement() {
+      let element = document.createElementNS(XUL_NS, "testwithoutdash", { is: "testwithoutdash-extended" });
+      ok(element instanceof TestWithoutDashExtended, "Should be an instance of TestWithoutDashExtended");
+      ok(element instanceof TestWithoutDash, "Should be an instance of TestWithoutDash");
+      is(element.getAttribute("is"), "testwithoutdash-extended", "The |is| attribute of the created element should be the extended type.");
+      document.querySelector("#content").appendChild(element);
+      is(element.textContent, "quux", "Should have set the textContent");
+
+      let element2 = element.cloneNode(false);
+      is(element2.localName, "testwithoutdash", "Should see the right tag");
+      is(element2.getAttribute("is"), "testwithoutdash-extended", "The |is| attribute of the created element should be the extended type.");
+      is(element2.textContent, "", "Shouldn't have cloned the textContent");
+      document.querySelector("#content").appendChild(element2);
+      is(element2.textContent, "quux", "Should have set the textContent");
+      ok(element2 instanceof TestWithoutDashExtended, "Should be an instance of TestWithoutDashExtended");
+      ok(element2 instanceof TestWithoutDash, "Should be an instance of TestWithoutDash");
+
+      let element3 = new TestWithoutDashExtended();
+      is(element3.localName, "testwithoutdash", "Should see the right tag");
+      is(element3.textContent, "", "Shouldn't have been inserted yet");
+      is(element3.namespaceURI, XUL_NS, "Should have set the right namespace");
+      document.querySelector("#content").appendChild(element3);
+      is(element3.textContent, "quux", "Should have set the textContent");
+      ok(element3 instanceof TestWithoutDashExtended, "Should be an instance of TestWithoutDashExtended");
+      ok(element3 instanceof TestWithoutDash, "Should be an instance of TestWithoutDash");
+    }
+
+    function nonCustomElementCreate() {
+      // All of these should be created as plain XUL elements without hitting
+      // any assertions.
+      let elements = [
+        document.createElementNS(XUL_NS, "axulelement", { is: "test-custom-element" }),
+        document.createElementNS(XUL_NS, "axulelement", { is: "testwithoutdash" }),
+        document.createElementNS(XUL_NS, "axulelement", { is: "test-custom-element-1" }),
+        document.createElementNS(XUL_NS, "name-with-dash", { is: "name-with-dash" }),
+        document.createElementNS(XUL_NS, "name-with-dash", { is: "another-name-with-dash" }),
+        document.createElementNS(XUL_NS, "testwithoutdash-extended"),
+        document.createElementNS(XUL_NS, "test-built-in-element"),
+        document.createElementNS(XUL_NS, "test-popup-extend"),
+        document.createElementNS(XUL_NS, "test-built-in-element-1")];
+
+      for (let element of elements) {
+        is(Object.getPrototypeOf(element), XULElement.prototype,
+          `<${element.localName} is="${element.getAttribute("is")}" /> should not be a custom element.`);
+      }
+    }
+
+    function runTest() {
+      basicCustomElementCreate();
+      parserBasicElementUpgrade();
+
+      tagNameWithoutDash();
+      upgradeAfterDefine();
+
+      basicElementCreateBuiltIn();
+      parserBasicElementUpgradeBuiltIn();
+
+      subclassElementCreateBuiltIn();
+      parserSubclassElementUpgradeBuiltIn();
+
+      upgradeAfterDefineBuiltIn();
+
+      throwForInvalidBuiltInName();
+      extendingWithoutDashCustomElement();
+
+      nonCustomElementCreate();
 
       SimpleTest.finish();
     }
   ]]>
   </script>
 
   <body xmlns="http://www.w3.org/1999/xhtml">
     <p id="display"></p>
     <div id="content" style="display: none">
       <test-custom-element id="element4" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"/>
       <testwithoutdash id="element5" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"/>
+      <axulelement id="element6" is="test-built-in-element" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"/>
+      <popup id="element7" is="test-popup-extend" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"/>
     </div>
     <pre id="test"></pre>
   </body>
 </window>
--- a/dom/xul/nsXULElement.cpp
+++ b/dom/xul/nsXULElement.cpp
@@ -186,17 +186,20 @@ nsXULElement* nsXULElement::Construct(al
 already_AddRefed<nsXULElement>
 nsXULElement::CreateFromPrototype(nsXULPrototypeElement* aPrototype,
                                   mozilla::dom::NodeInfo *aNodeInfo,
                                   bool aIsScriptable,
                                   bool aIsRoot)
 {
     RefPtr<mozilla::dom::NodeInfo> ni = aNodeInfo;
     nsCOMPtr<Element> baseElement;
-    NS_NewXULElement(getter_AddRefs(baseElement), ni.forget(), dom::FROM_PARSER_NETWORK);
+    NS_NewXULElement(getter_AddRefs(baseElement),
+                     ni.forget(),
+                     dom::FROM_PARSER_NETWORK,
+                     aPrototype->mIsAtom);
 
     if (baseElement) {
         nsXULElement* element = FromNode(baseElement);
 
         if (aPrototype->mHasIdAttribute) {
             element->SetHasID();
         }
         if (aPrototype->mHasClassAttribute) {
@@ -261,31 +264,32 @@ nsXULElement::CreateFromPrototype(nsXULP
                                                        aIsScriptable, aIsRoot);
     element.forget(aResult);
 
     return NS_OK;
 }
 
 nsresult
 NS_NewXULElement(Element** aResult, already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
-                 FromParser aFromParser)
+                 FromParser aFromParser, nsAtom* aIsAtom,
+                 mozilla::dom::CustomElementDefinition* aDefinition)
 {
     RefPtr<mozilla::dom::NodeInfo> nodeInfo = aNodeInfo;
 
     MOZ_ASSERT(nodeInfo, "need nodeinfo for non-proto Create");
 
     NS_ASSERTION(nodeInfo->NamespaceEquals(kNameSpaceID_XUL),
                  "Trying to create XUL elements that don't have the XUL namespace");
 
     nsIDocument* doc = nodeInfo->GetDocument();
     if (doc && !doc->AllowXULXBL()) {
         return NS_ERROR_NOT_AVAILABLE;
     }
 
-    return nsContentUtils::NewXULOrHTMLElement(aResult, nodeInfo, aFromParser, nullptr, nullptr);
+    return nsContentUtils::NewXULOrHTMLElement(aResult, nodeInfo, aFromParser, aIsAtom, aDefinition);
 }
 
 void
 NS_TrustedNewXULElement(Element** aResult,
                         already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
 {
     RefPtr<mozilla::dom::NodeInfo> ni = aNodeInfo;
     MOZ_ASSERT(ni, "need nodeinfo for non-proto Create");
@@ -2288,16 +2292,22 @@ nsXULPrototypeElement::SetAttrAt(uint32_
         !aValue.IsEmpty()) {
         mHasIdAttribute = true;
         // Store id as atom.
         // id="" means that the element has no id. Not that it has
         // emptystring as id.
         mAttributes[aPos].mValue.ParseAtom(aValue);
 
         return NS_OK;
+    } else if (mAttributes[aPos].mName.Equals(nsGkAtoms::is)) {
+        // Store is as atom.
+        mAttributes[aPos].mValue.ParseAtom(aValue);
+        mIsAtom = mAttributes[aPos].mValue.GetAtomValue();
+
+        return NS_OK;
     } else if (mAttributes[aPos].mName.Equals(nsGkAtoms::_class)) {
         mHasClassAttribute = true;
         // Compute the element's class list
         mAttributes[aPos].mValue.ParseAtomArray(aValue);
 
         return NS_OK;
     } else if (mAttributes[aPos].mName.Equals(nsGkAtoms::style)) {
         mHasStyleAttribute = true;
--- a/dom/xul/nsXULElement.h
+++ b/dom/xul/nsXULElement.h
@@ -145,17 +145,18 @@ class nsXULPrototypeElement : public nsX
 {
 public:
     nsXULPrototypeElement()
         : nsXULPrototypeNode(eType_Element),
           mNumAttributes(0),
           mHasIdAttribute(false),
           mHasClassAttribute(false),
           mHasStyleAttribute(false),
-          mAttributes(nullptr)
+          mAttributes(nullptr),
+          mIsAtom(nullptr)
     {
     }
 
     virtual ~nsXULPrototypeElement()
     {
         Unlink();
     }
 
@@ -188,16 +189,17 @@ public:
 
     RefPtr<mozilla::dom::NodeInfo> mNodeInfo;
 
     uint32_t                 mNumAttributes:29;
     uint32_t                 mHasIdAttribute:1;
     uint32_t                 mHasClassAttribute:1;
     uint32_t                 mHasStyleAttribute:1;
     nsXULPrototypeAttribute* mAttributes;         // [OWNER]
+    RefPtr<nsAtom>           mIsAtom;
 };
 
 namespace mozilla {
 namespace dom {
 class XULDocument;
 } // namespace dom
 } // namespace mozilla