From 107633cba40ab57ab5bf72a298d83a4d75d54917 Mon Sep 17 00:00:00 2001 From: Morgan Reschenberg Date: Tue, 10 Sep 2019 16:16:53 +0000 Subject: [PATCH] Bug 1277201: Fire a STATE_CHANGE event when a details element is opened or closed. r=eeejay Differential Revision: https://phabricator.services.mozilla.com/D44872 --HG-- extra : moz-landing-system : lando --- accessible/generic/DocAccessible.cpp | 35 ++++++++--- accessible/html/HTMLElementAccessibles.cpp | 24 ++++++++ accessible/html/HTMLElementAccessibles.h | 5 ++ .../mochitest/events/test_statechange.html | 60 ++++++++++++++++++- 4 files changed, 114 insertions(+), 10 deletions(-) diff --git a/accessible/generic/DocAccessible.cpp b/accessible/generic/DocAccessible.cpp index f34d8b07854e..9c4d3337936c 100644 --- a/accessible/generic/DocAccessible.cpp +++ b/accessible/generic/DocAccessible.cpp @@ -48,6 +48,7 @@ #include "mozilla/dom/DocumentType.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/MutationEventBinding.h" +#include "HTMLElementAccessibles.h" using namespace mozilla; using namespace mozilla::a11y; @@ -130,7 +131,7 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocAccessible, Accessible) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAnchorJumpElm) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInvalidationList) for (auto it = tmp->mARIAOwnsHash.ConstIter(); !it.Done(); it.Next()) { - nsTArray >* ar = it.UserData(); + nsTArray>* ar = it.UserData(); for (uint32_t i = 0; i < ar->Length(); i++) { NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mARIAOwnsHash entry item"); cb.NoteXPCOMChild(ar->ElementAt(i)); @@ -783,6 +784,22 @@ void DocAccessible::AttributeChangedImpl(Accessible* aAccessible, return; } + // When a details object has its open attribute changed + // we should fire a state-change event on the accessible of + // its main summary + if (aAttribute == nsGkAtoms::open) { + // FromDetails checks if the given accessible belongs to + // a details frame and also locates the accessible of its + // main summary. + if (HTMLSummaryAccessible* summaryAccessible = + HTMLSummaryAccessible::FromDetails(aAccessible)) { + RefPtr expandedChangeEvent = + new AccStateChangeEvent(summaryAccessible, states::EXPANDED); + FireDelayedEvent(expandedChangeEvent); + return; + } + } + // Check for namespaced ARIA attribute if (aNameSpaceID == kNameSpaceID_None) { // Check for hyphenated aria-foo property? @@ -1764,7 +1781,7 @@ void DocAccessible::UpdateRootElIfNeeded() { class InsertIterator final { public: InsertIterator(Accessible* aContext, - const nsTArray >* aNodes) + const nsTArray>* aNodes) : mChild(nullptr), mChildBefore(nullptr), mWalker(aContext), @@ -1796,7 +1813,7 @@ class InsertIterator final { Accessible* mChildBefore; TreeWalker mWalker; - const nsTArray >* mNodes; + const nsTArray>* mNodes; nsTHashtable> mProcessedNodes; uint32_t mNodesIdx; }; @@ -1876,7 +1893,7 @@ bool InsertIterator::Next() { } void DocAccessible::ProcessContentInserted( - Accessible* aContainer, const nsTArray >* aNodes) { + Accessible* aContainer, const nsTArray>* aNodes) { // Process insertions if the container accessible is still in tree. if (!aContainer->IsInDocument()) { return; @@ -2016,7 +2033,7 @@ void DocAccessible::ContentRemoved(Accessible* aChild) { MOZ_DIAGNOSTIC_ASSERT(aChild->Parent(), "Alive but unparented #1"); if (aChild->IsRelocated()) { - nsTArray >* owned = mARIAOwnsHash.Get(parent); + nsTArray>* owned = mARIAOwnsHash.Get(parent); MOZ_ASSERT(owned, "IsRelocated flag is out of sync with mARIAOwnsHash"); owned->RemoveElement(aChild); if (owned->Length() == 0) { @@ -2086,7 +2103,7 @@ void DocAccessible::DoARIAOwnsRelocation(Accessible* aOwner) { logging::TreeInfo("aria owns relocation", logging::eVerbose, aOwner); #endif - nsTArray >* owned = mARIAOwnsHash.LookupOrAdd(aOwner); + nsTArray>* owned = mARIAOwnsHash.LookupOrAdd(aOwner); IDRefsIterator iter(this, aOwner->Elm(), nsGkAtoms::aria_owns); uint32_t idx = 0; @@ -2196,7 +2213,7 @@ void DocAccessible::DoARIAOwnsRelocation(Accessible* aOwner) { } } -void DocAccessible::PutChildrenBack(nsTArray >* aChildren, +void DocAccessible::PutChildrenBack(nsTArray>* aChildren, uint32_t aStartIdx) { MOZ_ASSERT(aStartIdx <= aChildren->Length(), "Wrong removal index"); @@ -2287,7 +2304,7 @@ bool DocAccessible::MoveChild(Accessible* aChild, Accessible* aNewParent, // to update it if needed. if (aChild->IsRelocated()) { aChild->SetRelocated(false); - nsTArray >* owned = mARIAOwnsHash.Get(curParent); + nsTArray>* owned = mARIAOwnsHash.Get(curParent); MOZ_ASSERT(owned, "IsRelocated flag is out of sync with mARIAOwnsHash"); owned->RemoveElement(aChild); if (owned->Length() == 0) { @@ -2383,7 +2400,7 @@ void DocAccessible::UncacheChildrenInSubtree(Accessible* aRoot) { aRoot->mStateFlags |= eIsNotInDocument; RemoveDependentIDsFor(aRoot); - nsTArray >* owned = mARIAOwnsHash.Get(aRoot); + nsTArray>* owned = mARIAOwnsHash.Get(aRoot); uint32_t count = aRoot->ContentChildCount(); for (uint32_t idx = 0; idx < count; idx++) { Accessible* child = aRoot->ContentChildAt(idx); diff --git a/accessible/html/HTMLElementAccessibles.cpp b/accessible/html/HTMLElementAccessibles.cpp index 6083636cd8b5..93ab88b8f16a 100644 --- a/accessible/html/HTMLElementAccessibles.cpp +++ b/accessible/html/HTMLElementAccessibles.cpp @@ -151,6 +151,30 @@ uint64_t HTMLSummaryAccessible::NativeState() const { return state; } +HTMLSummaryAccessible* HTMLSummaryAccessible::FromDetails(Accessible* details) { + if (!dom::HTMLDetailsElement::FromNodeOrNull(details->GetContent())) { + return nullptr; + } + + HTMLSummaryAccessible* summaryAccessible = nullptr; + for (uint32_t i = 0; i < details->ChildCount(); i++) { + // Iterate through the children of our details accessible to locate main + // summary. This iteration includes the anonymous summary if the details + // element was not explicitly created with one. + Accessible* child = details->GetChildAt(i); + auto* summary = + mozilla::dom::HTMLSummaryElement::FromNodeOrNull(child->GetContent()); + if (summary && summary->IsMainSummary()) { + summaryAccessible = static_cast(child); + break; + } + } + + MOZ_ASSERT(summaryAccessible, + "Details objects should have at least one summary"); + return summaryAccessible; +} + //////////////////////////////////////////////////////////////////////////////// // HTMLSummaryAccessible: Widgets diff --git a/accessible/html/HTMLElementAccessibles.h b/accessible/html/HTMLElementAccessibles.h index bcb55b273527..955f482770f4 100644 --- a/accessible/html/HTMLElementAccessibles.h +++ b/accessible/html/HTMLElementAccessibles.h @@ -94,6 +94,11 @@ class HTMLSummaryAccessible : public HyperTextAccessibleWrap { HTMLSummaryAccessible(nsIContent* aContent, DocAccessible* aDoc); + // Check that the given Accessible belongs to a details frame. + // If so, find and return the accessible for the detail frame's + // main summary. + static HTMLSummaryAccessible* FromDetails(Accessible* aDetails); + // Accessible virtual uint64_t NativeState() const override; diff --git a/accessible/tests/mochitest/events/test_statechange.html b/accessible/tests/mochitest/events/test_statechange.html index 46a91832b4b9..4e96076cbd11 100644 --- a/accessible/tests/mochitest/events/test_statechange.html +++ b/accessible/tests/mochitest/events/test_statechange.html @@ -21,6 +21,27 @@ // ////////////////////////////////////////////////////////////////////////// // Invokers + function openNode(aIDDetails, aIDSummary, aIsOpen) { + this.DOMNode = getNode(aIDDetails); + + this.eventSeq = [ + new expandedStateChecker(aIsOpen, getNode(aIDSummary)), + ]; + + this.invoke = function expandNode_invoke() { + if (aIsOpen) { + this.DOMNode.setAttribute("open", ""); + } else { + this.DOMNode.removeAttribute("open"); + } + + }; + + this.getID = function expandNode_getID() { + return prettyName(aIDDetails) + " Open changed to '" + aIsOpen + "'"; + }; + } + function makeEditableDoc(aDocNode, aIsEnabled) { this.DOMNode = aDocNode; @@ -174,6 +195,16 @@ function doTests() { gQueue = new eventQueue(nsIAccessibleEvent.EVENT_STATE_CHANGE); + // Test opening details objects + gQueue.push(new openNode("detailsOpen", "summaryOpen", true)); + gQueue.push(new openNode("detailsOpen", "summaryOpen", false)); + gQueue.push(new openNode("detailsOpen1", "summaryOpen1", true)); + gQueue.push(new openNode("detailsOpen2", "summaryOpen2", true)); + gQueue.push(new openNode("detailsOpen3", "summaryOpen3", true)); + gQueue.push(new openNode("detailsOpen4", "summaryOpen4", true)); + gQueue.push(new openNode("detailsOpen5", "summaryOpen5", true)); + gQueue.push(new openNode("detailsOpen6", "summaryOpen6", true)); + // Test delayed editable state change var doc = document.getElementById("iframe").contentDocument; gQueue.push(new makeEditableDoc(doc)); @@ -212,7 +243,24 @@ addA11yLoadEvent(doTests); - + + +
opendetails can be opened
+
order doesn't matteropen
+
additional elements don't matter
open
+
summarycontent
+
summarycontent
+
summarycontent
+
summarycontent
+ +