From 79884b85d125f35ae91fc39a06a3439737c737a0 Mon Sep 17 00:00:00 2001 From: Eitan Isaacson Date: Tue, 19 Nov 2024 15:02:18 +0000 Subject: [PATCH] Bug 1769586 - P2: Implement a11y support of ARIA element reflection. r=Jamie Depends on D209767 Differential Revision: https://phabricator.services.mozilla.com/D209768 --- accessible/base/ARIAMap.cpp | 33 ++- accessible/base/ARIAMap.h | 25 ++ accessible/base/AccIterator.cpp | 17 +- accessible/base/AccIterator.h | 13 +- accessible/base/nsAccUtils.cpp | 16 ++ accessible/base/nsAccUtils.h | 2 + accessible/generic/DocAccessible.cpp | 62 ++++- accessible/generic/LocalAccessible.cpp | 3 +- .../e10s/browser_caching_relations_002.js | 244 ++++++++++++++++++ .../e10s/browser_treeupdate_ariaowns.js | 105 +++++++- accessible/tests/browser/e10s/head.js | 87 ++++++- dom/base/Element.cpp | 16 ++ dom/base/Element.h | 10 + dom/html/ElementInternals.cpp | 18 ++ dom/html/ElementInternals.h | 2 + 15 files changed, 622 insertions(+), 31 deletions(-) diff --git a/accessible/base/ARIAMap.cpp b/accessible/base/ARIAMap.cpp index fd5a30d24189..78c6eef7a830 100644 --- a/accessible/base/ARIAMap.cpp +++ b/accessible/base/ARIAMap.cpp @@ -1451,33 +1451,33 @@ static const AttrCharacteristics gWAIUnivAttrMap[] = { {nsGkAtoms::aria_checked, ATTR_BYPASSOBJ | ATTR_VALTOKEN }, /* exposes checkable obj attr */ {nsGkAtoms::aria_colcount, ATTR_VALINT }, {nsGkAtoms::aria_colindex, ATTR_VALINT }, - {nsGkAtoms::aria_controls, ATTR_BYPASSOBJ | ATTR_GLOBAL }, + {nsGkAtoms::aria_controls, ATTR_BYPASSOBJ | ATTR_GLOBAL | ATTR_REFLECT_ELEMENTS }, {nsGkAtoms::aria_current, ATTR_BYPASSOBJ_IF_FALSE | ATTR_VALTOKEN | ATTR_GLOBAL }, - {nsGkAtoms::aria_describedby, ATTR_BYPASSOBJ | ATTR_GLOBAL }, + {nsGkAtoms::aria_describedby, ATTR_BYPASSOBJ | ATTR_GLOBAL | ATTR_REFLECT_ELEMENTS }, // XXX Ideally, aria-description shouldn't expose a description object // attribute (i.e. it should have ATTR_BYPASSOBJ). However, until the // description-from attribute is implemented (bug 1726087), clients such as // NVDA depend on the description object attribute to work out whether the // accDescription originated from aria-description. {nsGkAtoms::aria_description, ATTR_GLOBAL }, - {nsGkAtoms::aria_details, ATTR_BYPASSOBJ | ATTR_GLOBAL }, + {nsGkAtoms::aria_details, ATTR_BYPASSOBJ | ATTR_GLOBAL | ATTR_REFLECT_ELEMENTS }, {nsGkAtoms::aria_disabled, ATTR_BYPASSOBJ | ATTR_VALTOKEN | ATTR_GLOBAL }, {nsGkAtoms::aria_dropeffect, ATTR_VALTOKEN | ATTR_GLOBAL }, - {nsGkAtoms::aria_errormessage, ATTR_BYPASSOBJ | ATTR_GLOBAL }, + {nsGkAtoms::aria_errormessage, ATTR_BYPASSOBJ | ATTR_GLOBAL | ATTR_REFLECT_ELEMENTS }, {nsGkAtoms::aria_expanded, ATTR_BYPASSOBJ | ATTR_VALTOKEN }, - {nsGkAtoms::aria_flowto, ATTR_BYPASSOBJ | ATTR_GLOBAL }, + {nsGkAtoms::aria_flowto, ATTR_BYPASSOBJ | ATTR_GLOBAL | ATTR_REFLECT_ELEMENTS }, {nsGkAtoms::aria_grabbed, ATTR_VALTOKEN | ATTR_GLOBAL }, {nsGkAtoms::aria_haspopup, ATTR_BYPASSOBJ_IF_FALSE | ATTR_VALTOKEN | ATTR_GLOBAL }, {nsGkAtoms::aria_hidden, ATTR_BYPASSOBJ | ATTR_VALTOKEN | ATTR_GLOBAL }, /* handled special way */ {nsGkAtoms::aria_invalid, ATTR_BYPASSOBJ | ATTR_VALTOKEN | ATTR_GLOBAL }, {nsGkAtoms::aria_label, ATTR_BYPASSOBJ | ATTR_GLOBAL }, - {nsGkAtoms::aria_labelledby, ATTR_BYPASSOBJ | ATTR_GLOBAL }, + {nsGkAtoms::aria_labelledby, ATTR_BYPASSOBJ | ATTR_GLOBAL | ATTR_REFLECT_ELEMENTS }, {nsGkAtoms::aria_level, ATTR_BYPASSOBJ }, /* handled via groupPosition */ {nsGkAtoms::aria_live, ATTR_VALTOKEN | ATTR_GLOBAL }, {nsGkAtoms::aria_modal, ATTR_BYPASSOBJ | ATTR_VALTOKEN | ATTR_GLOBAL }, {nsGkAtoms::aria_multiline, ATTR_BYPASSOBJ | ATTR_VALTOKEN }, {nsGkAtoms::aria_multiselectable, ATTR_BYPASSOBJ | ATTR_VALTOKEN }, - {nsGkAtoms::aria_owns, ATTR_BYPASSOBJ | ATTR_GLOBAL }, + {nsGkAtoms::aria_owns, ATTR_BYPASSOBJ | ATTR_GLOBAL | ATTR_REFLECT_ELEMENTS }, {nsGkAtoms::aria_orientation, ATTR_VALTOKEN }, {nsGkAtoms::aria_posinset, ATTR_BYPASSOBJ }, /* handled via groupPosition */ {nsGkAtoms::aria_pressed, ATTR_BYPASSOBJ | ATTR_VALTOKEN }, @@ -1737,3 +1737,22 @@ bool AttrIterator::ExposeAttr(AccAttributes* aTargetAttrs) const { } return false; } + +//////////////////////////////////////////////////////////////////////////////// +// AttrWithCharacteristicsIterator class +bool AttrWithCharacteristicsIterator::Next() { + for (mIdx++; mIdx < static_cast(ArrayLength(gWAIUnivAttrMap)); + mIdx++) { + if (gWAIUnivAttrMap[mIdx].characteristics & mCharacteristics) { + return true; + } + } + + return false; +} + +nsStaticAtom* AttrWithCharacteristicsIterator::AttrName() const { + return mIdx >= 0 + ? const_cast(gWAIUnivAttrMap[mIdx].attributeName) + : nullptr; +} diff --git a/accessible/base/ARIAMap.h b/accessible/base/ARIAMap.h index 2fa3ac80d4a7..984072e26779 100644 --- a/accessible/base/ARIAMap.h +++ b/accessible/base/ARIAMap.h @@ -124,6 +124,11 @@ const uint8_t ATTR_GLOBAL = 0x1 << 3; */ const uint8_t ATTR_VALINT = 0x1 << 4; +/** + * Indicates that the attribute can have reflected elements. + */ +const uint8_t ATTR_REFLECT_ELEMENTS = 0x1 << 5; + //////////////////////////////////////////////////////////////////////////////// // State map entry @@ -346,6 +351,26 @@ class AttrIterator { uint8_t mAttrCharacteristics; }; +class AttrWithCharacteristicsIterator { + public: + explicit AttrWithCharacteristicsIterator(uint8_t aCharacteristics) + : mIdx(-1), mCharacteristics(aCharacteristics) {} + + bool Next(); + + nsStaticAtom* AttrName() const; + + private: + AttrWithCharacteristicsIterator() = delete; + AttrWithCharacteristicsIterator(const AttrWithCharacteristicsIterator&) = + delete; + AttrWithCharacteristicsIterator& operator=( + const AttrWithCharacteristicsIterator&) = delete; + + int32_t mIdx; + uint8_t mCharacteristics; +}; + } // namespace aria } // namespace a11y } // namespace mozilla diff --git a/accessible/base/AccIterator.cpp b/accessible/base/AccIterator.cpp index d28d5fcbe962..2238450ba43a 100644 --- a/accessible/base/AccIterator.cpp +++ b/accessible/base/AccIterator.cpp @@ -5,12 +5,15 @@ #include "AccIterator.h" #include "AccGroupInfo.h" +#include "ARIAMap.h" #include "DocAccessible-inl.h" #include "LocalAccessible-inl.h" +#include "nsAccUtils.h" #include "XULTreeAccessible.h" #include "mozilla/a11y/DocAccessibleParent.h" #include "mozilla/dom/DocumentOrShadowRoot.h" +#include "mozilla/dom/Element.h" #include "mozilla/dom/HTMLLabelElement.h" using namespace mozilla; @@ -245,9 +248,14 @@ LocalAccessible* XULDescriptionIterator::Next() { IDRefsIterator::IDRefsIterator(DocAccessible* aDoc, nsIContent* aContent, nsAtom* aIDRefsAttr) - : mContent(aContent), mDoc(aDoc), mCurrIdx(0) { + : mContent(aContent), mDoc(aDoc), mCurrIdx(0), mElemIdx(0) { if (mContent->IsElement()) { mContent->AsElement()->GetAttr(aIDRefsAttr, mIDs); + if (mIDs.IsEmpty() && + (aria::AttrCharacteristicsFor(aIDRefsAttr) & ATTR_REFLECT_ELEMENTS)) { + nsAccUtils::GetARIAElementsAttr(mContent->AsElement(), aIDRefsAttr, + mElements); + } } } @@ -275,6 +283,13 @@ nsIContent* IDRefsIterator::NextElem() { if (refContent) return refContent; } + while (nsIContent* element = mElements.SafeElementAt(mElemIdx++)) { + if (nsCoreUtils::IsDescendantOfAnyShadowIncludingAncestor(element, + mContent)) { + return element; + } + } + return nullptr; } diff --git a/accessible/base/AccIterator.h b/accessible/base/AccIterator.h index 61b126c81294..5b98113a4387 100644 --- a/accessible/base/AccIterator.h +++ b/accessible/base/AccIterator.h @@ -16,6 +16,10 @@ class nsITreeView; namespace mozilla { +namespace dom { +class Element; +} + namespace a11y { class DocAccessibleParent; @@ -202,9 +206,10 @@ class XULDescriptionIterator : public AccIterable { }; /** - * Used to iterate through IDs, elements or accessibles pointed by IDRefs - * attribute. Note, any method used to iterate through IDs, elements, or - * accessibles moves iterator to next position. + * Used to iterate through elements referenced through explicitly set + * attr-elements or IDs listed in a content attribute. Note, any method used to + * iterate through IDs, elements, or accessibles moves iterator to next + * position. */ class IDRefsIterator : public AccIterable { public: @@ -240,6 +245,8 @@ class IDRefsIterator : public AccIterable { nsIContent* mContent; DocAccessible* mDoc; nsAString::index_type mCurrIdx; + nsTArray mElements; + uint32_t mElemIdx; }; /** diff --git a/accessible/base/nsAccUtils.cpp b/accessible/base/nsAccUtils.cpp index de2e97a4693a..7e0b51248226 100644 --- a/accessible/base/nsAccUtils.cpp +++ b/accessible/base/nsAccUtils.cpp @@ -568,6 +568,22 @@ const nsAttrValue* nsAccUtils::GetARIAAttr(dom::Element* aElement, return defaults->GetAttr(aName, kNameSpaceID_None); } +bool nsAccUtils::GetARIAElementsAttr(dom::Element* aElement, nsAtom* aName, + nsTArray& aElements) { + if (aElement->HasAttr(aName)) { + aElement->GetExplicitlySetAttrElements(aName, aElements); + return true; + } + + if (auto* element = nsGenericHTMLElement::FromNode(aElement)) { + if (auto* internals = element->GetInternals()) { + return internals->GetAttrElements(aName, aElements); + } + } + + return false; +} + bool nsAccUtils::ARIAAttrValueIs(dom::Element* aElement, const nsAtom* aName, const nsAString& aValue, nsCaseTreatment aCaseSensitive) { diff --git a/accessible/base/nsAccUtils.h b/accessible/base/nsAccUtils.h index 07e32f1cdbaf..ab615604acfe 100644 --- a/accessible/base/nsAccUtils.h +++ b/accessible/base/nsAccUtils.h @@ -288,6 +288,8 @@ class nsAccUtils { nsAString& aResult); static const nsAttrValue* GetARIAAttr(dom::Element* aElement, const nsAtom* aName); + static bool GetARIAElementsAttr(dom::Element* aElement, nsAtom* aName, + nsTArray& aElements); static bool ARIAAttrValueIs(dom::Element* aElement, const nsAtom* aName, const nsAString& aValue, nsCaseTreatment aCaseSensitive); diff --git a/accessible/generic/DocAccessible.cpp b/accessible/generic/DocAccessible.cpp index 814129dcc910..18e36d513666 100644 --- a/accessible/generic/DocAccessible.cpp +++ b/accessible/generic/DocAccessible.cpp @@ -7,6 +7,7 @@ #include "LocalAccessible-inl.h" #include "AccIterator.h" #include "AccAttributes.h" +#include "ARIAMap.h" #include "CachedTableAccessible.h" #include "DocAccessible-inl.h" #include "EventTree.h" @@ -803,11 +804,10 @@ void DocAccessible::AttributeWillChange(dom::Element* aElement, // Update dependent IDs cache. Take care of elements that are accessible // because dependent IDs cache doesn't contain IDs from non accessible - // elements. - if (aModType != dom::MutationEvent_Binding::ADDITION) { - RemoveDependentIDsFor(accessible, aAttribute); - RemoveDependentElementsFor(accessible, aAttribute); - } + // elements. We do this for attribute additions as well because there might + // be an ElementInternals default value. + RemoveDependentIDsFor(accessible, aAttribute); + RemoveDependentElementsFor(accessible, aAttribute); if (aAttribute == nsGkAtoms::id) { if (accessible->IsActiveDescendantId()) { @@ -1247,7 +1247,7 @@ void DocAccessible::BindToDocument(LocalAccessible* aAccessible, nsIContent* content = aAccessible->GetContent(); if (content->IsElement() && - content->AsElement()->HasAttr(nsGkAtoms::aria_owns)) { + nsAccUtils::HasARIAAttr(content->AsElement(), nsGkAtoms::aria_owns)) { mNotificationController->ScheduleRelocation(aAccessible); } } @@ -1897,6 +1897,28 @@ void DocAccessible::AddDependentElementsFor(LocalAccessible* aRelProvider, break; } } + + aria::AttrWithCharacteristicsIterator multipleElementsRelationIter( + ATTR_REFLECT_ELEMENTS); + while (multipleElementsRelationIter.Next()) { + nsStaticAtom* attr = multipleElementsRelationIter.AttrName(); + if (aRelAttr && aRelAttr != attr) { + continue; + } + nsTArray elements; + nsAccUtils::GetARIAElementsAttr(providerEl, attr, elements); + for (dom::Element* targetEl : elements) { + 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, @@ -1927,6 +1949,34 @@ void DocAccessible::RemoveDependentElementsFor(LocalAccessible* aRelProvider, break; } } + + aria::AttrWithCharacteristicsIterator multipleElementsRelationIter( + ATTR_REFLECT_ELEMENTS); + while (multipleElementsRelationIter.Next()) { + nsStaticAtom* attr = multipleElementsRelationIter.AttrName(); + if (aRelAttr && aRelAttr != attr) { + continue; + } + nsTArray elements; + nsAccUtils::GetARIAElementsAttr(providerEl, attr, elements); + for (dom::Element* targetEl : elements) { + 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, diff --git a/accessible/generic/LocalAccessible.cpp b/accessible/generic/LocalAccessible.cpp index 982509583115..af8b8f765060 100644 --- a/accessible/generic/LocalAccessible.cpp +++ b/accessible/generic/LocalAccessible.cpp @@ -2443,7 +2443,8 @@ Relation LocalAccessible::RelationByType(RelationType aType) const { case RelationType::DETAILS: { if (mContent->IsElement() && - mContent->AsElement()->HasAttr(nsGkAtoms::aria_details)) { + nsAccUtils::HasARIAAttr(mContent->AsElement(), + nsGkAtoms::aria_details)) { return Relation( new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_details)); } diff --git a/accessible/tests/browser/e10s/browser_caching_relations_002.js b/accessible/tests/browser/e10s/browser_caching_relations_002.js index 61d92ba4acf9..34661bb3e6c1 100644 --- a/accessible/tests/browser/e10s/browser_caching_relations_002.js +++ b/accessible/tests/browser/e10s/browser_caching_relations_002.js @@ -467,3 +467,247 @@ between }, { chrome: true, topLevel: true } ); + +/** + * Test relation defaults via element internals + */ +addAccessibleTask( + ` +
label
+ +
label2
+ +`, + async function (browser, accDoc) { + let host = findAccessibleChildByID(accDoc, "host"); + let dependant1 = findAccessibleChildByID(accDoc, "dependant1"); + let dependant2 = findAccessibleChildByID(accDoc, "dependant2"); + + function invokeSetInternals(reflectionAttrName, targetIds) { + if (targetIds) { + Logger.log( + `Setting internals reflected ${reflectionAttrName} attribute to ${targetIds} for host` + ); + } else { + Logger.log( + `Removing internals reflected ${reflectionAttrName} attribute from node with host` + ); + } + + return invokeContentTask( + browser, + [reflectionAttrName, targetIds], + (contentAttr, contentTargetIds) => { + let internals = content.document.getElementById("host").internals; + if (contentTargetIds) { + internals[contentAttr] = contentTargetIds.map(targetId => + content.document.getElementById(targetId) + ); + } else { + internals[contentAttr] = null; + } + } + ); + } + + async function testInternalsRelation( + attrName, + reflectionAttrName, + hostRelation, + dependantRelation + ) { + info(`setting default ${reflectionAttrName}`); + await invokeSetInternals(reflectionAttrName, ["dependant1"]); + await testCachedRelation(host, hostRelation, [dependant1]); + await testCachedRelation(dependant1, dependantRelation, [host]); + await testCachedRelation(dependant2, dependantRelation, null); + + info(`setting override ${attrName}`); + await invokeSetAttribute(browser, "host", attrName, "dependant2"); + await testCachedRelation(host, hostRelation, [dependant2]); + await testCachedRelation(dependant2, dependantRelation, [host]); + await testCachedRelation(dependant1, dependantRelation, null); + + info(`unsetting default ${reflectionAttrName} and ${attrName} override`); + await invokeSetInternals(reflectionAttrName, null); + await invokeSetAttribute(browser, "host", attrName, null); + await testCachedRelation(host, hostRelation, null); + await testCachedRelation(dependant2, dependantRelation, null); + await testCachedRelation(dependant1, dependantRelation, null); + } + + await testInternalsRelation( + "aria-labelledby", + "ariaLabelledByElements", + RELATION_LABELLED_BY, + RELATION_LABEL_FOR + ); + await testInternalsRelation( + "aria-describedby", + "ariaDescribedByElements", + RELATION_DESCRIBED_BY, + RELATION_DESCRIPTION_FOR + ); + await testInternalsRelation( + "aria-controls", + "ariaControlsElements", + RELATION_CONTROLLER_FOR, + RELATION_CONTROLLED_BY + ); + await testInternalsRelation( + "aria-flowto", + "ariaFlowToElements", + RELATION_FLOWS_TO, + RELATION_FLOWS_FROM + ); + await testInternalsRelation( + "aria-details", + "ariaDetailsElements", + RELATION_DETAILS, + RELATION_DETAILS_FOR + ); + await testInternalsRelation( + "aria-errormessage", + "ariaErrorMessageElements", + RELATION_ERRORMSG, + RELATION_ERRORMSG_FOR + ); + } +); + +/** + * Moving explicitly set elements across shadow DOM boundaries. + */ +addAccessibleTask( + ` +
+
Delicious
+
Nutritious
+
+ +
+ +`, + async function (browser, accDoc) { + const waitAndReturnRecreated = acc => { + const id = getAccessibleDOMNodeID(acc); + return waitForEvents([ + [EVENT_HIDE, acc], + [EVENT_SHOW, id], + ]).then(evts => evts[1].accessible); + }; + + let describedAcc = findAccessibleChildByID(accDoc, "describedElement"); + let accDescription1 = findAccessibleChildByID(accDoc, "buttonDescription1"); + let accDescription2 = findAccessibleChildByID(accDoc, "buttonDescription2"); + + // All elements were in the same scope, so relations are intact. + await testCachedRelation(describedAcc, RELATION_DESCRIBED_BY, [ + accDescription1, + accDescription2, + ]); + await testCachedRelation(accDescription1, RELATION_DESCRIPTION_FOR, [ + describedAcc, + ]); + await testCachedRelation(accDescription2, RELATION_DESCRIPTION_FOR, [ + describedAcc, + ]); + + let onRecreated = waitAndReturnRecreated(describedAcc); + await invokeContentTask(browser, [], () => { + const outerShadowRoot = + content.document.getElementById("outerShadowHost").shadowRoot; + const describedElement = + content.document.getElementById("describedElement"); + outerShadowRoot.appendChild(describedElement); + }); + + info("Waiting for described accessible to be recreated"); + describedAcc = await onRecreated; + // Relations should still be intact, we are referencing elements in a lighter scope. + await testCachedRelation(describedAcc, RELATION_DESCRIBED_BY, [ + accDescription1, + accDescription2, + ]); + await testCachedRelation(accDescription1, RELATION_DESCRIPTION_FOR, [ + describedAcc, + ]); + await testCachedRelation(accDescription2, RELATION_DESCRIPTION_FOR, [ + describedAcc, + ]); + + // Move the explicitly set elements into a deeper shadow DOM. + onRecreated = Promise.all([ + waitAndReturnRecreated(accDescription1), + waitAndReturnRecreated(accDescription2), + ]); + await invokeContentTask(browser, [], () => { + const buttonDescription1 = + content.document.getElementById("buttonDescription1"); + const buttonDescription2 = + content.document.getElementById("buttonDescription2"); + const innerShadowRoot = + content.document.getElementById("outerShadowHost").shadowRoot + .firstElementChild.shadowRoot; + innerShadowRoot.appendChild(buttonDescription1); + innerShadowRoot.appendChild(buttonDescription2); + }); + + [accDescription1, accDescription2] = await onRecreated; + + // Relation is severed, because relation dependants are no longer in a valid scope. + await testCachedRelation(describedAcc, RELATION_DESCRIBED_BY, []); + await testCachedRelation(accDescription1, RELATION_DESCRIPTION_FOR, []); + await testCachedRelation(accDescription2, RELATION_DESCRIPTION_FOR, []); + + // Move into the same shadow scope as the explicitly set elements. + onRecreated = waitAndReturnRecreated(describedAcc); + await invokeContentTask(browser, [], () => { + const outerShadowRoot = + content.document.getElementById("outerShadowHost").shadowRoot; + const describedElement = + outerShadowRoot.getElementById("describedElement"); + const innerShadowRoot = outerShadowRoot.firstElementChild.shadowRoot; + innerShadowRoot.appendChild(describedElement); + }); + + describedAcc = await onRecreated; + // Relation is restored, because target is now in same shadow scope. + await testCachedRelation(describedAcc, RELATION_DESCRIBED_BY, [ + accDescription1, + accDescription2, + ]); + await testCachedRelation(accDescription1, RELATION_DESCRIPTION_FOR, [ + describedAcc, + ]); + await testCachedRelation(accDescription2, RELATION_DESCRIPTION_FOR, [ + describedAcc, + ]); + } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js b/accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js index 78f52d316296..62ff3f5b83bf 100644 --- a/accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js +++ b/accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js @@ -7,7 +7,35 @@ /* import-globals-from ../../mochitest/role.js */ loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); -async function testContainer1(browser, accDoc) { +requestLongerTimeout(2); + +function invokeSetAriaOwns( + browser, + id, + children = null, + elementReflection = false +) { + if (!elementReflection) { + return invokeSetAttribute(browser, id, "aria-owns", children); + } + + return invokeContentTask( + browser, + [id, children], + (contentId, contentChildrenIds) => { + let elm = content.document.getElementById(contentId); + if (contentChildrenIds) { + elm.ariaOwnsElements = contentChildrenIds + .split(" ") + .map(childId => content.document.getElementById(childId)); + } else { + elm.ariaOwnsElements = null; + } + } + ); +} + +async function testContainer1(browser, accDoc, elementReflection = false) { const id = "t1_container"; const docID = getAccessibleDOMNodeID(accDoc); const acc = findAccessibleChildByID(accDoc, id); @@ -21,7 +49,12 @@ async function testContainer1(browser, accDoc) { /* ================ Change ARIA owns ====================================== */ let onReorder = waitForEvent(EVENT_REORDER, id); - await invokeSetAttribute(browser, id, "aria-owns", "t1_button t1_subdiv"); + await invokeSetAriaOwns( + browser, + id, + "t1_button t1_subdiv", + elementReflection + ); await onReorder; // children are swapped again, button and subdiv are appended to @@ -37,7 +70,7 @@ async function testContainer1(browser, accDoc) { /* ================ Remove ARIA owns ====================================== */ onReorder = waitForEvent(EVENT_REORDER, id); - await invokeSetAttribute(browser, id, "aria-owns"); + await invokeSetAriaOwns(browser, id, null, elementReflection); await onReorder; // children follow the DOM order @@ -48,7 +81,12 @@ async function testContainer1(browser, accDoc) { /* ================ Set ARIA owns ========================================= */ onReorder = waitForEvent(EVENT_REORDER, id); - await invokeSetAttribute(browser, id, "aria-owns", "t1_button t1_subdiv"); + await invokeSetAriaOwns( + browser, + id, + "t1_button t1_subdiv", + elementReflection + ); await onReorder; // children are swapped again, button and subdiv are appended to @@ -180,7 +218,7 @@ async function removeContainer(browser, accDoc) { testAccessibleTree(acc, tree); } -async function stealAndRecacheChildren(browser, accDoc) { +async function stealAndRecacheChildren(browser, accDoc, elementReflection) { const id1 = "t3_container1"; const id2 = "t3_container2"; const acc1 = findAccessibleChildByID(accDoc, id1); @@ -188,7 +226,7 @@ async function stealAndRecacheChildren(browser, accDoc) { /* ================ Attempt to steal from other ARIA owns ================= */ let onReorder = waitForEvent(EVENT_REORDER, id2); - await invokeSetAttribute(browser, id2, "aria-owns", "t3_child"); + await invokeSetAriaOwns(browser, id2, "t3_child", elementReflection); await invokeContentTask(browser, [id2], id => { let div = content.document.createElement("div"); div.setAttribute("role", "radio"); @@ -228,7 +266,7 @@ async function showHiddenElement(browser, accDoc) { testAccessibleTree(acc, tree); } -async function rearrangeARIAOwns(browser, accDoc) { +async function rearrangeARIAOwns(browser, accDoc, elementReflection) { const id = "t5_container"; const acc = findAccessibleChildByID(accDoc, id); const tests = [ @@ -244,7 +282,7 @@ async function rearrangeARIAOwns(browser, accDoc) { for (let { val, roleList } of tests) { let onReorder = waitForEvent(EVENT_REORDER, id); - await invokeSetAttribute(browser, id, "aria-owns", val); + await invokeSetAriaOwns(browser, id, val, elementReflection); await onReorder; let tree = { SECTION: [] }; @@ -293,6 +331,19 @@ addAccessibleTask( { iframe: true, remoteIframe: true } ); +addAccessibleTask( + "e10s/doc_treeupdate_ariaowns.html", + async function (browser, accDoc) { + await testContainer1(browser, accDoc, true); + await removeContainer(browser, accDoc); + await stealAndRecacheChildren(browser, accDoc, true); + await showHiddenElement(browser, accDoc); + await rearrangeARIAOwns(browser, accDoc, true); + await removeNotARIAOwnedEl(browser, accDoc); + }, + { iframe: true, remoteIframe: true } +); + // Test owning an ancestor which isn't created yet with an iframe in the // subtree. addAccessibleTask( @@ -455,3 +506,41 @@ addAccessibleTask( }, { chrome: false, iframe: true, remoteIframe: true } ); + +/** + * Test relation defaults via element internals + */ +addAccessibleTask( + ` + +
+
+
+
+
+ +
+
+
+ +`, + async function (browser, accDoc) { + let listbox = findAccessibleChildByID(accDoc, "listbox"); + is(listbox.children.length, 3, "got children"); + let onReorder = waitForEvent(EVENT_REORDER, "listbox"); + invokeSetAriaOwns(browser, "listbox", "l4"); + await onReorder; + } +); diff --git a/accessible/tests/browser/e10s/head.js b/accessible/tests/browser/e10s/head.js index e72af914d482..2c28af23f3aa 100644 --- a/accessible/tests/browser/e10s/head.js +++ b/accessible/tests/browser/e10s/head.js @@ -134,6 +134,50 @@ async function testCachedRelation(identifier, relType, relatedIdentifiers) { }, "No unexpected targets found."); } +/** + * Asynchronously set or remove content element's reflected elements attribute + * (in content process if e10s is enabled). + * @param {Object} browser current "tabbrowser" element + * @param {String} id content element id + * @param {String} attr attribute name + * @param {String?} value optional attribute value, if not present, remove + * attribute + * @return {Promise} promise indicating that attribute is set/removed + */ +function invokeSetReflectedElementsAttribute(browser, id, attr, targetIds) { + if (targetIds) { + Logger.log( + `Setting reflected ${attr} attribute to ${targetIds} for node with id: ${id}` + ); + } else { + Logger.log(`Removing reflected ${attr} attribute from node with id: ${id}`); + } + + return invokeContentTask( + browser, + [id, attr, targetIds], + (contentId, contentAttr, contentTargetIds) => { + let elm = content.document.getElementById(contentId); + if (contentTargetIds) { + elm[contentAttr] = contentTargetIds.map(targetId => + content.document.getElementById(targetId) + ); + } else { + elm[contentAttr] = null; + } + } + ); +} + +const REFLECTEDATTR_NAME_MAP = { + "aria-controls": "ariaControlsElements", + "aria-describedby": "ariaDescribedByElements", + "aria-details": "ariaDetailsElements", + "aria-errormessage": "ariaErrorMessageElements", + "aria-flowto": "ariaFlowToElements", + "aria-labelledby": "ariaLabelledByElements", +}; + async function testRelated( browser, accDoc, @@ -148,13 +192,14 @@ async function testRelated( /** * Test data has the format of: * { - * desc {String} description for better logging - * attrs {?Array} an optional list of attributes to update - * expected {Array} expected relation values for dependant1, dependant2 + * desc {String} description for better logging + * attrs {?Array} an optional list of attributes to update + * reflectedattr {?Array} an optional list of reflected attributes to update + * expected {Array} expected relation values for dependant1, dependant2 * and host respectively. * } */ - const tests = [ + let tests = [ { desc: "No attribute", expected: [null, null, null], @@ -181,13 +226,45 @@ async function testRelated( }, ]; - for (let { desc, attrs, expected } of tests) { + let reflectedAttrName = REFLECTEDATTR_NAME_MAP[attr]; + if (reflectedAttrName) { + tests = tests.concat([ + { + desc: "Set reflected attribute", + reflectedattr: [{ key: reflectedAttrName, value: ["dependant1"] }], + expected: [host, null, dependant1], + }, + { + desc: "Change reflected attribute", + reflectedattr: [{ key: reflectedAttrName, value: ["dependant2"] }], + expected: [null, host, dependant2], + }, + { + desc: "Change reflected attribute to multiple targets", + reflectedattr: [ + { key: reflectedAttrName, value: ["dependant2", "dependant1"] }, + ], + expected: [host, host, [dependant1, dependant2]], + }, + { + desc: "Remove reflected attribute", + reflectedattr: [{ key: reflectedAttrName, value: null }], + expected: [null, null, null], + }, + ]); + } + + for (let { desc, attrs, reflectedattr, expected } of tests) { info(desc); if (attrs) { for (let { key, value } of attrs) { await invokeSetAttribute(browser, "host", key, value); } + } else if (reflectedattr) { + for (let { key, value } of reflectedattr) { + await invokeSetReflectedElementsAttribute(browser, "host", key, value); + } } await testCachedRelation(dependant1, dependantRelation, expected[0]); diff --git a/dom/base/Element.cpp b/dom/base/Element.cpp index a3ace01df298..b931f910c7a7 100644 --- a/dom/base/Element.cpp +++ b/dom/base/Element.cpp @@ -1997,6 +1997,22 @@ Element* Element::GetExplicitlySetAttrElement(nsAtom* aAttr) const { return nullptr; } +void Element::GetExplicitlySetAttrElements( + nsAtom* aAttr, nsTArray& aElements) const { + if (const nsExtendedDOMSlots* slots = GetExistingExtendedDOMSlots()) { + if (auto attrElementsMaybeEntry = slots->mAttrElementsMap.Lookup(aAttr)) { + auto& [attrElements, cachedAttrElements] = attrElementsMaybeEntry.Data(); + if (attrElements) { + for (const nsWeakPtr& weakEl : *attrElements) { + if (nsCOMPtr attrEl = do_QueryReferent(weakEl)) { + aElements.AppendElement(attrEl); + } + } + } + } + } +} + void Element::GetElementsWithGrid(nsTArray>& aElements) { nsINode* cur = this; while (cur) { diff --git a/dom/base/Element.h b/dom/base/Element.h index a8d24887388f..7d14de4b8701 100644 --- a/dom/base/Element.h +++ b/dom/base/Element.h @@ -1339,6 +1339,16 @@ class Element : public FragmentOrElement { */ Element* GetExplicitlySetAttrElement(nsAtom* aAttr) const; + /** + * Gets the attribute elements for the given attribute. Unlike + * GetAttrAssociatedElements, this returns an uncached array of explicitly set + * elements without checking if they are a descendant of any of this element's + * shadow-including ancestors. It also does not attempt to retrieve elements + * using the ids set in the content attribute. + */ + void GetExplicitlySetAttrElements(nsAtom* aAttr, + nsTArray& aElements) const; + PseudoStyleType GetPseudoElementType() const { nsresult rv = NS_OK; auto raw = GetProperty(nsGkAtoms::pseudoProperty, &rv); diff --git a/dom/html/ElementInternals.cpp b/dom/html/ElementInternals.cpp index 73bcb97f4fb7..0f6cc11b865d 100644 --- a/dom/html/ElementInternals.cpp +++ b/dom/html/ElementInternals.cpp @@ -655,4 +655,22 @@ void ElementInternals::GetAttrElements( cachedAttrElements = std::move(elements); } +bool ElementInternals::GetAttrElements(nsAtom* aAttr, + nsTArray& aElements) { + aElements.Clear(); + auto attrElementsMaybeEntry = mAttrElementsMap.Lookup(aAttr); + if (!attrElementsMaybeEntry) { + return false; + } + + auto& [attrElements, cachedAttrElements] = attrElementsMaybeEntry.Data(); + for (const nsWeakPtr& weakEl : attrElements) { + if (nsCOMPtr attrEl = do_QueryReferent(weakEl)) { + aElements.AppendElement(attrEl); + } + } + + return true; +} + } // namespace mozilla::dom diff --git a/dom/html/ElementInternals.h b/dom/html/ElementInternals.h index 5e958720acc0..ab4658fa338b 100644 --- a/dom/html/ElementInternals.h +++ b/dom/html/ElementInternals.h @@ -193,6 +193,8 @@ class ElementInternals final : public nsIFormControl, nsresult SetAttr(nsAtom* aName, const nsAString& aValue); + bool GetAttrElements(nsAtom* aAttr, nsTArray& aElements); + const AttrArray& GetAttrs() const { return mAttrs; } DocGroup* GetDocGroup();