mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-27 06:43:32 +00:00
Bug 1769586 - P2: Implement a11y support of ARIA element reflection. r=Jamie
Depends on D209767 Differential Revision: https://phabricator.services.mozilla.com/D209768
This commit is contained in:
parent
f82db6902a
commit
79884b85d1
@ -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<int32_t>(ArrayLength(gWAIUnivAttrMap));
|
||||
mIdx++) {
|
||||
if (gWAIUnivAttrMap[mIdx].characteristics & mCharacteristics) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
nsStaticAtom* AttrWithCharacteristicsIterator::AttrName() const {
|
||||
return mIdx >= 0
|
||||
? const_cast<nsStaticAtom*>(gWAIUnivAttrMap[mIdx].attributeName)
|
||||
: nullptr;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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<dom::Element*> mElements;
|
||||
uint32_t mElemIdx;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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<dom::Element*>& 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) {
|
||||
|
@ -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<dom::Element*>& aElements);
|
||||
static bool ARIAAttrValueIs(dom::Element* aElement, const nsAtom* aName,
|
||||
const nsAString& aValue,
|
||||
nsCaseTreatment aCaseSensitive);
|
||||
|
@ -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<dom::Element*> 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<dom::Element*> 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,
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -467,3 +467,247 @@ between
|
||||
},
|
||||
{ chrome: true, topLevel: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* Test relation defaults via element internals
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`
|
||||
<div id="dependant1">label</div>
|
||||
<custom-checkbox id="host"></custom-checkbox>
|
||||
<div id="dependant2">label2</div>
|
||||
|
||||
<script>
|
||||
customElements.define("custom-checkbox",
|
||||
class extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.tabIndex = "0";
|
||||
this._internals = this.attachInternals();
|
||||
this._internals.role = "checkbox";
|
||||
this._internals.ariaChecked = "true";
|
||||
}
|
||||
get internals() {
|
||||
return this._internals;
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>`,
|
||||
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(
|
||||
`
|
||||
<div id="describedButtonContainer">
|
||||
<div id="buttonDescription1">Delicious</div>
|
||||
<div id="buttonDescription2">Nutritious</div>
|
||||
<div id="outerShadowHost"></div>
|
||||
<button id="describedElement">Button</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const buttonDescription1 = document.getElementById("buttonDescription1");
|
||||
const buttonDescription2 = document.getElementById("buttonDescription2");
|
||||
const outerShadowRoot = outerShadowHost.attachShadow({mode: "open"});
|
||||
const innerShadowHost = document.createElement("div");
|
||||
outerShadowRoot.appendChild(innerShadowHost);
|
||||
const innerShadowRoot = innerShadowHost.attachShadow({mode: "open"});
|
||||
|
||||
const describedElement = document.getElementById("describedElement");
|
||||
// Add some attr associated light DOM elements.
|
||||
describedElement.ariaDescribedByElements = [buttonDescription1, buttonDescription2];
|
||||
</script>`,
|
||||
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,
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
@ -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(
|
||||
`
|
||||
|
||||
<div role="listbox">
|
||||
<div role="listitem" id="l1"></div>
|
||||
<div role="listitem" id="l2"></div>
|
||||
<div role="listitem" id="l3"></div>
|
||||
</div>
|
||||
<custom-listbox id="listbox"></custom-listbox>
|
||||
<div role="listbox">
|
||||
<div role="listitem" id="l4"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
customElements.define("custom-listbox",
|
||||
class extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.tabIndex = "0"
|
||||
this._internals = this.attachInternals();
|
||||
this._internals.role = "listbox";
|
||||
this._internals.ariaOwnsElements = Array.from(this.previousElementSibling.children)
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>`,
|
||||
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;
|
||||
}
|
||||
);
|
||||
|
@ -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]);
|
||||
|
@ -1997,6 +1997,22 @@ Element* Element::GetExplicitlySetAttrElement(nsAtom* aAttr) const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Element::GetExplicitlySetAttrElements(
|
||||
nsAtom* aAttr, nsTArray<Element*>& 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<Element> attrEl = do_QueryReferent(weakEl)) {
|
||||
aElements.AppendElement(attrEl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Element::GetElementsWithGrid(nsTArray<RefPtr<Element>>& aElements) {
|
||||
nsINode* cur = this;
|
||||
while (cur) {
|
||||
|
@ -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<Element*>& aElements) const;
|
||||
|
||||
PseudoStyleType GetPseudoElementType() const {
|
||||
nsresult rv = NS_OK;
|
||||
auto raw = GetProperty(nsGkAtoms::pseudoProperty, &rv);
|
||||
|
@ -655,4 +655,22 @@ void ElementInternals::GetAttrElements(
|
||||
cachedAttrElements = std::move(elements);
|
||||
}
|
||||
|
||||
bool ElementInternals::GetAttrElements(nsAtom* aAttr,
|
||||
nsTArray<Element*>& aElements) {
|
||||
aElements.Clear();
|
||||
auto attrElementsMaybeEntry = mAttrElementsMap.Lookup(aAttr);
|
||||
if (!attrElementsMaybeEntry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& [attrElements, cachedAttrElements] = attrElementsMaybeEntry.Data();
|
||||
for (const nsWeakPtr& weakEl : attrElements) {
|
||||
if (nsCOMPtr<Element> attrEl = do_QueryReferent(weakEl)) {
|
||||
aElements.AppendElement(attrEl);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace mozilla::dom
|
||||
|
@ -193,6 +193,8 @@ class ElementInternals final : public nsIFormControl,
|
||||
|
||||
nsresult SetAttr(nsAtom* aName, const nsAString& aValue);
|
||||
|
||||
bool GetAttrElements(nsAtom* aAttr, nsTArray<Element*>& aElements);
|
||||
|
||||
const AttrArray& GetAttrs() const { return mAttrs; }
|
||||
|
||||
DocGroup* GetDocGroup();
|
||||
|
Loading…
Reference in New Issue
Block a user