Bug 2041502 - Make PresShell forward notifications to DocAccessible. r=layout-reviewers,jwatt

The main difference is that removals get processed before other
observers, like PresShell does. This prevents the options list from
getting re-filled, see the stack in the bug.

While at it, simplify the aria attribute notifications, they don't need
to walk the whole chain, as only DocAccessible cares about it. Route
them through the accessible service like the AttrElement ones.

Differential Revision: https://phabricator.services.mozilla.com/D304716
This commit is contained in:
Emilio Cobos Álvarez
2026-06-04 22:07:09 +00:00
committed by ealvarez@mozilla.com
parent aab0bd17f9
commit 1ffa4bb822
15 changed files with 143 additions and 158 deletions
@@ -755,6 +755,24 @@ void nsAccessibilityService::NotifyAttrElementChanged(
}
}
void nsAccessibilityService::NotifyARIAAttributeDefaultWillChange(
mozilla::dom::Element* aElement, nsAtom* aAttribute, AttrModType aModType) {
mozilla::dom::Document* doc = aElement->OwnerDoc();
MOZ_ASSERT(doc);
if (DocAccessible* docAcc = GetDocAccessible(doc)) {
docAcc->ARIAAttributeDefaultWillChange(aElement, aAttribute, aModType);
}
}
void nsAccessibilityService::NotifyARIAAttributeDefaultChanged(
mozilla::dom::Element* aElement, nsAtom* aAttribute, AttrModType aModType) {
mozilla::dom::Document* doc = aElement->OwnerDoc();
MOZ_ASSERT(doc);
if (DocAccessible* docAcc = GetDocAccessible(doc)) {
docAcc->ARIAAttributeDefaultChanged(aElement, aAttribute, aModType);
}
}
void nsAccessibilityService::AriaNotify(
nsINode* aNode, const nsAString& aAnnouncement,
const mozilla::dom::AriaNotificationOptions& aOptions) {
+11
View File
@@ -313,6 +313,17 @@ class nsAccessibilityService final : public mozilla::a11y::DocManager,
*/
void NotifyAttrElementChanged(mozilla::dom::Element* aElement, nsAtom* aAttr);
/**
* Notify accessibility that an ARIA attribute reflected from ElementInternals
* is about to change / has changed. See dom::ElementInternals.
*/
void NotifyARIAAttributeDefaultWillChange(mozilla::dom::Element* aElement,
nsAtom* aAttribute,
AttrModType aModType);
void NotifyARIAAttributeDefaultChanged(mozilla::dom::Element* aElement,
nsAtom* aAttribute,
AttrModType aModType);
void AriaNotify(nsINode* aNode, const nsAString& aAnnouncement,
const mozilla::dom::AriaNotificationOptions& aOptions);
-48
View File
@@ -151,8 +151,6 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocAccessible, LocalAccessible)
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocAccessible)
NS_INTERFACE_MAP_ENTRY(nsIDocumentObserver)
NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
NS_INTERFACE_MAP_END_INHERITING(HyperTextAccessible)
@@ -721,9 +719,6 @@ nsRect DocAccessible::RelativeBounds(nsIFrame** aRelativeFrame) const {
// DocAccessible protected member
nsresult DocAccessible::AddEventListeners() {
SelectionMgr()->AddDocSelectionListener(mPresShell);
// Add document observer.
mDocumentNode->AddObserver(this);
return NS_OK;
}
@@ -732,10 +727,6 @@ nsresult DocAccessible::RemoveEventListeners() {
// Remove listeners associated with content documents
NS_ASSERTION(mDocumentNode, "No document during removal of listeners.");
if (mDocumentNode) {
mDocumentNode->RemoveObserver(this);
}
if (mScrollWatchTimer) {
mScrollWatchTimer->Cancel();
mScrollWatchTimer = nullptr;
@@ -839,12 +830,6 @@ std::pair<nsPoint, nsRect> DocAccessible::ComputeScrollData(
return {scrollPoint, scrollRange};
}
////////////////////////////////////////////////////////////////////////////////
// nsIDocumentObserver
NS_IMPL_NSIDOCUMENTOBSERVER_CORE_STUB(DocAccessible)
NS_IMPL_NSIDOCUMENTOBSERVER_LOAD_STUB(DocAccessible)
// When a reflected element IDL attribute changes, we might get the following
// synchronous calls:
// 1. AttributeWillChange for the element.
@@ -1089,11 +1074,6 @@ void DocAccessible::ARIAActiveDescendantChanged(LocalAccessible* aAccessible) {
}
}
void DocAccessible::ContentAppended(nsIContent* aFirstNewContent,
const ContentAppendInfo&) {
MaybeHandleChangeToHiddenNameOrDescription(aFirstNewContent);
}
void DocAccessible::ElementStateChanged(dom::Document* aDocument,
dom::Element* aElement,
dom::ElementState aStateMask) {
@@ -1225,34 +1205,6 @@ void DocAccessible::ElementStateChanged(dom::Document* aDocument,
}
}
void DocAccessible::CharacterDataWillChange(nsIContent* aContent,
const CharacterDataChangeInfo&) {}
void DocAccessible::CharacterDataChanged(nsIContent* aContent,
const CharacterDataChangeInfo&) {
MaybeHandleChangeToHiddenNameOrDescription(aContent);
}
void DocAccessible::ContentInserted(nsIContent* aChild,
const ContentInsertInfo&) {
MaybeHandleChangeToHiddenNameOrDescription(aChild);
}
void DocAccessible::ContentWillBeRemoved(nsIContent* aChildNode,
const ContentRemoveInfo&) {
#ifdef A11Y_LOG
if (logging::IsEnabled(logging::eTree)) {
logging::MsgBegin("TREE", "DOM content removed; doc: %p", this);
logging::Node("container node", aChildNode->GetParent());
logging::Node("content node", aChildNode);
logging::MsgEnd();
}
#endif
ContentRemoved(aChildNode);
}
void DocAccessible::ParentChainChanged(nsIContent* aContent) {}
////////////////////////////////////////////////////////////////////////////////
// LocalAccessible
+23 -15
View File
@@ -43,7 +43,6 @@ class TNotification;
* all use this class to represent the doc they contain.
*/
class DocAccessible : public HyperTextAccessible,
public nsIDocumentObserver,
public nsSupportsWeakReference {
NS_DECL_ISUPPORTS_INHERITED
NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DocAccessible, LocalAccessible)
@@ -54,14 +53,11 @@ class DocAccessible : public HyperTextAccessible,
public:
DocAccessible(Document* aDocument, PresShell* aPresShell);
// nsIDocumentObserver
NS_DECL_NSIDOCUMENTOBSERVER
// LocalAccessible
virtual void Init();
virtual void Shutdown() override;
virtual nsIFrame* GetFrame() const override;
virtual nsINode* GetNode() const override;
void Shutdown() override;
nsIFrame* GetFrame() const override;
nsINode* GetNode() const override;
Document* DocumentNode() const { return mDocumentNode; }
virtual mozilla::a11y::ENameValueFlag DirectName(
@@ -442,6 +438,26 @@ class DocAccessible : public HyperTextAccessible,
*/
uint64_t EffectiveCacheDomains() const;
/**
* For hidden subtrees, fire a name/description change event if the subtree
* is a target of aria-labelledby/describedby.
* This does nothing if it is called on a node which is not part of a hidden
* aria-labelledby/describedby target.
*/
void MaybeHandleChangeToHiddenNameOrDescription(nsIContent* aChild);
void AttributeWillChange(dom::Element* aElement, int32_t aNameSpaceID,
nsAtom* aAttribute, AttrModType aModType);
virtual void AttributeChanged(dom::Element* aElement, int32_t aNameSpaceID,
nsAtom* aAttribute, AttrModType aModType,
const nsAttrValue* aOldValue);
void ElementStateChanged(dom::Document* aDocument, dom::Element* aElement,
dom::ElementState aStateMask);
void ARIAAttributeDefaultWillChange(dom::Element* aElement,
nsAtom* aAttribute, AttrModType aModType);
void ARIAAttributeDefaultChanged(dom::Element* aElement, nsAtom* aAttribute,
AttrModType aModType);
protected:
virtual ~DocAccessible();
@@ -860,14 +876,6 @@ class DocAccessible : public HyperTextAccessible,
*/
void TrackMovedAccessible(LocalAccessible* aAcc);
/**
* For hidden subtrees, fire a name/description change event if the subtree
* is a target of aria-labelledby/describedby.
* This does nothing if it is called on a node which is not part of a hidden
* aria-labelledby/describedby target.
*/
void MaybeHandleChangeToHiddenNameOrDescription(nsIContent* aChild);
void MaybeHandleChangeToAriaActions(LocalAccessible* aAcc,
const nsAtom* aAttribute);
+4 -4
View File
@@ -22,11 +22,11 @@ class DocAccessibleWrap : public DocAccessible {
virtual ~DocAccessibleWrap();
virtual void Shutdown() override;
void Shutdown() override;
virtual void AttributeChanged(dom::Element* aElement, int32_t aNameSpaceID,
nsAtom* aAttribute, AttrModType aModType,
const nsAttrValue* aOldValue) override;
void AttributeChanged(dom::Element* aElement, int32_t aNameSpaceID,
nsAtom* aAttribute, AttrModType aModType,
const nsAttrValue* aOldValue) override;
void QueueNewLiveRegion(LocalAccessible* aAccessible);
+20
View File
@@ -0,0 +1,20 @@
<!doctype html>
<style>
*:nth-child(odd) {
display: contents;
inset-block: 41%;
scroll-padding-block: 98% auto;
}
</style>
<script>
document.addEventListener("DOMContentLoaded", () => {
a.selectedIndex = 1;
a.remove(0);
window.__foo = a.options.length;
})
</script>
<datalist tabindex="-1">
</datalist>
<select id="a" size="37">
<optgroup>
<option>
@@ -19,6 +19,7 @@ load 1655983.html
load 1838250.html
load 1979253.html
load 2021475.html
load 2041502.html
# last_test_to_unload_testsuite.xhtml MUST be the last test in the list because it
# is responsible for shutting down accessibility service affecting later tests.
-16
View File
@@ -188,22 +188,6 @@ void MutationObservers::NotifyContentWillBeRemoved(
nsIMutationObserver::kContentWillBeRemoved);
}
void MutationObservers::NotifyARIAAttributeDefaultWillChange(
mozilla::dom::Element* aElement, nsAtom* aAttribute, AttrModType aModType) {
Notify<NotifyPresShell::No>(
aElement,
NOTIFIER(ARIAAttributeDefaultWillChange, aElement, aAttribute, aModType),
nsIMutationObserver::kARIAAttributeDefaultWillChange);
}
void MutationObservers::NotifyARIAAttributeDefaultChanged(
mozilla::dom::Element* aElement, nsAtom* aAttribute, AttrModType aModType) {
Notify<NotifyPresShell::No>(
aElement,
NOTIFIER(ARIAAttributeDefaultChanged, aElement, aAttribute, aModType),
nsIMutationObserver::kARIAAttributeDefaultChanged);
}
} // namespace mozilla
void MutationObservers::NotifyAnimationMutated(
-7
View File
@@ -126,13 +126,6 @@ class MutationObservers {
}
}
static void NotifyARIAAttributeDefaultWillChange(
mozilla::dom::Element* aElement, nsAtom* aAttribute,
AttrModType aModType);
static void NotifyARIAAttributeDefaultChanged(mozilla::dom::Element* aElement,
nsAtom* aAttribute,
AttrModType aModType);
/**
* Notify that an animation is added/changed/removed.
* @param aAnimation The animation we added/changed/removed.
+11 -38
View File
@@ -359,13 +359,6 @@ class nsIMutationObserver
virtual void ParentChainChanged(nsIContent* aContent) = 0;
virtual void ARIAAttributeDefaultWillChange(mozilla::dom::Element* aElement,
nsAtom* aAttribute,
AttrModType aModType) = 0;
virtual void ARIAAttributeDefaultChanged(mozilla::dom::Element* aElement,
nsAtom* aAttribute,
AttrModType aModType) = 0;
enum : uint32_t {
kNone = 0,
kCharacterDataWillChange = 1 << 0,
@@ -378,8 +371,6 @@ class nsIMutationObserver
kContentWillBeRemoved = 1 << 7,
kNodeWillBeDestroyed = 1 << 8,
kParentChainChanged = 1 << 9,
kARIAAttributeDefaultWillChange = 1 << 10,
kARIAAttributeDefaultChanged = 1 << 11,
kBeginUpdate = 1 << 12,
kEndUpdate = 1 << 13,
@@ -443,28 +434,16 @@ class nsIMutationObserver
#define NS_DECL_NSIMUTATIONOBSERVER_PARENTCHAINCHANGED \
virtual void ParentChainChanged(nsIContent* aContent) override;
#define NS_DECL_NSIMUTATIONOBSERVER_ARIAATTRIBUTEDEFAULTWILLCHANGE \
virtual void ARIAAttributeDefaultWillChange(mozilla::dom::Element* aElement, \
nsAtom* aAttribute, \
AttrModType aModType) override;
#define NS_DECL_NSIMUTATIONOBSERVER_ARIAATTRIBUTEDEFAULTCHANGED \
virtual void ARIAAttributeDefaultChanged(mozilla::dom::Element* aElement, \
nsAtom* aAttribute, \
AttrModType aModType) override;
#define NS_DECL_NSIMUTATIONOBSERVER \
NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATAWILLCHANGE \
NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED \
NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTEWILLCHANGE \
NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED \
NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED \
NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED \
NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED \
NS_DECL_NSIMUTATIONOBSERVER_NODEWILLBEDESTROYED \
NS_DECL_NSIMUTATIONOBSERVER_PARENTCHAINCHANGED \
NS_DECL_NSIMUTATIONOBSERVER_ARIAATTRIBUTEDEFAULTWILLCHANGE \
NS_DECL_NSIMUTATIONOBSERVER_ARIAATTRIBUTEDEFAULTCHANGED
#define NS_DECL_NSIMUTATIONOBSERVER \
NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATAWILLCHANGE \
NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED \
NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTEWILLCHANGE \
NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED \
NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED \
NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED \
NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED \
NS_DECL_NSIMUTATIONOBSERVER_NODEWILLBEDESTROYED \
NS_DECL_NSIMUTATIONOBSERVER_PARENTCHAINCHANGED
#define NS_IMPL_NSIMUTATIONOBSERVER_CORE_STUB(_class) \
void _class::NodeWillBeDestroyed(nsINode* aNode) {}
@@ -487,12 +466,6 @@ class nsIMutationObserver
} \
void _class::ContentWillBeRemoved(nsIContent* aChild, \
const ContentRemoveInfo&) {} \
void _class::ParentChainChanged(nsIContent* aContent) {} \
void _class::ARIAAttributeDefaultWillChange(mozilla::dom::Element* aElement, \
nsAtom* aAttribute, \
AttrModType aModType) {} \
void _class::ARIAAttributeDefaultChanged(mozilla::dom::Element* aElement, \
nsAtom* aAttribute, \
AttrModType aModType) {}
void _class::ParentChainChanged(nsIContent* aContent) {}
#endif /* nsIMutationObserver_h */
-14
View File
@@ -110,20 +110,6 @@ class MutationObserverWrapper final : public nsIMutationObserver {
mOwner->ParentChainChanged(aContent);
}
void ARIAAttributeDefaultWillChange(mozilla::dom::Element* aElement,
nsAtom* aAttribute,
AttrModType aModType) override {
MOZ_ASSERT(mOwner);
mOwner->ARIAAttributeDefaultWillChange(aElement, aAttribute, aModType);
}
void ARIAAttributeDefaultChanged(mozilla::dom::Element* aElement,
nsAtom* aAttribute,
AttrModType aModType) override {
MOZ_ASSERT(mOwner);
mOwner->ARIAAttributeDefaultChanged(aElement, aAttribute, aModType);
}
MozExternalRefCountType AddRefWrapper() {
nsrefcnt count = ++mRefCnt;
NS_LOG_ADDREF(this, count, "MutationObserverWrapper", sizeof(*this));
+10 -4
View File
@@ -456,10 +456,13 @@ nsresult ElementInternals::SetAttr(nsAtom* aName, const nsAString& aValue) {
Document* document = mTarget->GetComposedDoc();
mozAutoDocUpdate updateBatch(document, true);
#ifdef ACCESSIBILITY
const AttrModType modType =
mAttrs.HasAttr(aName) ? AttrModType::Modification : AttrModType::Addition;
MutationObservers::NotifyARIAAttributeDefaultWillChange(mTarget, aName,
modType);
if (auto* accService = GetAccService()) {
accService->NotifyARIAAttributeDefaultWillChange(mTarget, aName, modType);
}
#endif
nsAttrValue attrValue(aValue);
nsresult rs = NS_OK;
@@ -474,8 +477,11 @@ nsresult ElementInternals::SetAttr(nsAtom* aName, const nsAString& aValue) {
}
nsMutationGuard::DidMutate();
MutationObservers::NotifyARIAAttributeDefaultChanged(mTarget, aName, modType);
#ifdef ACCESSIBILITY
if (auto* accService = GetAccService()) {
accService->NotifyARIAAttributeDefaultChanged(mTarget, aName, modType);
}
#endif
return rs;
}
+45
View File
@@ -4560,6 +4560,12 @@ MOZ_CAN_RUN_SCRIPT_BOUNDARY void PresShell::CharacterDataChanged(
MOZ_ASSERT(!mIsDocumentGone, "Unexpected CharacterDataChanged");
MOZ_ASSERT(aContent->OwnerDoc() == mDocument, "Unexpected document");
#ifdef ACCESSIBILITY
if (mDocAccessible) {
mDocAccessible->MaybeHandleChangeToHiddenNameOrDescription(aContent);
}
#endif
nsAutoCauseReflowNotifier crNotifier(this);
mPresContext->RestyleManager()->CharacterDataChanged(aContent, aInfo);
@@ -4572,6 +4578,12 @@ MOZ_CAN_RUN_SCRIPT_BOUNDARY void PresShell::ElementStateChanged(
MOZ_ASSERT(!mIsDocumentGone, "Unexpected ContentStateChanged");
MOZ_ASSERT(aDocument == mDocument, "Unexpected aDocument");
#ifdef ACCESSIBILITY
if (mDocAccessible) {
mDocAccessible->ElementStateChanged(aDocument, aElement, aStateMask);
}
#endif
if (mDidInitialize) {
nsAutoCauseReflowNotifier crNotifier(this);
mPresContext->RestyleManager()->ElementStateChanged(aElement, aStateMask);
@@ -4621,6 +4633,13 @@ MOZ_CAN_RUN_SCRIPT_BOUNDARY void PresShell::AttributeWillChange(
MOZ_ASSERT(!mIsDocumentGone, "Unexpected AttributeWillChange");
MOZ_ASSERT(aElement->OwnerDoc() == mDocument, "Unexpected document");
#ifdef ACCESSIBILITY
if (mDocAccessible) {
mDocAccessible->AttributeWillChange(aElement, aNameSpaceID, aAttribute,
aModType);
}
#endif
// XXXwaterson it might be more elegant to wait until after the
// initial reflow to begin observing the document. That would
// squelch any other inappropriate notifications as well.
@@ -4638,6 +4657,13 @@ MOZ_CAN_RUN_SCRIPT_BOUNDARY void PresShell::AttributeChanged(
MOZ_ASSERT(!mIsDocumentGone, "Unexpected AttributeChanged");
MOZ_ASSERT(aElement->OwnerDoc() == mDocument, "Unexpected document");
#ifdef ACCESSIBILITY
if (mDocAccessible) {
mDocAccessible->AttributeChanged(aElement, aNameSpaceID, aAttribute,
aModType, aOldValue);
}
#endif
// XXXwaterson it might be more elegant to wait until after the
// initial reflow to begin observing the document. That would
// squelch any other inappropriate notifications as well.
@@ -4684,6 +4710,13 @@ MOZ_CAN_RUN_SCRIPT_BOUNDARY void PresShell::ContentAppended(
return;
}
#ifdef ACCESSIBILITY
if (mDocAccessible) {
mDocAccessible->MaybeHandleChangeToHiddenNameOrDescription(
aFirstNewContent);
}
#endif
mPresContext->EventStateManager()->ContentAppended(aFirstNewContent, aInfo);
if (aInfo.mOldParent) {
@@ -4713,6 +4746,12 @@ MOZ_CAN_RUN_SCRIPT_BOUNDARY void PresShell::ContentInserted(
mPresContext->EventStateManager()->ContentInserted(aChild, aInfo);
#ifdef ACCESSIBILITY
if (mDocAccessible) {
mDocAccessible->MaybeHandleChangeToHiddenNameOrDescription(aChild);
}
#endif
if (aInfo.mOldParent) {
MaybeDestroyFramesAndStyles(aChild, *mPresContext);
}
@@ -4738,6 +4777,12 @@ MOZ_CAN_RUN_SCRIPT_BOUNDARY void PresShell::ContentWillBeRemoved(
mPresContext->EventStateManager()->ContentRemoved(mDocument, aChild, aInfo);
#ifdef ACCESSIBILITY
if (mDocAccessible) {
mDocAccessible->ContentRemoved(aChild);
}
#endif
nsAutoCauseReflowNotifier crNotifier(this);
for (AutoConnectedAncestorTracker* tracker = mLastConnectedAncestorTracker;
@@ -203,12 +203,6 @@ void nsFormFillController::AttributeWillChange(mozilla::dom::Element*, int32_t,
void nsFormFillController::ParentChainChanged(nsIContent*) {}
void nsFormFillController::ARIAAttributeDefaultWillChange(
mozilla::dom::Element*, nsAtom*, AttrModType) {}
void nsFormFillController::ARIAAttributeDefaultChanged(mozilla::dom::Element*,
nsAtom*, AttrModType) {}
MOZ_CAN_RUN_SCRIPT_BOUNDARY
void nsFormFillController::NodeWillBeDestroyed(nsINode* aNode) {
MOZ_LOG(sLogger, LogLevel::Verbose, ("NodeWillBeDestroyed: %p", aNode));
-6
View File
@@ -145,12 +145,6 @@ void nsMenuGroupOwnerX::ContentInserted(nsIContent* aChild,
void nsMenuGroupOwnerX::ParentChainChanged(nsIContent* aContent) {}
void nsMenuGroupOwnerX::ARIAAttributeDefaultWillChange(mozilla::dom::Element*,
nsAtom*, AttrModType) {}
void nsMenuGroupOwnerX::ARIAAttributeDefaultChanged(mozilla::dom::Element*,
nsAtom*, AttrModType) {}
// For change management, we don't use a |nsSupportsHashtable| because
// we know that the lifetime of all these items is bounded by the
// lifetime of the menubar. No need to add any more strong refs to the