tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 1b5cf4cdf8e443dc9ada5c3ee53b06c2a1c4b678
parent 2394d9205a7a0b5baed751d875c023dd047487f9
Author: Alice Boxhall <95208+alice@users.noreply.github.com>
Date:   Wed, 24 Dec 2025 13:39:26 +0000

Bug 1981349 - Add referenceTarget support for label/for, label wrapped and output/for. r=credential-management-reviewers,webidl,dimi,Jamie,smaug

This makes significant changes to RelatedAccIterator, nsLabelsNodeList and nsContentList:

- RelatedAccIterator now eagerly constructs a TreeOrderedArray of related elements, walking up shadow roots from mDependentContent while the shadow root's reference target is either mDependentContent or a host of one of its ancestor shadow roots.
- nsLabelsNodeList now has an array of roots as well as mRootNode in the superclass nsContentList. This list is reset under certain conditions, adding each shadow root found by walking up from the labeled element until one is found which doesn't have either the labeled element or the host of one of its ancestor shadow roots as a reference target. The last element in this list is set as mRootNode in the superclass, and is either the containing document, or the subtree root of either the labeled element or the host element whose containing shadow root doesn't have it as a reference target. Each root has a mutation observer added to ensure that the list is marked dirty when necessary, and also that roots are removed from `mRoots` when they are destroyed.
- In order to enable nsLabelsNodeList to be a mutation observer for each root, nsContentList now inherits from nsStubMultiMutationObserver instead of nsStubMutationObserver.

This change also moves the ReferenceTargetChangeObserver-related methods from DocumentOrShadowRoot on to Element, and the related data structures on to nsExtendedDOMSlots.

It also introduces a distinction between GetControlForBindings, which returns an element in the same scope as the label element it's called on, and GetLabeledElementInternal, which returns the element which actually has the label element as its label.

Differential Revision: https://phabricator.services.mozilla.com/D261368

Diffstat:
Maccessible/base/AccIterator.cpp | 125+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Maccessible/base/AccIterator.h | 15+++++++++++----
Maccessible/base/nsCoreUtils.cpp | 2+-
Maccessible/generic/DocAccessible-inl.h | 1+
Maccessible/generic/DocAccessible.cpp | 2++
Maccessible/generic/LocalAccessible.cpp | 6+++---
Maccessible/html/HTMLElementAccessibles.cpp | 2+-
Mdom/base/DocumentOrShadowRoot.cpp | 56--------------------------------------------------------
Mdom/base/DocumentOrShadowRoot.h | 70----------------------------------------------------------------------
Mdom/base/Element.cpp | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mdom/base/Element.h | 39+++++++++++++++++++++++++++++++++++++++
Mdom/base/FragmentOrElement.h | 40++++++++++++++++++++++++++++++++++++++++
Mdom/base/ShadowRoot.cpp | 22+++++++++++-----------
Mdom/base/nsContentList.cpp | 252++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Mdom/base/nsContentList.h | 49++++++++++++++++++++++++++++++++++++++++---------
Mdom/events/EventStateManager.cpp | 2+-
Mdom/html/ElementInternals.cpp | 2+-
Mdom/html/HTMLInputElement.cpp | 8++++++--
Mdom/html/HTMLInputElement.h | 3++-
Mdom/html/HTMLLabelElement.cpp | 41+++++++++++++++++++++++------------------
Mdom/html/HTMLLabelElement.h | 4++--
Mdom/html/nsGenericHTMLElement.cpp | 14+++++++++-----
Mdom/html/nsGenericHTMLElement.h | 3++-
Mdom/webidl/HTMLButtonElement.webidl | 1+
Mdom/webidl/HTMLInputElement.webidl | 1+
Mdom/webidl/HTMLLabelElement.webidl | 2+-
Mdom/webidl/HTMLMeterElement.webidl | 1+
Mdom/webidl/HTMLOutputElement.webidl | 1+
Mdom/webidl/HTMLProgressElement.webidl | 1+
Mdom/webidl/HTMLSelectElement.webidl | 1+
Mdom/webidl/HTMLTextAreaElement.webidl | 1+
Atesting/web-platform/meta/shadow-dom/reference-target/tentative/dom-mutation.html.ini | 6++++++
Dtesting/web-platform/meta/shadow-dom/reference-target/tentative/label-descendant.html.ini | 12------------
Mtesting/web-platform/meta/shadow-dom/reference-target/tentative/label-for.html.ini | 35-----------------------------------
Dtesting/web-platform/meta/shadow-dom/reference-target/tentative/property-reflection-imperative-setup.html.ini | 21---------------------
Mtesting/web-platform/meta/shadow-dom/reference-target/tentative/property-reflection.html.ini | 23-----------------------
Mtesting/web-platform/tests/shadow-dom/reference-target/tentative/dom-mutation.html | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtesting/web-platform/tests/shadow-dom/reference-target/tentative/label-descendant.html | 207+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtesting/web-platform/tests/shadow-dom/reference-target/tentative/label-for.html | 286+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mtoolkit/components/formautofill/FormAutofillNative.cpp | 2+-
40 files changed, 1103 insertions(+), 439 deletions(-)

diff --git a/accessible/base/AccIterator.cpp b/accessible/base/AccIterator.cpp @@ -16,6 +16,7 @@ #include "mozilla/dom/Element.h" #include "mozilla/dom/ElementInternals.h" #include "mozilla/dom/HTMLLabelElement.h" +#include "mozilla/dom/TreeOrderedArrayInlines.h" using namespace mozilla; using namespace mozilla::a11y; @@ -77,68 +78,76 @@ RelatedAccIterator::RelatedAccIterator(DocAccessible* aDocument, nsAtom* aRelAttr) : mDocument(aDocument), mDependentContent(aDependentContent), - mRelAttr(aRelAttr), - mProviders(nullptr), - mIndex(0), - mIsWalkingDependentElements(false) { - if (!aDependentContent->IsElement()) return; - if (nsAtom* id = aDependentContent->GetID()) { - mProviders = mDocument->GetRelProviders(aDependentContent->AsElement(), id); + mRelAttr(aRelAttr) {} + +void RelatedAccIterator::Initialize() { + nsIContent* content = mDependentContent; + dom::DocumentOrShadowRoot* root = + content->GetUncomposedDocOrConnectedShadowRoot(); + + while (root) { + if (nsAtom* id = content->GetID()) { + DocAccessible::AttrRelProviders* idProviders = + mDocument->GetRelProviders(content->AsElement(), id); + + if (idProviders) { + for (auto& provider : *idProviders) { + if (mRelAttr && provider->mRelAttr != mRelAttr) { + continue; + } + + mRelatedNodes.Insert(*provider->mContent); + } + } + } + if (auto result = mDocument->mDependentElementsMap.Lookup(content)) { + DocAccessible::AttrRelProviders* elementProviders = &result.Data(); + if (elementProviders) { + for (auto& provider : *elementProviders) { + if (mRelAttr && provider->mRelAttr != mRelAttr) { + continue; + } + + if (nsCoreUtils::IsDescendantOfAnyShadowIncludingAncestor( + content, provider->mContent)) { + mRelatedNodes.Insert(*provider->mContent); + } + } + } + } + dom::ShadowRoot* shadow = content->GetContainingShadow(); + dom::Element* element = + content->IsElement() ? content->AsElement() : nullptr; + if (shadow && element && element == shadow->GetReferenceTargetElement()) { + content = shadow->Host(); + root = content->GetUncomposedDocOrConnectedShadowRoot(); + } else { + root = nullptr; + } } + + mInitialized = true; } LocalAccessible* RelatedAccIterator::Next() { - if (!mProviders || mIndex == mProviders->Length()) { - if (mIsWalkingDependentElements) { - // We've walked both dependent ids and dependent elements, so there are - // no more targets. - return nullptr; - } - // We've returned all dependent ids, but there might be dependent elements - // too. Walk those next. - mIsWalkingDependentElements = true; - mIndex = 0; - if (auto providers = - mDocument->mDependentElementsMap.Lookup(mDependentContent)) { - mProviders = &providers.Data(); - } else { - mProviders = nullptr; - return nullptr; - } + if (!mInitialized) { + Initialize(); } - while (mIndex < mProviders->Length()) { - const auto& provider = (*mProviders)[mIndex++]; + while (mNextIndex < mRelatedNodes.Length()) { + nsIContent* nextContent = mRelatedNodes[mNextIndex]; + mNextIndex++; - // Return related accessible for the given attribute. - if (mRelAttr && provider->mRelAttr != mRelAttr) { - continue; - } - // If we're walking elements (not ids), the explicitly set attr-element - // `mDependentContent` must be a descendant of any of the refering element - // `mProvider->mContent`'s shadow-including ancestors. - if (mIsWalkingDependentElements && - !nsCoreUtils::IsDescendantOfAnyShadowIncludingAncestor( - mDependentContent, provider->mContent)) { - continue; - } - LocalAccessible* related = mDocument->GetAccessible(provider->mContent); - if (related) { - return related; + LocalAccessible* next = mDocument->GetAccessible(nextContent); + if (next) { + return next; } - // If the document content is pointed by relation then return the - // document itself. - if (provider->mContent == mDocument->GetContent()) { + if (nextContent == mDocument->GetContent()) { return mDocument; } } - // We exhausted mProviders without returning anything. - if (!mIsWalkingDependentElements) { - // Call this function again to start walking the dependent elements. - return Next(); - } return nullptr; } @@ -156,7 +165,7 @@ HTMLLabelIterator::HTMLLabelIterator(DocAccessible* aDocument, bool HTMLLabelIterator::IsLabel(LocalAccessible* aLabel) { dom::HTMLLabelElement* labelEl = dom::HTMLLabelElement::FromNode(aLabel->GetContent()); - return labelEl && labelEl->GetControl() == mAcc->GetContent(); + return labelEl && labelEl->GetLabeledElementInternal() == mAcc->GetContent(); } LocalAccessible* HTMLLabelIterator::Next() { @@ -170,7 +179,21 @@ LocalAccessible* HTMLLabelIterator::Next() { } // Ignore ancestor label on not widget accessible. - if (mLabelFilter == eSkipAncestorLabel || !mAcc->IsWidget()) return nullptr; + if (mLabelFilter == eSkipAncestorLabel) { + return nullptr; + } + + if (!mAcc->IsWidget()) { + nsIContent* content = mAcc->GetContent(); + if (!content->IsElement()) { + return nullptr; + } + dom::Element* element = content->AsElement(); + // <output> is not a widget but is labelable. + if (!element->IsLabelable()) { + return nullptr; + } + } // Go up tree to get a name of ancestor label if there is one (an ancestor // <label> implicitly points to us). Don't go up farther than form or diff --git a/accessible/base/AccIterator.h b/accessible/base/AccIterator.h @@ -10,6 +10,8 @@ #include "Filters.h" #include "mozilla/a11y/DocAccessible.h" #include "nsTArray.h" +#include "nsContentUtils.h" +#include "mozilla/dom/TreeOrderedArray.h" #include <memory> @@ -18,7 +20,8 @@ class nsITreeView; namespace mozilla { namespace dom { class Element; -} +class HTMLLabelElement; +} // namespace dom namespace a11y { class DocAccessibleParent; @@ -102,12 +105,16 @@ class RelatedAccIterator : public AccIterable { RelatedAccIterator(const RelatedAccIterator&); RelatedAccIterator& operator=(const RelatedAccIterator&); + void Initialize(); + DocAccessible* mDocument; nsIContent* mDependentContent; nsAtom* mRelAttr; - DocAccessible::AttrRelProviders* mProviders; - uint32_t mIndex; - bool mIsWalkingDependentElements; + + dom::TreeOrderedArray<nsIContent*, TreeKind::ShadowIncludingDOM> + mRelatedNodes; + size_t mNextIndex = 0; + bool mInitialized = false; }; /** diff --git a/accessible/base/nsCoreUtils.cpp b/accessible/base/nsCoreUtils.cpp @@ -56,7 +56,7 @@ using mozilla::a11y::nsAccUtils; bool nsCoreUtils::IsLabelWithControl(nsIContent* aContent) { dom::HTMLLabelElement* label = dom::HTMLLabelElement::FromNode(aContent); - if (label && label->GetControl()) return true; + if (label && label->GetLabeledElementInternal()) return true; return false; } diff --git a/accessible/generic/DocAccessible-inl.h b/accessible/generic/DocAccessible-inl.h @@ -142,6 +142,7 @@ inline DocAccessible::AttrRelProviders* DocAccessible::GetRelProviders( inline DocAccessible::AttrRelProviders* DocAccessible::GetOrCreateRelProviders( dom::Element* aElement, nsAtom* aID) { + // TODO (bug 1983819): need to update when reference targets change dom::DocumentOrShadowRoot* docOrShadowRoot = aElement->GetUncomposedDocOrConnectedShadowRoot(); DependentIDsHashtable* hash = diff --git a/accessible/generic/DocAccessible.cpp b/accessible/generic/DocAccessible.cpp @@ -961,6 +961,8 @@ void DocAccessible::AttributeChanged(dom::Element* aElement, "DOM attribute change on an accessible detached from the tree"); if (aAttribute == nsGkAtoms::id) { + // TODO(1983819): updates to referenceTarget should trigger these same + // actions dom::Element* elm = accessible->Elm(); RelocateARIAOwnedIfNeeded(elm); ARIAActiveDescendantIDMaybeMoved(accessible); diff --git a/accessible/generic/LocalAccessible.cpp b/accessible/generic/LocalAccessible.cpp @@ -4128,11 +4128,11 @@ already_AddRefed<AccAttributes> LocalAccessible::BundleFieldsForCache( if (data.mType == RelationType::LABEL_FOR) { // Labels are a special case -- we need to validate that the target of // their `for` attribute is in fact labelable. DOM checks this when we - // call GetControl(). If a label contains an element we will return it - // here. + // call GetLabeledElementInternal(). If a label contains an element we + // will return it here. if (dom::HTMLLabelElement* labelEl = dom::HTMLLabelElement::FromNode(mContent)) { - rel.AppendTarget(mDoc, labelEl->GetControl()); + rel.AppendTarget(mDoc, labelEl->GetLabeledElementInternal()); } } else if (data.mType == RelationType::DETAILS) { if (relAtom == nsGkAtoms::aria_details) { diff --git a/accessible/html/HTMLElementAccessibles.cpp b/accessible/html/HTMLElementAccessibles.cpp @@ -49,7 +49,7 @@ Relation HTMLLabelAccessible::RelationByType(RelationType aType) const { Relation rel = AccessibleWrap::RelationByType(aType); if (aType == RelationType::LABEL_FOR) { dom::HTMLLabelElement* label = dom::HTMLLabelElement::FromNode(mContent); - rel.AppendTarget(mDoc, label->GetControl()); + rel.AppendTarget(mDoc, label->GetLabeledElementInternal()); } return rel; diff --git a/dom/base/DocumentOrShadowRoot.cpp b/dom/base/DocumentOrShadowRoot.cpp @@ -586,62 +586,6 @@ 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,40 +186,6 @@ 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. @@ -317,42 +283,6 @@ 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 @@ -2262,16 +2262,13 @@ bool IDTargetChangedAttrAssociatedElementCallback(Element* aOldElement, nsWeakPtr weakElement = data->mElement; if (nsCOMPtr<Element> element = do_QueryReferent(weakElement)) { - DocumentOrShadowRoot* root = element->GetContainingDocumentOrShadowRoot(); if (aOldElement) { - root->RemoveReferenceTargetChangeObserver( - aOldElement, ReferenceTargetChangedAttrAssociatedElementCallback, - aData); + aOldElement->RemoveReferenceTargetChangeObserver( + ReferenceTargetChangedAttrAssociatedElementCallback, aData); } if (aNewElement) { - root->AddReferenceTargetChangeObserver( - aNewElement, ReferenceTargetChangedAttrAssociatedElementCallback, - aData); + aNewElement->AddReferenceTargetChangeObserver( + ReferenceTargetChangedAttrAssociatedElementCallback, aData); } return element->AttrAssociatedElementUpdated(data->mAttr); @@ -2406,9 +2403,8 @@ void Element::IDREFAttributeValueChanged(nsAtom* aAttr, Element* oldIdTarget = docOrShadow->GetElementById(observerData->mLastKnownAttrValue); if (oldIdTarget) { - docOrShadow->RemoveReferenceTargetChangeObserver( - oldIdTarget, ReferenceTargetChangedAttrAssociatedElementCallback, - callbackData); + oldIdTarget->RemoveReferenceTargetChangeObserver( + ReferenceTargetChangedAttrAssociatedElementCallback, callbackData); } } @@ -2425,9 +2421,8 @@ void Element::IDREFAttributeValueChanged(nsAtom* aAttr, Element* newIdTarget = docOrShadow->GetElementById(idValue); if (newIdTarget) { - docOrShadow->AddReferenceTargetChangeObserver( - newIdTarget, ReferenceTargetChangedAttrAssociatedElementCallback, - callbackData); + newIdTarget->AddReferenceTargetChangeObserver( + ReferenceTargetChangedAttrAssociatedElementCallback, callbackData); } } @@ -2467,8 +2462,7 @@ void Element::AddDocOrShadowObserversForAttrAssociatedElement( observerData->mCallbackData.get(); if (explicitlySetAttrElement) { - aContainingDocOrShadow.AddReferenceTargetChangeObserver( - explicitlySetAttrElement, + explicitlySetAttrElement->AddReferenceTargetChangeObserver( ReferenceTargetChangedAttrAssociatedElementCallback, callbackData); } else { MOZ_ASSERT(observerData->mLastKnownAttrValue); @@ -2479,9 +2473,8 @@ void Element::AddDocOrShadowObserversForAttrAssociatedElement( Element* idTarget = aContainingDocOrShadow.GetElementById( observerData->mLastKnownAttrValue); if (idTarget) { - aContainingDocOrShadow.AddReferenceTargetChangeObserver( - idTarget, ReferenceTargetChangedAttrAssociatedElementCallback, - callbackData); + idTarget->AddReferenceTargetChangeObserver( + ReferenceTargetChangedAttrAssociatedElementCallback, callbackData); } } } @@ -2503,8 +2496,7 @@ void Element::RemoveDocOrShadowObserversForAttrAssociatedElement( observerData->mCallbackData.get(); if (explicitlySetAttrElement) { - aContainingDocOrShadow.RemoveReferenceTargetChangeObserver( - explicitlySetAttrElement, + explicitlySetAttrElement->RemoveReferenceTargetChangeObserver( ReferenceTargetChangedAttrAssociatedElementCallback, callbackData); } else if (observerData->mLastKnownAttrValue) { aContainingDocOrShadow.RemoveIDTargetObserver( @@ -2515,9 +2507,8 @@ void Element::RemoveDocOrShadowObserversForAttrAssociatedElement( Element* idTarget = aContainingDocOrShadow.GetElementById( observerData->mLastKnownAttrValue); if (idTarget) { - aContainingDocOrShadow.RemoveReferenceTargetChangeObserver( - idTarget, ReferenceTargetChangedAttrAssociatedElementCallback, - callbackData); + idTarget->RemoveReferenceTargetChangeObserver( + ReferenceTargetChangedAttrAssociatedElementCallback, callbackData); } } } @@ -2542,6 +2533,63 @@ void Element::UnbindAttrAssociatedElementObservers( } } +void Element::AddReferenceTargetChangeObserver( + ReferenceTargetChangeObserver aObserver, void* aData) { + if (!StaticPrefs::dom_shadowdom_referenceTarget_enabled()) { + return; + } + ExtendedDOMSlots()->mReferenceTargetObservers.Insert({aObserver, aData}); +} + +void Element::RemoveReferenceTargetChangeObserver( + ReferenceTargetChangeObserver aObserver, void* aData) { + if (!StaticPrefs::dom_shadowdom_referenceTarget_enabled()) { + return; + } + nsExtendedDOMSlots* slots = GetExistingExtendedDOMSlots(); + if (!slots) { + return; + } + slots->mReferenceTargetObservers.Remove({aObserver, aData}); +} + +void Element::NotifyReferenceTargetChanged() { + using ReferenceTargetChangeCallback = + FragmentOrElement::nsExtendedDOMSlots::ReferenceTargetChangeCallback; + + nsExtendedDOMSlots* slots = GetExistingExtendedDOMSlots(); + if (!slots) { + return; + } + + // TODO (bug 1983819): Adjust initial N for the increased number of callbacks + // once accessibility code is listening for reference target changes. + // At time of writing, 2 accounts for: + // - (Rarely) observer in ShadowRoot for nested shadow roots, and EITHER + // - Observer in nsLabelsNodeList to update the .labels property, OR + // - Observer added via AddAttrAssociatedElementObserver() call in + // nsGenericHTMLFormElement for form-associated elements using the form + // content attribute, to ensure the form's .elements list is updated. + AutoTArray<ReferenceTargetChangeCallback, 2> callbacks; + callbacks.SetCapacity(slots->mReferenceTargetObservers.Count()); + for (auto iter = slots->mReferenceTargetObservers.begin(); + iter != slots->mReferenceTargetObservers.end(); ++iter) { + const ReferenceTargetChangeCallback& from = *iter; + ReferenceTargetChangeCallback callback({from.mObserver, from.mData}); + callbacks.AppendElement(callback); + } + + for (const ReferenceTargetChangeCallback& callback : callbacks) { + if (!slots->mReferenceTargetObservers.Contains(callback)) { + continue; + } + bool keep = callback.mObserver(callback.mData); + if (!keep) { + slots->mReferenceTargetObservers.Remove(callback); + } + } +} + void Element::GetElementsWithGrid(nsTArray<RefPtr<Element>>& aElements) { dom::TreeIterator<dom::StyleChildrenIterator> iter(*this); while (nsIContent* cur = iter.GetCurrent()) { @@ -3687,7 +3735,7 @@ bool Element::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, return true; } - if (aAttribute == nsGkAtoms::form) { + if (aAttribute == nsGkAtoms::form || aAttribute == nsGkAtoms::_for) { aResult.ParseAtom(aValue); return true; } diff --git a/dom/base/Element.h b/dom/base/Element.h @@ -1460,6 +1460,45 @@ class Element : public FragmentOrElement { Maybe<nsTArray<RefPtr<dom::Element>>> GetExplicitlySetAttrElements( nsAtom* aAttr) const; + /** + * Callback called when an element's resolved reference target changes. + * @param aData The callback data which was stored using + * AddReferenceTargetChangeObserver. + * @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); + * - Recursively: that is, when 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(ReferenceTargetChangeObserver aObserver, + void* aData); + void RemoveReferenceTargetChangeObserver( + ReferenceTargetChangeObserver aObserver, void* aData); + /** + * Called when aElement's resolved reference target changes. + * @param aElement the element whose reference target has changed + */ + void NotifyReferenceTargetChanged(); + PseudoStyleType GetPseudoElementType() const { nsresult rv = NS_OK; auto raw = GetProperty(nsGkAtoms::pseudoProperty, &rv); diff --git a/dom/base/FragmentOrElement.h b/dom/base/FragmentOrElement.h @@ -309,6 +309,46 @@ class FragmentOrElement : public nsIContent { UniquePtr<AttrElementObserverCallbackData> mCallbackData; }; nsTHashMap<RefPtr<nsAtom>, AttrElementObserverData> mAttrElementObserverMap; + + /** + * Callback called when an element's resolved reference target changes. + * @param aData The callback data which was stored using + * AddReferenceTargetChangeObserver. + * @return true to keep the callback in the callback set, false to remove + * it. + */ + typedef bool (*ReferenceTargetChangeObserver)(void* aData); + + 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; + }; + nsTHashSet<ReferenceTargetChangeCallbackEntry> mReferenceTargetObservers; }; class nsDOMSlots : public nsIContent::nsContentSlots { diff --git a/dom/base/ShadowRoot.cpp b/dom/base/ShadowRoot.cpp @@ -946,12 +946,12 @@ bool ShadowRoot::ReferenceTargetIDTargetChanged(Element* aOldElement, void* aData) { ShadowRoot* shadowRoot = static_cast<ShadowRoot*>(aData); if (aOldElement) { - shadowRoot->RemoveReferenceTargetChangeObserver( - aOldElement, RecursiveReferenceTargetChanged, shadowRoot); + aOldElement->RemoveReferenceTargetChangeObserver( + RecursiveReferenceTargetChanged, shadowRoot); } if (aNewElement) { - shadowRoot->AddReferenceTargetChangeObserver( - aNewElement, RecursiveReferenceTargetChanged, shadowRoot); + aNewElement->AddReferenceTargetChangeObserver( + RecursiveReferenceTargetChanged, shadowRoot); } shadowRoot->NotifyReferenceTargetChangedObservers(); return true; @@ -976,6 +976,10 @@ void ShadowRoot::SetReferenceTarget(RefPtr<nsAtom> aTarget) { if (mReferenceTarget) { RemoveIDTargetObserver(mReferenceTarget, ReferenceTargetIDTargetChanged, this, false); + if (Element* oldElement = GetReferenceTargetElement()) { + oldElement->RemoveReferenceTargetChangeObserver( + RecursiveReferenceTargetChanged, this); + } } if (!aTarget) { @@ -986,8 +990,8 @@ void ShadowRoot::SetReferenceTarget(RefPtr<nsAtom> aTarget) { Element* referenceTargetElement = AddIDTargetObserver( mReferenceTarget, ReferenceTargetIDTargetChanged, this, false); if (referenceTargetElement) { - AddReferenceTargetChangeObserver(referenceTargetElement, - RecursiveReferenceTargetChanged, this); + referenceTargetElement->AddReferenceTargetChangeObserver( + RecursiveReferenceTargetChanged, this); } } @@ -999,9 +1003,5 @@ void ShadowRoot::NotifyReferenceTargetChangedObservers() { if (!host) { return; } - - DocumentOrShadowRoot* root = host->GetContainingDocumentOrShadowRoot(); - if (root) { - root->NotifyReferenceTargetChanged(host); - } + host->NotifyReferenceTargetChanged(); } diff --git a/dom/base/nsContentList.cpp b/dom/base/nsContentList.cpp @@ -22,6 +22,7 @@ #include "mozilla/dom/Document.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/HTMLCollectionBinding.h" +#include "mozilla/dom/HTMLLabelElement.h" #include "mozilla/dom/NodeInfoInlines.h" #include "mozilla/dom/NodeListBinding.h" #include "nsCCUncollectableMarker.h" @@ -883,6 +884,10 @@ bool nsContentList::MatchSelf(nsIContent* aContent) { return false; } +nsINode* nsContentList::GetNextNode(nsINode* aCurrent) { + return aCurrent->GetNextNode(mRootNode); +} + void nsContentList::PopulateSelf(uint32_t aNeededLength, uint32_t aExpectedElementsIfDirty) { if (!mRootNode) { @@ -908,7 +913,7 @@ void nsContentList::PopulateSelf(uint32_t aNeededLength, // start searching at the root. nsINode* cur = count ? mElements[count - 1].get() : mRootNode; do { - cur = cur->GetNextNode(mRootNode); + cur = GetNextNode(cur); if (!cur) { break; } @@ -1093,17 +1098,50 @@ JSObject* nsCacheableFuncStringHTMLCollection::WrapObject( //----------------------------------------------------- // nsLabelsNodeList +nsLabelsNodeList::nsLabelsNodeList(nsGenericHTMLElement* aLabeledElement, + nsINode* aSubtreeRoot, + nsContentListMatchFunc aMatchFunc, + nsContentListDestroyFunc aDestroyFunc) + : nsContentList(aSubtreeRoot, aMatchFunc, aDestroyFunc, aLabeledElement) { + WatchLabeledDescendantsOfNearestAncestorLabel(aLabeledElement); + if (ShadowRoot* shadow = ShadowRoot::FromNodeOrNull(aSubtreeRoot)) { + shadow->Host()->AddReferenceTargetChangeObserver(ResetRootsCallback, this); + } + mRoots.AppendElement(aSubtreeRoot); + ResetRoots(); +} + +nsLabelsNodeList::~nsLabelsNodeList() { + for (nsINode* root : mRoots) { + root->RemoveMutationObserver(this); + if (ShadowRoot* shadow = ShadowRoot::FromNodeOrNull(root)) { + Element* host = shadow->GetHost(); + if (host) { + host->RemoveReferenceTargetChangeObserver(ResetRootsCallback, this); + } + } + } +} + JSObject* nsLabelsNodeList::WrapObject(JSContext* cx, JS::Handle<JSObject*> aGivenProto) { return NodeList_Binding::Wrap(cx, this, aGivenProto); } +bool nsLabelsNodeList::NodeIsInScope(nsINode* aNode) { + for (nsINode* root : mRoots) { + if (nsContentUtils::IsInSameAnonymousTree(root, aNode)) { + return true; + } + } + return false; +} + void nsLabelsNodeList::AttributeChanged(Element* aElement, int32_t aNameSpaceID, nsAtom* aAttribute, AttrModType, const nsAttrValue* aOldValue) { MOZ_ASSERT(aElement, "Must have a content node to work with"); - if (mState == State::Dirty || - !nsContentUtils::IsInSameAnonymousTree(mRootNode, aElement)) { + if (mState == State::Dirty || !NodeIsInScope(aElement)) { return; } @@ -1120,11 +1158,9 @@ void nsLabelsNodeList::AttributeChanged(Element* aElement, int32_t aNameSpaceID, void nsLabelsNodeList::ContentAppended(nsIContent* aFirstNewContent, const ContentAppendInfo&) { nsIContent* container = aFirstNewContent->GetParent(); - // If a labelable element is moved to outside or inside of - // nested associated labels, we're gonna have to modify - // the content list. - if (mState != State::Dirty && - nsContentUtils::IsInSameAnonymousTree(mRootNode, container)) { + // If a labelable element is moved to outside or inside of nested associated + // labels, we're gonna have to modify the content list. + if (mState != State::Dirty && NodeIsInScope(container)) { SetDirty(); return; } @@ -1132,11 +1168,9 @@ void nsLabelsNodeList::ContentAppended(nsIContent* aFirstNewContent, void nsLabelsNodeList::ContentInserted(nsIContent* aChild, const ContentInsertInfo&) { - // If a labelable element is moved to outside or inside of - // nested associated labels, we're gonna have to modify - // the content list. - if (mState != State::Dirty && - nsContentUtils::IsInSameAnonymousTree(mRootNode, aChild)) { + // If a labelable element is moved to outside or inside of nested associated + // labels, we're gonna have to modify the content list. + if (mState != State::Dirty && NodeIsInScope(aChild)) { SetDirty(); return; } @@ -1144,36 +1178,184 @@ void nsLabelsNodeList::ContentInserted(nsIContent* aChild, void nsLabelsNodeList::ContentWillBeRemoved(nsIContent* aChild, const ContentRemoveInfo&) { - // If a labelable element is removed, we're gonna have to clean - // the content list. - if (mState != State::Dirty && - nsContentUtils::IsInSameAnonymousTree(mRootNode, aChild)) { + // If a labelable element is removed, we're gonna have to clean the content + // list. + if (mState != State::Dirty && NodeIsInScope(aChild)) { SetDirty(); return; } } -void nsLabelsNodeList::MaybeResetRoot(nsINode* aRootNode) { - MOZ_ASSERT(aRootNode, "Must have root"); - if (mRootNode == aRootNode) { +void nsLabelsNodeList::NodeWillBeDestroyed(nsINode* aNode) { + if (ShadowRoot* shadow = ShadowRoot::FromNodeOrNull(aNode)) { + if (Element* host = shadow->GetHost()) { + host->RemoveReferenceTargetChangeObserver(ResetRootsCallback, this); + } + } + mRoots.RemoveElement(aNode); +} + +// static +bool nsLabelsNodeList::ResetRootsCallback(void* aData) { + nsLabelsNodeList* list = (nsLabelsNodeList*)aData; + list->ResetRoots(); + return true; +} + +// static +bool nsLabelsNodeList::SetDirtyCallback(void* aData) { + nsLabelsNodeList* list = (nsLabelsNodeList*)aData; + list->SetDirty(); + return true; +} + +void nsLabelsNodeList::WatchLabeledDescendantsOfNearestAncestorLabel( + Element* labeledHost) { + if (!StaticPrefs::dom_shadowdom_referenceTarget_enabled()) { return; } + MOZ_ASSERT(labeledHost); + Element* parentElement = labeledHost->GetParentElement(); + while (parentElement) { + if (HTMLLabelElement* label = HTMLLabelElement::FromNode(parentElement)) { + // Use GetControlForBindings() to get the element in the same scope as the + // label, instead of the deep labeled element. + if (Element* labeledElement = label->GetControlForBindings()) { + if (labeledElement != labeledHost) { + // If the labeled element's reference target changes such that it's no + // longer labelable, our labeled element might become the target for + // the ancestor label. + labeledElement->AddReferenceTargetChangeObserver(SetDirtyCallback, + this); + } + } + return; + } + parentElement = parentElement->GetParentElement(); + } +} +void nsLabelsNodeList::ResetRoots() { MOZ_ASSERT(mIsLiveList, "nsLabelsNodeList is always a live list"); - if (mRootNode) { - mRootNode->RemoveMutationObserver(this); + + nsGenericHTMLElement* labeledElement = + static_cast<nsGenericHTMLElement*>(mData); + MOZ_ASSERT(labeledElement, "Must have labeled element"); + + nsTArray<nsINode*> newRoots; + + Element* labeledElementOrHost = labeledElement; + bool labeledElementOrHostIsInShadowTree = false; + ShadowRoot* shadowRoot = labeledElement->GetContainingShadow(); + while (shadowRoot) { + newRoots.AppendElement(shadowRoot); + // If reference target is not enabled, GetReferenceTargetElement() will + // always return nullptr. + if (shadowRoot->GetReferenceTargetElement() != labeledElementOrHost) { + labeledElementOrHostIsInShadowTree = true; + break; + } + labeledElementOrHost = shadowRoot->Host(); + WatchLabeledDescendantsOfNearestAncestorLabel(labeledElementOrHost); + shadowRoot = labeledElementOrHost->GetContainingShadow(); } - mRootNode = aRootNode; - mRootNode->AddMutationObserver(this); + + // If the outermost labeled element or host is in a shadow tree, its + // containing shadow root is already in newRoots. + if (!labeledElementOrHostIsInShadowTree) { + // `labeledHost` is either `labeledElement`, or the shadow host which has + // `labeledElement` as its resolved reference target. + DocumentOrShadowRoot* doc = labeledElementOrHost->GetUncomposedDoc(); + if (doc) { + newRoots.AppendElement(&doc->AsNode()); + } else if (newRoots.IsEmpty()) { + newRoots.AppendElement(labeledElementOrHost->SubtreeRoot()); + } + } + + if (newRoots == mRoots) { + return; + } + MOZ_ASSERT(!newRoots.IsEmpty(), "Must have at least one root"); + + for (nsINode* root : mRoots) { + if (!newRoots.Contains(root)) { + root->RemoveMutationObserver(this); + } + + // Only the outermost shadow root should have this as a + // ReferenceTargetChangedObserver, to avoid duplicated notifications. + if (ShadowRoot* shadow = ShadowRoot::FromNodeOrNull(root)) { + Element* host = shadow->GetHost(); + if (host) { + host->RemoveReferenceTargetChangeObserver(ResetRootsCallback, this); + } + } + } + for (nsINode* root : newRoots) { + if (!mRoots.Contains(root)) { + root->AddMutationObserver(this); + } + } + + mRoots = std::move(newRoots); + mRootNode = mRoots.LastElement(); + + if (labeledElementOrHostIsInShadowTree) { + ShadowRoot* shadow = ShadowRoot::FromNodeOrNull(mRootNode); + MOZ_ASSERT(shadow); + shadow->Host()->AddReferenceTargetChangeObserver(ResetRootsCallback, this); + } + labeledElementOrHost->AddReferenceTargetChangeObserver(ResetRootsCallback, + this); + SetDirty(); } -void nsLabelsNodeList::PopulateSelf(uint32_t aNeededLength, - uint32_t aExpectedElementsIfDirty) { - if (!mRootNode) { - return; +nsINode* nsLabelsNodeList::GetNextNode(nsINode* aCurrent) { + nsGenericHTMLElement* labeledElement = (nsGenericHTMLElement*)mData; + MOZ_ASSERT(labeledElement, "Must have labeled element"); + MOZ_ASSERT(mRootNode, "Must have root node"); + + nsINode* next = nullptr; + + // If aCurrent's resolved reference target is the labeled element, descend + // into aCurrent's shadow root, if it has one. (Otherwise, ignore shadow + // roots.) + if (aCurrent->IsElement()) { + Element* curElement = aCurrent->AsElement(); + ShadowRoot* curShadow = curElement->GetShadowRoot(); + if (curShadow && curElement->ResolveReferenceTarget() == labeledElement) { + next = curShadow->GetFirstChild(); + } + } + if (next) { + return next; + } + + // Default case: just get the next node in the current tree. + next = aCurrent->GetNextNode(); + if (next) { + return next; } + // If we descended into a shadow tree, back out of it until we find an + // adjacent node, or hit a shadow root which doesn't have the current element + // as its reference target. + nsINode* cur = aCurrent; + while (!next) { + ShadowRoot* shadow = cur->GetContainingShadow(); + if (!shadow || shadow->GetReferenceTargetElement() == cur) { + break; + } + cur = shadow->Host(); + next = cur->GetNextNode(); + } + return next; +} + +void nsLabelsNodeList::PopulateSelf(uint32_t aNeededLength, + uint32_t aExpectedElementsIfDirty) { // Start searching at the root. nsINode* cur = mRootNode; if (mElements.IsEmpty() && cur->IsElement() && Match(cur->AsElement())) { @@ -1183,3 +1365,17 @@ void nsLabelsNodeList::PopulateSelf(uint32_t aNeededLength, nsContentList::PopulateSelf(aNeededLength, aExpectedElementsIfDirty); } + +void nsLabelsNodeList::LastRelease() { + for (nsINode* root : mRoots) { + root->RemoveMutationObserver(this); + if (ShadowRoot* shadow = ShadowRoot::FromNodeOrNull(root)) { + if (Element* host = shadow->GetHost()) { + host->RemoveReferenceTargetChangeObserver(ResetRootsCallback, this); + } + } + } + mRoots.Clear(); + + nsContentList::LastRelease(); +} diff --git a/dom/base/nsContentList.h b/dom/base/nsContentList.h @@ -200,7 +200,7 @@ struct nsContentListKey { */ class nsContentList : public nsBaseContentList, public nsIHTMLCollection, - public nsStubMutationObserver { + public nsStubMultiMutationObserver { protected: enum class State : uint8_t { // The list is up to date and need not do any walking to be able to answer @@ -388,6 +388,8 @@ class nsContentList : public nsBaseContentList, */ bool MatchSelf(nsIContent* aContent); + virtual nsINode* GetNextNode(nsINode* aCurrent); + /** * Populate our list. Stop once we have at least aNeededLength * elements. At the end of PopulateSelf running, either the last @@ -619,25 +621,31 @@ class nsCacheableFuncStringHTMLCollection class nsLabelsNodeList final : public nsContentList { public: - nsLabelsNodeList(nsINode* aRootNode, nsContentListMatchFunc aFunc, - nsContentListDestroyFunc aDestroyFunc, void* aData) - : nsContentList(aRootNode, aFunc, aDestroyFunc, aData) {} + nsLabelsNodeList(nsGenericHTMLElement* aLabeledElement, nsINode* aSubtreeRoot, + nsContentListMatchFunc aMatchFunc, + nsContentListDestroyFunc aDestroyFunc); NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + NS_DECL_NSIMUTATIONOBSERVER_NODEWILLBEDESTROYED JSObject* WrapObject(JSContext* cx, JS::Handle<JSObject*> aGivenProto) override; /** - * Reset root, mutation observer, and clear content list - * if the root has been changed. - * - * @param aRootNode The node under which to limit our search. + * Reset roots, mutation observers and reference target observers, and clear + * content list if the roots have changed. */ - void MaybeResetRoot(nsINode* aRootNode); + void ResetRoots(); + + void LastRelease() override; + + protected: + virtual ~nsLabelsNodeList(); + + nsINode* GetNextNode(nsINode* aCurrent) override; private: /** @@ -651,5 +659,28 @@ class nsLabelsNodeList final : public nsContentList { */ void PopulateSelf(uint32_t aNeededLength, uint32_t aExpectedElementsIfDirty = 0) override; + + bool NodeIsInScope(nsINode* aNode); + + static bool ResetRootsCallback(void* aData); + static bool SetDirtyCallback(void* aData); + + void WatchLabeledDescendantsOfNearestAncestorLabel(Element* labeledHost); + + /** + * An array of all relevant subtree roots for the labeled element. + * + * A labeled element's labels may include nodes from multiple roots, since + * each shadow root may have a reference target allowing labels to refer to an + * element within the shadow root, potentially recusively. + * + * This structure is populated by walking up from the labeled element, + * adding each subtree root in turn and walking out to the next one if the + * labeled element or the host of the previous root is the reference target of + * its subtree root. + * + * The last element in this array must always be the same as mRootNode. + */ + nsTArray<nsINode*> mRoots; }; #endif // nsContentList_h___ diff --git a/dom/events/EventStateManager.cpp b/dom/events/EventStateManager.cpp @@ -6534,7 +6534,7 @@ static Element* GetLabelTarget(nsIContent* aPossibleLabel) { mozilla::dom::HTMLLabelElement::FromNode(aPossibleLabel); if (!label) return nullptr; - return label->GetLabeledElement(); + return label->GetLabeledElementInternal(); } /* static */ diff --git a/dom/html/ElementInternals.cpp b/dom/html/ElementInternals.cpp @@ -341,7 +341,7 @@ already_AddRefed<nsINodeList> ElementInternals::GetLabels( "Target element is not a form-associated custom element"); return nullptr; } - return mTarget->Labels(); + return mTarget->LabelsInternal(); } nsGenericHTMLElement* ElementInternals::GetValidationAnchor( diff --git a/dom/html/HTMLInputElement.cpp b/dom/html/HTMLInputElement.cpp @@ -7571,12 +7571,16 @@ void HTMLInputElement::GetWebkitEntries( aSequence.AppendElements(mFileData->mEntries); } -already_AddRefed<nsINodeList> HTMLInputElement::GetLabels() { +already_AddRefed<nsINodeList> HTMLInputElement::GetLabelsForBindings() { + return GetLabelsInternal(); +} + +already_AddRefed<nsINodeList> HTMLInputElement::GetLabelsInternal() { if (!IsLabelable()) { return nullptr; } - return nsGenericHTMLElement::Labels(); + return nsGenericHTMLElement::LabelsInternal(); } void HTMLInputElement::MaybeFireInputPasswordRemoved() { diff --git a/dom/html/HTMLInputElement.h b/dom/html/HTMLInputElement.h @@ -693,7 +693,8 @@ class HTMLInputElement final : public TextControlElement, // <input> element. bool StepsInputValue(const WidgetKeyboardEvent&) const; - already_AddRefed<nsINodeList> GetLabels(); + already_AddRefed<nsINodeList> GetLabelsForBindings(); + already_AddRefed<nsINodeList> GetLabelsInternal(); MOZ_CAN_RUN_SCRIPT void Select(); diff --git a/dom/html/HTMLLabelElement.cpp b/dom/html/HTMLLabelElement.cpp @@ -43,7 +43,8 @@ Element* HTMLLabelElement::GetFormForBindings() const { HTMLFormElement* HTMLLabelElement::GetFormInternal() const { // Not all labeled things have a form association. Stick to the ones that do. - const auto* formControl = nsIFormControl::FromNodeOrNull(GetControl()); + const auto* formControl = + nsIFormControl::FromNodeOrNull(GetLabeledElementInternal()); if (!formControl) { return nullptr; } @@ -51,6 +52,17 @@ HTMLFormElement* HTMLLabelElement::GetFormInternal() const { return formControl->GetFormInternal(); } +nsGenericHTMLElement* HTMLLabelElement::GetControlForBindings() const { + nsINode* retargeted = + nsContentUtils::Retarget(GetLabeledElementInternal(), this); + if (!retargeted) { + return nullptr; + } + Element* element = retargeted->AsElement(); + MOZ_ASSERT(element); + return static_cast<nsGenericHTMLElement*>(element); +} + void HTMLLabelElement::Focus(const FocusOptions& aOptions, const CallerType aCallerType, ErrorResult& aError) { @@ -61,7 +73,7 @@ void HTMLLabelElement::Focus(const FocusOptions& aOptions, } } - if (RefPtr<Element> elem = GetLabeledElement()) { + if (RefPtr<Element> elem = GetLabeledElementInternal()) { return elem->Focus(aOptions, aCallerType, aError); } } @@ -85,7 +97,7 @@ nsresult HTMLLabelElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { } // Strong ref because event dispatch is going to happen. - RefPtr<Element> content = GetLabeledElement(); + RefPtr<Element> content = GetLabeledElementInternal(); if (!content || content->IsDisabled()) { return NS_OK; @@ -181,7 +193,7 @@ nsresult HTMLLabelElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { Result<bool, nsresult> HTMLLabelElement::PerformAccesskey( bool aKeyCausesActivation, bool aIsTrustedEvent) { if (!aKeyCausesActivation) { - RefPtr<Element> element = GetLabeledElement(); + RefPtr<Element> element = GetLabeledElementInternal(); if (element) { return element->PerformAccesskey(aKeyCausesActivation, aIsTrustedEvent); } @@ -203,7 +215,7 @@ Result<bool, nsresult> HTMLLabelElement::PerformAccesskey( return true; } -nsGenericHTMLElement* HTMLLabelElement::GetLabeledElement() const { +nsGenericHTMLElement* HTMLLabelElement::GetLabeledElementInternal() const { nsAutoString elementId; if (!GetAttr(nsGkAtoms::_for, elementId)) { @@ -214,17 +226,7 @@ nsGenericHTMLElement* HTMLLabelElement::GetLabeledElement() const { // We have a @for. The id has to be linked to an element in the same tree // and this element should be a labelable form control. - Element* element = nullptr; - - if (ShadowRoot* shadowRoot = GetContainingShadow()) { - element = shadowRoot->GetElementById(elementId); - } else if (Document* doc = GetUncomposedDoc()) { - element = doc->GetElementById(elementId); - } else { - element = - nsContentUtils::MatchElementId(SubtreeRoot()->AsContent(), elementId); - } - + Element* element = GetAttrAssociatedElementInternal(nsGkAtoms::_for); if (element && element->IsLabelable()) { return static_cast<nsGenericHTMLElement*>(element); } @@ -236,8 +238,11 @@ nsGenericHTMLElement* HTMLLabelElement::GetFirstLabelableDescendant() const { for (nsIContent* cur = nsINode::GetFirstChild(); cur; cur = cur->GetNextNode(this)) { Element* element = Element::FromNode(cur); - if (element && element->IsLabelable()) { - return static_cast<nsGenericHTMLElement*>(element); + if (element) { + Element* referenceTarget = element->ResolveReferenceTarget(); + if (referenceTarget && referenceTarget->IsLabelable()) { + return static_cast<nsGenericHTMLElement*>(referenceTarget); + } } } diff --git a/dom/html/HTMLLabelElement.h b/dom/html/HTMLLabelElement.h @@ -39,7 +39,7 @@ class HTMLLabelElement final : public nsGenericHTMLElement { void SetHtmlFor(const nsAString& aHtmlFor) { SetHTMLAttr(nsGkAtoms::_for, aHtmlFor); } - nsGenericHTMLElement* GetControl() const { return GetLabeledElement(); } + nsGenericHTMLElement* GetControlForBindings() const; using nsGenericHTMLElement::Focus; virtual void Focus(const FocusOptions& aOptions, @@ -54,7 +54,7 @@ class HTMLLabelElement final : public nsGenericHTMLElement { bool aKeyCausesActivation, bool aIsTrustedEvent) override; virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; - nsGenericHTMLElement* GetLabeledElement() const; + nsGenericHTMLElement* GetLabeledElementInternal() const; protected: virtual ~HTMLLabelElement(); diff --git a/dom/html/nsGenericHTMLElement.cpp b/dom/html/nsGenericHTMLElement.cpp @@ -427,7 +427,7 @@ nsresult nsGenericHTMLElement::BindToTree(BindContext& aContext, // as well. nsExtendedDOMSlots* slots = GetExistingExtendedDOMSlots(); if (slots && slots->mLabelsList) { - slots->mLabelsList->MaybeResetRoot(SubtreeRoot()); + slots->mLabelsList->ResetRoots(); } return rv; @@ -475,7 +475,7 @@ void nsGenericHTMLElement::UnbindFromTree(UnbindContext& aContext) { // Invalidate .labels list. It will be repopulated when used the next time. nsExtendedDOMSlots* slots = GetExistingExtendedDOMSlots(); if (slots && slots->mLabelsList) { - slots->mLabelsList->MaybeResetRoot(SubtreeRoot()); + slots->mLabelsList->ResetRoots(); } } @@ -1742,17 +1742,21 @@ bool nsGenericHTMLElement::MatchLabelsElement(Element* aElement, int32_t aNamespaceID, nsAtom* aAtom, void* aData) { HTMLLabelElement* element = HTMLLabelElement::FromNode(aElement); - return element && element->GetControl() == aData; + return element && element->GetLabeledElementInternal() == aData; } -already_AddRefed<nsINodeList> nsGenericHTMLElement::Labels() { +already_AddRefed<nsINodeList> nsGenericHTMLElement::LabelsForBindings() { + return LabelsInternal(); +} + +already_AddRefed<nsINodeList> nsGenericHTMLElement::LabelsInternal() { MOZ_ASSERT(IsLabelable(), "Labels() only allow labelable elements to use it."); nsExtendedDOMSlots* slots = ExtendedDOMSlots(); if (!slots->mLabelsList) { slots->mLabelsList = - new nsLabelsNodeList(SubtreeRoot(), MatchLabelsElement, nullptr, this); + new nsLabelsNodeList(this, SubtreeRoot(), MatchLabelsElement, nullptr); } RefPtr<nsLabelsNodeList> labels = slots->mLabelsList; diff --git a/dom/html/nsGenericHTMLElement.h b/dom/html/nsGenericHTMLElement.h @@ -681,7 +681,8 @@ class nsGenericHTMLElement : public nsGenericHTMLElementBase { static bool MatchLabelsElement(Element* aElement, int32_t aNamespaceID, nsAtom* aAtom, void* aData); - already_AddRefed<nsINodeList> Labels(); + already_AddRefed<nsINodeList> LabelsForBindings(); + already_AddRefed<nsINodeList> LabelsInternal(); static bool LegacyTouchAPIEnabled(JSContext* aCx, JSObject* aObj); diff --git a/dom/webidl/HTMLButtonElement.webidl b/dom/webidl/HTMLButtonElement.webidl @@ -44,6 +44,7 @@ interface HTMLButtonElement : HTMLElement { boolean reportValidity(); undefined setCustomValidity(DOMString error); + [BinaryName=labelsForBindings] readonly attribute NodeList labels; [Pref="dom.element.commandfor.enabled", BinaryName="commandForElementForBindings", CEReactions] attribute Element? commandForElement; diff --git a/dom/webidl/HTMLInputElement.webidl b/dom/webidl/HTMLInputElement.webidl @@ -116,6 +116,7 @@ interface HTMLInputElement : HTMLElement { boolean reportValidity(); undefined setCustomValidity(DOMString error); + [BinaryName=labelsForBindings] readonly attribute NodeList? labels; undefined select(); diff --git a/dom/webidl/HTMLLabelElement.webidl b/dom/webidl/HTMLLabelElement.webidl @@ -18,5 +18,5 @@ interface HTMLLabelElement : HTMLElement { [BinaryName=formForBindings] readonly attribute Element? form; [CEReactions] attribute DOMString htmlFor; - readonly attribute HTMLElement? control; + [BinaryName="controlForBindings"] readonly attribute HTMLElement? control; }; diff --git a/dom/webidl/HTMLMeterElement.webidl b/dom/webidl/HTMLMeterElement.webidl @@ -28,5 +28,6 @@ interface HTMLMeterElement : HTMLElement { attribute double high; [CEReactions, SetterThrows] attribute double optimum; + [BinaryName=labelsForBindings] readonly attribute NodeList labels; }; diff --git a/dom/webidl/HTMLOutputElement.webidl b/dom/webidl/HTMLOutputElement.webidl @@ -38,5 +38,6 @@ interface HTMLOutputElement : HTMLElement { boolean reportValidity(); undefined setCustomValidity(DOMString error); + [BinaryName=labelsForBindings] readonly attribute NodeList labels; }; diff --git a/dom/webidl/HTMLProgressElement.webidl b/dom/webidl/HTMLProgressElement.webidl @@ -20,5 +20,6 @@ interface HTMLProgressElement : HTMLElement { [CEReactions, SetterThrows] attribute double max; readonly attribute double position; + [BinaryName=labelsForBindings] readonly attribute NodeList labels; }; diff --git a/dom/webidl/HTMLSelectElement.webidl b/dom/webidl/HTMLSelectElement.webidl @@ -59,6 +59,7 @@ interface HTMLSelectElement : HTMLElement { [Throws, Pref="dom.select.showPicker.enabled"] undefined showPicker(); + [BinaryName=labelsForBindings] readonly attribute NodeList labels; // https://www.w3.org/Bugs/Public/show_bug.cgi?id=20720 diff --git a/dom/webidl/HTMLTextAreaElement.webidl b/dom/webidl/HTMLTextAreaElement.webidl @@ -62,6 +62,7 @@ interface HTMLTextAreaElement : HTMLElement { boolean reportValidity(); undefined setCustomValidity(DOMString error); + [BinaryName=labelsForBindings] readonly attribute NodeList labels; undefined select(); diff --git a/testing/web-platform/meta/shadow-dom/reference-target/tentative/dom-mutation.html.ini b/testing/web-platform/meta/shadow-dom/reference-target/tentative/dom-mutation.html.ini @@ -0,0 +1,6 @@ +[dom-mutation.html] + [.labels property is updated when for attribute changes on label outside of shadow root] + expected: FAIL + + [.labels property is updated when ID changes on input] + expected: FAIL diff --git a/testing/web-platform/meta/shadow-dom/reference-target/tentative/label-descendant.html.ini b/testing/web-platform/meta/shadow-dom/reference-target/tentative/label-descendant.html.ini @@ -1,12 +0,0 @@ -[label-descendant.html] - [Label applies to descendant custom element that uses shadowrootreferencetarget (Input 1)] - expected: FAIL - - [Label applies to descendant custom element that uses shadowrootreferencetarget (Input 1 via Options)] - expected: FAIL - - [Label applies to multiple layers of descendant custom elements that use shadowrootreferencetarget (Input 2)] - expected: FAIL - - [Label applies to multiple layers of descendant custom elements that use shadowrootreferencetarget (Input 2 via Options)] - expected: FAIL diff --git a/testing/web-platform/meta/shadow-dom/reference-target/tentative/label-for.html.ini b/testing/web-platform/meta/shadow-dom/reference-target/tentative/label-for.html.ini @@ -1,36 +1 @@ [label-for.html] - [Label for attribute targets a custom element using shadowrootreferencetarget] - expected: FAIL - - [Label for attribute targets a custom element using shadowrootreferencetarget inside multiple layers of shadow roots] - expected: FAIL - - [Multiple labels targeting a custom element using shadowrootreferencetarget inside multiple layers of shadow roots] - expected: FAIL - - [Setting .htmlFor property to target a custom element using shadowrootreferencetarget] - expected: FAIL - - [Implicit <label> association should work with a custom element targeting 'button'] - expected: FAIL - - [Implicit <label> association should work with a custom element targeting 'input'] - expected: FAIL - - [Implicit <label> association should work with a custom element targeting 'meter'] - expected: FAIL - - [Implicit <label> association should work with a custom element targeting 'output'] - expected: FAIL - - [Implicit <label> association should work with a custom element targeting 'progress'] - expected: FAIL - - [Implicit <label> association should work with a custom element targeting 'select'] - expected: FAIL - - [Implicit <label> association should work with a custom element targeting 'textarea'] - expected: FAIL - - [Implicit <label> association should apply to only the first labelable custom element] - 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,21 +0,0 @@ -[property-reflection-imperative-setup.html] - [label.control has reflection behavior ReflectsHostReadOnly when pointing to button with reference target with imperative setup] - expected: FAIL - - [label.control has reflection behavior ReflectsHostReadOnly when pointing to input with reference target with imperative setup] - expected: FAIL - - [label.control has reflection behavior ReflectsHostReadOnly when pointing to meter with reference target with imperative setup] - expected: FAIL - - [label.control has reflection behavior ReflectsHostReadOnly when pointing to output with reference target with imperative setup] - expected: FAIL - - [label.control has reflection behavior ReflectsHostReadOnly when pointing to progress with reference target with imperative setup] - expected: FAIL - - [label.control has reflection behavior ReflectsHostReadOnly when pointing to select with reference target with imperative setup] - expected: FAIL - - [label.control has reflection behavior ReflectsHostReadOnly when pointing to textarea 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 @@ -1,25 +1,2 @@ [property-reflection.html] prefs: [accessibility.ARIAElementReflection.enabled:true] - [The .labels property of the referenced input element should point to the referencing label element] - expected: FAIL - - [label.control has reflection behavior ReflectsHostReadOnly when pointing to button with reference target] - expected: FAIL - - [label.control has reflection behavior ReflectsHostReadOnly when pointing to input with reference target] - expected: FAIL - - [label.control has reflection behavior ReflectsHostReadOnly when pointing to meter with reference target] - expected: FAIL - - [label.control has reflection behavior ReflectsHostReadOnly when pointing to output with reference target] - expected: FAIL - - [label.control has reflection behavior ReflectsHostReadOnly when pointing to progress with reference target] - expected: FAIL - - [label.control has reflection behavior ReflectsHostReadOnly when pointing to select with reference target] - expected: FAIL - - [label.control has reflection behavior ReflectsHostReadOnly when pointing to textarea with reference target] - expected: FAIL diff --git a/testing/web-platform/tests/shadow-dom/reference-target/tentative/dom-mutation.html b/testing/web-platform/tests/shadow-dom/reference-target/tentative/dom-mutation.html @@ -33,6 +33,7 @@ promise_test(async t => { const test_container = await setup_test(); const host1 = test_container.querySelector("#host1"); const label1 = host1.shadowRoot.querySelector("#label1"); + label1.id = "new_id"; assert_equals(await test_driver.get_computed_label(input1), ""); }, "Changing the ID of the referenced element results in an empty computed label"); @@ -109,7 +110,93 @@ promise_test(async t => { real_label1.id = "new_id"; assert_equals(await test_driver.get_computed_label(input1), ""); }, "Changing the ID of the nested referenced element results in an empty computed label"); +</script> + +<label id="label2" for="x-input2">Input 2</label> +<x-input2 id="x-input2"> + <template shadowrootmode="open" shadowrootreferencetarget="input2"> + <input id="input2"> + </template> +</x-input2> + +<script> +promise_test(async t => { + const x_input = document.getElementById('x-input2'); + const input = x_input.shadowRoot.getElementById('input2'); + const label = document.getElementById('label2'); + label.htmlFor = ''; + assert_array_equals(Array.from(input['labels']), []); + + label.htmlFor = x_input.id; + assert_array_equals(Array.from(input['labels']), [label]); +}, ".labels property is updated when for attribute changes on label outside of shadow root"); </script> + +<label id="label3" for="x-input3">Input 3</label> +<x-input3 id="x-input3"> + <template shadowrootmode="open" shadowrootreferencetarget="input3"> + <input id="input3"> + </template> +</x-input3> + +<script> +promise_test(async t => { + const x_input = document.getElementById('x-input3'); + const input = x_input.shadowRoot.getElementById('input3'); + const label = document.getElementById('label3'); + + x_input.removeAttribute('id'); + assert_array_equals(Array.from(input['labels']), []); + + x_input.id = label.htmlFor; + assert_array_equals(Array.from(input['labels']), [label]); +}, ".labels property is updated when ID changes on input"); +</script> + +<label id="label4" for="x-input4">Input 4</label> +<x-input4 id="x-input4"> + <template shadowrootmode="open" shadowrootreferencetarget="input4"> + <input id="input4"> + </template> +</x-input4> + +<script> +promise_test(async t => { + const x_input = document.getElementById('x-input4'); + const input = x_input.shadowRoot.getElementById('input4'); + const label = document.getElementById('label4'); + + x_input.shadowRoot.referenceTarget = null; + assert_array_equals(Array.from(input['labels']), []); + + x_input.shadowRoot.referenceTarget = input.id; + assert_array_equals(Array.from(input['labels']), [label]); +}, ".labels property is updated when reference target changes on shadow root"); +</script> + +<label id="label5-A">A + <x-input5 id="x-input5"> + <template shadowrootmode="open" shadowrootreferencetarget="input5"> + <input id="input5"> + </template> + </x-input5> +</label> +<label id="label5-B"></label> + +<script> +promise_test(async t => { + const x_input = document.getElementById('x-input5'); + const input = x_input.shadowRoot.getElementById('input5'); + const A = document.getElementById('label5-A'); + + assert_array_equals(Array.from(input['labels']), [A]); + + const B = document.getElementById('label5-B'); + B.appendChild(x_input); + assert_array_equals(Array.from(input['labels']), [B]); +}, ".labels property is updated when wrapped label changes"); +</script> + </body> </html> diff --git a/testing/web-platform/tests/shadow-dom/reference-target/tentative/label-descendant.html b/testing/web-platform/tests/shadow-dom/reference-target/tentative/label-descendant.html @@ -2,6 +2,7 @@ <html> <head> +<script src="/html/resources/common.js"></script> <script src="/resources/testharness.js"></script> <script src="/resources/testharnessreport.js"></script> <script src="/resources/testdriver.js"></script> @@ -97,6 +98,212 @@ testDeepImplicitLabelAssociation('x-outer2-a', 'Input 2 via Options'); </script> + +<div id="test-container4"></div> +<script> +// The HTML5_LABELABLE_ELEMENTS are defined in https://html.spec.whatwg.org/#category-label +for(let referenced_element_type of HTML5_LABELABLE_ELEMENTS) { + promise_test(async t => { + const test_container = document.querySelector("#test-container4"); + test_container.setHTMLUnsafe(` + <label> + <fancy-element id="fancy"> + <template shadowrootmode="open" shadowrootreferencetarget="target"> + <${referenced_element_type} id="target"></${referenced_element_type}> + </template> + </fancy-element> + fancy custom element inside label + </label>`); + + const fancy_element = document.getElementById('fancy'); + const target_element = fancy_element.shadowRoot.getElementById('target'); + + assert_equals(await test_driver.get_computed_label(fancy_element), ""); + assert_equals(await test_driver.get_computed_label(target_element), "fancy custom element inside label"); + }, "Implicit <label> association should work with a custom element targeting '" + referenced_element_type + "' for computed name"); + + promise_test(async t => { + const test_container = document.querySelector("#test-container4"); + test_container.setHTMLUnsafe(` + <label> + <fancy-element id="fancy"> + <template shadowrootmode="open" shadowrootreferencetarget="target"> + <${referenced_element_type} id="target"></${referenced_element_type}> + </template> + </fancy-element> + fancy custom element inside label + </label>`); + + const fancy_element = document.getElementById('fancy'); + const target_element = fancy_element.shadowRoot.getElementById('target'); + const label = test_container.querySelector('label'); + + assert_array_equals(Array.from(target_element['labels']), [label]); + }, "Implicit <label> association should work with a custom element targeting '" + referenced_element_type + "' for .labels"); +} +</script> + + +<label id="label5"> + <fancy-input id="fancy-input5-1"> + <template shadowrootmode="open" shadowrootreferencetarget="real-input5-1"> + <input id="real-input5-1"> + </template> + </fancy-input> + <fancy-input id="fancy-input5-2"> + <template shadowrootmode="open" shadowrootreferencetarget="real-input5-2"> + <input id="real-input5-2"> + </template> + </fancy-input> + fancy input inside label +</label> +<script> + promise_test(async t => { + const fancy_input1 = document.getElementById('fancy-input5-1'); + const fancy_input2 = document.getElementById('fancy-input5-2'); + const real_input1 = fancy_input1.shadowRoot.getElementById('real-input5-1'); + const real_input2 = fancy_input2.shadowRoot.getElementById('real-input5-2'); + + assert_equals(await test_driver.get_computed_label(fancy_input1), ""); + assert_equals(await test_driver.get_computed_label(fancy_input2), ""); + assert_equals(await test_driver.get_computed_label(real_input1), "fancy input inside label"); + assert_equals(await test_driver.get_computed_label(real_input2), ""); + + fancy_input2.after(fancy_input1); + assert_equals(await test_driver.get_computed_label(fancy_input1), ""); + assert_equals(await test_driver.get_computed_label(fancy_input2), ""); + assert_equals(await test_driver.get_computed_label(real_input1), ""); + assert_equals(await test_driver.get_computed_label(real_input2), "fancy input inside label"); + }, "Implicit <label> association should apply to only the first labelable custom element for computed name"); +</script> + + +<label id="label6"> + <fancy-input id="fancy-input6-1-outer"> + <template shadowrootmode="open" > + <fancy-input id="fancy-input6-1-inner"> + <template shadowrootmode="open"> + <input id="real-input6-1"> + </template> + </fancy-input> + </template> + </fancy-input> + <fancy-input id="fancy-input6-2"> + <template shadowrootmode="open"> + <input id="real-input6-2"> + </template> + </fancy-input> + fancy input inside label +</label> + +<script> + promise_test(async t => { + const fancy_input1_outer = document.getElementById('fancy-input6-1-outer'); + fancy_input1_outer.shadowRoot.referenceTarget = 'fancy-input6-1-inner'; + const fancy_input1_inner = fancy_input1_outer.shadowRoot.getElementById('fancy-input6-1-inner'); + fancy_input1_inner.shadowRoot.referenceTarget = 'real-input6-1'; + const fancy_input2 = document.getElementById('fancy-input6-2'); + fancy_input2.shadowRoot.referenceTarget = 'real-input6-2'; + const real_input1 = fancy_input1_inner.shadowRoot.getElementById('real-input6-1'); + const real_input2 = fancy_input2.shadowRoot.getElementById('real-input6-2'); + const label = document.getElementById('label6'); + + assert_array_equals(Array.from(real_input1['labels']), [label], "real_input1 initial"); + assert_array_equals(Array.from(real_input2['labels']), [], "real_input2 initial"); + + fancy_input2.after(fancy_input1_outer); + assert_array_equals(Array.from(real_input1['labels']), [], "real_input1 after swap"); + assert_array_equals(Array.from(real_input2['labels']), [label], "real_input2 after swap"); + + fancy_input2.shadowRoot.referenceTarget = null; + assert_array_equals(Array.from(real_input1['labels']), [label], "real_input1 after fancy_input2 referenceTarget change"); + assert_array_equals(Array.from(real_input2['labels']), [], "real_input2 after fancy_input2 referenceTarget change"); + + fancy_input1_inner.shadowRoot.referenceTarget = null; + assert_array_equals(Array.from(real_input1['labels']), [], "real_input1 after fancy_input1_inner referenceTarget change"); + assert_array_equals(Array.from(real_input2['labels']), [], "real_input2 after fancy_input1_inner referenceTarget change"); + + fancy_input1_inner.shadowRoot.referenceTarget = "real-input6-1"; + assert_array_equals(Array.from(real_input1['labels']), [label], "real_input1 after second fancy_input1_inner referenceTarget change"); + assert_array_equals(Array.from(real_input2['labels']), [], "real_input2 after second fancy_input1_inner referenceTarget change"); + }, "Implicit <label> association should apply to only the first labelable custom element for .labels"); +</script> + + +<label id="label-input7" for="fancy-input7-outer">Input 7 + <fancy-input id="fancy-input7-outer"> + <template shadowRootMode="open" shadowRootReferenceTarget="fancy-input7-inner"> + <fancy-input id="fancy-input7-inner"> + <template shadowrootmode="open" shadowrootreferencetarget="input7-1"> + <input id="input7-1"> + <input id="input7-2"> + </template> + </fancy-input> + </template> + </fancy-input> +</label> +<script> + promise_test(async t => { + const outer_x_input = document.getElementById('fancy-input7-outer'); + const outer_shadow = outer_x_input.shadowRoot; + const inner_x_input = outer_shadow.getElementById('fancy-input7-inner'); + const inner_shadow = inner_x_input.shadowRoot; + const input1 = inner_shadow.getElementById('input7-1'); + const input2 = inner_shadow.getElementById('input7-2'); + + assert_equals(await test_driver.get_computed_label(input1), "Input 7"); + assert_equals(await test_driver.get_computed_label(input2), ""); + + inner_shadow.referenceTarget = "input7-2"; + + assert_equals(await test_driver.get_computed_label(input1), ""); + assert_equals(await test_driver.get_computed_label(input2), "Input 7"); + + outer_shadow.referenceTarget = null; + + assert_equals(await test_driver.get_computed_label(input1), ""); + assert_equals(await test_driver.get_computed_label(input2), ""); + }, "Changing the reference target causes label association to change for computed name"); +</script> + +<label id="label-input8" for="fancy-input8-outer">Input 8 + <fancy-input id="fancy-input8-outer"> + <template shadowRootMode="open" shadowRootReferenceTarget="fancy-input8-inner"> + <fancy-input id="fancy-input8-inner"> + <template shadowrootmode="open" shadowrootreferencetarget="input8-1"> + <input id="input8-1"> + <input id="input8-2"> + </template> + </fancy-input> + </template> + </fancy-input> +</label> +<script> + promise_test(async t => { + const label = document.getElementById('label-input8'); + const outer_x_input = document.getElementById('fancy-input8-outer'); + const outer_shadow = outer_x_input.shadowRoot; + const inner_x_input = outer_shadow.getElementById('fancy-input8-inner'); + const inner_shadow = inner_x_input.shadowRoot; + const input1 = inner_shadow.getElementById('input8-1'); + const input2 = inner_shadow.getElementById('input8-2'); + + assert_array_equals(Array.from(input1['labels']), [label], 'input1 before change'); + assert_array_equals(Array.from(input2['labels']), [], 'input2 before change'); + + inner_shadow.referenceTarget = "input8-2"; + + assert_array_equals(Array.from(input1['labels']), [], 'input1 after change'); + assert_array_equals(Array.from(input2['labels']), [label], 'input2 after change'); + + outer_shadow.referenceTarget = null; + + assert_array_equals(Array.from(input1['labels']), [], 'input1 after null'); + assert_array_equals(Array.from(input2['labels']), [], 'input2 after null'); + }, "Changing the reference target causes label association to change for .labels"); + +</script> + </body> </html> diff --git a/testing/web-platform/tests/shadow-dom/reference-target/tentative/label-for.html b/testing/web-platform/tests/shadow-dom/reference-target/tentative/label-for.html @@ -13,7 +13,7 @@ <body> -<label for="x-input1">Input 1</label> +<label id="label1" for="x-input1">Input 1</label> <x-input1 id="x-input1"> <template shadowrootmode="open" shadowrootreferencetarget="input1"> <input id="input1"> @@ -27,10 +27,18 @@ // The label should apply to the input element and not the host. assert_equals(await test_driver.get_computed_label(x_input), ""); assert_equals(await test_driver.get_computed_label(input), "Input 1"); - }, "Label for attribute targets a custom element using shadowrootreferencetarget"); + }, "Label for attribute targeting a custom element using shadowrootreferencetarget works for computed name"); + + promise_test(async t => { + const x_input = document.getElementById('x-input1'); + const input = x_input.shadowRoot.getElementById('input1'); + const label = document.getElementById('label1'); + + assert_array_equals(Array.from(input['labels']), [label]); +}, "Label for attribute targeting a custom element using shadowrootreferencetarget works for .labels property"); </script> -<label for="x-outer2">Input 2</label> +<label id="label2" for="x-outer2">Input 2</label> <x-outer2 id="x-outer2"> <template shadowrootmode="open" shadowrootreferencetarget="x-inner2"> <x-inner2 id="x-inner2"> @@ -50,40 +58,65 @@ assert_equals(await test_driver.get_computed_label(outer), ""); assert_equals(await test_driver.get_computed_label(inner), ""); assert_equals(await test_driver.get_computed_label(input), "Input 2"); - }, "Label for attribute targets a custom element using shadowrootreferencetarget inside multiple layers of shadow roots"); + }, "Label for attribute targeting a custom element using shadowrootreferencetarget inside multiple layers of shadow roots works for computed name"); + + promise_test(async t => { + const outer = document.getElementById('x-outer2'); + const inner = outer.shadowRoot.getElementById('x-inner2'); + const input = inner.shadowRoot.getElementById('input2'); + const label = document.getElementById('label2'); + + assert_array_equals(Array.from(input['labels']), [label]); + }, "Label for attribute targeting a custom element using shadowrootreferencetarget inside multiple layers of shadow roots works for .labels property"); + </script> -<label for="x-outer3">A</label> +<label id="label3-A" for="x-outer3">A</label> <x-outer3 id="x-outer3"> <template shadowrootmode="open" shadowrootreferencetarget="x-inner3"> - <label for="x-inner3">B</label> + <label id="label3-B" for="x-inner3">B</label> <x-inner3 id="x-inner3"> <template shadowrootmode="open" shadowrootreferencetarget="input3"> - <label for="input3">C</label> + <label id="label3-C" for="input3">C</label> <input id="input3"> - <label for="input3">D</label> + <label id="label3-D" for="input3">D</label> </template> </x-inner3> - <label for="x-inner3">E</label> + <label id="label3-E" for="x-inner3">E</label> </template> </x-outer3> -<label for="x-outer3">F</label> +<label id="label3-F" for="x-outer3">F</label> <script> promise_test(async t => { const outer = document.getElementById('x-outer3'); const inner = outer.shadowRoot.getElementById('x-inner3'); const input = inner.shadowRoot.getElementById('input3'); const computed_label = await test_driver.get_computed_label(input); + assert_equals(computed_label, "A B C D E F"); - }, "Multiple labels targeting a custom element using shadowrootreferencetarget inside multiple layers of shadow roots"); + }, "Multiple labels targeting a custom element using shadowrootreferencetarget inside multiple layers of shadow roots works for computed name"); + + promise_test(async t => { + const outer = document.getElementById('x-outer3'); + const inner = outer.shadowRoot.getElementById('x-inner3'); + const input = inner.shadowRoot.getElementById('input3'); + const A = document.getElementById('label3-A'); + const B = outer.shadowRoot.getElementById('label3-B'); + const C = inner.shadowRoot.getElementById('label3-C'); + const D = inner.shadowRoot.getElementById('label3-D'); + const E = outer.shadowRoot.getElementById('label3-E'); + const F = document.getElementById('label3-F'); + + assert_array_equals(Array.from(input['labels']), [A, B, C, D, E, F]); + }, "Multiple labels targeting a custom element using shadowrootreferencetarget inside multiple layers of shadow roots works for .labels property"); </script> <label id="label-input4">Input 4</label> -<x-input1 id="x-input4"> +<x-input4 id="x-input4"> <template shadowrootmode="open" shadowrootreferencetarget="input4"> <input id="input4"> </template> -</x-input1> +</x-input4> <script> promise_test(async t => { const label = document.getElementById('label-input4'); @@ -94,61 +127,200 @@ // The label should apply to the input element and not the host. assert_equals(await test_driver.get_computed_label(x_input), ""); assert_equals(await test_driver.get_computed_label(input), "Input 4"); - }, "Setting .htmlFor property to target a custom element using shadowrootreferencetarget"); + }, "Setting .htmlFor property to target a custom element using shadowrootreferencetarget works for computed name"); + + promise_test(async t => { + const label = document.getElementById('label-input4'); + label.htmlFor = "x-input4"; + const x_input = document.getElementById('x-input4'); + const input = x_input.shadowRoot.getElementById('input4'); + + assert_array_equals(Array.from(input['labels']), [label]); + }, "Setting .htmlFor property to target a custom element using shadowrootreferencetarget works for .labels"); + +</script> + + +<label id="label-input5" for="x-input5-outer">Input 5</label> +<x-input5 id="x-input5-outer"> + <template shadowRootMode="open" shadowRootReferenceTarget="x-input5-inner"> + <x-input5 id="x-input5-inner"> + <template shadowrootmode="open" shadowrootreferencetarget="input5-1"> + <input id="input5-1"> + <input id="input5-2"> + </template> + </x-input5> + </template> +</x-input5> +<script> + promise_test(async t => { + const outer_x_input = document.getElementById('x-input5-outer'); + const outer_shadow_root = outer_x_input.shadowRoot; + const inner_x_input = outer_shadow_root.getElementById('x-input5-inner') + const inner_shadow_root = inner_x_input.shadowRoot; + const input1 = inner_shadow_root.getElementById('input5-1'); + const input2 = inner_shadow_root.getElementById('input5-2'); + + assert_equals(await test_driver.get_computed_label(input1), "Input 5"); + assert_equals(await test_driver.get_computed_label(input2), ""); + + inner_shadow_root.referenceTarget = 'input5-2'; + + assert_equals(await test_driver.get_computed_label(input1), ""); + assert_equals(await test_driver.get_computed_label(input2), "Input 5"); + + outer_shadow_root.referenceTarget = null; + + assert_equals(await test_driver.get_computed_label(input1), ""); + assert_equals(await test_driver.get_computed_label(input2), ""); + }, "Modifying the reference target changes the computed label when using label/for"); </script> -<div id="test-container"></div> +<label id="label-input6" for="x-input6-outer">Input 6</label> +<x-input6 id="x-input6-outer"> + <template shadowRootMode="open" shadowRootReferenceTarget="x-input6-inner"> + <x-input6 id="x-input6-inner"> + <template shadowrootmode="open" shadowrootreferencetarget="input6-1"> + <input id="input6-1"> + <input id="input6-2"> + </template> + </x-input6> + </template> +</x-input6> <script> -// The HTML5_LABELABLE_ELEMENTS are defined in https://html.spec.whatwg.org/#category-label -for(let referenced_element_type of HTML5_LABELABLE_ELEMENTS) { promise_test(async t => { - const test_container = document.querySelector("#test-container"); - test_container.setHTMLUnsafe(` - <label> - <fancy-element id="fancy"> - <template shadowrootmode="open" shadowrootreferencetarget="target"> - <${referenced_element_type} id="target"></${referenced_element_type}> - </template> - </fancy-element> - fancy custom element inside label - </label>`); - - const fancy_element = document.getElementById('fancy'); - const target_element = fancy_element.shadowRoot.getElementById('target'); - - assert_equals(await test_driver.get_computed_label(fancy_element), ""); - assert_equals(await test_driver.get_computed_label(target_element), "fancy custom element inside label"); - }, "Implicit <label> association should work with a custom element targeting '" + referenced_element_type + "'"); -} + const label = document.getElementById('label-input6') + const outer_x_input = document.getElementById('x-input6-outer'); + const outer_shadow_root = outer_x_input.shadowRoot; + const inner_x_input = outer_shadow_root.getElementById('x-input6-inner') + const inner_shadow_root = inner_x_input.shadowRoot; + const input1 = inner_shadow_root.getElementById('input6-1'); + const input2 = inner_shadow_root.getElementById('input6-2'); + + assert_array_equals(Array.from(input1['labels']), [label], 'input1 before changing reference target'); + assert_array_equals(Array.from(input2['labels']), [], 'input2 before changing reference target'); + + inner_shadow_root.referenceTarget = 'input6-2'; + + assert_array_equals(Array.from(input1['labels']), [], 'input1 after changing reference target'); + assert_array_equals(Array.from(input2['labels']), [label], 'input2 after changing reference target'); + + outer_shadow_root.referenceTarget = null; + + assert_array_equals(Array.from(input1['labels']), []); + assert_array_equals(Array.from(input2['labels']), []); + }, "Modifying the reference target changes .labels when using label/for"); + </script> -<label> -<fancy-input id="fancy-input1"> - <template shadowrootmode="open" shadowrootreferencetarget="real-input"> - <input id="real-input"> +<label id="label-input7-1" for="x-input7-1">Input 7 - 1</label> +<x-input7 id="x-input7-1"> + <template shadowRootMode="open" shadowRootReferenceTarget="input7"> + <input id="input7"> </template> -</fancy-input> -<fancy-input id="fancy-input2"> - <template shadowrootmode="open" shadowrootreferencetarget="real-input"> - <input id="real-input"> +</x-input7> + +<label id="label-input7-2" for="x-input7-2">Input 7 - 2</label> +<x-input7 id="x-input7-2"> + <template shadowRootMode="open" shadowRootReferenceTarget="input7"> </template> -</fancy-input> -fancy input inside label -</label> +</x-input7> <script> promise_test(async t => { - const fancy_input1 = document.getElementById('fancy-input1'); - const fancy_input2 = document.getElementById('fancy-input2'); - const real_input1 = fancy_input1.shadowRoot.getElementById('real-input'); - const real_input2 = fancy_input2.shadowRoot.getElementById('real-input'); - - assert_equals(await test_driver.get_computed_label(fancy_input1), ""); - assert_equals(await test_driver.get_computed_label(fancy_input2), ""); - assert_equals(await test_driver.get_computed_label(real_input1), "fancy input inside label"); - assert_equals(await test_driver.get_computed_label(real_input2), ""); - }, "Implicit <label> association should apply to only the first labelable custom element"); + const label_1 = document.getElementById('label-input7-1') + const x_input_1 = document.getElementById('x-input7-1'); + const shadow_root_1 = x_input_1.shadowRoot; + const input = shadow_root_1.getElementById('input7'); + + assert_array_equals(Array.from(input['labels']), [label_1], 'input before changing shadow root'); + + const label_2 = document.getElementById('label-input7-2') + const x_input_2 = document.getElementById('x-input7-2'); + const shadow_root_2 = x_input_2.shadowRoot; + shadow_root_2.append(input); + + assert_array_equals(Array.from(input['labels']), [label_2], 'input after changing shadow root'); + }, "Moving an input from one shadow root to another causes its .labels to be updated"); + </script> -</body> +<script> + promise_test(async t => { + const container = document.createElement('div'); + container.id = 'container8'; + const outer_label = document.createElement('label'); + outer_label.id = 'outer-label8'; + outer_label.htmlFor = 'outer-input8'; + const light_input = document.createElement('input'); + light_input.id = 'outer-input8' + assert_array_equals(Array.from(light_input.labels), [], 'before inserting light input'); + + container.append(outer_label, light_input); + assert_array_equals(Array.from(light_input.labels), [outer_label], 'after inserting light input in container'); + + document.body.append(container); + assert_array_equals(Array.from(light_input.labels), [outer_label], 'after inserting container in document'); + + light_input.remove(); + assert_array_equals(Array.from(light_input.labels), [], 'after removing light input from document'); + + const inner_x_input = document.createElement('x-input8'); + inner_x_input.id = 'inner-x-input8'; + const inner_shadow_root = inner_x_input.attachShadow({mode: 'open'}); + const inner_label = document.createElement('label'); + inner_label.id = 'inner-label8'; + inner_label.htmlFor = 'inner-input8'; + inner_shadow_root.appendChild(inner_label); + + const inner_input = document.createElement('input'); + inner_input.id = 'inner-input8'; + inner_shadow_root.appendChild(inner_input); + + const outer_x_input = document.createElement('x-input8'); + outer_x_input.id = 'outer-x-input8'; + const outer_shadow_root = outer_x_input.attachShadow({mode: 'open'}); + outer_shadow_root.referenceTarget = 'inner-x-input8'; + const intermediate_label = document.createElement('label'); + intermediate_label.id = 'intermediate-label8'; + intermediate_label.htmlFor = 'inner-x-input8'; + outer_shadow_root.appendChild(intermediate_label); + outer_shadow_root.appendChild(inner_x_input); + + assert_array_equals(Array.from(inner_input['labels']), [inner_label], 'After inner shadow host is attached, before reference target is set'); + + inner_shadow_root.referenceTarget = 'inner-input8'; + assert_array_equals(Array.from(inner_input['labels']), [intermediate_label, inner_label], 'After inner shadow reference target is set'); + + outer_label.htmlFor = 'outer-x-input8'; + outer_label.after(outer_x_input); + assert_array_equals(Array.from(inner_input['labels']), [outer_label, intermediate_label, inner_label], 'After outer shadow host is inserted in the document'); + + outer_x_input.remove(); + assert_array_equals(Array.from(inner_input['labels']), [intermediate_label, inner_label], 'After outer shadow host is removed from the document'); + + outer_label.after(outer_x_input); + assert_array_equals(Array.from(inner_input['labels']), [outer_label, intermediate_label, inner_label], 'After outer shadow host is re-inserted'); + + inner_x_input.remove(); + assert_array_equals(Array.from(inner_input['labels']), [inner_label], 'After inner shadow host is removed from the document'); + + intermediate_label.after(inner_x_input); + assert_array_equals(Array.from(inner_input['labels']), [outer_label, intermediate_label, inner_label], 'After inner shadow host is re-inserted'); + + outer_shadow_root.referenceTarget = null; + assert_array_equals(Array.from(inner_input['labels']), [intermediate_label, inner_label], 'After outer shadow root reference target is set to null'); + + inner_shadow_root.referenceTarget = null; + assert_array_equals(Array.from(inner_input['labels']), [inner_label], 'After inner shadow root reference target is set to null'); + + inner_shadow_root.referenceTarget = 'inner-input8'; + assert_array_equals(Array.from(inner_input['labels']), [intermediate_label, inner_label], 'After inner shadow root reference target is re-set'); + + outer_shadow_root.referenceTarget = 'inner-x-input8'; + assert_array_equals(Array.from(inner_input['labels']), [outer_label, intermediate_label, inner_label], 'After outer shadow root reference target is re-set'); + }, 'Attaching a shadow root, inserting and removing a shadow host, and changing reference targets all cause label association to be updated'); + +</script> +</body> </html> diff --git a/toolkit/components/formautofill/FormAutofillNative.cpp b/toolkit/components/formautofill/FormAutofillNative.cpp @@ -1149,7 +1149,7 @@ void FormAutofillImpl::GetFormAutofillConfidences( if (NS_WARN_IF(!label)) { continue; } - auto* control = label->GetControl(); + auto* control = label->GetLabeledElementInternal(); if (!control) { continue; }