From 7991e8091eabd05255126591b5373f31f9077fde Mon Sep 17 00:00:00 2001 From: Eitan Isaacson Date: Fri, 7 Feb 2025 00:47:55 +0000 Subject: [PATCH] Bug 1912520 - Morph aria-owns to controls relationship when container is combobox. r=Jamie Differential Revision: https://phabricator.services.mozilla.com/D236931 --- accessible/base/nsAccUtils.cpp | 10 +++++ accessible/base/nsAccUtils.h | 2 + accessible/generic/DocAccessible.cpp | 7 ++++ accessible/generic/LocalAccessible.cpp | 34 ++++++++++++++--- .../e10s/browser_treeupdate_ariaowns.js | 38 ++++++++++++++++++- 5 files changed, 85 insertions(+), 6 deletions(-) diff --git a/accessible/base/nsAccUtils.cpp b/accessible/base/nsAccUtils.cpp index 87c6b75ddde2..9355ab05578d 100644 --- a/accessible/base/nsAccUtils.cpp +++ b/accessible/base/nsAccUtils.cpp @@ -637,3 +637,13 @@ int32_t nsAccUtils::FindARIAAttrValueIn(dom::Element* aElement, } return index; } + +bool nsAccUtils::IsEditableARIACombobox(const LocalAccessible* aAccessible) { + const nsRoleMapEntry* roleMap = aAccessible->ARIARoleMap(); + if (!roleMap || roleMap->role != roles::EDITCOMBOBOX) { + return false; + } + + return aAccessible->IsTextField() || + aAccessible->Elm()->State().HasState(dom::ElementState::READWRITE); +} diff --git a/accessible/base/nsAccUtils.h b/accessible/base/nsAccUtils.h index 0b4f8e6a944f..2534cf460e41 100644 --- a/accessible/base/nsAccUtils.h +++ b/accessible/base/nsAccUtils.h @@ -301,6 +301,8 @@ class nsAccUtils { const nsAtom* aName, AttrArray::AttrValuesArray* aValues, nsCaseTreatment aCaseSensitive); + + static bool IsEditableARIACombobox(const LocalAccessible* aAccessible); }; } // namespace a11y diff --git a/accessible/generic/DocAccessible.cpp b/accessible/generic/DocAccessible.cpp index 32feecce09fc..f4f083a449ab 100644 --- a/accessible/generic/DocAccessible.cpp +++ b/accessible/generic/DocAccessible.cpp @@ -2470,6 +2470,13 @@ void DocAccessible::DoARIAOwnsRelocation(LocalAccessible* aOwner) { logging::TreeInfo("aria owns relocation", logging::eVerbose, aOwner); #endif + const nsRoleMapEntry* roleMap = aOwner->ARIARoleMap(); + if (roleMap && roleMap->role == roles::EDITCOMBOBOX) { + // The READWRITE state of a combobox may sever aria-owns relations + // we fallback to "controls" relations. + QueueCacheUpdate(aOwner, CacheDomain::Relations); + } + nsTArray>* owned = mARIAOwnsHash.GetOrInsertNew(aOwner); diff --git a/accessible/generic/LocalAccessible.cpp b/accessible/generic/LocalAccessible.cpp index 54c3922f9c9c..44e66861ed5b 100644 --- a/accessible/generic/LocalAccessible.cpp +++ b/accessible/generic/LocalAccessible.cpp @@ -2281,14 +2281,32 @@ Relation LocalAccessible::RelationByType(RelationType aType) const { return Relation(); } - case RelationType::CONTROLLED_BY: - return Relation(new RelatedAccIterator(Document(), mContent, - nsGkAtoms::aria_controls)); + case RelationType::CONTROLLED_BY: { + Relation rel(new RelatedAccIterator(Document(), mContent, + nsGkAtoms::aria_controls)); + RelatedAccIterator owners(Document(), mContent, nsGkAtoms::aria_owns); + if (LocalAccessible* owner = owners.Next()) { + if (nsAccUtils::IsEditableARIACombobox(owner)) { + MOZ_ASSERT(!IsRelocated(), + "Child is not relocated to editable combobox"); + rel.AppendTarget(owner); + } + } + + return rel; + } case RelationType::CONTROLLER_FOR: { Relation rel(new AssociatedElementsIterator(mDoc, mContent, nsGkAtoms::aria_controls)); rel.AppendIter(new HTMLOutputIterator(Document(), mContent)); + if (nsAccUtils::IsEditableARIACombobox(this)) { + AssociatedElementsIterator iter(mDoc, mContent, nsGkAtoms::aria_owns); + while (Accessible* owned_child = iter.Next()) { + MOZ_ASSERT(!owned_child->AsLocal()->IsRelocated()); + rel.AppendTarget(owned_child->AsLocal()); + } + } return rel; } @@ -4080,11 +4098,17 @@ already_AddRefed LocalAccessible::BundleFieldsForCache( dom::HTMLLabelElement::FromNode(mContent)) { rel.AppendTarget(mDoc, labelEl->GetControl()); } - } else if (data.mType == RelationType::DETAILS) { + } else if (data.mType == RelationType::DETAILS || + data.mType == RelationType::CONTROLLER_FOR) { // We need to use RelationByType for details because it might include // popovertarget. Nothing exposes an implicit reverse details // relation, so using RelationByType here is fine. - rel = RelationByType(RelationType::DETAILS); + // + // We need to use RelationByType for controls because it might include + // failed aria-owned relocations or it may be an output element. + // Nothing exposes an implicit reverse controls relation, so using + // RelationByType here is fine. + rel = RelationByType(data.mType); } else { // We use an AssociatedElementsIterator here instead of calling // RelationByType directly because we only want to cache explicit diff --git a/accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js b/accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js index c26eabab7011..24ca0d31c38b 100644 --- a/accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js +++ b/accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js @@ -622,8 +622,8 @@ addAccessibleTask( async function (browser, accDoc) { const p = findAccessibleChildByID(accDoc, "p"); const textbox = findAccessibleChildByID(accDoc, "textbox"); - testStates(textbox, 0, EXT_STATE_EDITABLE, 0, 0); + testStates(textbox, 0, EXT_STATE_EDITABLE, 0, 0); isnot(getAccessibleDOMNodeID(p.lastChild), "btn", "'p' owns relocated btn"); is(textbox.value, "Hello"); @@ -700,3 +700,39 @@ addAccessibleTask( ); } ); + +addAccessibleTask( + ` +
+
    +
  • apple
  • +
  • peach
  • +
+`, + async (browser, accDoc) => { + const combobox = findAccessibleChildByID(accDoc, "box"); + const listbox = findAccessibleChildByID(accDoc, "listbox"); + + testStates(combobox, 0, EXT_STATE_EDITABLE, 0, 0); + is(combobox.childCount, 0, "combobox has no children"); + await testCachedRelation(combobox, RELATION_CONTROLLER_FOR, [listbox]); + await testCachedRelation(listbox, RELATION_CONTROLLED_BY, [combobox]); + + let expectedEvents = Promise.all([ + waitForStateChange(combobox, EXT_STATE_EDITABLE, false, true), + waitForEvent(EVENT_REORDER, accDoc), + ]); + await invokeContentTask(browser, [], () => { + content.document.getElementById("box").contentEditable = false; + }); + await expectedEvents; + await testCachedRelation(combobox, RELATION_CONTROLLER_FOR, []); + await testCachedRelation(listbox, RELATION_CONTROLLED_BY, []); + is(combobox.childCount, 1, "combobox has listbox"); + } +);