Bug 1219299 - rework aria-owns implementation, r=yzen

This commit is contained in:
Alexander Surkov
2015-10-29 18:08:48 -04:00
parent 3c486b1d0b
commit f19d67034f
12 changed files with 697 additions and 309 deletions

View File

@@ -324,8 +324,14 @@ IDRefsIterator::GetElem(const nsDependentSubstring& aID)
Accessible*
IDRefsIterator::Next()
{
nsIContent* nextElm = NextElem();
return nextElm ? mDoc->GetAccessible(nextElm) : nullptr;
nsIContent* nextEl = nullptr;
while ((nextEl = NextElem())) {
Accessible* acc = mDoc->GetAccessible(nextEl);
if (acc) {
return acc;
}
}
return nullptr;
}

View File

@@ -295,12 +295,16 @@ EventQueue::CoalesceReorderEvents(AccEvent* aTailEvent)
AccReorderEvent* thisReorder = downcast_accEvent(thisEvent);
AccReorderEvent* tailReorder = downcast_accEvent(aTailEvent);
uint32_t eventType = thisReorder->IsShowHideEventTarget(tailParent);
if (eventType == nsIAccessibleEvent::EVENT_SHOW)
if (eventType == nsIAccessibleEvent::EVENT_SHOW) {
tailReorder->DoNotEmitAll();
else if (eventType == nsIAccessibleEvent::EVENT_HIDE)
}
else if (eventType == nsIAccessibleEvent::EVENT_HIDE) {
NS_ERROR("Accessible tree was modified after it was removed! Huh?");
else
}
else {
aTailEvent->mEventRule = AccEvent::eDoNotEmit;
mEvents[index].swap(mEvents[count - 1]);
}
return;
}

View File

@@ -54,6 +54,7 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(NotificationController)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHangingChildDocuments)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContentInsertions)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEvents)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRelocations)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(NotificationController, AddRef)
@@ -86,6 +87,7 @@ NotificationController::Shutdown()
mContentInsertions.Clear();
mNotifications.Clear();
mEvents.Clear();
mRelocations.Clear();
}
void
@@ -351,6 +353,16 @@ NotificationController::WillRefresh(mozilla::TimeStamp aTime)
// modification are done.
mDocument->ProcessInvalidationList();
// We cannot rely on DOM tree to keep aria-owns relations updated. Make
// a validation to remove dead links.
mDocument->ValidateARIAOwned();
// Process relocation list.
for (uint32_t idx = 0; idx < mRelocations.Length(); idx++) {
mDocument->DoARIAOwnsRelocation(mRelocations[idx]);
}
mRelocations.Clear();
// If a generic notification occurs after this point then we may be allowed to
// process it synchronously. However we do not want to reenter if fireing
// events causes script to run.

View File

@@ -133,6 +133,16 @@ public:
nsIContent* aStartChildNode,
nsIContent* aEndChildNode);
/**
* Pend an accessible subtree relocation.
*/
void ScheduleRelocation(Accessible* aOwner)
{
if (!mRelocations.Contains(aOwner) && mRelocations.AppendElement(aOwner)) {
ScheduleProcessing();
}
}
/**
* Start to observe refresh to make notifications and events processing after
* layout.
@@ -303,6 +313,11 @@ private:
* use SwapElements() on it.
*/
nsTArray<RefPtr<Notification> > mNotifications;
/**
* Holds all scheduled relocations.
*/
nsTArray<RefPtr<Accessible> > mRelocations;
};
} // namespace a11y

View File

@@ -116,7 +116,7 @@ TreeWalker::Next(ChildrenIterator* aIter, Accessible** aAccesible,
// Ignore the accessible and its subtree if it was repositioned by means of
// aria-owns.
if (accessible) {
if (accessible->IsRepositioned()) {
if (accessible->IsRelocated()) {
*aSkipSubtree = true;
} else {
*aAccesible = accessible;

View File

@@ -910,13 +910,13 @@ public:
* Get/set repositioned bit indicating that the accessible was moved in
* the accessible tree, i.e. the accessible tree structure differs from DOM.
*/
bool IsRepositioned() const { return mStateFlags & eRepositioned; }
void SetRepositioned(bool aRepositioned)
bool IsRelocated() const { return mStateFlags & eRelocated; }
void SetRelocated(bool aRelocated)
{
if (aRepositioned)
mStateFlags |= eRepositioned;
if (aRelocated)
mStateFlags |= eRelocated;
else
mStateFlags &= ~eRepositioned;
mStateFlags &= ~eRelocated;
}
/**
@@ -1009,9 +1009,9 @@ protected:
eSubtreeMutating = 1 << 6, // subtree is being mutated
eIgnoreDOMUIEvent = 1 << 7, // don't process DOM UI events for a11y events
eSurvivingInUpdate = 1 << 8, // parent drops children to recollect them
eRepositioned = 1 << 9, // accessible was moved in tree
eRelocated = 1 << 9, // accessible was moved in tree
eLastStateFlag = eRepositioned
eLastStateFlag = eRelocated
};
/**

View File

@@ -130,9 +130,13 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocAccessible, Accessible)
}
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAccessibleCache)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAnchorJumpElm)
for (uint32_t i = 0; i < tmp->mARIAOwnsInvalidationList.Length(); ++i) {
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mARIAOwnsInvalidationList[i].mOwner)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mARIAOwnsInvalidationList[i].mChild)
for (auto it = tmp->mARIAOwnsHash.ConstIter(); !it.Done(); it.Next()) {
nsTArray<RefPtr<Accessible> >* ar = it.UserData();
for (uint32_t i = 0; i < ar->Length(); i++) {
NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb,
"mARIAOwnsHash entry item");
cb.NoteXPCOMChild(ar->ElementAt(i));
}
}
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
@@ -144,10 +148,7 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocAccessible, Accessible)
tmp->mNodeToAccessibleMap.Clear();
NS_IMPL_CYCLE_COLLECTION_UNLINK(mAccessibleCache)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mAnchorJumpElm)
for (uint32_t i = 0; i < tmp->mARIAOwnsInvalidationList.Length(); ++i) {
NS_IMPL_CYCLE_COLLECTION_UNLINK(mARIAOwnsInvalidationList[i].mOwner)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mARIAOwnsInvalidationList[i].mChild)
}
tmp->mARIAOwnsHash.Clear();
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(DocAccessible)
@@ -731,6 +732,10 @@ DocAccessible::AttributeWillChange(nsIDocument* aDocument,
if (aModType != nsIDOMMutationEvent::ADDITION)
RemoveDependentIDsFor(accessible, aAttribute);
if (aAttribute == nsGkAtoms::id) {
RelocateARIAOwnedIfNeeded(aElement);
}
// Store the ARIA attribute old value so that it can be used after
// attribute change. Note, we assume there's no nested ARIA attribute
// changes. If this happens then we should end up with keeping a stack of
@@ -906,6 +911,10 @@ DocAccessible::AttributeChangedImpl(Accessible* aAccessible,
return;
}
if (aAttribute == nsGkAtoms::id) {
RelocateARIAOwnedIfNeeded(elm);
}
// ARIA or XUL selection
if ((aAccessible->GetContent()->IsXULElement() &&
aAttribute == nsGkAtoms::selected) ||
@@ -1040,6 +1049,10 @@ DocAccessible::ARIAAttributeChanged(Accessible* aAccessible, nsIAtom* aAttribute
FireDelayedEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, aAccessible);
return;
}
if (aAttribute == nsGkAtoms::aria_owns) {
mNotificationController->ScheduleRelocation(aAccessible);
}
}
void
@@ -1271,6 +1284,15 @@ DocAccessible::BindToDocument(Accessible* aAccessible,
aAccessible->SetRoleMapEntry(aRoleMapEntry);
AddDependentIDsFor(aAccessible);
if (aAccessible->HasOwnContent()) {
nsIContent* el = aAccessible->GetContent();
if (el->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_owns)) {
mNotificationController->ScheduleRelocation(aAccessible);
}
RelocateARIAOwnedIfNeeded(el);
}
}
void
@@ -1365,67 +1387,6 @@ DocAccessible::ProcessInvalidationList()
}
mInvalidationList.Clear();
// Alter the tree according to aria-owns (seize the trees).
for (uint32_t idx = 0; idx < mARIAOwnsInvalidationList.Length(); idx++) {
Accessible* owner = mARIAOwnsInvalidationList[idx].mOwner;
if (!owner->IsInDocument()) { // eventually died before we've got here
continue;
}
Accessible* child = GetAccessible(mARIAOwnsInvalidationList[idx].mChild);
if (!child || !child->IsInDocument()) {
continue;
}
Accessible* oldParent = child->Parent();
if (!oldParent) {
NS_ERROR("The accessible is in document but doesn't have a parent");
continue;
}
int32_t idxInParent = child->IndexInParent();
// XXX: update context flags
{
RefPtr<AccReorderEvent> reorderEvent = new AccReorderEvent(oldParent);
RefPtr<AccMutationEvent> hideEvent =
new AccHideEvent(child, child->GetContent(), false);
FireDelayedEvent(hideEvent);
reorderEvent->AddSubMutationEvent(hideEvent);
AutoTreeMutation mut(oldParent);
oldParent->RemoveChild(child);
MaybeNotifyOfValueChange(oldParent);
FireDelayedEvent(reorderEvent);
}
bool isReinserted = false;
{
AutoTreeMutation mut(owner);
isReinserted = owner->AppendChild(child);
}
Accessible* newParent = owner;
if (!isReinserted) {
AutoTreeMutation mut(oldParent);
oldParent->InsertChildAt(idxInParent, child);
newParent = oldParent;
}
RefPtr<AccReorderEvent> reorderEvent = new AccReorderEvent(newParent);
RefPtr<AccMutationEvent> showEvent =
new AccShowEvent(child, child->GetContent());
FireDelayedEvent(showEvent);
reorderEvent->AddSubMutationEvent(showEvent);
MaybeNotifyOfValueChange(newParent);
FireDelayedEvent(reorderEvent);
child->SetRepositioned(isReinserted);
}
mARIAOwnsInvalidationList.Clear();
}
Accessible*
@@ -1635,31 +1596,6 @@ DocAccessible::AddDependentIDsFor(Accessible* aRelProvider, nsIAtom* aRelAttr)
if (!HasAccessible(dependentContent)) {
mInvalidationList.AppendElement(dependentContent);
}
// Update ARIA owns cache.
if (relAttr == nsGkAtoms::aria_owns) {
// ARIA owns cannot refer to itself or a parent. Ignore
// the element if so.
nsIContent* parentEl = relProviderEl;
while (parentEl && parentEl != dependentContent) {
parentEl = parentEl->GetParent();
}
if (!parentEl) {
// ARIA owns element cannot refer to an element in parents chain
// of other ARIA owns element (including that ARIA owns element)
// if it's inside of a dependent element subtree of that
// ARIA owns element. Applied recursively.
if (!IsInARIAOwnsLoop(relProviderEl, dependentContent)) {
nsTArray<nsIContent*>* list =
mARIAOwnsHash.LookupOrAdd(aRelProvider);
list->AppendElement(dependentContent);
mARIAOwnsInvalidationList.AppendElement(
ARIAOwnsPair(aRelProvider, dependentContent));
}
}
}
}
}
}
@@ -1709,59 +1645,6 @@ DocAccessible::RemoveDependentIDsFor(Accessible* aRelProvider,
}
}
// aria-owns has gone, put the children back.
if (relAttr == nsGkAtoms::aria_owns) {
nsTArray<nsIContent*>* children = mARIAOwnsHash.Get(aRelProvider);
if (children) {
nsTArray<Accessible*> containers;
// Remove ARIA owned elements from where they belonged.
RefPtr<AccReorderEvent> reorderEvent = new AccReorderEvent(aRelProvider);
{
AutoTreeMutation mut(aRelProvider);
for (uint32_t idx = 0; idx < children->Length(); idx++) {
nsIContent* childEl = children->ElementAt(idx);
Accessible* child = GetAccessible(childEl);
if (child && child->IsRepositioned()) {
{
RefPtr<AccMutationEvent> hideEvent =
new AccHideEvent(child, childEl, false);
FireDelayedEvent(hideEvent);
reorderEvent->AddSubMutationEvent(hideEvent);
aRelProvider->RemoveChild(child);
}
// Collect DOM-order containers to update their trees.
child->SetRepositioned(false);
Accessible* container = GetContainerAccessible(childEl);
if (!containers.Contains(container)) {
containers.AppendElement(container);
}
}
}
}
mARIAOwnsHash.Remove(aRelProvider);
for (uint32_t idx = 0; idx < mARIAOwnsInvalidationList.Length();) {
if (mARIAOwnsInvalidationList[idx].mOwner == aRelProvider) {
mARIAOwnsInvalidationList.RemoveElementAt(idx);
continue;
}
idx++;
}
MaybeNotifyOfValueChange(aRelProvider);
FireDelayedEvent(reorderEvent);
// Reinserted previously ARIA owned elements into the tree
// (restore a DOM-like order).
for (uint32_t idx = 0; idx < containers.Length(); idx++) {
UpdateTreeOnInsertion(containers[idx]);
}
}
}
// If the relation attribute is given then we don't have anything else to
// check.
if (aRelAttr)
@@ -1769,45 +1652,6 @@ DocAccessible::RemoveDependentIDsFor(Accessible* aRelProvider,
}
}
bool
DocAccessible::IsInARIAOwnsLoop(nsIContent* aOwnerEl, nsIContent* aDependentEl)
{
// ARIA owns element cannot refer to an element in parents chain of other ARIA
// owns element (including that ARIA owns element) if it's inside of
// a dependent element subtree of that ARIA owns element.
for (auto it = mARIAOwnsHash.Iter(); !it.Done(); it.Next()) {
Accessible* otherOwner = it.Key();
nsIContent* parentEl = otherOwner->GetContent();
while (parentEl && parentEl != aDependentEl) {
parentEl = parentEl->GetParent();
}
// The dependent element of this ARIA owns element contains some other ARIA
// owns element, make sure this ARIA owns element is not in a subtree of
// a dependent element of that other ARIA owns element. If not then
// continue a check recursively.
if (parentEl) {
nsTArray<nsIContent*>* childEls = it.UserData();
for (uint32_t idx = 0; idx < childEls->Length(); idx++) {
nsIContent* childEl = childEls->ElementAt(idx);
nsIContent* parentEl = aOwnerEl;
while (parentEl && parentEl != childEl) {
parentEl = parentEl->GetParent();
}
if (parentEl) {
return true;
}
if (IsInARIAOwnsLoop(aOwnerEl, childEl)) {
return true;
}
}
}
}
return false;
}
bool
DocAccessible::UpdateAccessibleOnAttrChange(dom::Element* aElement,
nsIAtom* aAttribute)
@@ -2023,11 +1867,6 @@ DocAccessible::UpdateTreeOnRemoval(Accessible* aContainer, nsIContent* aChildNod
}
}
// We may not have an integral DOM tree to remove all aria-owns relations
// from the tree. Validate all relations after timeout to workaround that.
mNotificationController->ScheduleNotification<DocAccessible>
(this, &DocAccessible::ValidateARIAOwned);
// Content insertion/removal is not cause of accessible tree change.
if (updateFlags == eNoAccessible)
return;
@@ -2111,23 +1950,244 @@ DocAccessible::UpdateTreeInternal(Accessible* aChild, bool aIsInsert,
return updateFlags;
}
void
DocAccessible::RelocateARIAOwnedIfNeeded(nsIContent* aElement)
{
if (!aElement->HasID())
return;
AttrRelProviderArray* list =
mDependentIDsHash.Get(nsDependentAtomString(aElement->GetID()));
if (list) {
for (uint32_t idx = 0; idx < list->Length(); idx++) {
if (list->ElementAt(idx)->mRelAttr == nsGkAtoms::aria_owns) {
Accessible* owner = GetAccessible(list->ElementAt(idx)->mContent);
if (owner) {
mNotificationController->ScheduleRelocation(owner);
}
}
}
}
}
void
DocAccessible::ValidateARIAOwned()
{
for (auto it = mARIAOwnsHash.Iter(); !it.Done(); it.Next()) {
nsTArray<nsIContent*>* childEls = it.UserData();
for (uint32_t idx = 0; idx < childEls->Length(); idx++) {
nsIContent* childEl = childEls->ElementAt(idx);
Accessible* child = GetAccessible(childEl);
if (child && child->IsInDocument() && !child->GetFrame()) {
if (!child->Parent()) {
NS_ERROR("An element in the document doesn't have a parent?");
Accessible* owner = it.Key();
nsTArray<RefPtr<Accessible> >* children = it.UserData();
// Owner is about to die, put children back if applicable.
if (!owner->IsInDocument()) {
PutChildrenBack(children, 0);
it.Remove();
continue;
}
UpdateTreeOnRemoval(child->Parent(), childEl);
for (uint32_t idx = 0; idx < children->Length(); idx++) {
Accessible* child = children->ElementAt(idx);
if (!child->IsInDocument()) {
children->RemoveElementAt(idx);
idx--;
continue;
}
NS_ASSERTION(child->Parent(), "No parent for ARIA owned?");
// If DOM node doesn't have a frame anymore then shutdown its accessible.
if (child->Parent() && !child->GetFrame()) {
UpdateTreeOnRemoval(child->Parent(), child->GetContent());
children->RemoveElementAt(idx);
idx--;
continue;
}
NS_ASSERTION(child->Parent() == owner,
"Illigally stolen ARIA owned child!");
}
if (children->Length() == 0) {
it.Remove();
}
}
}
void
DocAccessible::DoARIAOwnsRelocation(Accessible* aOwner)
{
nsTArray<RefPtr<Accessible> >* children = mARIAOwnsHash.LookupOrAdd(aOwner);
IDRefsIterator iter(this, aOwner->Elm(), nsGkAtoms::aria_owns);
Accessible* child = nullptr;
uint32_t arrayIdx = 0, insertIdx = aOwner->ChildCount() - children->Length();
while ((child = iter.Next())) {
// Same child on same position, no change.
if (child->Parent() == aOwner &&
child->IndexInParent() == static_cast<int32_t>(insertIdx)) {
NS_ASSERTION(child == children->ElementAt(arrayIdx), "Not in sync!");
insertIdx++; arrayIdx++;
continue;
}
NS_ASSERTION(children->SafeElementAt(arrayIdx) != child, "Already in place!");
nsTArray<RefPtr<Accessible> >::index_type idx = children->IndexOf(child);
if (idx < arrayIdx) {
continue; // ignore second entry of same ID
}
// A new child is found, check for loops.
if (child->Parent() != aOwner) {
Accessible* parent = aOwner;
while (parent && parent != child && !parent->IsDoc()) {
parent = parent->Parent();
}
// A referred child cannot be a parent of the owner.
if (parent == child) {
continue;
}
}
if (child->Parent() == aOwner) {
MoveChild(child, insertIdx - 1);
children->InsertElementAt(arrayIdx, child);
arrayIdx++;
} else if (SeizeChild(aOwner, child, insertIdx)) {
children->InsertElementAt(arrayIdx, child);
insertIdx++; arrayIdx++;
}
}
// Put back children that are not seized anymore.
PutChildrenBack(children, arrayIdx);
if (children->Length() == 0) {
mARIAOwnsHash.Remove(aOwner);
}
}
bool
DocAccessible::SeizeChild(Accessible* aNewParent, Accessible* aChild,
int32_t aIdxInParent)
{
Accessible* oldParent = aChild->Parent();
NS_PRECONDITION(oldParent, "No parent?");
int32_t oldIdxInParent = aChild->IndexInParent();
RefPtr<AccReorderEvent> reorderEvent = new AccReorderEvent(oldParent);
RefPtr<AccMutationEvent> hideEvent =
new AccHideEvent(aChild, aChild->GetContent(), false);
reorderEvent->AddSubMutationEvent(hideEvent);
{
AutoTreeMutation mut(oldParent);
oldParent->RemoveChild(aChild);
}
bool isReinserted = false;
{
AutoTreeMutation mut(aNewParent);
isReinserted = aNewParent->InsertChildAt(aIdxInParent, aChild);
}
if (!isReinserted) {
AutoTreeMutation mut(oldParent);
oldParent->InsertChildAt(oldIdxInParent, aChild);
return false;
}
// The child may be stolen from other ARIA owns element.
if (aChild->IsRelocated()) {
nsTArray<RefPtr<Accessible> >* children = mARIAOwnsHash.Get(oldParent);
children->RemoveElement(aChild);
}
FireDelayedEvent(hideEvent);
MaybeNotifyOfValueChange(oldParent);
FireDelayedEvent(reorderEvent);
reorderEvent = new AccReorderEvent(aNewParent);
RefPtr<AccMutationEvent> showEvent =
new AccShowEvent(aChild, aChild->GetContent());
reorderEvent->AddSubMutationEvent(showEvent);
FireDelayedEvent(showEvent);
MaybeNotifyOfValueChange(aNewParent);
FireDelayedEvent(reorderEvent);
aChild->SetRelocated(true);
return true;
}
void
DocAccessible::MoveChild(Accessible* aChild, int32_t aIdxInParent)
{
NS_PRECONDITION(aChild->Parent(), "No parent?");
Accessible* parent = aChild->Parent();
RefPtr<AccReorderEvent> reorderEvent = new AccReorderEvent(parent);
RefPtr<AccMutationEvent> hideEvent =
new AccHideEvent(aChild, aChild->GetContent(), false);
reorderEvent->AddSubMutationEvent(hideEvent);
AutoTreeMutation mut(parent);
parent->RemoveChild(aChild);
parent->InsertChildAt(aIdxInParent, aChild);
aChild->SetRelocated(true);
FireDelayedEvent(hideEvent);
RefPtr<AccMutationEvent> showEvent =
new AccShowEvent(aChild, aChild->GetContent());
reorderEvent->AddSubMutationEvent(showEvent);
FireDelayedEvent(showEvent);
MaybeNotifyOfValueChange(parent);
FireDelayedEvent(reorderEvent);
}
void
DocAccessible::PutChildrenBack(nsTArray<RefPtr<Accessible> >* aChildren,
uint32_t aStartIdx)
{
nsTArray<Accessible*> containers;
for (auto idx = aStartIdx; idx < aChildren->Length(); idx++) {
Accessible* child = aChildren->ElementAt(idx);
// If the child is in the tree then remove it from the owner.
if (child->IsInDocument()) {
Accessible* owner = child->Parent();
RefPtr<AccReorderEvent> reorderEvent = new AccReorderEvent(owner);
RefPtr<AccMutationEvent> hideEvent =
new AccHideEvent(child, child->GetContent(), false);
reorderEvent->AddSubMutationEvent(hideEvent);
{
AutoTreeMutation mut(owner);
owner->RemoveChild(child);
child->SetRelocated(false);
}
FireDelayedEvent(hideEvent);
MaybeNotifyOfValueChange(owner);
FireDelayedEvent(reorderEvent);
}
Accessible* container = GetContainerAccessible(child->GetContent());
if (container &&
containers.IndexOf(container) == nsTArray<Accessible*>::NoIndex) {
containers.AppendElement(container);
}
}
// And put it back where it belongs to.
aChildren->RemoveElementsAt(aStartIdx, aChildren->Length() - aStartIdx);
for (uint32_t idx = 0; idx < containers.Length(); idx++) {
UpdateTreeOnInsertion(containers[idx]);
}
}
void

View File

@@ -286,13 +286,9 @@ public:
*/
Accessible* ARIAOwnedAt(Accessible* aParent, uint32_t aIndex) const
{
nsTArray<nsIContent*>* childrenEl = mARIAOwnsHash.Get(aParent);
if (childrenEl) {
nsIContent* childEl = childrenEl->SafeElementAt(aIndex);
Accessible* child = GetAccessible(childEl);
if (child && child->IsRepositioned()) {
return child;
}
nsTArray<RefPtr<Accessible> >* children = mARIAOwnsHash.Get(aParent);
if (children) {
return children->SafeElementAt(aIndex);
}
return nullptr;
}
@@ -436,12 +432,6 @@ protected:
void RemoveDependentIDsFor(Accessible* aRelProvider,
nsIAtom* aRelAttr = nullptr);
/**
* Return true if given ARIA owner element and its referred content make
* the loop closed.
*/
bool IsInARIAOwnsLoop(nsIContent* aOwnerEl, nsIContent* aDependentEl);
/**
* Update or recreate an accessible depending on a changed attribute.
*
@@ -513,11 +503,38 @@ protected:
uint32_t UpdateTreeInternal(Accessible* aChild, bool aIsInsert,
AccReorderEvent* aReorderEvent);
/**
* Schedule ARIA owned element relocation if needed.
*/
void RelocateARIAOwnedIfNeeded(nsIContent* aEl);
/**
* Validates all aria-owns connections and updates the tree accordingly.
*/
void ValidateARIAOwned();
/**
* Steals or puts back accessible subtrees.
*/
void DoARIAOwnsRelocation(Accessible* aOwner);
/**
* Moves the child from old parent under new one.
*/
bool SeizeChild(Accessible* aNewParent, Accessible* aChild,
int32_t aIdxInParent);
/**
* Move the child under same parent.
*/
void MoveChild(Accessible* aChild, int32_t aIdxInParent);
/**
* Moves children back under their original parents.
*/
void PutChildrenBack(nsTArray<RefPtr<Accessible> >* aChildren,
uint32_t aStartIdx);
/**
* Create accessible tree.
*
@@ -649,14 +666,12 @@ protected:
AttrRelProvider& operator =(const AttrRelProvider&);
};
typedef nsTArray<nsAutoPtr<AttrRelProvider> > AttrRelProviderArray;
typedef nsClassHashtable<nsStringHashKey, AttrRelProviderArray>
DependentIDsHashtable;
/**
* The cache of IDs pointed by relation attributes.
*/
DependentIDsHashtable mDependentIDsHash;
typedef nsTArray<nsAutoPtr<AttrRelProvider> > AttrRelProviderArray;
nsClassHashtable<nsStringHashKey, AttrRelProviderArray>
mDependentIDsHash;
friend class RelatedAccIterator;
@@ -669,24 +684,11 @@ protected:
nsTArray<nsIContent*> mInvalidationList;
/**
* Holds a list of aria-owns relations.
* Holds a list of aria-owns relocations.
*/
nsClassHashtable<nsPtrHashKey<Accessible>, nsTArray<nsIContent*> >
nsClassHashtable<nsPtrHashKey<Accessible>, nsTArray<RefPtr<Accessible> > >
mARIAOwnsHash;
struct ARIAOwnsPair {
ARIAOwnsPair(Accessible* aOwner, nsIContent* aChild) :
mOwner(aOwner), mChild(aChild) { }
ARIAOwnsPair(const ARIAOwnsPair& aPair) :
mOwner(aPair.mOwner), mChild(aPair.mChild) { }
ARIAOwnsPair& operator =(const ARIAOwnsPair& aPair)
{ mOwner = aPair.mOwner; mChild = aPair.mChild; return *this; }
RefPtr<Accessible> mOwner;
nsCOMPtr<nsIContent> mChild;
};
nsTArray<ARIAOwnsPair> mARIAOwnsInvalidationList;
/**
* Used to process notification from core and accessible events.
*/

View File

@@ -51,9 +51,11 @@
{
this.menuNode = getNode(aMenuID);
// Because of aria-owns processing we may have menupopup start fired before
// related show event.
this.eventSeq = [
new invokerChecker(EVENT_SHOW, this.menuNode),
new invokerChecker(EVENT_MENUPOPUP_START, this.menuNode),
new asyncInvokerChecker(EVENT_MENUPOPUP_START, this.menuNode),
new invokerChecker(EVENT_REORDER, getNode(aParentMenuID))
];

View File

@@ -28,7 +28,7 @@
{
var tree =
{ SECTION: [ // t1_1
{ SECTION: [ // t1_2
{ HEADING: [ // t1_2
// no kids, no loop
] }
] };
@@ -36,8 +36,8 @@
tree =
{ SECTION: [ // t2_1
{ SECTION: [ // t2_2
{ SECTION: [ // t2_3
{ GROUPING: [ // t2_2
{ HEADING: [ // t2_3
// no kids, no loop
] }
] }
@@ -46,9 +46,9 @@
tree =
{ SECTION: [ // t3_3
{ SECTION: [ // t3_1
{ SECTION: [ // t3_2
{ SECTION: [ // DOM child of t3_2
{ GROUPING: [ // t3_1
{ NOTE: [ // t3_2
{ HEADING: [ // DOM child of t3_2
// no kids, no loop
] }
] }
@@ -58,7 +58,7 @@
tree =
{ SECTION: [ // t4_1
{ SECTION: [ // DOM child of t4_1
{ GROUPING: [ // DOM child of t4_1, aria-owns ignored
// no kids, no loop
] }
] };
@@ -66,11 +66,11 @@
tree =
{ SECTION: [ // t5_1
{ SECTION: [ // DOM child of t5_1
{ SECTION: [ // t5_2
{ SECTION: [ // DOM child of t5_2
{ SECTION: [ // t5_3
{ SECTION: [ // DOM child of t5_3
{ GROUPING: [ // DOM child of t5_1
{ NOTE: [ // t5_2
{ HEADING: [ // DOM child of t5_2
{ FORM: [ // t5_3
{ TOOLTIP: [ // DOM child of t5_3
// no kids, no loop
]}
]}
@@ -80,6 +80,14 @@
] };
testAccessibleTree("t5_1", tree);
tree =
{ SECTION: [ // t6_1
{ RADIOBUTTON: [ ] },
{ CHECKBUTTON: [ ] }, // t6_3, rearranged by aria-owns
{ PUSHBUTTON: [ ] }, // t6_2, rearranged by aria-owns
] };
testAccessibleTree("t6_1", tree);
SimpleTest.finish();
}
@@ -96,24 +104,37 @@
<pre id="test">
</pre>
<!-- simple loop -->
<div id="t1_1" aria-owns="t1_2"></div>
<div id="t1_2" aria-owns="t1_1"></div>
<div id="t1_2" aria-owns="t1_1" role="heading"></div>
<div id="t2_2" aria-owns="t2_3"></div>
<!-- loop -->
<div id="t2_2" aria-owns="t2_3" role="group"></div>
<div id="t2_1" aria-owns="t2_2"></div>
<div id="t2_3" aria-owns="t2_1"></div>
<div id="t2_3" aria-owns="t2_1" role="heading"></div>
<div id="t3_1" aria-owns="t3_2"></div>
<div id="t3_2">
<div aria-owns="t3_3"></div>
<!-- loop #2 -->
<div id="t3_1" aria-owns="t3_2" role="group"></div>
<div id="t3_2" role="note">
<div aria-owns="t3_3" role="heading"></div>
</div>
<div id="t3_3" aria-owns="t3_1"></div>
<div id="t4_1"><div aria-owns="t4_1"></div></div>
<!-- self loop -->
<div id="t4_1"><div aria-owns="t4_1" role="group"></div></div>
<!-- natural and aria-owns hierarchy -->
<div id="t5_1"><div aria-owns="t5_2" role="group"></div></div>
<div id="t5_2" role="note"><div aria-owns="t5_3" role="heading"></div></div>
<div id="t5_3" role="form"><div aria-owns="t5_1" role="tooltip"></div></div>
<!-- rearrange children -->
<div id="t6_1" aria-owns="t6_3 t6_2">
<div id="t6_2" role="button"></div>
<div id="t6_3" role="checkbox"></div>
<div role="radio"></div>
</div>
<div id="t5_1"><div aria-owns="t5_2"></div>
<div id="t5_2"><div aria-owns="t5_3"></div></div>
<div id="t5_3"><div aria-owns="t5_1"></div></div>
</body>
</html>

View File

@@ -26,11 +26,11 @@
function removeARIAOwns()
{
this.eventSeq = [
new invokerChecker(EVENT_HIDE, getNode("t2_checkbox")),
new invokerChecker(EVENT_HIDE, getNode("t2_button")),
new invokerChecker(EVENT_SHOW, getNode("t2_button")),
new invokerChecker(EVENT_SHOW, getNode("t2_checkbox")),
new invokerChecker(EVENT_REORDER, getNode("container2"))
new invokerChecker(EVENT_HIDE, getNode("t1_checkbox")),
new invokerChecker(EVENT_HIDE, getNode("t1_button")),
new invokerChecker(EVENT_SHOW, getNode("t1_button")),
new invokerChecker(EVENT_SHOW, getNode("t1_checkbox")),
new invokerChecker(EVENT_REORDER, getNode("t1_container"))
];
this.invoke = function removeARIAOwns_invoke()
@@ -43,9 +43,9 @@
] },
{ PUSHBUTTON: [ ] }
] };
testAccessibleTree("container2", tree);
testAccessibleTree("t1_container", tree);
getNode("container2").removeAttribute("aria-owns");
getNode("t1_container").removeAttribute("aria-owns");
}
this.finalCheck = function removeARIAOwns_finalCheck()
@@ -58,7 +58,7 @@
{ SECTION: [] }
] }
] };
testAccessibleTree("container2", tree);
testAccessibleTree("t1_container", tree);
}
this.getID = function removeARIAOwns_getID()
@@ -70,16 +70,17 @@
function setARIAOwns()
{
this.eventSeq = [
new invokerChecker(EVENT_HIDE, getNode("t2_button")),
new invokerChecker(EVENT_SHOW, getNode("t2_button")),
new invokerChecker(EVENT_HIDE, getNode("t2_subdiv")),
new invokerChecker(EVENT_SHOW, getNode("t2_subdiv")),
new invokerChecker(EVENT_REORDER, getNode("container2"))
new invokerChecker(EVENT_HIDE, getNode("t1_button")),
new invokerChecker(EVENT_SHOW, getNode("t1_button")),
new invokerChecker(EVENT_HIDE, getNode("t1_subdiv")),
new invokerChecker(EVENT_SHOW, getNode("t1_subdiv")),
new invokerChecker(EVENT_REORDER, getNode("t1_container"))
];
this.invoke = function setARIAOwns_invoke()
{
getNode("container2").setAttribute("aria-owns", "t2_button t2_subdiv");
getNode("t1_container").
setAttribute("aria-owns", "t1_button t1_subdiv");
}
this.finalCheck = function setARIAOwns_finalCheck()
@@ -88,11 +89,11 @@
// the children.
var tree =
{ SECTION: [
{ CHECKBUTTON: [ ] }, // div
{ PUSHBUTTON: [ ] }, // button
{ SECTION: [ ] } // subdiv
{ CHECKBUTTON: [ ] }, // checkbox
{ PUSHBUTTON: [ ] }, // button, rearranged by ARIA own
{ SECTION: [ ] } // subdiv from the subtree, ARIA owned
] };
testAccessibleTree("container2", tree);
testAccessibleTree("t1_container", tree);
}
this.getID = function setARIAOwns_getID()
@@ -101,19 +102,53 @@
}
}
function addIdToARIAOwns()
{
this.eventSeq = [
new invokerChecker(EVENT_HIDE, getNode("t1_group")),
new invokerChecker(EVENT_SHOW, getNode("t1_group")),
new invokerChecker(EVENT_REORDER, document)
];
this.invoke = function addIdToARIAOwns_invoke()
{
getNode("t1_container").
setAttribute("aria-owns", "t1_button t1_subdiv t1_group");
}
this.finalCheck = function addIdToARIAOwns_finalCheck()
{
// children are swapped again, button and subdiv are appended to
// the children.
var tree =
{ SECTION: [
{ CHECKBUTTON: [ ] }, // t1_checkbox
{ PUSHBUTTON: [ ] }, // button, t1_button
{ SECTION: [ ] }, // subdiv from the subtree, t1_subdiv
{ GROUPING: [ ] } // group from outside, t1_group
] };
testAccessibleTree("t1_container", tree);
}
this.getID = function addIdToARIAOwns_getID()
{
return "Add id to @aria-owns attribute value";
}
}
function appendEl()
{
this.eventSeq = [
new invokerChecker(EVENT_SHOW, getNode, "child3"),
new invokerChecker(EVENT_REORDER, getNode("container2"))
new invokerChecker(EVENT_SHOW, getNode, "t1_child3"),
new invokerChecker(EVENT_REORDER, getNode("t1_container"))
];
this.invoke = function appendEl_invoke()
{
var div = document.createElement("div");
div.setAttribute("id", "child3");
div.setAttribute("id", "t1_child3");
div.setAttribute("role", "radio")
getNode("container2").appendChild(div);
getNode("t1_container").appendChild(div);
}
this.finalCheck = function appendEl_finalCheck()
@@ -124,10 +159,11 @@
{ SECTION: [
{ CHECKBUTTON: [ ] },
{ RADIOBUTTON: [ ] },
{ PUSHBUTTON: [ ] }, // ARIA owned
{ SECTION: [ ] } // ARIA owned
{ PUSHBUTTON: [ ] }, // ARIA owned, t1_button
{ SECTION: [ ] }, // ARIA owned, t1_subdiv
{ GROUPING: [ ] } // ARIA owned, t1_group
] };
testAccessibleTree("container2", tree);
testAccessibleTree("t1_container", tree);
}
this.getID = function appendEl_getID()
@@ -139,15 +175,15 @@
function removeEl()
{
this.eventSeq = [
new invokerChecker(EVENT_HIDE, getNode, "t2_checkbox"),
new invokerChecker(EVENT_SHOW, getNode, "t2_checkbox"),
new invokerChecker(EVENT_REORDER, getNode("container2"))
new invokerChecker(EVENT_HIDE, getNode, "t1_checkbox"),
new invokerChecker(EVENT_SHOW, getNode, "t1_checkbox"),
new invokerChecker(EVENT_REORDER, getNode("t1_container"))
];
this.invoke = function removeEl_invoke()
{
// remove a container of t2_subdiv
getNode("t2_span").parentNode.removeChild(getNode("t2_span"));
// remove a container of t1_subdiv
getNode("t1_span").parentNode.removeChild(getNode("t1_span"));
}
this.finalCheck = function removeEl_finalCheck()
@@ -157,14 +193,214 @@
{ SECTION: [
{ CHECKBUTTON: [ ] },
{ RADIOBUTTON: [ ] },
{ PUSHBUTTON: [ ] } // ARIA owned
{ PUSHBUTTON: [ ] }, // ARIA owned, t1_button
{ GROUPING: [ ] } // ARIA owned, t1_group
] };
testAccessibleTree("container2", tree);
testAccessibleTree("t1_container", tree);
}
this.getID = function removeEl_getID()
{
return "Remove a container of ARIA ownded element";
return "Remove a container of ARIA owned element";
}
}
function removeId()
{
this.eventSeq = [
new invokerChecker(EVENT_HIDE, getNode("t1_group")),
new invokerChecker(EVENT_SHOW, getNode("t1_group")),
new invokerChecker(EVENT_REORDER, document)
];
this.invoke = function removeId_invoke()
{
getNode("t1_group").removeAttribute("id");
}
this.finalCheck = function removeId_finalCheck()
{
var tree =
{ SECTION: [
{ CHECKBUTTON: [ ] },
{ RADIOBUTTON: [ ] },
{ PUSHBUTTON: [ ] } // ARIA owned, t1_button
] };
testAccessibleTree("t1_container", tree);
}
this.getID = function removeId_getID()
{
return "Remove ID from ARIA owned element";
}
}
function setId()
{
this.eventSeq = [
new invokerChecker(EVENT_HIDE, getNode("t1_grouptmp")),
new invokerChecker(EVENT_SHOW, getNode("t1_grouptmp")),
new invokerChecker(EVENT_REORDER, document)
];
this.invoke = function setId_invoke()
{
getNode("t1_grouptmp").setAttribute("id", "t1_group");
}
this.finalCheck = function setId_finalCheck()
{
var tree =
{ SECTION: [
{ CHECKBUTTON: [ ] },
{ RADIOBUTTON: [ ] },
{ PUSHBUTTON: [ ] }, // ARIA owned, t1_button
{ GROUPING: [ ] } // ARIA owned, t1_group, previously t1_grouptmp
] };
testAccessibleTree("t1_container", tree);
}
this.getID = function setId_getID()
{
return "Set ID that is referred by ARIA owns";
}
}
/**
* Remove an accessible DOM element containing an element referred by
* ARIA owns.
*/
function removeA11eteiner()
{
this.eventSeq = [
new invokerChecker(EVENT_REORDER, getNode("t2_container1"))
];
this.invoke = function removeA11eteiner_invoke()
{
var tree =
{ SECTION: [
{ CHECKBUTTON: [ ] } // ARIA owned, 't2_owned'
] };
testAccessibleTree("t2_container1", tree);
getNode("t2_container2").removeChild(getNode("t2_container3"));
}
this.finalCheck = function removeA11eteiner_finalCheck()
{
var tree =
{ SECTION: [
] };
testAccessibleTree("t2_container1", tree);
}
this.getID = function removeA11eteiner_getID()
{
return "Remove an accessible DOM element containing an element referred by ARIA owns";
}
}
/**
* Steal an element from other ARIA owns element. This use case guarantees
* that result of setAttribute/removeAttribute doesn't depend on their order.
*/
function stealFromOtherARIAOwns()
{
this.eventSeq = [
new invokerChecker(EVENT_REORDER, getNode("t3_container2"))
];
this.invoke = function stealFromOtherARIAOwns_invoke()
{
getNode("t3_container2").setAttribute("aria-owns", "t3_child");
}
this.finalCheck = function stealFromOtherARIAOwns_finalCheck()
{
var tree =
{ SECTION: [
] };
testAccessibleTree("t3_container1", tree);
tree =
{ SECTION: [
{ CHECKBUTTON: [
] }
] };
testAccessibleTree("t3_container2", tree);
}
this.getID = function stealFromOtherARIAOwns_getID()
{
return "Steal an element from other ARIA owns element";
}
}
function appendElToRecacheChildren()
{
this.eventSeq = [
new invokerChecker(EVENT_REORDER, getNode("t3_container2"))
];
this.invoke = function appendElToRecacheChildren_invoke()
{
var div = document.createElement("div");
div.setAttribute("role", "radio")
getNode("t3_container2").appendChild(div);
}
this.finalCheck = function appendElToRecacheChildren_finalCheck()
{
var tree =
{ SECTION: [
] };
testAccessibleTree("t3_container1", tree);
tree =
{ SECTION: [
{ RADIOBUTTON: [ ] },
{ CHECKBUTTON: [ ] } // ARIA owned
] };
testAccessibleTree("t3_container2", tree);
}
this.getID = function appendElToRecacheChildren_getID()
{
return "Append a child under @aria-owns element to trigger children recache";
}
}
function showHiddenElement()
{
this.eventSeq = [
new invokerChecker(EVENT_REORDER, getNode("t4_container1"))
];
this.invoke = function showHiddenElement_invoke()
{
var tree =
{ SECTION: [
{ RADIOBUTTON: [] }
] };
testAccessibleTree("t4_container1", tree);
getNode("t4_child1").style.display = "block";
}
this.finalCheck = function showHiddenElement_finalCheck()
{
var tree =
{ SECTION: [
{ CHECKBUTTON: [] },
{ RADIOBUTTON: [] }
] };
testAccessibleTree("t4_container1", tree);
}
this.getID = function showHiddenElement_getID()
{
return "Show hidden ARIA owns referred element";
}
}
@@ -181,10 +417,24 @@
{
gQueue = new eventQueue();
// test1
gQueue.push(new removeARIAOwns());
gQueue.push(new setARIAOwns());
gQueue.push(new addIdToARIAOwns());
gQueue.push(new appendEl());
gQueue.push(new removeEl());
gQueue.push(new removeId());
gQueue.push(new setId());
// test2
gQueue.push(new removeA11eteiner());
// test3
gQueue.push(new stealFromOtherARIAOwns());
gQueue.push(new appendElToRecacheChildren());
// test4
gQueue.push(new showHiddenElement());
gQueue.invoke(); // SimpleTest.finish() will be called in the end
}
@@ -202,15 +452,31 @@
<pre id="test">
</pre>
<div id="container2" aria-owns="t2_checkbox t2_button">
<div role="button" id="t2_button"></div>
<div role="checkbox" id="t2_checkbox">
<span id="t2_span">
<div id="t2_subdiv"></div>
<div id="t1_container" aria-owns="t1_checkbox t1_button">
<div role="button" id="t1_button"></div>
<div role="checkbox" id="t1_checkbox">
<span id="t1_span">
<div id="t1_subdiv"></div>
</span>
</div>
</div>
<div id="t1_group" role="group"></div>
<div id="t1_grouptmp" role="group"></div>
<div id="t2_container1" aria-owns="t2_owned"></div>
<div id="t2_container2">
<div id="t2_container3"><div id="t2_owned" role="checkbox"></div></div>
</div>
<div id="t3_container1" aria-owns="t3_child"></div>
<div id="t3_child" role="checkbox"></div>
<div id="t3_container2"></div>
<div id="t4_container1" aria-owns="t4_child1 t4_child2"></div>
<div id="t4_container2">
<div id="t4_child1" style="display:none" role="checkbox"></div>
<div id="t4_child2" role="radio"></div>
</div>
</body>
</html>

View File

@@ -22,9 +22,9 @@
this.eventSeq = [
new invokerChecker(EVENT_HIDE, getNode("child")),
new invokerChecker(EVENT_REORDER, this.containerNode),
new invokerChecker(EVENT_HIDE, getNode("childDoc")),
new invokerChecker(EVENT_SHOW, "newChildDoc")
new invokerChecker(EVENT_SHOW, "newChildDoc"),
new invokerChecker(EVENT_REORDER, this.containerNode)
];
this.invoke = function runTest_invoke()