mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-12-02 01:48:05 +00:00
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:
parent
282b6c7b96
commit
fea219d6cd
@ -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();
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -109,9 +109,6 @@ NotificationController::ScheduleContentInsertion(Accessible* aContainer,
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// NotificationCollector: protected
|
||||
|
||||
void
|
||||
NotificationController::ScheduleProcessing()
|
||||
{
|
||||
@ -123,6 +120,9 @@ NotificationController::ScheduleProcessing()
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// NotificationCollector: protected
|
||||
|
||||
bool
|
||||
NotificationController::IsUpdatePending()
|
||||
{
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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++)
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -1,6 +1,7 @@
|
||||
[DEFAULT]
|
||||
|
||||
[test_ariadialog.html]
|
||||
[test_ariaowns.html]
|
||||
[test_bug852150.xhtml]
|
||||
[test_bug883708.xhtml]
|
||||
[test_bug884251.xhtml]
|
||||
|
234
accessible/tests/mochitest/treeupdate/test_ariaowns.html
Normal file
234
accessible/tests/mochitest/treeupdate/test_ariaowns.html
Normal 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>
|
@ -200,6 +200,7 @@ public:
|
||||
#endif
|
||||
|
||||
nsIContent* GetNextChild();
|
||||
nsIContent* Parent() const { return mOriginalContent; }
|
||||
|
||||
private:
|
||||
enum IteratorPhase
|
||||
|
Loading…
Reference in New Issue
Block a user