Bug 1879255 part 2: Add a dependent elements map to DocAccessible and use it for popoverTargetElement. r=morgan

As well as getting an invoker's popover target, we need to be able to do the reverse: get a popover's invokers.
We can already do this when the popovertarget content attribute is set to a string id using the dependent ids map.
However, the popover target can also be explicitly set to a DOM element using the .popoverTargetElement WebIDL attribute.
For this, we need a new map which maps from target elements instead of target ids.
RelatedAccIterator has also been updated to use this map.
DocAccessible::QueueCacheUpdateForDependentRelations had to be updated as well.
Rather than duplicating logic, RelatedAccIterator has been taught how to optionally return all relations and QueueCacheUpdateForDependentRelations now uses RelatedAccIterator.

Differential Revision: https://phabricator.services.mozilla.com/D201661
This commit is contained in:
James Teh 2024-02-21 00:05:21 +00:00
parent 33de5c2029
commit df9bf2b1df
8 changed files with 376 additions and 37 deletions

View file

@ -71,7 +71,12 @@ AccIterator::IteratorState::IteratorState(const LocalAccessible* aParent,
RelatedAccIterator::RelatedAccIterator(DocAccessible* aDocument, RelatedAccIterator::RelatedAccIterator(DocAccessible* aDocument,
nsIContent* aDependentContent, nsIContent* aDependentContent,
nsAtom* aRelAttr) nsAtom* aRelAttr)
: mDocument(aDocument), mRelAttr(aRelAttr), mProviders(nullptr), mIndex(0) { : mDocument(aDocument),
mDependentContent(aDependentContent),
mRelAttr(aRelAttr),
mProviders(nullptr),
mIndex(0),
mIsWalkingDependentElements(false) {
nsAutoString id; nsAutoString id;
if (aDependentContent->IsElement() && if (aDependentContent->IsElement() &&
aDependentContent->AsElement()->GetAttr(nsGkAtoms::id, id)) { aDependentContent->AsElement()->GetAttr(nsGkAtoms::id, id)) {
@ -80,26 +85,57 @@ RelatedAccIterator::RelatedAccIterator(DocAccessible* aDocument,
} }
LocalAccessible* RelatedAccIterator::Next() { LocalAccessible* RelatedAccIterator::Next() {
if (!mProviders) return nullptr; 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;
}
}
while (mIndex < mProviders->Length()) { while (mIndex < mProviders->Length()) {
const auto& provider = (*mProviders)[mIndex++]; const auto& provider = (*mProviders)[mIndex++];
// Return related accessible for the given attribute. // Return related accessible for the given attribute.
if (provider->mRelAttr == mRelAttr) { if (mRelAttr && provider->mRelAttr != mRelAttr) {
LocalAccessible* related = mDocument->GetAccessible(provider->mContent); continue;
if (related) { }
return related; // 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;
}
// If the document content is pointed by relation then return the // If the document content is pointed by relation then return the
// document itself. // document itself.
if (provider->mContent == mDocument->GetContent()) { if (provider->mContent == mDocument->GetContent()) {
return mDocument; return mDocument;
}
} }
} }
// We exhausted mProviders without returning anything.
if (!mIsWalkingDependentElements) {
// Call this function again to start walking the dependent elements.
return Next();
}
return nullptr; return nullptr;
} }

View file

@ -67,7 +67,9 @@ class AccIterator : public AccIterable {
/** /**
* Allows to traverse through related accessibles that are pointing to the given * Allows to traverse through related accessibles that are pointing to the given
* dependent accessible by relation attribute. * dependent accessible by relation attribute. This is typically used to query
* implicit reverse relations; e.g. calculating the LABEL_FOR relation for a
* label where that label was referenced using aria-labelledby.
*/ */
class RelatedAccIterator : public AccIterable { class RelatedAccIterator : public AccIterable {
public: public:
@ -79,7 +81,7 @@ class RelatedAccIterator : public AccIterable {
* @param aDependentContent [in] the content of dependent accessible that * @param aDependentContent [in] the content of dependent accessible that
* relations were requested for * relations were requested for
* @param aRelAttr [in] relation attribute that relations are * @param aRelAttr [in] relation attribute that relations are
* pointed by * pointed by, null for all relations
*/ */
RelatedAccIterator(DocAccessible* aDocument, nsIContent* aDependentContent, RelatedAccIterator(DocAccessible* aDocument, nsIContent* aDependentContent,
nsAtom* aRelAttr); nsAtom* aRelAttr);
@ -97,9 +99,11 @@ class RelatedAccIterator : public AccIterable {
RelatedAccIterator& operator=(const RelatedAccIterator&); RelatedAccIterator& operator=(const RelatedAccIterator&);
DocAccessible* mDocument; DocAccessible* mDocument;
nsIContent* mDependentContent;
nsAtom* mRelAttr; nsAtom* mRelAttr;
DocAccessible::AttrRelProviders* mProviders; DocAccessible::AttrRelProviders* mProviders;
uint32_t mIndex; uint32_t mIndex;
bool mIsWalkingDependentElements;
}; };
/** /**

View file

@ -620,3 +620,20 @@ bool nsCoreUtils::IsDocumentVisibleConsideringInProcessAncestors(
} while ((parent = parent->GetInProcessParentDocument())); } while ((parent = parent->GetInProcessParentDocument()));
return true; return true;
} }
bool nsCoreUtils::IsDescendantOfAnyShadowIncludingAncestor(
nsINode* aDescendant, nsINode* aStartAncestor) {
const nsINode* descRoot = aDescendant->SubtreeRoot();
nsINode* ancRoot = aStartAncestor->SubtreeRoot();
for (;;) {
if (ancRoot == descRoot) {
return true;
}
auto* shadow = mozilla::dom::ShadowRoot::FromNode(ancRoot);
if (!shadow || !shadow->GetHost()) {
break;
}
ancRoot = shadow->GetHost()->SubtreeRoot();
}
return false;
}

View file

@ -324,6 +324,13 @@ class nsCoreUtils {
*/ */
static bool IsDocumentVisibleConsideringInProcessAncestors( static bool IsDocumentVisibleConsideringInProcessAncestors(
const Document* aDocument); const Document* aDocument);
/**
* Return true if `aDescendant` is a descendant of any of `aStartAncestor`'s
* shadow-including ancestors.
*/
static bool IsDescendantOfAnyShadowIncludingAncestor(nsINode* aDescendant,
nsINode* aStartAncestor);
}; };
#endif #endif

View file

@ -67,6 +67,9 @@ static nsStaticAtom* const kRelationAttrs[] = {
static const uint32_t kRelationAttrsLen = ArrayLength(kRelationAttrs); static const uint32_t kRelationAttrsLen = ArrayLength(kRelationAttrs);
static nsStaticAtom* const kSingleElementRelationIdlAttrs[] = {
nsGkAtoms::popovertarget};
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// Constructor/desctructor // Constructor/desctructor
@ -383,25 +386,25 @@ void DocAccessible::QueueCacheUpdate(LocalAccessible* aAcc,
void DocAccessible::QueueCacheUpdateForDependentRelations( void DocAccessible::QueueCacheUpdateForDependentRelations(
LocalAccessible* aAcc) { LocalAccessible* aAcc) {
if (!mIPCDoc || !aAcc || !aAcc->Elm() || !aAcc->IsInDocument() || if (!mIPCDoc || !aAcc || !aAcc->IsInDocument() || aAcc->IsDefunct()) {
aAcc->IsDefunct()) {
return; return;
} }
nsAutoString ID; dom::Element* el = aAcc->Elm();
aAcc->DOMNodeID(ID); if (!el) {
if (AttrRelProviders* list = GetRelProviders(aAcc->Elm(), ID)) { return;
// We call this function when we've noticed an ID change, or when an acc }
// is getting bound to its document. We need to ensure any existing accs
// that depend on this acc's ID have their rel cache entries updated. // We call this function when we've noticed an ID change, or when an acc
for (const auto& provider : *list) { // is getting bound to its document. We need to ensure any existing accs
LocalAccessible* relatedAcc = GetAccessible(provider->mContent); // that depend on this acc's ID or Element have their relation cache entries
if (!relatedAcc || relatedAcc->IsDefunct() || // updated.
!relatedAcc->IsInDocument() || RelatedAccIterator iter(this, el, nullptr);
mInsertedAccessibles.Contains(relatedAcc)) { while (LocalAccessible* relatedAcc = iter.Next()) {
continue; if (relatedAcc->IsDefunct() || !relatedAcc->IsInDocument() ||
} mInsertedAccessibles.Contains(relatedAcc)) {
QueueCacheUpdate(relatedAcc, CacheDomain::Relations); continue;
} }
QueueCacheUpdate(relatedAcc, CacheDomain::Relations);
} }
} }
@ -507,6 +510,7 @@ void DocAccessible::Shutdown() {
} }
mDependentIDsHashes.Clear(); mDependentIDsHashes.Clear();
mDependentElementsMap.Clear();
mNodeToAccessibleMap.Clear(); mNodeToAccessibleMap.Clear();
mAnchorJumpElm = nullptr; mAnchorJumpElm = nullptr;
@ -1150,6 +1154,7 @@ void DocAccessible::BindToDocument(LocalAccessible* aAccessible,
if (aAccessible->HasOwnContent()) { if (aAccessible->HasOwnContent()) {
AddDependentIDsFor(aAccessible); AddDependentIDsFor(aAccessible);
AddDependentElementsFor(aAccessible);
nsIContent* content = aAccessible->GetContent(); nsIContent* content = aAccessible->GetContent();
if (content->IsElement() && if (content->IsElement() &&
@ -1769,6 +1774,61 @@ void DocAccessible::RemoveDependentIDsFor(LocalAccessible* aRelProvider,
} }
} }
void DocAccessible::AddDependentElementsFor(LocalAccessible* aRelProvider,
nsAtom* aRelAttr) {
dom::Element* providerEl = aRelProvider->Elm();
if (!providerEl) {
return;
}
for (nsStaticAtom* attr : kSingleElementRelationIdlAttrs) {
if (aRelAttr && aRelAttr != attr) {
continue;
}
if (dom::Element* targetEl =
providerEl->GetExplicitlySetAttrElement(attr)) {
AttrRelProviders& providers =
mDependentElementsMap.LookupOrInsert(targetEl);
AttrRelProvider* provider = new AttrRelProvider(attr, providerEl);
providers.AppendElement(provider);
}
// If the relation attribute was given, we've already handled it. We don't
// have anything else to check.
if (aRelAttr) {
break;
}
}
}
void DocAccessible::RemoveDependentElementsFor(LocalAccessible* aRelProvider,
nsAtom* aRelAttr) {
dom::Element* providerEl = aRelProvider->Elm();
if (!providerEl) {
return;
}
for (nsStaticAtom* attr : kSingleElementRelationIdlAttrs) {
if (aRelAttr && aRelAttr != attr) {
continue;
}
if (dom::Element* targetEl =
providerEl->GetExplicitlySetAttrElement(attr)) {
if (auto providers = mDependentElementsMap.Lookup(targetEl)) {
providers.Data().RemoveElementsBy([attr,
providerEl](const auto& provider) {
return provider->mRelAttr == attr && provider->mContent == providerEl;
});
if (providers.Data().IsEmpty()) {
providers.Remove();
}
}
}
// If the relation attribute was given, we've already handled it. We don't
// have anything else to check.
if (aRelAttr) {
break;
}
}
}
bool DocAccessible::UpdateAccessibleOnAttrChange(dom::Element* aElement, bool DocAccessible::UpdateAccessibleOnAttrChange(dom::Element* aElement,
nsAtom* aAttribute) { nsAtom* aAttribute) {
if (aAttribute == nsGkAtoms::role) { if (aAttribute == nsGkAtoms::role) {
@ -2003,7 +2063,7 @@ bool InsertIterator::Next() {
return false; return false;
} }
void DocAccessible::MaybeFireEventsForChangedPopover(LocalAccessible *aAcc) { void DocAccessible::MaybeFireEventsForChangedPopover(LocalAccessible* aAcc) {
dom::Element* el = aAcc->Elm(); dom::Element* el = aAcc->Elm();
if (!el || !el->IsHTMLElement() || !el->HasAttr(nsGkAtoms::popover)) { if (!el || !el->IsHTMLElement() || !el->HasAttr(nsGkAtoms::popover)) {
return; // Not a popover. return; // Not a popover.
@ -2620,6 +2680,7 @@ void DocAccessible::UncacheChildrenInSubtree(LocalAccessible* aRoot) {
MaybeFireEventsForChangedPopover(aRoot); MaybeFireEventsForChangedPopover(aRoot);
aRoot->mStateFlags |= eIsNotInDocument; aRoot->mStateFlags |= eIsNotInDocument;
RemoveDependentIDsFor(aRoot); RemoveDependentIDsFor(aRoot);
RemoveDependentElementsFor(aRoot);
// The parent of the removed subtree is about to be cleared, so we must do // The parent of the removed subtree is about to be cleared, so we must do
// this here rather than in LocalAccessible::UnbindFromParent because we need // this here rather than in LocalAccessible::UnbindFromParent because we need

View file

@ -121,7 +121,7 @@ class DocAccessible : public HyperTextAccessible,
void QueueCacheUpdate(LocalAccessible* aAcc, uint64_t aNewDomain); void QueueCacheUpdate(LocalAccessible* aAcc, uint64_t aNewDomain);
/** /**
* Walks the mDependentIDsHashes list for the given accessible and * Walks the dependent ids and elements maps for the given accessible and
* queues a CacheDomain::Relations cache update fore each related acc. * queues a CacheDomain::Relations cache update fore each related acc.
* We call this when we observe an ID mutation or when an acc is bound * We call this when we observe an ID mutation or when an acc is bound
* to its document. * to its document.
@ -485,6 +485,35 @@ class DocAccessible : public HyperTextAccessible,
void RemoveDependentIDsFor(LocalAccessible* aRelProvider, void RemoveDependentIDsFor(LocalAccessible* aRelProvider,
nsAtom* aRelAttr = nullptr); nsAtom* aRelAttr = nullptr);
/**
* Add dependent elements targeted by a relation attribute on an accessible
* element to the dependent elements cache. This is used for reflected IDL
* attributes which return DOM elements and reflect a content attribute, where
* the IDL attribute has been set to an element. For example, if the
* .popoverTargetElement IDL attribute is set to an element using JS, the
* target element will be added to the dependent elements cache. If the
* relation attribute is not specified, then all relation attributes are
* checked.
*
* @param aRelProvider [in] the accessible with the relation IDL attribute.
* @param aRelAttr [in, optional] the name of the reflected content attribute.
* For example, for the popoverTargetElement IDL attribute, this would be
* "popovertarget".
*/
void AddDependentElementsFor(LocalAccessible* aRelProvider,
nsAtom* aRelAttr = nullptr);
/**
* Remove dependent elements targeted by a relation attribute on an accessible
* element from the dependent elements cache. If the relation attribute is
* not specified, then all relation attributes are checked.
*
* @param aRelProvider [in] the accessible with the relation IDL attribute.
* @param aRelAttr [in, optional] the name of the reflected content attribute.
*/
void RemoveDependentElementsFor(LocalAccessible* aRelProvider,
nsAtom* aRelAttr = nullptr);
/** /**
* Update or recreate an accessible depending on a changed attribute. * Update or recreate an accessible depending on a changed attribute.
* *
@ -727,12 +756,35 @@ class DocAccessible : public HyperTextAccessible,
void RemoveRelProvidersIfEmpty(dom::Element* aElement, const nsAString& aID); void RemoveRelProvidersIfEmpty(dom::Element* aElement, const nsAString& aID);
/** /**
* The cache of IDs pointed by relation attributes. * A map used to look up the target node for an implicit reverse relation
* where the target of the explicit relation is specified as an id.
* For example:
* <div id="label">Name:</div><input aria-labelledby="label">
* The div should get a LABEL_FOR relation targeting the input. To facilitate
* that, mDependentIDsHashes maps from "label" to an AttrRelProvider
* specifying aria-labelledby and the input. Because ids are scoped to the
* nearest ancestor document or shadow root, mDependentIDsHashes maps from the
* DocumentOrShadowRoot first.
*/ */
nsClassHashtable<nsPtrHashKey<dom::DocumentOrShadowRoot>, nsClassHashtable<nsPtrHashKey<dom::DocumentOrShadowRoot>,
DependentIDsHashtable> DependentIDsHashtable>
mDependentIDsHashes; mDependentIDsHashes;
/**
* A map used to look up the target element for an implicit reverse relation
* where the target of the explicit relation is also specified as an element.
* This is similar to mDependentIDsHashes, except that this is used when a
* DOM property is used to set the relation target element directly, rather
* than using an id. For example:
* <button>More info</button><div popover>Some info</div>
* The button's .popoverTargetElement property is set to the div so that the
* button invokes the popover.
* To facilitate finding the invoker given the popover, mDependentElementsMap
* maps from the div to an AttrRelProvider specifying popovertarget and the
* button.
*/
nsTHashMap<nsIContent*, AttrRelProviders> mDependentElementsMap;
friend class RelatedAccIterator; friend class RelatedAccIterator;
/** /**

View file

@ -293,7 +293,7 @@ addAccessibleTask(
); );
/** /**
* Test details relations on popovers and their invokers. * Test details relations for the popovertarget content attribute.
*/ */
addAccessibleTask( addAccessibleTask(
` `
@ -304,7 +304,7 @@ addAccessibleTask(
<div id="popover" popover>popover</div> <div id="popover" popover>popover</div>
<div id="details">details</div> <div id="details">details</div>
`, `,
async function testPopover(browser, docAcc) { async function testPopoverContent(browser, docAcc) {
// The popover is hidden, so nothing should be referring to it. // The popover is hidden, so nothing should be referring to it.
const hide = findAccessibleChildByID(docAcc, "hide"); const hide = findAccessibleChildByID(docAcc, "hide");
await testCachedRelation(hide, RELATION_DETAILS, []); await testCachedRelation(hide, RELATION_DETAILS, []);
@ -330,7 +330,7 @@ addAccessibleTask(
await testCachedRelation(toggleSibling, RELATION_DETAILS, []); await testCachedRelation(toggleSibling, RELATION_DETAILS, []);
await testCachedRelation(popover, RELATION_DETAILS_FOR, toggle1); await testCachedRelation(popover, RELATION_DETAILS_FOR, toggle1);
info("Setting toggle2 popovertargetaction"); info("Setting toggle2 popovertarget");
await invokeSetAttribute(browser, "toggle2", "popovertarget", "popover"); await invokeSetAttribute(browser, "toggle2", "popovertarget", "popover");
await testCachedRelation(toggle2, RELATION_DETAILS, popover); await testCachedRelation(toggle2, RELATION_DETAILS, popover);
await testCachedRelation(popover, RELATION_DETAILS_FOR, [toggle1, toggle2]); await testCachedRelation(popover, RELATION_DETAILS_FOR, [toggle1, toggle2]);
@ -364,3 +364,88 @@ addAccessibleTask(
}, },
{ chrome: false, topLevel: true } { chrome: false, topLevel: true }
); );
/**
* Test details relations for the popoverTargetElement WebIDL attribute.
*/
addAccessibleTask(
`
<button id="toggle1">toggle1</button>
<button id="toggle2">toggle2</button>
between
<div id="popover1" popover>popover1</div>
<button id="toggle3">toggle3</button>
<div id="shadowHost"><template shadowrootmode="open">
<button id="toggle4">toggle4</button>
between
<div id="popover2" popover>popover2</div>
<button id="toggle5">toggle5</button>
</template></div>
<script>
const toggle1 = document.getElementById("toggle1");
const toggle2 = document.getElementById("toggle2");
const popover1 = document.getElementById("popover1");
toggle1.popoverTargetElement = popover1;
toggle2.popoverTargetElement = popover1;
const toggle3 = document.getElementById("toggle3");
const shadow = document.getElementById("shadowHost").shadowRoot;
const toggle4 = shadow.getElementById("toggle4");
const popover2 = shadow.getElementById("popover2");
toggle3.popoverTargetElement = popover2;
toggle4.popoverTargetElement = popover2;
const toggle5 = shadow.getElementById("toggle5");
toggle5.popoverTargetElement = popover1;
</script>
`,
async function testPopoverIdl(browser, docAcc) {
// No popover is showing, so there shouldn't be any details relations.
const toggle1 = findAccessibleChildByID(docAcc, "toggle1");
await testCachedRelation(toggle1, RELATION_DETAILS, []);
const toggle2 = findAccessibleChildByID(docAcc, "toggle2");
await testCachedRelation(toggle2, RELATION_DETAILS, []);
const toggle3 = findAccessibleChildByID(docAcc, "toggle3");
await testCachedRelation(toggle3, RELATION_DETAILS, []);
const toggle4 = findAccessibleChildByID(docAcc, "toggle4");
await testCachedRelation(toggle4, RELATION_DETAILS, []);
const toggle5 = findAccessibleChildByID(docAcc, "toggle5");
await testCachedRelation(toggle5, RELATION_DETAILS, []);
info("Showing popover1");
let shown = waitForEvent(EVENT_SHOW, "popover1");
toggle1.doAction(0);
const popover1 = (await shown).accessible;
await testCachedRelation(toggle1, RELATION_DETAILS, popover1);
await testCachedRelation(toggle2, RELATION_DETAILS, popover1);
// toggle5 is inside the shadow DOM and popover1 is outside, so the target
// is valid.
await testCachedRelation(toggle5, RELATION_DETAILS, popover1);
await testCachedRelation(popover1, RELATION_DETAILS_FOR, [
toggle1,
toggle2,
toggle5,
]);
info("Hiding popover1");
let hidden = waitForEvent(EVENT_HIDE, popover1);
toggle1.doAction(0);
await hidden;
await testCachedRelation(toggle1, RELATION_DETAILS, []);
await testCachedRelation(toggle2, RELATION_DETAILS, []);
await testCachedRelation(toggle5, RELATION_DETAILS, []);
info("Showing popover2");
shown = waitForEvent(EVENT_SHOW, "popover2");
toggle4.doAction(0);
const popover2 = (await shown).accessible;
// toggle4 is in the same shadow DOM as popover2.
await testCachedRelation(toggle4, RELATION_DETAILS, popover2);
// toggle3 is outside popover2's shadow DOM, so the target isn't valid.
await testCachedRelation(toggle3, RELATION_DETAILS, []);
await testCachedRelation(popover2, RELATION_DETAILS_FOR, [toggle4]);
info("Hiding popover2");
hidden = waitForEvent(EVENT_HIDE, popover2);
toggle4.doAction(0);
await hidden;
await testCachedRelation(toggle4, RELATION_DETAILS, []);
},
{ chrome: true, topLevel: true }
);

View file

@ -3,6 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict"; "use strict";
requestLongerTimeout(2);
/* import-globals-from ../../mochitest/role.js */ /* import-globals-from ../../mochitest/role.js */
/* import-globals-from ../../mochitest/states.js */ /* import-globals-from ../../mochitest/states.js */
@ -484,7 +485,7 @@ addAccessibleTask(
); );
/** /**
* Test caching of the expanded state for popover target element. * Test caching of the expanded state for the popovertarget content attribute.
*/ */
addAccessibleTask( addAccessibleTask(
` `
@ -550,3 +551,79 @@ addAccessibleTask(
}, },
{ chrome: true, topLevel: true, remoteIframe: true } { chrome: true, topLevel: true, remoteIframe: true }
); );
/**
* Test caching of the expanded state for the popoverTargetElement WebIDL
* attribute.
*/
addAccessibleTask(
`
<button id="toggle1">toggle</button>
<div id="popover1" popover>popover1</div>
<button id="toggle2">toggle2</button>
<button id="toggle3">toggle3</button>
<div id="shadowHost"><template shadowrootmode="open">
<button id="toggle4">toggle4</button>
<div id="popover2" popover>popover2</div>
<button id="toggle5">toggle5</button>
</template></div>
<script>
const toggle1 = document.getElementById("toggle1");
const popover1 = document.getElementById("popover1");
toggle1.popoverTargetElement = popover1;
toggle2.popoverTargetElement = popover1;
const toggle3 = document.getElementById("toggle3");
const shadow = document.getElementById("shadowHost").shadowRoot;
const toggle4 = shadow.getElementById("toggle4");
const popover2 = shadow.getElementById("popover2");
toggle3.popoverTargetElement = popover2;
toggle4.popoverTargetElement = popover2;
const toggle5 = shadow.getElementById("toggle5");
toggle5.popoverTargetElement = popover1;
</script>
`,
async function (browser, docAcc) {
const toggle1 = findAccessibleChildByID(docAcc, "toggle1");
testStates(toggle1, STATE_COLLAPSED);
const toggle2 = findAccessibleChildByID(docAcc, "toggle2");
testStates(toggle2, STATE_COLLAPSED);
const toggle5 = findAccessibleChildByID(docAcc, "toggle5");
// toggle5 is inside the shadow DOM and popover1 is outside, so the target
// is valid.
testStates(toggle5, STATE_COLLAPSED);
// Changes to the popover should fire events on all invokers.
const changeEvents = [
[EVENT_STATE_CHANGE, toggle1],
[EVENT_STATE_CHANGE, toggle2],
[EVENT_STATE_CHANGE, toggle5],
];
info("Showing popover1");
let changed = waitForEvents(changeEvents);
toggle1.doAction(0);
await changed;
testStates(toggle1, STATE_EXPANDED);
testStates(toggle2, STATE_EXPANDED);
info("Hiding popover1");
changed = waitForEvents(changeEvents);
toggle1.doAction(0);
await changed;
testStates(toggle1, STATE_COLLAPSED);
testStates(toggle2, STATE_COLLAPSED);
const toggle3 = findAccessibleChildByID(docAcc, "toggle3");
// toggle3 is outside popover2's shadow DOM, so the target isn't valid.
testStates(
toggle3,
0,
0,
STATE_EXPANDED | STATE_COLLAPSED,
EXT_STATE_EXPANDABLE
);
const toggle4 = findAccessibleChildByID(docAcc, "toggle4");
// toggle4 is in the same shadow DOM as popover2.
testStates(toggle4, STATE_COLLAPSED);
},
{ chrome: true, topLevel: true }
);