Bug 1133213 - make aria-owns to alter the accessible tree, fire show/hide mutation events as we do for the accessible tree alterations, r=yzen, f=davidb

This commit is contained in:
Alexander Surkov 2015-09-11 20:54:27 -04:00
parent 282b6c7b96
commit fea219d6cd
16 changed files with 614 additions and 100 deletions

View File

@ -104,8 +104,9 @@ AccReorderEvent::IsShowHideEventTarget(const Accessible* aTarget) const
////////////////////////////////////////////////////////////////////////////////
AccHideEvent::
AccHideEvent(Accessible* aTarget, nsINode* aTargetNode) :
AccMutationEvent(::nsIAccessibleEvent::EVENT_HIDE, aTarget, aTargetNode)
AccHideEvent(Accessible* aTarget, nsINode* aTargetNode, bool aNeedsShutdown) :
AccMutationEvent(::nsIAccessibleEvent::EVENT_HIDE, aTarget, aTargetNode),
mNeedsShutdown(aNeedsShutdown)
{
mNextSibling = mAccessible->NextSibling();
mPrevSibling = mAccessible->PrevSibling();

View File

@ -250,7 +250,8 @@ protected:
class AccHideEvent: public AccMutationEvent
{
public:
AccHideEvent(Accessible* aTarget, nsINode* aTargetNode);
AccHideEvent(Accessible* aTarget, nsINode* aTargetNode,
bool aNeedsShutdown = true);
// Event
static const EventGroup kEventGroup = eHideEvent;
@ -263,8 +264,10 @@ public:
Accessible* TargetParent() const { return mParent; }
Accessible* TargetNextSibling() const { return mNextSibling; }
Accessible* TargetPrevSibling() const { return mPrevSibling; }
bool NeedsShutdown() const { return mNeedsShutdown; }
protected:
bool mNeedsShutdown;
nsRefPtr<Accessible> mNextSibling;
nsRefPtr<Accessible> mPrevSibling;

View File

@ -546,8 +546,10 @@ EventQueue::ProcessEventQueue()
}
}
if (event->mEventType == nsIAccessibleEvent::EVENT_HIDE)
AccHideEvent* hideEvent = downcast_accEvent(event);
if (hideEvent && hideEvent->NeedsShutdown()) {
mDocument->ShutdownChildrenInSubtree(event->mAccessible);
}
if (!mDocument)
return;

View File

@ -109,9 +109,6 @@ NotificationController::ScheduleContentInsertion(Accessible* aContainer,
}
}
////////////////////////////////////////////////////////////////////////////////
// NotificationCollector: protected
void
NotificationController::ScheduleProcessing()
{
@ -123,6 +120,9 @@ NotificationController::ScheduleProcessing()
}
}
////////////////////////////////////////////////////////////////////////////////
// NotificationCollector: protected
bool
NotificationController::IsUpdatePending()
{

View File

@ -8,6 +8,8 @@
#include "EventQueue.h"
#include "mozilla/IndexSequence.h"
#include "mozilla/Tuple.h"
#include "nsCycleCollectionParticipant.h"
#include "nsRefreshDriver.h"
@ -54,32 +56,32 @@ private:
* longer than the document accessible owning the notification controller
* that this notification is processed by.
*/
template<class Class, class Arg>
template<class Class, class ... Args>
class TNotification : public Notification
{
public:
typedef void (Class::*Callback)(Arg*);
typedef void (Class::*Callback)(Args* ...);
TNotification(Class* aInstance, Callback aCallback, Arg* aArg) :
mInstance(aInstance), mCallback(aCallback), mArg(aArg) { }
TNotification(Class* aInstance, Callback aCallback, Args* ... aArgs) :
mInstance(aInstance), mCallback(aCallback), mArgs(aArgs...) { }
virtual ~TNotification() { mInstance = nullptr; }
virtual void Process() override
{
(mInstance->*mCallback)(mArg);
mInstance = nullptr;
mCallback = nullptr;
mArg = nullptr;
}
{ ProcessHelper(typename IndexSequenceFor<Args...>::Type()); }
private:
TNotification(const TNotification&);
TNotification& operator = (const TNotification&);
template <size_t... Indices>
void ProcessHelper(IndexSequence<Indices...>)
{
(mInstance->*mCallback)(Get<Indices>(mArgs)...);
}
Class* mInstance;
Callback mCallback;
nsRefPtr<Arg> mArg;
Tuple<nsRefPtr<Args> ...> mArgs;
};
/**
@ -131,6 +133,12 @@ public:
nsIContent* aStartChildNode,
nsIContent* aEndChildNode);
/**
* Start to observe refresh to make notifications and events processing after
* layout.
*/
void ScheduleProcessing();
/**
* Process the generic notification synchronously if there are no pending
* layout changes and no notifications are pending or being processed right
@ -165,13 +173,12 @@ public:
* @note The caller must guarantee that the given instance still exists when
* the notification is processed.
*/
template<class Class, class Arg>
template<class Class>
inline void ScheduleNotification(Class* aInstance,
typename TNotification<Class, Arg>::Callback aMethod,
Arg* aArg)
typename TNotification<Class>::Callback aMethod)
{
nsRefPtr<Notification> notification =
new TNotification<Class, Arg>(aInstance, aMethod, aArg);
new TNotification<Class>(aInstance, aMethod);
if (notification && mNotifications.AppendElement(notification))
ScheduleProcessing();
}
@ -187,12 +194,6 @@ protected:
nsCycleCollectingAutoRefCnt mRefCnt;
NS_DECL_OWNINGTHREAD
/**
* Start to observe refresh to make notifications and events processing after
* layout.
*/
void ScheduleProcessing();
/**
* Return true if the accessible tree state update is pending.
*/

View File

@ -6,6 +6,7 @@
#include "TreeWalker.h"
#include "Accessible.h"
#include "AccIterator.h"
#include "nsAccessibilityService.h"
#include "DocAccessible.h"
@ -50,20 +51,16 @@ TreeWalker::NextChild()
if (mStateStack.IsEmpty())
return nullptr;
dom::AllChildrenIterator* top = &mStateStack[mStateStack.Length() - 1];
ChildrenIterator* top = &mStateStack[mStateStack.Length() - 1];
while (top) {
while (nsIContent* childNode = top->GetNextChild()) {
bool isSubtreeHidden = false;
Accessible* accessible = mFlags & eWalkCache ?
mDoc->GetAccessible(childNode) :
GetAccService()->GetOrCreateAccessible(childNode, mContext,
&isSubtreeHidden);
if (accessible)
return accessible;
Accessible* child = nullptr;
bool skipSubtree = false;
while (nsIContent* childNode = Next(top, &child, &skipSubtree)) {
if (child)
return child;
// Walk down into subtree to find accessibles.
if (!isSubtreeHidden && childNode->IsElement())
if (!skipSubtree && childNode->IsElement())
top = PushState(childNode);
}
@ -82,9 +79,8 @@ TreeWalker::NextChild()
return nullptr;
nsIContent* parent = parentNode->AsElement();
top = mStateStack.AppendElement(dom::AllChildrenIterator(parent,
mChildFilter));
while (nsIContent* childNode = top->GetNextChild()) {
top = PushState(parent);
while (nsIContent* childNode = Next(top)) {
if (childNode == mAnchorNode) {
mAnchorNode = parent;
return NextChild();
@ -101,7 +97,47 @@ TreeWalker::NextChild()
return nullptr;
}
dom::AllChildrenIterator*
nsIContent*
TreeWalker::Next(ChildrenIterator* aIter, Accessible** aAccesible,
bool* aSkipSubtree)
{
nsIContent* childEl = aIter->mDOMIter.GetNextChild();
if (!aAccesible)
return childEl;
*aAccesible = nullptr;
*aSkipSubtree = false;
if (childEl) {
Accessible* accessible = mFlags & eWalkCache ?
mDoc->GetAccessible(childEl) :
GetAccService()->GetOrCreateAccessible(childEl, mContext, aSkipSubtree);
// Ignore the accessible and its subtree if it was repositioned by means of
// aria-owns.
if (accessible) {
if (accessible->IsRepositioned()) {
*aSkipSubtree = true;
} else {
*aAccesible = accessible;
}
}
return childEl;
}
// At last iterate over ARIA owned children.
Accessible* parent = mDoc->GetAccessible(aIter->mDOMIter.Parent());
if (parent) {
Accessible* child = mDoc->ARIAOwnedAt(parent, aIter->mARIAOwnsIdx++);
if (child) {
*aAccesible = child;
return child->GetContent();
}
}
return nullptr;
}
TreeWalker::ChildrenIterator*
TreeWalker::PopState()
{
size_t length = mStateStack.Length();

View File

@ -57,27 +57,37 @@ private:
TreeWalker(const TreeWalker&);
TreeWalker& operator =(const TreeWalker&);
struct ChildrenIterator {
ChildrenIterator(nsIContent* aNode, uint32_t aFilter) :
mDOMIter(aNode, aFilter), mARIAOwnsIdx(0) { }
dom::AllChildrenIterator mDOMIter;
uint32_t mARIAOwnsIdx;
};
nsIContent* Next(ChildrenIterator* aIter, Accessible** aAccessible = nullptr,
bool* aSkipSubtree = nullptr);
/**
* Create new state for the given node and push it on top of stack.
*
* @note State stack is used to navigate up/down the DOM subtree during
* accessible children search.
*/
dom::AllChildrenIterator* PushState(nsIContent* aContent)
ChildrenIterator* PushState(nsIContent* aContent)
{
return mStateStack.AppendElement(dom::AllChildrenIterator(aContent,
mChildFilter));
return mStateStack.AppendElement(ChildrenIterator(aContent, mChildFilter));
}
/**
* Pop state from stack.
*/
dom::AllChildrenIterator* PopState();
ChildrenIterator* PopState();
DocAccessible* mDoc;
Accessible* mContext;
nsIContent* mAnchorNode;
nsAutoTArray<dom::AllChildrenIterator, 20> mStateStack;
nsAutoTArray<ChildrenIterator, 20> mStateStack;
int32_t mChildFilter;
uint32_t mFlags;
};

View File

@ -1974,8 +1974,8 @@ Accessible::BindToParent(Accessible* aParent, uint32_t aIndexInParent)
if (mParent) {
if (mParent != aParent) {
NS_ERROR("Adopting child!");
mParent->RemoveChild(this);
mParent->InvalidateChildrenGroupInfo();
mParent->RemoveChild(this);
} else {
NS_ERROR("Binding to the same parent!");
return;

View File

@ -160,6 +160,8 @@ public:
return DOMNode.forget();
}
nsIContent* GetContent() const { return mContent; }
mozilla::dom::Element* Elm() const
{ return mContent->IsElement() ? mContent->AsElement() : nullptr; }
/**
* Return node type information of DOM node associated with the accessible.
@ -899,6 +901,19 @@ public:
mStateFlags &= ~eSurvivingInUpdate;
}
/**
* 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)
{
if (aRepositioned)
mStateFlags |= eRepositioned;
else
mStateFlags &= ~eRepositioned;
}
/**
* Return true if this accessible has a parent whose name depends on this
* accessible.
@ -914,7 +929,6 @@ public:
void SetARIAHidden(bool aIsDefined);
protected:
virtual ~Accessible();
/**
@ -990,8 +1004,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
eLastStateFlag = eSurvivingInUpdate
eLastStateFlag = eRepositioned
};
/**
@ -1106,7 +1121,7 @@ protected:
int32_t mIndexInParent;
static const uint8_t kChildrenFlagsBits = 2;
static const uint8_t kStateFlagsBits = 9;
static const uint8_t kStateFlagsBits = 10;
static const uint8_t kContextFlagsBits = 2;
static const uint8_t kTypeBits = 6;
static const uint8_t kGenericTypesBits = 14;

View File

@ -707,7 +707,7 @@ DocAccessible::AttributeWillChange(nsIDocument* aDocument,
// because dependent IDs cache doesn't contain IDs from non accessible
// elements.
if (aModType != nsIDOMMutationEvent::ADDITION)
RemoveDependentIDsFor(aElement, aAttribute);
RemoveDependentIDsFor(accessible, aAttribute);
// Store the ARIA attribute old value so that it can be used after
// attribute change. Note, we assume there's no nested ARIA attribute
@ -769,7 +769,7 @@ DocAccessible::AttributeChanged(nsIDocument* aDocument,
// dependent IDs cache when its accessible is created.
if (aModType == nsIDOMMutationEvent::MODIFICATION ||
aModType == nsIDOMMutationEvent::ADDITION) {
AddDependentIDsFor(aElement, aAttribute);
AddDependentIDsFor(accessible, aAttribute);
}
}
@ -1241,9 +1241,7 @@ DocAccessible::BindToDocument(Accessible* aAccessible,
aAccessible->SetRoleMapEntry(aRoleMapEntry);
nsIContent* content = aAccessible->GetContent();
if (content && content->IsElement())
AddDependentIDsFor(content->AsElement());
AddDependentIDsFor(aAccessible);
}
void
@ -1338,6 +1336,49 @@ 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;
Accessible* child = GetAccessible(mARIAOwnsInvalidationList[idx].mChild);
if (!child) {
continue;
}
// XXX: update context flags
{
Accessible* oldParent = child->Parent();
nsRefPtr<AccReorderEvent> reorderEvent = new AccReorderEvent(oldParent);
nsRefPtr<AccMutationEvent> hideEvent =
new AccHideEvent(child, child->GetContent(), false);
FireDelayedEvent(hideEvent);
reorderEvent->AddSubMutationEvent(hideEvent);
AutoTreeMutation mut(oldParent);
oldParent->RemoveChild(child);
MaybeNotifyOfValueChange(oldParent);
FireDelayedEvent(reorderEvent);
}
{
nsRefPtr<AccReorderEvent> reorderEvent = new AccReorderEvent(owner);
nsRefPtr<AccMutationEvent> showEvent =
new AccShowEvent(child, child->GetContent());
FireDelayedEvent(showEvent);
reorderEvent->AddSubMutationEvent(showEvent);
AutoTreeMutation mut(owner);
owner->AppendChild(child);
MaybeNotifyOfValueChange(owner);
FireDelayedEvent(reorderEvent);
}
child->SetRepositioned(true);
}
mARIAOwnsInvalidationList.Clear();
}
Accessible*
@ -1496,26 +1537,29 @@ DocAccessible::ProcessLoad()
}
void
DocAccessible::AddDependentIDsFor(dom::Element* aRelProviderElm,
nsIAtom* aRelAttr)
DocAccessible::AddDependentIDsFor(Accessible* aRelProvider, nsIAtom* aRelAttr)
{
dom::Element* relProviderEl = aRelProvider->Elm();
if (!relProviderEl)
return;
for (uint32_t idx = 0; idx < kRelationAttrsLen; idx++) {
nsIAtom* relAttr = *kRelationAttrs[idx];
if (aRelAttr && aRelAttr != relAttr)
continue;
if (relAttr == nsGkAtoms::_for) {
if (!aRelProviderElm->IsAnyOfHTMLElements(nsGkAtoms::label,
nsGkAtoms::output))
if (!relProviderEl->IsAnyOfHTMLElements(nsGkAtoms::label,
nsGkAtoms::output))
continue;
} else if (relAttr == nsGkAtoms::control) {
if (!aRelProviderElm->IsAnyOfXULElements(nsGkAtoms::label,
nsGkAtoms::description))
if (!relProviderEl->IsAnyOfXULElements(nsGkAtoms::label,
nsGkAtoms::description))
continue;
}
IDRefsIterator iter(this, aRelProviderElm, relAttr);
IDRefsIterator iter(this, relProviderEl, relAttr);
while (true) {
const nsDependentSubstring id = iter.NextID();
if (id.IsEmpty())
@ -1531,7 +1575,7 @@ DocAccessible::AddDependentIDsFor(dom::Element* aRelProviderElm,
if (providers) {
AttrRelProvider* provider =
new AttrRelProvider(relAttr, aRelProviderElm);
new AttrRelProvider(relAttr, relProviderEl);
if (provider) {
providers->AppendElement(provider);
@ -1540,8 +1584,48 @@ DocAccessible::AddDependentIDsFor(dom::Element* aRelProviderElm,
// children invalidation (this happens immediately after the caching
// is finished).
nsIContent* dependentContent = iter.GetElem(id);
if (dependentContent && !HasAccessible(dependentContent)) {
mInvalidationList.AppendElement(dependentContent);
if (dependentContent) {
if (!HasAccessible(dependentContent)) {
mInvalidationList.AppendElement(dependentContent);
}
if (relAttr == nsGkAtoms::aria_owns) {
// Dependent content cannot point to other aria-owns content or
// their parents. Ignore it if so.
// XXX: note, this alg may make invalid the scenario when X owns Y
// and Y owns Z, we should have something smarter to handle that.
bool isvalid = true;
for (auto it = mARIAOwnsHash.Iter(); !it.Done(); it.Next()) {
Accessible* owner = it.Key();
nsIContent* parentEl = owner->GetContent();
while (parentEl && parentEl != dependentContent) {
parentEl = parentEl->GetParent();
}
if (parentEl) {
isvalid = false;
break;
}
}
if (isvalid) {
// ARIA owns also cannot refer to itself or a parent.
nsIContent* parentEl = relProviderEl;
while (parentEl && parentEl != dependentContent) {
parentEl = parentEl->GetParent();
}
if (parentEl) {
isvalid = false;
}
if (isvalid) {
nsTArray<nsIContent*>* list =
mARIAOwnsHash.LookupOrAdd(aRelProvider);
list->AppendElement(dependentContent);
mARIAOwnsInvalidationList.AppendElement(
ARIAOwnsPair(aRelProvider, dependentContent));
}
}
}
}
}
}
@ -1552,18 +1636,25 @@ DocAccessible::AddDependentIDsFor(dom::Element* aRelProviderElm,
if (aRelAttr)
break;
}
// Make sure to schedule the tree update if needed.
mNotificationController->ScheduleProcessing();
}
void
DocAccessible::RemoveDependentIDsFor(dom::Element* aRelProviderElm,
DocAccessible::RemoveDependentIDsFor(Accessible* aRelProvider,
nsIAtom* aRelAttr)
{
dom::Element* relProviderElm = aRelProvider->Elm();
if (!relProviderElm)
return;
for (uint32_t idx = 0; idx < kRelationAttrsLen; idx++) {
nsIAtom* relAttr = *kRelationAttrs[idx];
if (aRelAttr && aRelAttr != *kRelationAttrs[idx])
continue;
IDRefsIterator iter(this, aRelProviderElm, relAttr);
IDRefsIterator iter(this, relProviderElm, relAttr);
while (true) {
const nsDependentSubstring id = iter.NextID();
if (id.IsEmpty())
@ -1574,7 +1665,7 @@ DocAccessible::RemoveDependentIDsFor(dom::Element* aRelProviderElm,
for (uint32_t jdx = 0; jdx < providers->Length(); ) {
AttrRelProvider* provider = (*providers)[jdx];
if (provider->mRelAttr == relAttr &&
provider->mContent == aRelProviderElm)
provider->mContent == relProviderElm)
providers->RemoveElement(provider);
else
jdx++;
@ -1584,6 +1675,59 @@ DocAccessible::RemoveDependentIDsFor(dom::Element* aRelProviderElm,
}
}
// 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.
nsRefPtr<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()) {
{
nsRefPtr<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)
@ -1806,6 +1950,11 @@ 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;
@ -1889,6 +2038,21 @@ DocAccessible::UpdateTreeInternal(Accessible* aChild, bool aIsInsert,
return updateFlags;
}
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->GetFrame()) {
UpdateTreeOnRemoval(child->Parent(), childEl);
}
}
}
}
void
DocAccessible::CacheChildrenInSubtree(Accessible* aRoot,
Accessible** aFocusedAcc)
@ -1926,10 +2090,7 @@ void
DocAccessible::UncacheChildrenInSubtree(Accessible* aRoot)
{
aRoot->mStateFlags |= eIsNotInDocument;
nsIContent* rootContent = aRoot->GetContent();
if (rootContent && rootContent->IsElement())
RemoveDependentIDsFor(rootContent->AsElement());
RemoveDependentIDsFor(aRoot);
uint32_t count = aRoot->ContentChildCount();
for (uint32_t idx = 0; idx < count; idx++)

View File

@ -32,7 +32,7 @@ class DocManager;
class NotificationController;
class DocAccessibleChild;
class RelatedAccIterator;
template<class Class, class Arg>
template<class Class, class ... Args>
class TNotification;
class DocAccessible : public HyperTextAccessibleWrap,
@ -281,6 +281,22 @@ public:
*/
Accessible* GetAccessibleOrDescendant(nsINode* aNode) const;
/**
* Returns aria-owns seized child at the given index.
*/
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;
}
}
return nullptr;
}
/**
* Return true if the given ID is referred by relation attribute.
*
@ -406,7 +422,7 @@ protected:
* @param aRelProvider [in] accessible that element has relation attribute
* @param aRelAttr [in, optional] relation attribute
*/
void AddDependentIDsFor(dom::Element* aRelProviderElm,
void AddDependentIDsFor(Accessible* aRelProvider,
nsIAtom* aRelAttr = nullptr);
/**
@ -417,7 +433,7 @@ protected:
* @param aRelProvider [in] accessible that element has relation attribute
* @param aRelAttr [in, optional] relation attribute
*/
void RemoveDependentIDsFor(dom::Element* aRelProviderElm,
void RemoveDependentIDsFor(Accessible* aRelProvider,
nsIAtom* aRelAttr = nullptr);
/**
@ -491,6 +507,11 @@ protected:
uint32_t UpdateTreeInternal(Accessible* aChild, bool aIsInsert,
AccReorderEvent* aReorderEvent);
/**
* Validates all aria-owns connections and updates the tree accordingly.
*/
void ValidateARIAOwned();
/**
* Create accessible tree.
*
@ -646,6 +667,25 @@ protected:
*/
nsTArray<nsIContent*> mInvalidationList;
/**
* Holds a list of aria-owns relations.
*/
nsClassHashtable<nsPtrHashKey<Accessible>, nsTArray<nsIContent*> >
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; }
Accessible* mOwner;
nsIContent* mChild;
};
nsTArray<ARIAOwnsPair> mARIAOwnsInvalidationList;
/**
* Used to process notification from core and accessible events.
*/

View File

@ -452,26 +452,39 @@ function testAccessibleTree(aAccOrElmOrID, aAccTree, aFlags)
var children = acc.children;
var childCount = children.length;
if (accTree.children.length != childCount) {
for (var i = 0; i < Math.max(accTree.children.length, childCount); i++) {
var accChild;
try {
accChild = children.queryElementAt(i, nsIAccessible);
if (!accTree.children[i]) {
testChild = accTree.children[i];
if (!testChild) {
ok(false, prettyName(acc) + " has an extra child at index " + i +
" : " + prettyName(accChild));
continue;
}
if (accChild.role !== accTree.children[i].role) {
var key = Object.keys(testChild)[0];
var roleName = "ROLE_" + key;
if (roleName in nsIAccessibleRole) {
testChild = {
role: nsIAccessibleRole[roleName],
children: testChild[key]
};
}
if (accChild.role !== testChild.role) {
ok(false, prettyName(accTree) + " and " + prettyName(acc) +
" have different children at index " + i + " : " +
prettyName(accTree.children[i]) + ", " + prettyName(accChild));
prettyName(testChild) + ", " + prettyName(accChild));
}
info("Matching " + prettyName(accTree) + " and " + prettyName(acc) +
" child at index " + i + " : " + prettyName(accChild));
} catch (e) {
ok(false, prettyName(accTree) + " has an extra child at index " + i +
" : " + prettyName(accTree.children[i]));
" : " + prettyName(testChild) + ", " + e);
throw e;
}
}
} else {

View File

@ -87,19 +87,16 @@
tree =
{ SECTION: [ // container
{ LABEL: [ // label, has aria-owns
{ TEXT_LEAF: [ ] }
{ TEXT_LEAF: [ ] },
{ LABEL: [ // label, referenced by aria-owns
{ TEXT_LEAF: [ ] }
] },
] },
{ TEXT_LEAF: [ ] },
{ LABEL: [ // label, referenced by aria-owns
{ TEXT_LEAF: [ ] }
] },
{ TEXT_LEAF: [ ] },
{ LABEL: [ // label, has aria-owns
{ TEXT_LEAF: [ ] }
] },
{ TEXT_LEAF: [ ] },
{ LABEL: [ // label, referenced by aria-owns
{ TEXT_LEAF: [ ] }
{ TEXT_LEAF: [ ] },
{ LABEL: [ // label, referenced by aria-owns
{ TEXT_LEAF: [ ] }
] }
] }
] };
testAccessibleTree("airaglobalprop_cnt", tree);
@ -172,12 +169,11 @@
</ul>
</div>
<div id="airaglobalprop_cnt">
<label role="presentation" aria-owns="ariaowned">has aria-owns</label>
<label role="presentation" id="ariaowned">referred by aria-owns</label>
<label role="none" aria-owns="ariaowned2">has aria-owns</label>
<label role="none" id="ariaowned2">referred by aria-owns</label>
</div>
<div id="airaglobalprop_cnt"><label
role="presentation" aria-owns="ariaowned">has aria-owns</label><label
role="presentation" id="ariaowned">referred by aria-owns</label><label
role="none" aria-owns="ariaowned2">has aria-owns</label><label
role="none" id="ariaowned2">referred by aria-owns</label></div>
</body>
</html>

View File

@ -1,6 +1,7 @@
[DEFAULT]
[test_ariadialog.html]
[test_ariaowns.html]
[test_bug852150.xhtml]
[test_bug883708.xhtml]
[test_bug884251.xhtml]

View File

@ -0,0 +1,234 @@
<!DOCTYPE html>
<html>
<head>
<title>@aria-owns attribute testing</title>
<link rel="stylesheet" type="text/css"
href="chrome://mochikit/content/tests/SimpleTest/test.css" />
<script type="application/javascript"
src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript"
src="../common.js"></script>
<script type="application/javascript"
src="../role.js"></script>
<script type="application/javascript"
src="../events.js"></script>
<script type="application/javascript">
////////////////////////////////////////////////////////////////////////////
// Invokers
////////////////////////////////////////////////////////////////////////////
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"))
];
this.invoke = function removeARIAOwns_invoke()
{
// children are swapped
var tree =
{ SECTION: [
{ CHECKBUTTON: [
{ SECTION: [] }
] },
{ PUSHBUTTON: [ ] }
] };
testAccessibleTree("container2", tree);
getNode("container2").removeAttribute("aria-owns");
}
this.finalCheck = function removeARIAOwns_finalCheck()
{
// children follow the DOM order
var tree =
{ SECTION: [
{ PUSHBUTTON: [ ] },
{ CHECKBUTTON: [
{ SECTION: [] }
] }
] };
testAccessibleTree("container2", tree);
}
this.getID = function removeARIAOwns_getID()
{
return "Remove @aria-owns attribute";
}
}
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"))
];
this.invoke = function setARIAOwns_invoke()
{
getNode("container2").setAttribute("aria-owns", "t2_button t2_subdiv");
}
this.finalCheck = function setARIAOwns_finalCheck()
{
// children are swapped again, button and subdiv are appended to
// the children.
var tree =
{ SECTION: [
{ CHECKBUTTON: [ ] }, // div
{ PUSHBUTTON: [ ] }, // button
{ SECTION: [ ] } // subdiv
] };
testAccessibleTree("container2", tree);
}
this.getID = function setARIAOwns_getID()
{
return "Set @aria-owns attribute";
}
}
function appendEl()
{
this.eventSeq = [
new invokerChecker(EVENT_SHOW, getNode, "child3"),
new invokerChecker(EVENT_REORDER, getNode("container2"))
];
this.invoke = function appendEl_invoke()
{
var div = document.createElement("div");
div.setAttribute("id", "child3");
div.setAttribute("role", "radio")
getNode("container2").appendChild(div);
}
this.finalCheck = function appendEl_finalCheck()
{
// children are invalidated, they includes aria-owns swapped kids and
// newly inserted child.
var tree =
{ SECTION: [
{ CHECKBUTTON: [ ] },
{ RADIOBUTTON: [ ] },
{ PUSHBUTTON: [ ] }, // ARIA owned
{ SECTION: [ ] } // ARIA owned
] };
testAccessibleTree("container2", tree);
}
this.getID = function appendEl_getID()
{
return "Append child under @aria-owns element";
}
}
function removeEl()
{
this.eventSeq = [
new invokerChecker(EVENT_HIDE, getNode, "t2_checkbox"),
new invokerChecker(EVENT_SHOW, getNode, "t2_checkbox"),
new invokerChecker(EVENT_REORDER, getNode("container2"))
];
this.invoke = function removeEl_invoke()
{
// remove a container of t2_subdiv
getNode("t2_span").parentNode.removeChild(getNode("t2_span"));
}
this.finalCheck = function removeEl_finalCheck()
{
// subdiv should go away
var tree =
{ SECTION: [
{ CHECKBUTTON: [ ] },
{ RADIOBUTTON: [ ] },
{ PUSHBUTTON: [ ] } // ARIA owned
] };
testAccessibleTree("container2", tree);
}
this.getID = function removeEl_getID()
{
return "Remove a container of ARIA ownded element";
}
}
////////////////////////////////////////////////////////////////////////////
// Test
////////////////////////////////////////////////////////////////////////////
gA11yEventDumpToConsole = true;
enableLogging("tree"); // debug stuff
var gQueue = null;
function doTest()
{
// nested and recursive aria-owns
var tree =
{ SECTION: [ // container
{ SECTION: [ // child
{ SECTION: [ // mid div
{ SECTION: [] } // grandchild
] }
] }
] };
testAccessibleTree("container", tree);
// dynamic tests
gQueue = new eventQueue();
gQueue.push(new removeARIAOwns());
gQueue.push(new setARIAOwns());
gQueue.push(new appendEl());
gQueue.push(new removeEl());
gQueue.invoke(); // SimpleTest.finish() will be called in the end
}
SimpleTest.waitForExplicitFinish();
addA11yLoadEvent(doTest);
</script>
</head>
<body>
<p id="display"></p>
<div id="content" style="display: none"></div>
<pre id="test">
</pre>
<div id="container" aria-owns="child" aria-label="container"></div>
<div id="child" aria-label="child">
<div aria-owns="grandchild" aria-label="midchild"></div>
</div>
<div id="grandchild" aria-owns="container" aria-label="grandchild"></div>
<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>
</span>
</div>
</div>
</body>
</html>

View File

@ -200,6 +200,7 @@ public:
#endif
nsIContent* GetNextChild();
nsIContent* Parent() const { return mOriginalContent; }
private:
enum IteratorPhase