commit 2394d9205a7a0b5baed751d875c023dd047487f9 parent 571a17ff820b6a34d72b69b9ae5752020b193d74 Author: Alice Boxhall <95208+alice@users.noreply.github.com> Date: Wed, 24 Dec 2025 13:39:26 +0000 Bug 1981346 - Add referenceTarget support for the form attribute. r=credential-management-reviewers,webidl,dimi,Jamie,smaug This change adds attr-associated element observers, which observe changes in the element associated with a single-element attribute. For example, if an element has an attribute |attr| with a value |value|, and an AttrTargetObserver has been added for |attr| on that element, the observer would fire if an element with an ID matching |value| was added to the element's document or shadow root. Attr-associated element observers are tracked in mAttrElementObserverMap in an element's nsExtendedDOMSlots. An attr-associated element can change in multiple ways: - The attribute value changes; - An element matching the content attribute value is added or removed from the element's document or shadow root; - The explicitly-set attr element is added or removed from the element's document or shadow root; - The resolved reference target for the explicitly set attr-element or the element whose ID matches the string value of the attribute changes. The resolved reference target can likewise change in multiple ways: - The referenceTarget property of the element's shadow root is added, changed or removed; - An element whose ID matches the element's shadow root's referenceTarget value is added or removed from the shadow root; - The resolved reference target of the element whose ID matches the shadow root's referenceTarget value changes. These changes are tracked via reference target change observers, which observe changes to an element's resolved reference target. Reference target change observers are added on the host element's document or shadow root, and fired when one of the above circumstances occurs. Differential Revision: https://phabricator.services.mozilla.com/D261367 Diffstat:
51 files changed, 970 insertions(+), 195 deletions(-)
diff --git a/accessible/generic/LocalAccessible.cpp b/accessible/generic/LocalAccessible.cpp @@ -2302,7 +2302,7 @@ Relation LocalAccessible::RelationByType(RelationType aType) const { if (mContent->IsHTMLElement()) { // HTML form controls implements nsIFormControl interface. if (auto* control = nsIFormControl::FromNode(mContent)) { - if (dom::HTMLFormElement* form = control->GetForm()) { + if (dom::HTMLFormElement* form = control->GetFormInternal()) { return Relation(mDoc, form->GetDefaultSubmitElement()); } } diff --git a/accessible/html/HTMLFormControlAccessible.cpp b/accessible/html/HTMLFormControlAccessible.cpp @@ -117,7 +117,8 @@ Relation HTMLRadioButtonAccessible::ComputeGroupAttributes( RefPtr<nsContentList> inputElms; - if (dom::Element* formElm = nsIFormControl::FromNode(mContent)->GetForm()) { + if (dom::Element* formElm = + nsIFormControl::FromNode(mContent)->GetFormInternal()) { inputElms = NS_GetContentList(formElm, namespaceId, tagName); } else { inputElms = NS_GetContentList(mContent->OwnerDoc(), namespaceId, tagName); @@ -427,7 +428,7 @@ uint64_t HTMLTextFieldAccessible::NativeState() const { mContent->AsElement()->GetAttr(nsGkAtoms::autocomplete, autocomplete); if (!autocomplete.LowerCaseEqualsLiteral("off")) { - Element* formElement = input->GetForm(); + Element* formElement = input->GetFormInternal(); if (formElement) { formElement->GetAttr(nsGkAtoms::autocomplete, autocomplete); } diff --git a/dom/base/DocumentOrShadowRoot.cpp b/dom/base/DocumentOrShadowRoot.cpp @@ -586,6 +586,62 @@ void DocumentOrShadowRoot::RemoveIDTargetObserver(nsAtom* aID, entry->RemoveContentChangeCallback(aObserver, aData, aForImage); } +void DocumentOrShadowRoot::AddReferenceTargetChangeObserver( + Element* aElement, ReferenceTargetChangeObserver aObserver, void* aData) { + if (!StaticPrefs::dom_shadowdom_referenceTarget_enabled()) { + return; + } + + MOZ_ASSERT(aElement); + MOZ_ASSERT(aElement->GetContainingDocumentOrShadowRoot() == this); + auto& callbackEntries = mReferenceTargetObserverMap.LookupOrInsert(aElement); + callbackEntries.Insert({aObserver, aData}); +} + +void DocumentOrShadowRoot::RemoveReferenceTargetChangeObserver( + Element* aElement, ReferenceTargetChangeObserver aObserver, void* aData) { + if (!StaticPrefs::dom_shadowdom_referenceTarget_enabled()) { + return; + } + + MOZ_ASSERT(aElement); + + auto entry = mReferenceTargetObserverMap.Lookup(aElement); + if (!entry) { + return; + } + nsTHashSet<ReferenceTargetChangeCallbackEntry>& callbacks = entry.Data(); + callbacks.Remove({aObserver, aData}); + + if (entry.Data().IsEmpty()) { + entry.Remove(); + } +} + +void DocumentOrShadowRoot::NotifyReferenceTargetChanged(Element* aElement) { + if (!StaticPrefs::dom_shadowdom_referenceTarget_enabled()) { + return; + } + + MOZ_ASSERT(aElement); + + auto entry = mReferenceTargetObserverMap.Lookup(aElement); + if (!entry) { + return; + } + + for (auto iter = entry.Data().begin(); iter != entry.Data().end(); ++iter) { + const ReferenceTargetChangeCallback& callback = *iter; + bool keep = callback.mObserver(callback.mData); + if (!keep) { + entry.Data().Remove(iter); + } + } + if (entry.Data().IsEmpty()) { + entry.Remove(); + } +} + Element* DocumentOrShadowRoot::LookupImageElement(nsAtom* aId) { if (aId->IsEmpty()) { return nullptr; diff --git a/dom/base/DocumentOrShadowRoot.h b/dom/base/DocumentOrShadowRoot.h @@ -186,6 +186,40 @@ class DocumentOrShadowRoot { void* aData, bool aForImage); /** + * Callback called when a shadow root's reference target changes. + * @return true to keep the callback in the callback set, false to remove it. + */ + typedef bool (*ReferenceTargetChangeObserver)(void* aData); + /** + * Listen for changes to the given element's resolved reference target. This + * could happen in a number of ways: + * - The given element's shadow root's referenceTarget property changes, or is + * added or removed; + * - The element referred to by the referenceTarget property changes (e.g. + * because an element with that ID is added to the shadow root); + * - The resolved reference target of the element referred to by the + * referenceTarget property changes for one of the above two reasons (i.e. + * the referenceTarget property refers to an element foo, which also has a + * shadow root, and foo's resolved reference target changes) + * @param aElement an element on which to listen for resolved + * reference target changes. The element must have this document or shadow + * root as its root (i.e. aElement.GetUncomposedDocOrConnectedShadowRoot() == + * this). + * @param aObserver the callback to fire when the resolved reference target + * changes. + * @param aData data to pass to the callback. + */ + void AddReferenceTargetChangeObserver(Element* aElement, + ReferenceTargetChangeObserver aObserver, + void* aData); + void RemoveReferenceTargetChangeObserver( + Element* aElement, ReferenceTargetChangeObserver aObserver, void* aData); + /** + * Called when aElement's resolved reference target changes. + */ + void NotifyReferenceTargetChanged(Element* aElement); + + /** * Lookup an image element using its associated ID, which is usually provided * by |-moz-element()|. Similar to GetElementById, with the difference that * elements set using mozSetImageElement have higher priority. @@ -283,6 +317,42 @@ class DocumentOrShadowRoot { */ nsTHashtable<IdentifierMapEntry> mIdentifierMap; + /** + * mReferenceTargetObserverMap keeps track of the callbacks which should be + * called when an element's resolved reference target changes. + * See AddReferenceTargetChangeObserver(). + */ + struct ReferenceTargetChangeCallback { + ReferenceTargetChangeObserver mObserver; + void* mData; + }; + struct ReferenceTargetChangeCallbackEntry : public PLDHashEntryHdr { + typedef const ReferenceTargetChangeCallback KeyType; + typedef const ReferenceTargetChangeCallback* KeyTypePointer; + + explicit ReferenceTargetChangeCallbackEntry( + const ReferenceTargetChangeCallback* aKey) + : mKey(*aKey) {} + ReferenceTargetChangeCallbackEntry( + ReferenceTargetChangeCallbackEntry&& aOther) + : PLDHashEntryHdr(std::move(aOther)), mKey(std::move(aOther.mKey)) {} + + KeyType GetKey() const { return mKey; } + bool KeyEquals(KeyTypePointer aKey) const { + return aKey->mObserver == mKey.mObserver && aKey->mData == mKey.mData; + } + + static KeyTypePointer KeyToPointer(KeyType& aKey) { return &aKey; } + static PLDHashNumber HashKey(KeyTypePointer aKey) { + return HashGeneric(aKey->mObserver, aKey->mData); + } + enum { ALLOW_MEMMOVE = true }; + + ReferenceTargetChangeCallback mKey; + }; + nsTHashMap<Element*, nsTHashSet<ReferenceTargetChangeCallbackEntry>> + mReferenceTargetObserverMap; + // Always non-null, see comment in the constructor as to why a pointer instead // of a reference. nsINode* mAsNode; diff --git a/dom/base/Element.cpp b/dom/base/Element.cpp @@ -1594,6 +1594,14 @@ Element* Element::ResolveReferenceTarget() const { return const_cast<Element*>(element); } +Element* Element::RetargetReferenceTargetForBindings(Element* aElement) const { + if (!StaticPrefs::dom_shadowdom_referenceTarget_enabled()) { + return aElement; + } + + return Element::FromNodeOrNull(nsContentUtils::Retarget(aElement, this)); +} + void Element::GetAttribute(const nsAString& aName, DOMString& aReturn) { const nsAttrValue* val = mAttrs.GetAttr( aName, @@ -2228,6 +2236,312 @@ Maybe<nsTArray<RefPtr<dom::Element>>> Element::GetExplicitlySetAttrElements( return Nothing(); } +bool ReferenceTargetChangedAttrAssociatedElementCallback(void* aData) { + using AttrElementObserverCallbackData = + FragmentOrElement::nsExtendedDOMSlots::AttrElementObserverCallbackData; + + AttrElementObserverCallbackData* data = + static_cast<AttrElementObserverCallbackData*>(aData); + nsWeakPtr weakElement = data->mElement; + + if (nsCOMPtr<Element> element = do_QueryReferent(weakElement)) { + return element->AttrAssociatedElementUpdated(data->mAttr); + } + + return false; +} + +bool IDTargetChangedAttrAssociatedElementCallback(Element* aOldElement, + Element* aNewElement, + void* aData) { + using AttrElementObserverCallbackData = + FragmentOrElement::nsExtendedDOMSlots::AttrElementObserverCallbackData; + + AttrElementObserverCallbackData* data = + static_cast<AttrElementObserverCallbackData*>(aData); + + nsWeakPtr weakElement = data->mElement; + if (nsCOMPtr<Element> element = do_QueryReferent(weakElement)) { + DocumentOrShadowRoot* root = element->GetContainingDocumentOrShadowRoot(); + if (aOldElement) { + root->RemoveReferenceTargetChangeObserver( + aOldElement, ReferenceTargetChangedAttrAssociatedElementCallback, + aData); + } + if (aNewElement) { + root->AddReferenceTargetChangeObserver( + aNewElement, ReferenceTargetChangedAttrAssociatedElementCallback, + aData); + } + + return element->AttrAssociatedElementUpdated(data->mAttr); + } + + return false; +} + +Element* Element::AddAttrAssociatedElementObserver( + nsAtom* aAttr, AttrTargetObserver aObserver) { + using AttrElementObserverData = + FragmentOrElement::nsExtendedDOMSlots::AttrElementObserverData; + using AttrElementObserverCallbackData = + FragmentOrElement::nsExtendedDOMSlots::AttrElementObserverCallbackData; + + AttrElementObserverData& observerData = + ExtendedDOMSlots()->mAttrElementObserverMap.LookupOrInsert(aAttr); + + // TODO (bug 1997286): Observe explicitly set attr-element binding/unbinding. + + if (!observerData.mCallbackData) { + observerData.mCallbackData.reset(new AttrElementObserverCallbackData()); + observerData.mCallbackData->mAttr = aAttr; + observerData.mCallbackData->mElement = do_GetWeakReference(this); + + const nsAttrValue* value = GetParsedAttr(aAttr); + MOZ_ASSERT(value); + if (!value->IsEmptyString()) { + RefPtr<nsAtom> idValue = value->GetAsAtom(); + observerData.mLastKnownAttrValue = idValue; + } + + DocumentOrShadowRoot* docOrShadow = GetUncomposedDocOrConnectedShadowRoot(); + if (docOrShadow) { + AddDocOrShadowObserversForAttrAssociatedElement(*docOrShadow, aAttr); + } + } + + Element* lastAttrElement; + if (nsCOMPtr<Element> element = + do_QueryReferent(observerData.mLastKnownAttrElement)) { + lastAttrElement = element.get(); + } else { + lastAttrElement = GetAttrAssociatedElementInternal(aAttr); + observerData.mLastKnownAttrElement = do_GetWeakReference(lastAttrElement); + } + + observerData.mObservers.Insert(aObserver); + + return lastAttrElement; +} + +void Element::RemoveAttrAssociatedElementObserver( + nsAtom* aAttr, AttrTargetObserver aObserver) { + using AttrElementObserverData = + FragmentOrElement::nsExtendedDOMSlots::AttrElementObserverData; + + AttrElementObserverData* observerData = GetAttrElementObserverData(aAttr); + if (!observerData) { + return; + } + + DocumentOrShadowRoot* docOrShadow = GetUncomposedDocOrConnectedShadowRoot(); + if (docOrShadow) { + RemoveDocOrShadowObserversForAttrAssociatedElement(*docOrShadow, aAttr); + } + observerData->mObservers.Remove(aObserver); + + if (observerData->mObservers.IsEmpty()) { + DeleteAttrAssociatedElementObserverData(aAttr); + } +} + +bool Element::AttrAssociatedElementUpdated(nsAtom* aAttr) { + using AttrElementObserverData = + FragmentOrElement::nsExtendedDOMSlots::AttrElementObserverData; + + AttrElementObserverData* observerData = GetAttrElementObserverData(aAttr); + if (!observerData) { + return false; + } + + Element* newAttrElement = GetAttrAssociatedElementInternal(aAttr); + + nsCOMPtr<Element> oldAttrElement = + do_QueryReferent(observerData->mLastKnownAttrElement); + + for (auto iter = observerData->mObservers.begin(); + iter != observerData->mObservers.end(); ++iter) { + AttrTargetObserver observer = *iter; + bool keep = observer(oldAttrElement.get(), newAttrElement, this); + if (!keep) { + observerData->mObservers.Remove(iter); + } + } + + if (observerData->mObservers.IsEmpty()) { + DeleteAttrAssociatedElementObserverData(aAttr); + return false; + } + + return true; +} + +void Element::IDREFAttributeValueChanged(nsAtom* aAttr, + const nsAttrValue* aValue) { + using AttrElementObserverData = + FragmentOrElement::nsExtendedDOMSlots::AttrElementObserverData; + using AttrElementObserverCallbackData = + FragmentOrElement::nsExtendedDOMSlots::AttrElementObserverCallbackData; + + if (!AttrAssociatedElementUpdated(aAttr)) { + return; + } + + DocumentOrShadowRoot* docOrShadow = GetUncomposedDocOrConnectedShadowRoot(); + if (!docOrShadow) { + return; + } + + AttrElementObserverData* observerData = GetAttrElementObserverData(aAttr); + if (!observerData) { + return; + } + + AttrElementObserverCallbackData* callbackData = + observerData->mCallbackData.get(); + if (observerData->mLastKnownAttrValue) { + docOrShadow->RemoveIDTargetObserver( + observerData->mLastKnownAttrValue, + IDTargetChangedAttrAssociatedElementCallback, callbackData, false); + Element* oldIdTarget = + docOrShadow->GetElementById(observerData->mLastKnownAttrValue); + if (oldIdTarget) { + docOrShadow->RemoveReferenceTargetChangeObserver( + oldIdTarget, ReferenceTargetChangedAttrAssociatedElementCallback, + callbackData); + } + } + + if (!aValue || aValue->GetAtomValue()->IsEmpty()) { + observerData->mLastKnownAttrValue = nullptr; + return; + } + + RefPtr<nsAtom> idValue = aValue->GetAsAtom(); + observerData->mLastKnownAttrValue = idValue; + docOrShadow->AddIDTargetObserver(idValue, + IDTargetChangedAttrAssociatedElementCallback, + callbackData, false); + + Element* newIdTarget = docOrShadow->GetElementById(idValue); + if (newIdTarget) { + docOrShadow->AddReferenceTargetChangeObserver( + newIdTarget, ReferenceTargetChangedAttrAssociatedElementCallback, + callbackData); + } +} + +FragmentOrElement::nsExtendedDOMSlots::AttrElementObserverData* +Element::GetAttrElementObserverData(nsAtom* aAttr) { + if (const nsExtendedDOMSlots* slots = GetExistingExtendedDOMSlots()) { + if (auto entry = slots->mAttrElementObserverMap.Lookup(aAttr)) { + return &entry.Data(); + } + } + return nullptr; +} + +void Element::DeleteAttrAssociatedElementObserverData(nsAtom* aAttr) { + DocumentOrShadowRoot* docOrShadow = GetUncomposedDocOrConnectedShadowRoot(); + if (docOrShadow) { + RemoveDocOrShadowObserversForAttrAssociatedElement(*docOrShadow, aAttr); + } + + ExtendedDOMSlots()->mAttrElementObserverMap.Remove(aAttr); +} + +void Element::AddDocOrShadowObserversForAttrAssociatedElement( + DocumentOrShadowRoot& aContainingDocOrShadow, nsAtom* aAttr) { + using AttrElementObserverData = + FragmentOrElement::nsExtendedDOMSlots::AttrElementObserverData; + using AttrElementObserverCallbackData = + FragmentOrElement::nsExtendedDOMSlots::AttrElementObserverCallbackData; + + AttrElementObserverData* observerData = GetAttrElementObserverData(aAttr); + if (!observerData) { + return; + } + + Element* explicitlySetAttrElement = GetExplicitlySetAttrElement(aAttr); + AttrElementObserverCallbackData* callbackData = + observerData->mCallbackData.get(); + + if (explicitlySetAttrElement) { + aContainingDocOrShadow.AddReferenceTargetChangeObserver( + explicitlySetAttrElement, + ReferenceTargetChangedAttrAssociatedElementCallback, callbackData); + } else { + MOZ_ASSERT(observerData->mLastKnownAttrValue); + aContainingDocOrShadow.AddIDTargetObserver( + observerData->mLastKnownAttrValue, + IDTargetChangedAttrAssociatedElementCallback, callbackData, false); + + Element* idTarget = aContainingDocOrShadow.GetElementById( + observerData->mLastKnownAttrValue); + if (idTarget) { + aContainingDocOrShadow.AddReferenceTargetChangeObserver( + idTarget, ReferenceTargetChangedAttrAssociatedElementCallback, + callbackData); + } + } +} + +void Element::RemoveDocOrShadowObserversForAttrAssociatedElement( + DocumentOrShadowRoot& aContainingDocOrShadow, nsAtom* aAttr) { + using AttrElementObserverData = + FragmentOrElement::nsExtendedDOMSlots::AttrElementObserverData; + using AttrElementObserverCallbackData = + FragmentOrElement::nsExtendedDOMSlots::AttrElementObserverCallbackData; + + AttrElementObserverData* observerData = GetAttrElementObserverData(aAttr); + if (!observerData) { + return; + } + + Element* explicitlySetAttrElement = GetExplicitlySetAttrElement(aAttr); + AttrElementObserverCallbackData* callbackData = + observerData->mCallbackData.get(); + + if (explicitlySetAttrElement) { + aContainingDocOrShadow.RemoveReferenceTargetChangeObserver( + explicitlySetAttrElement, + ReferenceTargetChangedAttrAssociatedElementCallback, callbackData); + } else if (observerData->mLastKnownAttrValue) { + aContainingDocOrShadow.RemoveIDTargetObserver( + observerData->mLastKnownAttrValue, + IDTargetChangedAttrAssociatedElementCallback, + observerData->mCallbackData.get(), false); + + Element* idTarget = aContainingDocOrShadow.GetElementById( + observerData->mLastKnownAttrValue); + if (idTarget) { + aContainingDocOrShadow.RemoveReferenceTargetChangeObserver( + idTarget, ReferenceTargetChangedAttrAssociatedElementCallback, + callbackData); + } + } +} + +void Element::BindAttrAssociatedElementObservers( + DocumentOrShadowRoot& aContainingDocOrShadow) { + if (const nsExtendedDOMSlots* slots = GetExistingExtendedDOMSlots()) { + for (const RefPtr<nsAtom>& attr : slots->mAttrElementObserverMap.Keys()) { + AddDocOrShadowObserversForAttrAssociatedElement(aContainingDocOrShadow, + attr); + } + } +} + +void Element::UnbindAttrAssociatedElementObservers( + DocumentOrShadowRoot& aContainingDocOrShadow) { + if (const nsExtendedDOMSlots* slots = GetExistingExtendedDOMSlots()) { + for (const RefPtr<nsAtom>& attr : slots->mAttrElementObserverMap.Keys()) { + RemoveDocOrShadowObserversForAttrAssociatedElement(aContainingDocOrShadow, + attr); + } + } +} + void Element::GetElementsWithGrid(nsTArray<RefPtr<Element>>& aElements) { dom::TreeIterator<dom::StyleChildrenIterator> iter(*this); while (nsIContent* cur = iter.GetCurrent()) { @@ -2531,6 +2845,12 @@ nsresult Element::BindToTree(BindContext& aContext, nsINode& aParent) { /* aForceInDataDoc = */ false); } + DocumentOrShadowRoot* containingDocOrShadow = + GetUncomposedDocOrConnectedShadowRoot(); + if (containingDocOrShadow) { + BindAttrAssociatedElementObservers(*containingDocOrShadow); + } + // XXXbz script execution during binding can trigger some of these // postcondition asserts.... But we do want that, since things will // generally be quite broken when that happens. @@ -2564,6 +2884,12 @@ static bool WillDetachFromShadowOnUnbind(const Element& aElement, void Element::UnbindFromTree(UnbindContext& aContext) { const bool nullParent = aContext.IsUnbindRoot(this); + DocumentOrShadowRoot* containingDocOrShadow = + GetUncomposedDocOrConnectedShadowRoot(); + if (containingDocOrShadow) { + UnbindAttrAssociatedElementObservers(*containingDocOrShadow); + } + HandleShadowDOMRelatedRemovalSteps(nullParent); if (HasFlag(ELEMENT_IN_CONTENT_IDENTIFIER_FOR_LCP)) { @@ -3361,6 +3687,11 @@ bool Element::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, return true; } + if (aAttribute == nsGkAtoms::form) { + aResult.ParseAtom(aValue); + return true; + } + if (aNamespaceID == kNameSpaceID_None) { if (aAttribute == nsGkAtoms::_class || aAttribute == nsGkAtoms::part || aAttribute == nsGkAtoms::aria_actions || @@ -3443,6 +3774,7 @@ void Element::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, } } else if (aName == nsGkAtoms::aria_activedescendant) { ClearExplicitlySetAttrElement(aName); + IDREFAttributeValueChanged(aName, aValue); } else if (aName == nsGkAtoms::aria_controls || aName == nsGkAtoms::aria_describedby || aName == nsGkAtoms::aria_details || diff --git a/dom/base/Element.h b/dom/base/Element.h @@ -1388,6 +1388,46 @@ class Element : public FragmentOrElement { nsAtom* aAttr, bool* aUseCachedValue, Nullable<nsTArray<RefPtr<Element>>>& aElements); + typedef bool (*AttrTargetObserver)(Element* aOldElement, Element* aNewElement, + Element* thisElement); + /** + * Add an attr-associated element observer for a given attribute. The observer + * will fire whenever the element associated with |aAttr| for this element + * changes. This can occur in multiple scenarios: + * - The attribute value or explicitly set attr-element changes; + * - An element with an ID matching the attribute value is added or removed + * from the document or shadow root containing the element with the + * attribute; + * - The explicitly set attr-element is added or removed from the document or + * shadow root containing the element with the attribute; + * - The reference target of the element directly referred to by the attribute + * changes. + * @return the current attr-associated element for |aAttr| for this element, + * if any. + */ + Element* AddAttrAssociatedElementObserver(nsAtom* aAttr, + AttrTargetObserver aObserver); + void RemoveAttrAssociatedElementObserver(nsAtom* aAttr, + AttrTargetObserver aObserver); + bool AttrAssociatedElementUpdated(nsAtom* aAttr); + + protected: + void IDREFAttributeValueChanged(nsAtom* aAttr, const nsAttrValue* aValue); + + private: + FragmentOrElement::nsExtendedDOMSlots::AttrElementObserverData* + GetAttrElementObserverData(nsAtom* aAttr); + void DeleteAttrAssociatedElementObserverData(nsAtom* aAttr); + void AddDocOrShadowObserversForAttrAssociatedElement( + DocumentOrShadowRoot& aContainingDocOrShadow, nsAtom* aAttr); + void RemoveDocOrShadowObserversForAttrAssociatedElement( + DocumentOrShadowRoot& aContainingDocOrShadow, nsAtom* aAttr); + void BindAttrAssociatedElementObservers( + DocumentOrShadowRoot& aContainingDocOrShadow); + void UnbindAttrAssociatedElementObservers( + DocumentOrShadowRoot& aContainingDocOrShadow); + + public: /** * Sets an attribute element for the given attribute. * https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#explicitly-set-attr-element @@ -1561,6 +1601,7 @@ class Element : public FragmentOrElement { } Element* ResolveReferenceTarget() const; + Element* RetargetReferenceTargetForBindings(Element* aElement) const; const Maybe<float> GetLastRememberedBSize() const { const nsExtendedDOMSlots* slots = GetExistingExtendedDOMSlots(); diff --git a/dom/base/FormData.cpp b/dom/base/FormData.cpp @@ -331,7 +331,7 @@ already_AddRefed<FormData> FormData::Constructor( // 1.1.2. If submitter's form owner is not this form element, then throw a // "NotFoundError" DOMException. - if (fc->GetForm() != aFormElement) { + if (fc->GetFormInternal() != aFormElement) { aRv.ThrowNotFoundError("The submitter is not owned by this form."); return nullptr; } diff --git a/dom/base/FragmentOrElement.h b/dom/base/FragmentOrElement.h @@ -23,6 +23,7 @@ #include "nsIContent.h" // base class #include "nsIHTMLCollection.h" #include "nsIWeakReferenceUtils.h" +#include "nsTHashSet.h" class ContentUnbinder; class nsContentList; @@ -285,6 +286,29 @@ class FragmentOrElement : public nsIContent { nsTHashMap<RefPtr<nsAtom>, std::pair<Maybe<nsTArray<nsWeakPtr>>, Maybe<nsTArray<RefPtr<Element>>>>> mAttrElementsMap; + + typedef bool (*AttrTargetObserver)(Element* aOldElement, + Element* aNewelement, + Element* aThisElement); + struct AttrElementObserverCallbackData { + nsWeakPtr mElement; + RefPtr<nsAtom> mAttr; + }; + struct AttrElementObserverData { + // Used as the value for |aOldElement| when calling an AttrTargetObserver + // callback. + nsWeakPtr mLastKnownAttrElement; // TODO: should be an array + + // Used to add/remove ID target observers when the attribute value changes + // or the attribute host is added to or removed from a document or shadow + // root. + RefPtr<nsAtom> mLastKnownAttrValue; // TODO: should be a ParsedAttr + nsTHashSet<AttrTargetObserver> mObservers; + + // Used for removing the IDTargetObserver(s) + UniquePtr<AttrElementObserverCallbackData> mCallbackData; + }; + nsTHashMap<RefPtr<nsAtom>, AttrElementObserverData> mAttrElementObserverMap; }; class nsDOMSlots : public nsIContent::nsContentSlots { diff --git a/dom/base/RadioGroupContainer.cpp b/dom/base/RadioGroupContainer.cpp @@ -24,7 +24,7 @@ struct nsRadioGroupStruct { * A strong pointer to the currently selected radio button. */ RefPtr<HTMLInputElement> mSelectedRadioButton; - TreeOrderedArray<RefPtr<HTMLInputElement>> mRadioButtons; + TreeOrderedArray<RefPtr<HTMLInputElement>, TreeKind::DOM> mRadioButtons; uint32_t mRequiredRadioCount; bool mGroupSuffersFromValueMissing; }; diff --git a/dom/base/ShadowRoot.cpp b/dom/base/ShadowRoot.cpp @@ -939,6 +939,69 @@ void ShadowRoot::GetHTML(const GetHTMLOptions& aOptions, nsAString& aResult) { this, true, aResult, aOptions.mSerializableShadowRoots, aOptions.mShadowRoots); } + +// static +bool ShadowRoot::ReferenceTargetIDTargetChanged(Element* aOldElement, + Element* aNewElement, + void* aData) { + ShadowRoot* shadowRoot = static_cast<ShadowRoot*>(aData); + if (aOldElement) { + shadowRoot->RemoveReferenceTargetChangeObserver( + aOldElement, RecursiveReferenceTargetChanged, shadowRoot); + } + if (aNewElement) { + shadowRoot->AddReferenceTargetChangeObserver( + aNewElement, RecursiveReferenceTargetChanged, shadowRoot); + } + shadowRoot->NotifyReferenceTargetChangedObservers(); + return true; +} + +// static +bool ShadowRoot::RecursiveReferenceTargetChanged(void* aData) { + ShadowRoot* shadowRoot = static_cast<ShadowRoot*>(aData); + shadowRoot->NotifyReferenceTargetChangedObservers(); + return true; +} + void ShadowRoot::SetReferenceTarget(RefPtr<nsAtom> aTarget) { - mReferenceTarget = std::move(aTarget); + if (!StaticPrefs::dom_shadowdom_referenceTarget_enabled()) { + return; + } + + if (aTarget == mReferenceTarget) { + return; + } + + if (mReferenceTarget) { + RemoveIDTargetObserver(mReferenceTarget, ReferenceTargetIDTargetChanged, + this, false); + } + + if (!aTarget) { + mReferenceTarget = nullptr; + } else { + mReferenceTarget = std::move(aTarget); + + Element* referenceTargetElement = AddIDTargetObserver( + mReferenceTarget, ReferenceTargetIDTargetChanged, this, false); + if (referenceTargetElement) { + AddReferenceTargetChangeObserver(referenceTargetElement, + RecursiveReferenceTargetChanged, this); + } + } + + NotifyReferenceTargetChangedObservers(); +} + +void ShadowRoot::NotifyReferenceTargetChangedObservers() { + Element* host = GetHost(); + if (!host) { + return; + } + + DocumentOrShadowRoot* root = host->GetContainingDocumentOrShadowRoot(); + if (root) { + root->NotifyReferenceTargetChanged(host); + } } diff --git a/dom/base/ShadowRoot.h b/dom/base/ShadowRoot.h @@ -343,6 +343,12 @@ class ShadowRoot final : public DocumentFragment, public DocumentOrShadowRoot { RefPtr<nsAtom> mReferenceTarget; + static bool ReferenceTargetIDTargetChanged(Element* aOldElement, + Element* aNewElement, void* aData); + static bool RecursiveReferenceTargetChanged(void* aData); + + void NotifyReferenceTargetChangedObservers(); + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; }; diff --git a/dom/base/TreeOrderedArray.h b/dom/base/TreeOrderedArray.h @@ -8,6 +8,7 @@ #define mozilla_dom_TreeOrderedArray_h #include "FastFrontRemovableArray.h" +#include "nsContentUtils.h" class nsINode; template <typename T> @@ -16,7 +17,7 @@ class RefPtr; namespace mozilla::dom { // A sorted tree-ordered list of pointers (either raw or RefPtr) to nodes. -template <typename NodePointer> +template <typename NodePointer, TreeKind K = TreeKind::DOM> class TreeOrderedArray : public FastFrontRemovableArray<NodePointer, 1> { using Base = FastFrontRemovableArray<NodePointer, 1>; diff --git a/dom/base/TreeOrderedArrayInlines.h b/dom/base/TreeOrderedArrayInlines.h @@ -15,8 +15,9 @@ namespace mozilla::dom { -template <typename Node> -size_t TreeOrderedArray<Node>::Insert(Node& aNode, nsINode* aCommonAncestor) { +template <typename Node, TreeKind K> +size_t TreeOrderedArray<Node, K>::Insert(Node& aNode, + nsINode* aCommonAncestor) { static_assert(std::is_base_of_v<nsINode, Node>, "Should be a node"); auto span = Base::AsSpan(); @@ -35,8 +36,8 @@ size_t TreeOrderedArray<Node>::Insert(Node& aNode, nsINode* aCommonAncestor) { auto* curNode = static_cast<Node*>(aNode); MOZ_DIAGNOSTIC_ASSERT(curNode != &mNode, "Tried to insert a node already in the list"); - return nsContentUtils::CompareTreePosition<TreeKind::DOM>( - &mNode, curNode, mCommonAncestor, &mCache); + return nsContentUtils::CompareTreePosition<K>(&mNode, curNode, + mCommonAncestor, &mCache); } }; diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp @@ -1420,7 +1420,7 @@ bool nsContentUtils::IsAutocompleteEnabled(mozilla::dom::Element* aElement) { if (autocomplete.IsEmpty()) { auto* control = nsGenericHTMLFormControlElement::FromNode(aElement); - auto* form = control->GetForm(); + auto* form = control->GetFormInternal(); if (!form) { return true; } @@ -4122,7 +4122,7 @@ void nsContentUtils::GenerateStateKey(nsIContent* aContent, Document* aDocument, KeyAppendInt(int32_t(control->ControlType()), aKey); // If in a form, add form name / index of form / index in form - HTMLFormElement* formElement = control->GetForm(); + HTMLFormElement* formElement = control->GetFormInternal(); if (formElement) { if (IsAutocompleteOff(formElement)) { aKey.Truncate(); diff --git a/dom/events/IMEStateManager.cpp b/dom/events/IMEStateManager.cpp @@ -1937,7 +1937,7 @@ MOZ_CAN_RUN_SCRIPT static void GetActionHint(const IMEState& aState, // return won't submit the form, use "maybenext". bool willSubmit = false; bool isLastElement = false; - HTMLFormElement* formElement = inputElement->GetForm(); + HTMLFormElement* formElement = inputElement->GetFormInternal(); // is this a form and does it have a default submit element? if (formElement) { if (formElement->IsLastActiveElement(inputElement)) { diff --git a/dom/html/ElementInternals.cpp b/dom/html/ElementInternals.cpp @@ -149,7 +149,7 @@ void ElementInternals::SetFormValue( } // https://html.spec.whatwg.org/#dom-elementinternals-form -HTMLFormElement* ElementInternals::GetForm(ErrorResult& aRv) const { +Element* ElementInternals::GetFormForBindings(ErrorResult& aRv) const { MOZ_ASSERT(mTarget); if (!mTarget->IsFormAssociatedElement()) { @@ -157,7 +157,8 @@ HTMLFormElement* ElementInternals::GetForm(ErrorResult& aRv) const { "Target element is not a form-associated custom element"); return nullptr; } - return GetForm(); + + return GetFormForBindings(); } // https://html.spec.whatwg.org/commit-snapshots/3ad5159be8f27e110a70cefadcb50fc45ec21b05/#dom-elementinternals-setvalidity @@ -362,6 +363,10 @@ CustomStateSet* ElementInternals::States() { return mCustomStateSet; } +Element* ElementInternals::GetFormForBindings() const { + return GetFormInternal(); +}; + void ElementInternals::SetForm(HTMLFormElement* aForm) { mForm = aForm; } void ElementInternals::ClearForm(bool aRemoveFromForm, bool aUnbindOrDelete) { diff --git a/dom/html/ElementInternals.h b/dom/html/ElementInternals.h @@ -77,7 +77,7 @@ class ElementInternals final : public nsIFormControl, void SetFormValue(const Nullable<FileOrUSVStringOrFormData>& aValue, const Optional<Nullable<FileOrUSVStringOrFormData>>& aState, ErrorResult& aRv); - mozilla::dom::HTMLFormElement* GetForm(ErrorResult& aRv) const; + mozilla::dom::Element* GetFormForBindings(ErrorResult& aRv) const; void SetValidity(const ValidityStateFlags& aFlags, const Optional<nsAString>& aMessage, const Optional<NonNull<nsGenericHTMLElement>>& aAnchor, @@ -96,7 +96,10 @@ class ElementInternals final : public nsIFormControl, mozilla::dom::HTMLFieldSetElement* GetFieldSet() override { return mFieldSet; } - mozilla::dom::HTMLFormElement* GetForm() const override { return mForm; } + mozilla::dom::Element* GetFormForBindings() const override; + mozilla::dom::HTMLFormElement* GetFormInternal() const override { + return mForm; + } void SetForm(mozilla::dom::HTMLFormElement* aForm) override; void ClearForm(bool aRemoveFromForm, bool aUnbindOrDelete) override; NS_IMETHOD Reset() override; diff --git a/dom/html/HTMLElement.cpp b/dom/html/HTMLElement.cpp @@ -363,7 +363,7 @@ void HTMLElement::SetFormInternal(HTMLFormElement* aForm, bool aBindToTree) { HTMLFormElement* HTMLElement::GetFormInternal() const { ElementInternals* internals = GetElementInternals(); MOZ_ASSERT(internals); - return internals->GetForm(); + return internals->GetFormInternal(); } void HTMLElement::SetFieldSetInternal(HTMLFieldSetElement* aFieldset) { diff --git a/dom/html/HTMLFormControlsCollection.h b/dom/html/HTMLFormControlsCollection.h @@ -99,13 +99,15 @@ class HTMLFormControlsCollection final : public nsIHTMLCollection, // Holds WEAK references - bug 36639 // NOTE(emilio): These are not guaranteed to be descendants of mForm, because // of the form attribute, though that's likely. - TreeOrderedArray<nsGenericHTMLFormElement*> mElements; + TreeOrderedArray<nsGenericHTMLFormElement*, TreeKind::ShadowIncludingDOM> + mElements; // This array holds on to all form controls that are not contained // in mElements (form.elements in JS, see ShouldBeInFormControl()). // This is needed to properly clean up the bi-directional references // (both weak and strong) between the form and its form controls. - TreeOrderedArray<nsGenericHTMLFormElement*> mNotInElements; + TreeOrderedArray<nsGenericHTMLFormElement*, TreeKind::ShadowIncludingDOM> + mNotInElements; NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(HTMLFormControlsCollection) diff --git a/dom/html/HTMLFormElement.cpp b/dom/html/HTMLFormElement.cpp @@ -349,7 +349,7 @@ void HTMLFormElement::RequestSubmit(nsGenericHTMLElement* aSubmitter, // 1.2. If submitter's form owner is not this form element, then throw a // "NotFoundError" DOMException. - if (fc->GetForm() != this) { + if (fc->GetFormInternal() != this) { aRv.ThrowNotFoundError("The submitter is not owned by this form."); return; } @@ -403,11 +403,13 @@ static void MarkOrphans(Span<T*> aArray) { } } -static void CollectOrphans(nsINode* aRemovalRoot, - TreeOrderedArray<nsGenericHTMLFormElement*>& aArray +static void CollectOrphans( + nsINode* aRemovalRoot, + TreeOrderedArray<nsGenericHTMLFormElement*, TreeKind::ShadowIncludingDOM>& + aArray #ifdef DEBUG - , - HTMLFormElement* aThisForm + , + HTMLFormElement* aThisForm #endif ) { // Put a script blocker around all the notifications we're about to do. @@ -441,7 +443,7 @@ static void CollectOrphans(nsINode* aRemovalRoot, if (!removed) { const auto* fc = nsIFormControl::FromNode(node); MOZ_ASSERT(fc); - HTMLFormElement* form = fc->GetForm(); + HTMLFormElement* form = fc->GetFormInternal(); NS_ASSERTION(form == aThisForm, "How did that happen?"); } #endif /* DEBUG */ @@ -480,7 +482,7 @@ static void CollectOrphans(nsINode* aRemovalRoot, #ifdef DEBUG if (!removed) { - HTMLFormElement* form = node->GetForm(); + HTMLFormElement* form = node->GetFormInternal(); NS_ASSERTION(form == aThisForm, "How did that happen?"); } #endif /* DEBUG */ @@ -1132,8 +1134,9 @@ nsresult HTMLFormElement::AddElement(nsGenericHTMLFormElement* aChild, // Determine whether to add the new element to the elements or // the not-in-elements list. bool childInElements = HTMLFormControlsCollection::ShouldBeInElements(fc); - TreeOrderedArray<nsGenericHTMLFormElement*>& controlList = - childInElements ? mControls->mElements : mControls->mNotInElements; + TreeOrderedArray<nsGenericHTMLFormElement*, TreeKind::ShadowIncludingDOM>& + controlList = + childInElements ? mControls->mElements : mControls->mNotInElements; const size_t insertedIndex = controlList.Insert(*aChild, this); const bool lastElement = controlList.Length() == insertedIndex + 1; @@ -1235,8 +1238,9 @@ nsresult HTMLFormElement::RemoveElement(nsGenericHTMLFormElement* aChild, // Determine whether to remove the child from the elements list // or the not in elements list. bool childInElements = HTMLFormControlsCollection::ShouldBeInElements(fc); - TreeOrderedArray<nsGenericHTMLFormElement*>& controls = - childInElements ? mControls->mElements : mControls->mNotInElements; + TreeOrderedArray<nsGenericHTMLFormElement*, TreeKind::ShadowIncludingDOM>& + controls = + childInElements ? mControls->mElements : mControls->mNotInElements; // Find the index of the child. This will be used later if necessary // to find the default submit. diff --git a/dom/html/HTMLImageElement.cpp b/dom/html/HTMLImageElement.cpp @@ -586,7 +586,7 @@ JSObject* HTMLImageElement::WrapNode(JSContext* aCx, } #ifdef DEBUG -HTMLFormElement* HTMLImageElement::GetForm() const { return mForm; } +HTMLFormElement* HTMLImageElement::GetFormInternal() const { return mForm; } #endif void HTMLImageElement::SetForm(HTMLFormElement* aForm) { diff --git a/dom/html/HTMLImageElement.h b/dom/html/HTMLImageElement.h @@ -201,7 +201,7 @@ class HTMLImageElement final : public nsGenericHTMLElement, } #ifdef DEBUG - HTMLFormElement* GetForm() const; + HTMLFormElement* GetFormInternal() const; #endif void SetForm(HTMLFormElement* aForm); void ClearForm(bool aRemoveFromForm); diff --git a/dom/html/HTMLInputElement.cpp b/dom/html/HTMLInputElement.cpp @@ -1797,11 +1797,7 @@ void HTMLInputElement::SetValue(const nsAString& aValue, CallerType aCallerType, } Element* HTMLInputElement::GetListForBindings() const { - HTMLDataListElement* list = GetListInternal(); - if (!StaticPrefs::dom_shadowdom_referenceTarget_enabled()) { - return list; - } - return Element::FromNodeOrNull(nsContentUtils::Retarget(list, this)); + return RetargetReferenceTargetForBindings(GetListInternal()); } HTMLDataListElement* HTMLInputElement::GetListInternal() const { diff --git a/dom/html/HTMLInputElement.h b/dom/html/HTMLInputElement.h @@ -116,8 +116,8 @@ class HTMLInputElement final : public TextControlElement, public: using ConstraintValidation::GetValidationMessage; - using nsGenericHTMLFormControlElementWithState::GetForm; using nsGenericHTMLFormControlElementWithState::GetFormAction; + using nsGenericHTMLFormControlElementWithState::GetFormForBindings; using ValueSetterOption = TextControlState::ValueSetterOption; using ValueSetterOptions = TextControlState::ValueSetterOptions; diff --git a/dom/html/HTMLLabelElement.cpp b/dom/html/HTMLLabelElement.cpp @@ -11,6 +11,7 @@ #include "mozilla/EventDispatcher.h" #include "mozilla/MouseEvents.h" +#include "mozilla/dom/HTMLFormElement.h" #include "mozilla/dom/HTMLLabelElementBinding.h" #include "mozilla/dom/MouseEventBinding.h" #include "mozilla/dom/ShadowRoot.h" @@ -36,14 +37,18 @@ JSObject* HTMLLabelElement::WrapNode(JSContext* aCx, NS_IMPL_ELEMENT_CLONE(HTMLLabelElement) -HTMLFormElement* HTMLLabelElement::GetForm() const { +Element* HTMLLabelElement::GetFormForBindings() const { + return RetargetReferenceTargetForBindings(GetFormInternal()); +} + +HTMLFormElement* HTMLLabelElement::GetFormInternal() const { // Not all labeled things have a form association. Stick to the ones that do. const auto* formControl = nsIFormControl::FromNodeOrNull(GetControl()); if (!formControl) { return nullptr; } - return formControl->GetForm(); + return formControl->GetFormInternal(); } void HTMLLabelElement::Focus(const FocusOptions& aOptions, diff --git a/dom/html/HTMLLabelElement.h b/dom/html/HTMLLabelElement.h @@ -31,7 +31,8 @@ class HTMLLabelElement final : public nsGenericHTMLElement { // Element virtual bool IsInteractiveHTMLContent() const override { return true; } - HTMLFormElement* GetForm() const; + Element* GetFormForBindings() const; + HTMLFormElement* GetFormInternal() const; void GetHtmlFor(nsString& aHtmlFor) { GetHTMLAttr(nsGkAtoms::_for, aHtmlFor); } diff --git a/dom/html/HTMLLegendElement.cpp b/dom/html/HTMLLegendElement.cpp @@ -128,9 +128,16 @@ HTMLLegendElement::LegendAlignValue HTMLLegendElement::LogicalAlign( } } -HTMLFormElement* HTMLLegendElement::GetForm() const { +Element* HTMLLegendElement::GetFormForBindings() const { + HTMLFormElement* form = GetFormInternal(); + if (!form) { + return nullptr; + } + return RetargetReferenceTargetForBindings(form); +} +HTMLFormElement* HTMLLegendElement::GetFormInternal() const { const auto* fieldsetControl = nsIFormControl::FromNodeOrNull(GetFieldSet()); - return fieldsetControl ? fieldsetControl->GetForm() : nullptr; + return fieldsetControl ? fieldsetControl->GetFormInternal() : nullptr; } JSObject* HTMLLegendElement::WrapNode(JSContext* aCx, diff --git a/dom/html/HTMLLegendElement.h b/dom/html/HTMLLegendElement.h @@ -62,8 +62,8 @@ class HTMLLegendElement final : public nsGenericHTMLElement { /** * WebIDL Interface */ - - HTMLFormElement* GetForm() const; + Element* GetFormForBindings() const; + HTMLFormElement* GetFormInternal() const; void GetAlign(DOMString& aAlign) { GetHTMLAttr(nsGkAtoms::align, aAlign); } @@ -72,7 +72,7 @@ class HTMLLegendElement final : public nsGenericHTMLElement { } nsINode* GetScopeChainParent() const override { - Element* form = GetForm(); + Element* form = GetFormInternal(); return form ? form : nsGenericHTMLElement::GetScopeChainParent(); } diff --git a/dom/html/HTMLOptionElement.cpp b/dom/html/HTMLOptionElement.cpp @@ -41,9 +41,14 @@ HTMLOptionElement::~HTMLOptionElement() = default; NS_IMPL_ELEMENT_CLONE(HTMLOptionElement) -mozilla::dom::HTMLFormElement* HTMLOptionElement::GetForm() { +mozilla::dom::Element* HTMLOptionElement::GetFormForBindings() { + HTMLFormElement* form = GetFormInternal(); + return RetargetReferenceTargetForBindings(form); +} + +mozilla::dom::HTMLFormElement* HTMLOptionElement::GetFormInternal() { HTMLSelectElement* selectControl = GetSelect(); - return selectControl ? selectControl->GetForm() : nullptr; + return selectControl ? selectControl->GetFormInternal() : nullptr; } void HTMLOptionElement::SetSelectedInternal(bool aValue, bool aNotify) { diff --git a/dom/html/HTMLOptionElement.h b/dom/html/HTMLOptionElement.h @@ -73,7 +73,8 @@ class HTMLOptionElement final : public nsGenericHTMLElement { SetHTMLBoolAttr(nsGkAtoms::disabled, aValue, aRv); } - HTMLFormElement* GetForm(); + Element* GetFormForBindings(); + HTMLFormElement* GetFormInternal(); void GetRenderedLabel(nsAString& aLabel) { if (!GetAttr(nsGkAtoms::label, aLabel) || aLabel.IsEmpty()) { diff --git a/dom/html/HTMLTextAreaElement.h b/dom/html/HTMLTextAreaElement.h @@ -188,8 +188,9 @@ class HTMLTextAreaElement final : public TextControlElement, void SetDisabled(bool aDisabled, ErrorResult& aError) { SetHTMLBoolAttr(nsGkAtoms::disabled, aDisabled, aError); } - // nsGenericHTMLFormControlElementWithState::GetForm is fine - using nsGenericHTMLFormControlElementWithState::GetForm; + // nsGenericHTMLFormControlElementWithState::GetForm* is fine + using nsGenericHTMLFormControlElementWithState::GetFormForBindings; + using nsGenericHTMLFormControlElementWithState::GetFormInternal; int32_t MaxLength() const { return GetIntAttr(nsGkAtoms::maxlength, -1); } int32_t UsedMaxLength() const final { return MaxLength(); } void SetMaxLength(int32_t aMaxLength, ErrorResult& aError) { diff --git a/dom/html/nsGenericHTMLElement.cpp b/dom/html/nsGenericHTMLElement.cpp @@ -1864,11 +1864,11 @@ void nsGenericHTMLFormElement::UnbindFromTree(UnbindContext& aContext) { } } - // We have to remove the form id observer if there was one. + // We have to remove the form attribute observer if there was one. // We will re-add one later if needed (during bind to tree). if (nsContentUtils::HasNonEmptyAttr(this, kNameSpaceID_None, nsGkAtoms::form)) { - RemoveFormIdObserver(); + RemoveFormAttributeObserver(); } } @@ -1911,17 +1911,6 @@ void nsGenericHTMLFormElement::BeforeSetAttr(int32_t aNameSpaceID, form->RemoveElement(this, false); } - - if (aName == nsGkAtoms::form) { - // If @form isn't set or set to the empty string, there were no observer - // so we don't have to remove it. - if (nsContentUtils::HasNonEmptyAttr(this, kNameSpaceID_None, - nsGkAtoms::form)) { - // The current form id observer is no longer needed. - // A new one may be added in AfterSetAttr. - RemoveFormIdObserver(); - } - } } return nsGenericHTMLElement::BeforeSetAttr(aNameSpaceID, aName, aValue, @@ -1963,18 +1952,22 @@ void nsGenericHTMLFormElement::AfterSetAttr( } if (aName == nsGkAtoms::form) { - // We need a new form id observer. - DocumentOrShadowRoot* docOrShadow = - GetUncomposedDocOrConnectedShadowRoot(); - if (docOrShadow) { - Element* formIdElement = nullptr; - if (aValue && !aValue->IsEmptyString()) { - formIdElement = AddFormIdObserver(); + bool hadOldValue = aOldValue && !aOldValue->GetAtomValue()->IsEmpty(); + bool hasNewValue = aValue && !aValue->GetAtomValue()->IsEmpty(); + if (hadOldValue || hasNewValue) { + if (!hadOldValue && hasNewValue) { + AddFormAttributeObserver(); } - // Because we have a new @form value (or no more @form), we have to - // update our form owner. - UpdateFormOwner(false, formIdElement); + // Fire the observer, which will update the form owner if necessary. + IDREFAttributeValueChanged(aName, aValue); + + if (hadOldValue && !hasNewValue) { + RemoveFormAttributeObserver(); + } + } else if (aValue && aValue->GetAtomValue()->IsEmpty()) { + // Ensure that empty @form value clears the form owner. + ClearForm(true, false); } } } @@ -1990,45 +1983,33 @@ void nsGenericHTMLFormElement::ForgetFieldSet(nsIContent* aFieldset) { } } -Element* nsGenericHTMLFormElement::AddFormIdObserver() { +Element* nsGenericHTMLFormElement::AddFormAttributeObserver() { MOZ_ASSERT(IsFormAssociatedElement()); nsAutoString formId; - DocumentOrShadowRoot* docOrShadow = GetUncomposedDocOrConnectedShadowRoot(); GetAttr(nsGkAtoms::form, formId); NS_ASSERTION(!formId.IsEmpty(), "@form value should not be the empty string!"); - RefPtr<nsAtom> atom = NS_Atomize(formId); - return docOrShadow->AddIDTargetObserver(atom, FormIdUpdated, this, false); + return AddAttrAssociatedElementObserver(nsGkAtoms::form, + FormAttributeUpdated); } -void nsGenericHTMLFormElement::RemoveFormIdObserver() { +void nsGenericHTMLFormElement::RemoveFormAttributeObserver() { MOZ_ASSERT(IsFormAssociatedElement()); - DocumentOrShadowRoot* docOrShadow = GetUncomposedDocOrConnectedShadowRoot(); - if (!docOrShadow) { - return; - } - - nsAutoString formId; - GetAttr(nsGkAtoms::form, formId); - NS_ASSERTION(!formId.IsEmpty(), - "@form value should not be the empty string!"); - RefPtr<nsAtom> atom = NS_Atomize(formId); - - docOrShadow->RemoveIDTargetObserver(atom, FormIdUpdated, this, false); + RemoveAttrAssociatedElementObserver(nsGkAtoms::form, FormAttributeUpdated); } /* static */ -bool nsGenericHTMLFormElement::FormIdUpdated(Element* aOldElement, - Element* aNewElement, - void* aData) { - nsGenericHTMLFormElement* element = - static_cast<nsGenericHTMLFormElement*>(aData); - - NS_ASSERTION(element->IsHTMLElement(), "aData should be an HTML element"); +bool nsGenericHTMLFormElement::FormAttributeUpdated(Element* aOldElement, + Element* aNewElement, + Element* thisElement) { + NS_ASSERTION(thisElement->IsHTMLElement(), + "thisElement should be an HTML element"); + nsGenericHTMLFormElement* element = + static_cast<nsGenericHTMLFormElement*>(thisElement); element->UpdateFormOwner(false, aNewElement); return true; @@ -2119,19 +2100,22 @@ void nsGenericHTMLFormElement::UpdateFormOwner(bool aBindToTree, Element* element = nullptr; if (aBindToTree) { - element = AddFormIdObserver(); + element = AddFormAttributeObserver(); } else { element = aFormIdElement; } - NS_ASSERTION(!IsInComposedDoc() || - element == GetUncomposedDocOrConnectedShadowRoot() - ->GetElementById(formId), - "element should be equals to the current element " - "associated with the id in @form!"); + NS_ASSERTION( + !IsInComposedDoc() || + element == GetAttrAssociatedElementInternal(nsGkAtoms::form), + "element should be equals to the current element " + "associated via @form!"); if (element && element->IsHTMLElement(nsGkAtoms::form) && - nsContentUtils::IsInSameAnonymousTree(this, element)) { + element->GetClosestNativeAnonymousSubtreeRoot() == + GetClosestNativeAnonymousSubtreeRoot() && + (StaticPrefs::dom_shadowdom_referenceTarget_enabled() || + element->GetContainingShadow() == GetContainingShadow())) { form = static_cast<HTMLFormElement*>(element); SetFormInternal(form, aBindToTree); } @@ -2639,6 +2623,14 @@ HTMLFieldSetElement* nsGenericHTMLFormControlElement::GetFieldSet() { return GetFieldSetInternal(); } +mozilla::dom::Element* nsGenericHTMLFormControlElement::GetFormForBindings() + const { + if (!mForm) { + return nullptr; + } + return RetargetReferenceTargetForBindings(mForm); +} + void nsGenericHTMLFormControlElement::SetForm(HTMLFormElement* aForm) { MOZ_ASSERT(aForm, "Don't pass null here"); NS_ASSERTION(!mForm, diff --git a/dom/html/nsGenericHTMLElement.h b/dom/html/nsGenericHTMLElement.h @@ -1120,25 +1120,26 @@ class nsGenericHTMLFormElement : public nsGenericHTMLElement { void UpdateFieldSet(bool aNotify); /** - * Add a form id observer which will observe when the element with the id in + * Add a form attribute observer which will observe when the element + * associated with * @form will change. * * @return The element associated with the current id in @form (may be null). */ - Element* AddFormIdObserver(); + Element* AddFormAttributeObserver(); /** - * Remove the form id observer. + * Remove the form attribute attribute observer. */ - void RemoveFormIdObserver(); + void RemoveFormAttributeObserver(); /** - * This method is a a callback for IDTargetObserver (from Document). - * It will be called each time the element associated with the id in @form + * This method is a a callback for AttrAssociatedElementUpdated (from + * Element). It will be called each time the element associated with @form * changes. */ - static bool FormIdUpdated(Element* aOldElement, Element* aNewElement, - void* aData); + static bool FormAttributeUpdated(Element* aOldElement, Element* aNewElement, + Element* thisElement); // Returns true if the event should not be handled from GetEventTargetParent bool IsElementDisabledForEvents(mozilla::WidgetEvent* aEvent, @@ -1202,7 +1203,8 @@ class nsGenericHTMLFormControlElement : public nsGenericHTMLFormElement, // nsIFormControl mozilla::dom::HTMLFieldSetElement* GetFieldSet() override; - mozilla::dom::HTMLFormElement* GetForm() const override { return mForm; } + mozilla::dom::Element* GetFormForBindings() const override; + mozilla::dom::HTMLFormElement* GetFormInternal() const override; void SetForm(mozilla::dom::HTMLFormElement* aForm) override; void ClearForm(bool aRemoveFromForm, bool aUnbindOrDelete) override; @@ -1217,7 +1219,6 @@ class nsGenericHTMLFormControlElement : public nsGenericHTMLFormElement, bool DoesReadWriteApply() const override; void SetFormInternal(mozilla::dom::HTMLFormElement* aForm, bool aBindToTree) override; - mozilla::dom::HTMLFormElement* GetFormInternal() const override; mozilla::dom::HTMLFieldSetElement* GetFieldSetInternal() const override; void SetFieldSetInternal( mozilla::dom::HTMLFieldSetElement* aFieldset) override; diff --git a/dom/html/nsIConstraintValidation.cpp b/dom/html/nsIConstraintValidation.cpp @@ -101,7 +101,7 @@ void nsIConstraintValidation::SetValidityState(ValidityStateType aState, nsCOMPtr<nsIFormControl> formCtrl = do_QueryInterface(this); NS_ASSERTION(formCtrl, "This interface should be used by form elements!"); - if (HTMLFormElement* form = formCtrl->GetForm()) { + if (HTMLFormElement* form = formCtrl->GetFormInternal()) { form->UpdateValidity(IsValid()); } if (HTMLFieldSetElement* fieldSet = formCtrl->GetFieldSet()) { @@ -124,7 +124,7 @@ void nsIConstraintValidation::SetBarredFromConstraintValidation(bool aBarred) { // If the element is going to be barred from constraint validation, we can // inform the form and fieldset that we are now valid. Otherwise, we are now // invalid. - if (HTMLFormElement* form = formCtrl->GetForm()) { + if (HTMLFormElement* form = formCtrl->GetFormInternal()) { form->UpdateValidity(aBarred); } HTMLFieldSetElement* fieldSet = formCtrl->GetFieldSet(); diff --git a/dom/html/nsIFormControl.h b/dom/html/nsIFormControl.h @@ -111,10 +111,16 @@ class nsIFormControl : public nsISupports { virtual mozilla::dom::HTMLFieldSetElement* GetFieldSet() = 0; /** + * Get the form for this form control, retargeted to the appropriate scope. + * @return the form + */ + virtual mozilla::dom::Element* GetFormForBindings() const = 0; + + /** * Get the form for this form control. * @return the form */ - virtual mozilla::dom::HTMLFormElement* GetForm() const = 0; + virtual mozilla::dom::HTMLFormElement* GetFormInternal() const = 0; /** * Set the form for this form control. @@ -285,7 +291,7 @@ bool nsIFormControl::IsConceptButton() const { } bool nsIFormControl::IsButtonControl() const { - return IsConceptButton() && (!GetForm() || !IsSubmitControl()); + return IsConceptButton() && (!GetFormInternal() || !IsSubmitControl()); } bool nsIFormControl::AllowDraggableChildren() const { diff --git a/dom/webidl/ElementInternals.webidl b/dom/webidl/ElementInternals.webidl @@ -17,8 +17,8 @@ interface ElementInternals { undefined setFormValue((File or USVString or FormData)? value, optional (File or USVString or FormData)? state); - [Throws] - readonly attribute HTMLFormElement? form; + [Throws, BinaryName=formForBindings] + readonly attribute Element? form; [Throws] undefined setValidity(optional ValidityStateFlags flags = {}, diff --git a/dom/webidl/HTMLButtonElement.webidl b/dom/webidl/HTMLButtonElement.webidl @@ -17,8 +17,8 @@ interface HTMLButtonElement : HTMLElement { [CEReactions, SetterThrows, Pure] attribute boolean disabled; - [Pure] - readonly attribute HTMLFormElement? form; + [Pure, BinaryName=formForBindings] + readonly attribute Element? form; [CEReactions, SetterThrows, Pure] attribute DOMString formAction; [CEReactions, SetterThrows, Pure] diff --git a/dom/webidl/HTMLFieldSetElement.webidl b/dom/webidl/HTMLFieldSetElement.webidl @@ -17,7 +17,8 @@ interface HTMLFieldSetElement : HTMLElement { [CEReactions, SetterThrows] attribute boolean disabled; - readonly attribute HTMLFormElement? form; + [BinaryName=formForBindings] + readonly attribute Element? form; [CEReactions, SetterThrows] attribute DOMString name; diff --git a/dom/webidl/HTMLInputElement.webidl b/dom/webidl/HTMLInputElement.webidl @@ -42,7 +42,8 @@ interface HTMLInputElement : HTMLElement { attribute DOMString dirName; [CEReactions, Pure, SetterThrows] attribute boolean disabled; - readonly attribute HTMLFormElement? form; + [BinaryName=formForBindings] + readonly attribute Element? form; [Pure] attribute FileList? files; [CEReactions, Pure, SetterThrows] diff --git a/dom/webidl/HTMLLabelElement.webidl b/dom/webidl/HTMLLabelElement.webidl @@ -15,7 +15,7 @@ interface HTMLLabelElement : HTMLElement { [HTMLConstructor] constructor(); - readonly attribute HTMLFormElement? form; + [BinaryName=formForBindings] readonly attribute Element? form; [CEReactions] attribute DOMString htmlFor; readonly attribute HTMLElement? control; diff --git a/dom/webidl/HTMLLegendElement.webidl b/dom/webidl/HTMLLegendElement.webidl @@ -17,7 +17,7 @@ interface HTMLLegendElement : HTMLElement { [HTMLConstructor] constructor(); - readonly attribute HTMLFormElement? form; + [BinaryName=formForBindings] readonly attribute Element? form; }; // http://www.whatwg.org/specs/web-apps/current-work/#other-elements,-attributes-and-apis diff --git a/dom/webidl/HTMLObjectElement.webidl b/dom/webidl/HTMLObjectElement.webidl @@ -25,8 +25,8 @@ interface HTMLObjectElement : HTMLElement { attribute DOMString name; [CEReactions, Pure, SetterThrows] attribute DOMString useMap; - [Pure] - readonly attribute HTMLFormElement? form; + [Pure, BinaryName=formForBindings] + readonly attribute Element? form; [CEReactions, Pure, SetterThrows] attribute DOMString width; [CEReactions, Pure, SetterThrows] diff --git a/dom/webidl/HTMLOptionElement.webidl b/dom/webidl/HTMLOptionElement.webidl @@ -18,7 +18,8 @@ interface HTMLOptionElement : HTMLElement { [CEReactions, SetterThrows] attribute boolean disabled; - readonly attribute HTMLFormElement? form; + [BinaryName=formForBindings] + readonly attribute Element? form; [CEReactions, SetterThrows] attribute DOMString label; [CEReactions, SetterThrows] diff --git a/dom/webidl/HTMLOutputElement.webidl b/dom/webidl/HTMLOutputElement.webidl @@ -18,7 +18,8 @@ interface HTMLOutputElement : HTMLElement { [PutForwards=value, Constant] readonly attribute DOMTokenList htmlFor; - readonly attribute HTMLFormElement? form; + [BinaryName=formForBindings] + readonly attribute Element? form; [CEReactions, SetterThrows, Pure] attribute DOMString name; diff --git a/dom/webidl/HTMLSelectElement.webidl b/dom/webidl/HTMLSelectElement.webidl @@ -15,8 +15,8 @@ interface HTMLSelectElement : HTMLElement { attribute DOMString autocomplete; [CEReactions, SetterThrows, Pure] attribute boolean disabled; - [Pure] - readonly attribute HTMLFormElement? form; + [Pure, BinaryName=formForBindings] + readonly attribute Element? form; [CEReactions, SetterThrows, Pure] attribute boolean multiple; [CEReactions, SetterThrows, Pure] diff --git a/dom/webidl/HTMLTextAreaElement.webidl b/dom/webidl/HTMLTextAreaElement.webidl @@ -26,8 +26,8 @@ interface HTMLTextAreaElement : HTMLElement { attribute DOMString dirName; [CEReactions, SetterThrows, Pure] attribute boolean disabled; - [Pure] - readonly attribute HTMLFormElement? form; + [Pure, BinaryName=formForBindings] + readonly attribute Element? form; // attribute DOMString inputMode; [CEReactions, SetterThrows, Pure] attribute long maxLength; diff --git a/testing/web-platform/meta/shadow-dom/reference-target/tentative/form.html.ini b/testing/web-platform/meta/shadow-dom/reference-target/tentative/form.html.ini @@ -1,15 +0,0 @@ -[form.html] - [Reference target works with form attribute.] - expected: FAIL - - [Reference target works with form attribute via options.] - expected: FAIL - - [Reference target works with setAttribute('form')] - expected: FAIL - - [Reference target works with form-associated custom element.] - expected: FAIL - - [Reference target works with nested shadow trees.] - expected: FAIL diff --git a/testing/web-platform/meta/shadow-dom/reference-target/tentative/property-reflection-imperative-setup.html.ini b/testing/web-platform/meta/shadow-dom/reference-target/tentative/property-reflection-imperative-setup.html.ini @@ -1,22 +1,4 @@ [property-reflection-imperative-setup.html] - [button.form has reflection behavior ReflectsHostReadOnly when pointing to form with reference target with imperative setup] - expected: FAIL - - [input.form has reflection behavior ReflectsHostReadOnly when pointing to form with reference target with imperative setup] - expected: FAIL - - [output.form has reflection behavior ReflectsHostReadOnly when pointing to form with reference target with imperative setup] - expected: FAIL - - [select.form has reflection behavior ReflectsHostReadOnly when pointing to form with reference target with imperative setup] - expected: FAIL - - [textarea.form has reflection behavior ReflectsHostReadOnly when pointing to form with reference target with imperative setup] - expected: FAIL - - [object.form has reflection behavior ReflectsHostReadOnly when pointing to form with reference target with imperative setup] - expected: FAIL - [label.control has reflection behavior ReflectsHostReadOnly when pointing to button with reference target with imperative setup] expected: FAIL @@ -37,6 +19,3 @@ [label.control has reflection behavior ReflectsHostReadOnly when pointing to textarea with reference target with imperative setup] expected: FAIL - - [fieldset.form has reflection behavior ReflectsHostReadOnly when pointing to form with reference target with imperative setup] - expected: FAIL diff --git a/testing/web-platform/meta/shadow-dom/reference-target/tentative/property-reflection.html.ini b/testing/web-platform/meta/shadow-dom/reference-target/tentative/property-reflection.html.ini @@ -3,24 +3,6 @@ [The .labels property of the referenced input element should point to the referencing label element] expected: FAIL - [button.form has reflection behavior ReflectsHostReadOnly when pointing to form with reference target] - expected: FAIL - - [input.form has reflection behavior ReflectsHostReadOnly when pointing to form with reference target] - expected: FAIL - - [output.form has reflection behavior ReflectsHostReadOnly when pointing to form with reference target] - expected: FAIL - - [select.form has reflection behavior ReflectsHostReadOnly when pointing to form with reference target] - expected: FAIL - - [textarea.form has reflection behavior ReflectsHostReadOnly when pointing to form with reference target] - expected: FAIL - - [object.form has reflection behavior ReflectsHostReadOnly when pointing to form with reference target] - expected: FAIL - [label.control has reflection behavior ReflectsHostReadOnly when pointing to button with reference target] expected: FAIL @@ -41,6 +23,3 @@ [label.control has reflection behavior ReflectsHostReadOnly when pointing to textarea with reference target] expected: FAIL - - [fieldset.form has reflection behavior ReflectsHostReadOnly when pointing to form with reference target] - expected: FAIL diff --git a/testing/web-platform/tests/shadow-dom/reference-target/tentative/form.html b/testing/web-platform/tests/shadow-dom/reference-target/tentative/form.html @@ -168,5 +168,209 @@ assert_equals(realForm.elements[2], input, "The 3rd element should be the input inside the real form."); }, "Reference target works with nested shadow trees."); </script> + + + <button id="reset-button-6" type="reset" form="fancy-form-6"></button> + + + <script> + test(function() { + const resetButton = document.getElementById("reset-button-6"); + assert_equals(resetButton.form, null, "The reset button doesn't have a form association before the form is created."); + + // Construct disconnected custom element with shadow root + const fancyForm = document.createElement("fancy-form-6"); + fancyForm.id = "fancy-form-6"; + fancyForm.attachShadow({ mode: "open", referenceTarget: "real-form" }); + const input = document.createElement("input"); + input.id = "input-in-shadow"; + input.setAttribute("value", "default value"); + input.setAttribute("form", "real-form"); + fancyForm.shadowRoot.appendChild(input); + assert_equals(input.form, null, "The inner input doesn't have a form association before the form is created."); + + const realForm = document.createElement("form"); + realForm.id = "real-form"; + fancyForm.shadowRoot.appendChild(realForm); + + assert_equals(realForm.elements.length, 0, "When the form is in disconnected DOM, it has no associated form elements."); + assert_equals(resetButton.form, null, "The reset button should not have a form before the form is connected to the document."); + assert_equals(input.form, null, "The inner input element should not have a form before the form is connected to the document."); + + // Connect the custom element to the document + resetButton.after(fancyForm); + + assert_equals(realForm.elements.length, 2, "Once the form is connected, it has two associated form elements."); + assert_equals(realForm.elements[0], resetButton, "The first element should be the reset button outside the shadow root."); + assert_equals(realForm.elements[1], input, "The second element should be the input element inside the shadow root."); + assert_equals(resetButton.form, fancyForm, "The reset button should show the shadow host as its form property."); + assert_equals(input.form, realForm, "The inner input element should show the actual form element as its form property."); + input.value = "new value"; + assert_equals(input.value, "new value", "Before the reset button is clicked, the input element's value should be 'new value'."); + resetButton.click(); + assert_equals(input.value, "default value", "After the reset button is clicked, the input element's value should be 'default value'.") + + // Setting referenceTarget to null causes the reset button to no longer be + // associated with the form, but the inner input remains associated. + fancyForm.shadowRoot.referenceTarget = null; + assert_equals(realForm.elements.length, 1, "Once the reference target is set to null, the form should only have one form element."); + assert_equals(realForm.elements[0], input, "The sole element associated with the form should be the input element."); + assert_equals(resetButton.form, null, "The reset button should have null as its form property."); + assert_equals(input.form, realForm, "The inner input element should still have the form as its form property."); + + input.value = "second new value"; + assert_equals("second new value", input.value, "After the reference target is re-set, before the reset button is clicked, the input element's value should be 'new value'."); + resetButton.click(); + assert_equals("second new value", input.value, "After the reference target is re-set, after the reset button is clicked, the input element's value should still be 'new new value'.") + + realForm.remove(); + assert_equals(realForm.elements.length, 0, "Before the inner-fancy-form containing the real form is inserted, it should have no associated form elements."); + assert_equals(input.form, null, "After the real form is removed from the first shadow root, the input should have no associated form"); + assert_equals(resetButton.form, null, "After the real form is removed from the first shadow root, the reset button should have no associated form."); + + // Adding a form nested inside another fancy-form with the appropriate + // referenceTarget values still causes the form association to be set up + const innerFancyForm = document.createElement('fancy-form-6'); + innerFancyForm.id = "inner-fancy-form"; + innerFancyForm.attachShadow({mode: "open", referenceTarget: "real-form"}); + innerFancyForm.shadowRoot.appendChild(realForm); + fancyForm.shadowRoot.referenceTarget = "inner-fancy-form"; + fancyForm.shadowRoot.appendChild(innerFancyForm); + assert_equals(realForm.elements.length, 1, "After the inner-fancy-form is connected, the real form should have 1 associated form elements"); + assert_equals(realForm.elements[0], resetButton, "The associated element should be the reset button outside the shadow root."); + assert_equals(resetButton.form, fancyForm, "The reset button should have the outer fancy-form-6 as its form property."); + assert_equals(input.form, null, "The inner input element should have null as its form property."); + + input.setAttribute("form", "inner-fancy-form"); + assert_equals(realForm.elements.length, 2, "After the input's form attribute is updated, the real form should have 2 associated form elements"); + assert_equals(realForm.elements[0], resetButton, "The first associated element should be the reset button outside the shadow root."); + assert_equals(realForm.elements[1], input, "The second associated element should be the input inside the first shadow root."); + assert_equals(resetButton.form, fancyForm, "The reset button should have the outer fancy-form-6 as its form property."); + assert_equals(input.form, innerFancyForm, "The inner input element should have the inner fancy-form-6 as its form property."); + + input.value = "third new value"; + assert_equals("third new value", input.value, "After the form is added to the nested shadow root, before the reset button is clicked, the input element's value should be 'new value'."); + resetButton.click(); + assert_equals("default value", input.value, "After the form is added to the nested shadow root, after the reset button is clicked, the input element's value should still be 'new new value'.") + + // Prepending an element with the same ID as the inner form causes all form associations to be reset to null + const fakeForm = document.createElement("div"); + fakeForm.id = "real-form"; + realForm.before(fakeForm); + assert_equals(realForm.elements.length, 0, "After an element with the same ID is inserted, the form should have no associated form elements."); + assert_equals(input.form, null, "After an element with the same ID as the form is inserted, the input should have no associated form"); + assert_equals(resetButton.form, null, "After an element with the same ID as the form is inserted, the reset button should have no associated form."); + + // Changing the ID of the inner form, and the reference target of the inner-fancy-form, causes the form associations to be recreated. + realForm.id = "real-form-redux"; + innerFancyForm.shadowRoot.referenceTarget = "real-form-redux"; + assert_equals(realForm.elements.length, 2, "After the ID of the inner form and reference target of inner-fancy-form are changed, the real form should have 2 associated form elements"); + assert_equals(realForm.elements[0], resetButton, "After the ID of the inner form and reference target of inner-fancy-form are changed, the first associated element should be the reset button outside the shadow root."); + assert_equals(realForm.elements[1], input, "After the ID of the inner form and reference target of inner-fancy-form are changed, the second associated element should be the input inside the first shadow root."); + assert_equals(resetButton.form, fancyForm, "After the ID of the inner form and reference target of inner-fancy-form are changed, the reset button should have the outer fancy-form-6 as its form property."); + assert_equals(input.form, innerFancyForm, "After the ID of the inner form and reference target of inner-fancy-form are changed, the inner input element should have the inner fancy-form-6 as its form property."); + + // Prepending an element with the same ID as the inner-fancy-form causes all form associations to be reset to null again + const fakeInnerFancyForm = document.createElement("div"); + fakeInnerFancyForm.id = "inner-fancy-form" + innerFancyForm.before(fakeInnerFancyForm); + + assert_equals(realForm.elements.length, 0, "After an element with the same ID as its shadow host is inserted, the form should have no associated form elements."); + assert_equals(input.form, null, "After an element with the same ID as the form's shadow host is inserted, the input should have no associated form"); + assert_equals(resetButton.form, null, "After an element with the same ID as the form's shadow host is inserted, the reset button should have no associated form."); + + // Removing that element should cause the form associations to be re-created + fakeInnerFancyForm.remove(); + assert_equals(realForm.elements.length, 2, "After the fake inner-fancy-form is removed, the real form should have 2 associated form elements"); + assert_equals(realForm.elements[0], resetButton, "After the fake inner-fancy-form is removed, the first associated element should be the reset button outside the shadow root."); + assert_equals(realForm.elements[1], input, "After the fake inner-fancy-form is removed, the second associated element should be the input inside the first shadow root."); + assert_equals(resetButton.form, fancyForm, "After the fake inner-fancy-form is removed, the reset button should have the outer fancy-form-6 as its form property."); + assert_equals(input.form, innerFancyForm, "After the fake inner-fancy-form is removed, the inner input element should have the inner fancy-form-6 as its form property."); + + // Removing the ID of the real form causes all form associations to be reset to null + realForm.removeAttribute("id") + assert_equals(realForm.elements.length, 0, "After removing the ID of the real form, the form should have no associated form elements."); + assert_equals(input.form, null, "After removing the ID of the real form, the input should have no associated form"); + assert_equals(resetButton.form, null, "After removing the ID of the real form, the reset button should have no associated form."); + + // Reinstating the ID causes all associations to be re-created again + realForm.id = "real-form-redux"; + assert_equals(realForm.elements.length, 2, "After the ID of the inner form is reinstated, the real form should have 2 associated form elements"); + assert_equals(realForm.elements[0], resetButton, "After the ID of the inner form is reinstated, the first associated element should be the reset button outside the shadow root."); + assert_equals(realForm.elements[1], input, "After the ID of the inner form is reinstated, the second associated element should be the input inside the first shadow root."); + assert_equals(resetButton.form, fancyForm, "After the ID of the inner form is reinstated, the reset button should have the outer fancy-form-6 as its form property."); + assert_equals(input.form, innerFancyForm, "After the ID of the inner form is reinstated, the inner input element should have the inner fancy-form-6 as its form property."); + + // Removing the inner form should cause the form-associated elements to have their forms set to null. + realForm.remove(); + assert_equals(resetButton.form, null, "After removing the real form, the reset button should have the null form property."); + assert_equals(input.form, null, "After removing the real form, the inner input element should have null as its form property."); + }, "Form association is updated when form is inserted in or removed from shadow DOM, including in nested shadow DOM"); + </script> + + <input type="image" id="image-input-7" form="fancy-form-7"> + <fancy-form-7 id="fancy-form-7"> + <template shadowrootmode="open" shadowrootreferencetarget="real-form"> + <form id="real-form"> + </form> + </template> + </fancy-form-7> + + <script> + test(function() { + let imageInput = document.getElementById("image-input-7"); + let fancyForm = document.getElementById("fancy-form-7"); + let realForm = fancyForm.shadowRoot.getElementById("real-form"); + + assert_equals(imageInput.form, fancyForm); + assert_equals(realForm.elements.length, 0); + }, "Form association works for image inputs, which aren't in the elements collection"); + </script> + + + <input id="input-8" form="fancy-form-8" value="default value"> + <fancy-form-8 id="fancy-form-8"> + <template shadowRootMode="open"> + <fancy-form-8 id="inner-fancy-form"> + <template shadowRootMode="open"> + <form id="real-form"> + <button type="reset" id="reset">Reset</button> + </form> + </template> + </fancy-form-8> + </template> + </fancy-form-8> + + <script> + test(function() { + let input = document.getElementById("input-8"); + let outerFancyForm = document.getElementById("fancy-form-8"); + let innerFancyForm = outerFancyForm.shadowRoot.getElementById("inner-fancy-form"); + let realForm = innerFancyForm.shadowRoot.getElementById("real-form"); + let resetButton = innerFancyForm.shadowRoot.getElementById("reset"); + + assert_equals(input.form, null, "Form is null before reference target change"); + assert_equals(input.value, "default value", "Value is default"); + + input.value = "new value"; + assert_equals(input.value, "new value", "Value is updated"); + + resetButton.click(); + assert_equals(input.value, "new value", "Before reference target change, reset button doesn't reset outer input"); + + outerFancyForm.shadowRoot.referenceTarget = "inner-fancy-form"; + assert_equals(input.form, null, "Form is still null after outer reference target change"); + assert_equals(input.value, "new value", "Value is updated"); + + resetButton.click(); + assert_equals(input.value, "new value", "After outer reference target change, reset button doesn't reset outer input"); + + innerFancyForm.shadowRoot.referenceTarget = "real-form"; + assert_equals(input.form, outerFancyForm, "Form is outer-fancy-form after reference target change"); + + resetButton.click(); + assert_equals(input.value, "default value", "After inner reference target change, reset resets outer input"); + }, "Changing the reference target of a nested shadow root sets form association"); + </script> </body> </html> diff --git a/toolkit/components/formautofill/FormAutofillNative.cpp b/toolkit/components/formautofill/FormAutofillNative.cpp @@ -1025,7 +1025,7 @@ bool FormAutofillImpl::IsExpirationMonthLikely(Element& aElement) { Element* FormAutofillImpl::FindRootForField(Element* aElement) { if (const auto* control = nsGenericHTMLFormControlElement::FromNode(aElement)) { - if (Element* form = control->GetForm()) { + if (Element* form = control->GetFormInternal()) { return form; } }