Autogenerated with:
cp dom/.clang-format accessible/ && mach format accessible/**.{cpp,h,mm}
Manual fixes:
* Atk individual includes need to become atk/atk.h because otherwise some symbols incorrectly get hidden visibility.
Differential Revision: https://phabricator.services.mozilla.com/D311692
947 lines
29 KiB
C++
947 lines
29 KiB
C++
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "Accessible.h"
|
|
|
|
#include "ARIAMap.h"
|
|
#include "Pivot.h"
|
|
#include "Relation.h"
|
|
#include "States.h"
|
|
#include "mozilla/BasicEvents.h"
|
|
#include "mozilla/Components.h"
|
|
#include "mozilla/ProfilerMarkers.h"
|
|
#include "mozilla/a11y/FocusManager.h"
|
|
#include "mozilla/a11y/HyperTextAccessibleBase.h"
|
|
#include "nsAccUtils.h"
|
|
#include "nsIStringBundle.h"
|
|
#include "nsIURI.h"
|
|
|
|
#ifdef A11Y_LOG
|
|
# include "nsAccessibilityService.h"
|
|
#endif
|
|
|
|
using namespace mozilla;
|
|
using namespace mozilla::a11y;
|
|
|
|
Accessible::Accessible()
|
|
: mType(eNoType),
|
|
mGenericTypes(static_cast<uint32_t>(0)),
|
|
mRoleMapEntryIndex(aria::NO_ROLE_MAP_ENTRY_INDEX) {}
|
|
|
|
Accessible::Accessible(AccType aType, AccGenericType aGenericTypes,
|
|
uint8_t aRoleMapEntryIndex)
|
|
: mType(aType),
|
|
mGenericTypes(static_cast<uint32_t>(aGenericTypes)),
|
|
mRoleMapEntryIndex(aRoleMapEntryIndex) {}
|
|
|
|
void Accessible::StaticAsserts() const {
|
|
static_assert(kHighestAccType <= (1 << kTypeBits) - 1,
|
|
"Accessible::mType was oversized by eLastAccType!");
|
|
static_assert(
|
|
eLastAccGenericType <= (1 << kGenericTypesBits) - 1,
|
|
"Accessible::mGenericType was oversized by eLastAccGenericType!");
|
|
}
|
|
|
|
mozilla::a11y::role Accessible::Role() const {
|
|
const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
|
|
mozilla::a11y::role r =
|
|
(!roleMapEntry || roleMapEntry->roleRule != kUseMapRole)
|
|
? NativeRole()
|
|
: roleMapEntry->role;
|
|
r = ARIATransformRole(r);
|
|
return GetMinimumRole(r);
|
|
}
|
|
|
|
bool Accessible::IsBefore(const Accessible* aAcc) const {
|
|
// Build the chain of parents.
|
|
const Accessible* thisP = this;
|
|
const Accessible* otherP = aAcc;
|
|
AutoTArray<const Accessible*, 30> thisParents, otherParents;
|
|
do {
|
|
thisParents.AppendElement(thisP);
|
|
thisP = thisP->Parent();
|
|
} while (thisP);
|
|
do {
|
|
otherParents.AppendElement(otherP);
|
|
otherP = otherP->Parent();
|
|
} while (otherP);
|
|
|
|
// Find where the parent chain differs.
|
|
uint32_t thisPos = thisParents.Length(), otherPos = otherParents.Length();
|
|
for (uint32_t len = std::min(thisPos, otherPos); len > 0; --len) {
|
|
const Accessible* thisChild = thisParents.ElementAt(--thisPos);
|
|
const Accessible* otherChild = otherParents.ElementAt(--otherPos);
|
|
if (thisChild != otherChild) {
|
|
return thisChild->IndexInParent() < otherChild->IndexInParent();
|
|
}
|
|
}
|
|
|
|
// If the ancestries are the same length (both thisPos and otherPos are 0),
|
|
// we should have returned by now.
|
|
MOZ_ASSERT(thisPos != 0 || otherPos != 0);
|
|
// At this point, one of the ancestries is a superset of the other, so one of
|
|
// thisPos or otherPos should be 0.
|
|
MOZ_ASSERT(thisPos != otherPos);
|
|
// If the other Accessible is deeper than this one (otherPos > 0), this
|
|
// Accessible comes before the other.
|
|
return otherPos > 0;
|
|
}
|
|
|
|
const Accessible* Accessible::GetClosestCommonInclusiveAncestor(
|
|
const Accessible* aAcc) const {
|
|
if (aAcc == this) {
|
|
return this;
|
|
}
|
|
|
|
// Build the chain of parents.
|
|
const Accessible* thisAnc = this;
|
|
const Accessible* otherAnc = aAcc;
|
|
AutoTArray<const Accessible*, 30> thisAncs, otherAncs;
|
|
do {
|
|
thisAncs.AppendElement(thisAnc);
|
|
thisAnc = thisAnc->Parent();
|
|
} while (thisAnc);
|
|
do {
|
|
otherAncs.AppendElement(otherAnc);
|
|
otherAnc = otherAnc->Parent();
|
|
} while (otherAnc);
|
|
|
|
// Find where the parent chain differs.
|
|
size_t thisPos = thisAncs.Length(), otherPos = otherAncs.Length();
|
|
const Accessible* common = nullptr;
|
|
for (size_t len = std::min(thisPos, otherPos); len > 0; --len) {
|
|
const Accessible* thisChild = thisAncs.ElementAt(--thisPos);
|
|
const Accessible* otherChild = otherAncs.ElementAt(--otherPos);
|
|
if (thisChild != otherChild) {
|
|
break;
|
|
}
|
|
common = thisChild;
|
|
}
|
|
return common;
|
|
}
|
|
|
|
Accessible* Accessible::FocusedChild() {
|
|
Accessible* doc = nsAccUtils::DocumentFor(this);
|
|
Accessible* child = doc->FocusedChild();
|
|
if (child && (child == this || child->Parent() == this)) {
|
|
return child;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
const nsRoleMapEntry* Accessible::ARIARoleMap() const {
|
|
return aria::GetRoleMapFromIndex(mRoleMapEntryIndex);
|
|
}
|
|
|
|
bool Accessible::HasARIARole() const {
|
|
return mRoleMapEntryIndex != aria::NO_ROLE_MAP_ENTRY_INDEX;
|
|
}
|
|
|
|
bool Accessible::IsARIARole(nsAtom* aARIARole) const {
|
|
const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
|
|
return roleMapEntry && roleMapEntry->Is(aARIARole);
|
|
}
|
|
|
|
bool Accessible::HasStrongARIARole() const {
|
|
const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
|
|
return roleMapEntry && roleMapEntry->roleRule == kUseMapRole;
|
|
}
|
|
|
|
role Accessible::GetMinimumRole(role aRole) const {
|
|
if (aRole != roles::TEXT && aRole != roles::TEXT_CONTAINER &&
|
|
aRole != roles::SECTION) {
|
|
// This isn't a generic role, so aRole is specific enough.
|
|
return aRole;
|
|
}
|
|
|
|
if (IsPopover()) {
|
|
return roles::GROUPING;
|
|
}
|
|
return aRole;
|
|
}
|
|
|
|
bool Accessible::HasGenericType(AccGenericType aType) const {
|
|
const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
|
|
return (mGenericTypes & aType) ||
|
|
(roleMapEntry && roleMapEntry->IsOfType(aType));
|
|
}
|
|
|
|
nsIntRect Accessible::BoundsInCSSPixels() const {
|
|
return BoundsInAppUnits().ToNearestPixels(AppUnitsPerCSSPixel());
|
|
}
|
|
|
|
LayoutDeviceIntSize Accessible::Size() const { return Bounds().Size(); }
|
|
|
|
LayoutDeviceIntPoint Accessible::Position(uint32_t aCoordType) {
|
|
LayoutDeviceIntPoint point = Bounds().TopLeft();
|
|
nsAccUtils::ConvertScreenCoordsTo(&point.x.value, &point.y.value, aCoordType,
|
|
this);
|
|
return point;
|
|
}
|
|
|
|
bool Accessible::IsTextRole() {
|
|
if (!IsHyperText()) {
|
|
return false;
|
|
}
|
|
|
|
const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
|
|
if (roleMapEntry && (roleMapEntry->role == roles::GRAPHIC ||
|
|
roleMapEntry->role == roles::IMAGE_MAP ||
|
|
roleMapEntry->role == roles::SLIDER ||
|
|
roleMapEntry->role == roles::PROGRESSBAR ||
|
|
roleMapEntry->role == roles::SEPARATOR ||
|
|
roleMapEntry->role == roles::METER)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Accessible::IsEditableRoot() const {
|
|
if (IsTextField()) {
|
|
// A text field is always an editable root.
|
|
return true;
|
|
}
|
|
|
|
const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
|
|
if (roleMapEntry && (roleMapEntry->role == roles::ENTRY ||
|
|
roleMapEntry->role == roles::SEARCHBOX)) {
|
|
// An aria text field is always an editable root.
|
|
return true;
|
|
}
|
|
|
|
if (!IsEditable()) {
|
|
return false;
|
|
}
|
|
|
|
if (IsDoc()) {
|
|
return true;
|
|
}
|
|
|
|
Accessible* parent = Parent();
|
|
if (parent && !parent->IsEditable()) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
uint32_t Accessible::StartOffset() {
|
|
MOZ_ASSERT(IsLink(), "StartOffset is called not on hyper link!");
|
|
Accessible* parent = Parent();
|
|
HyperTextAccessibleBase* hyperText =
|
|
parent ? parent->AsHyperTextBase() : nullptr;
|
|
return hyperText ? hyperText->GetChildOffset(this) : 0;
|
|
}
|
|
|
|
uint32_t Accessible::EndOffset() {
|
|
MOZ_ASSERT(IsLink(), "EndOffset is called on not hyper link!");
|
|
Accessible* parent = Parent();
|
|
HyperTextAccessibleBase* hyperText =
|
|
parent ? parent->AsHyperTextBase() : nullptr;
|
|
return hyperText ? (hyperText->GetChildOffset(this) + 1) : 0;
|
|
}
|
|
|
|
GroupPos Accessible::GroupPosition() {
|
|
GroupPos groupPos;
|
|
|
|
// Try aria-row/colcount/index.
|
|
if (IsTableRow()) {
|
|
Accessible* table = nsAccUtils::TableFor(this);
|
|
if (table) {
|
|
if (auto count = table->GetIntARIAAttr(nsGkAtoms::aria_rowcount)) {
|
|
if (*count >= 0) {
|
|
groupPos.setSize = *count;
|
|
}
|
|
}
|
|
}
|
|
if (auto index = GetIntARIAAttr(nsGkAtoms::aria_rowindex)) {
|
|
groupPos.posInSet = *index;
|
|
}
|
|
if (groupPos.setSize && groupPos.posInSet) {
|
|
return groupPos;
|
|
}
|
|
}
|
|
if (IsTableCell()) {
|
|
Accessible* table;
|
|
for (table = Parent(); table; table = table->Parent()) {
|
|
if (table->IsTable()) {
|
|
break;
|
|
}
|
|
}
|
|
if (table) {
|
|
if (auto count = table->GetIntARIAAttr(nsGkAtoms::aria_colcount)) {
|
|
if (*count >= 0) {
|
|
groupPos.setSize = *count;
|
|
}
|
|
}
|
|
}
|
|
if (auto index = GetIntARIAAttr(nsGkAtoms::aria_colindex)) {
|
|
groupPos.posInSet = *index;
|
|
}
|
|
if (groupPos.setSize && groupPos.posInSet) {
|
|
return groupPos;
|
|
}
|
|
}
|
|
|
|
// Get group position from ARIA attributes.
|
|
ARIAGroupPosition(&groupPos.level, &groupPos.setSize, &groupPos.posInSet);
|
|
|
|
// If ARIA is missed and the accessible is visible then calculate group
|
|
// position from hierarchy.
|
|
if (State() & states::INVISIBLE) return groupPos;
|
|
|
|
// Calculate group level if ARIA is missed.
|
|
if (groupPos.level == 0) {
|
|
groupPos.level = GetLevel(false);
|
|
}
|
|
|
|
// Calculate position in group and group size if ARIA is missed.
|
|
if (groupPos.posInSet == 0 || groupPos.setSize == 0) {
|
|
int32_t posInSet = 0, setSize = 0;
|
|
GetPositionAndSetSize(&posInSet, &setSize);
|
|
if (posInSet != 0 && setSize != 0) {
|
|
if (groupPos.posInSet == 0) groupPos.posInSet = posInSet;
|
|
|
|
if (groupPos.setSize == 0) groupPos.setSize = setSize;
|
|
}
|
|
}
|
|
|
|
return groupPos;
|
|
}
|
|
|
|
int32_t Accessible::GetLevel(bool aFast) const {
|
|
int32_t level = 0;
|
|
if (!Parent()) return level;
|
|
|
|
roles::Role role = Role();
|
|
if (role == roles::OUTLINEITEM) {
|
|
// Always expose 'level' attribute for 'outlineitem' accessible. The number
|
|
// of nested 'grouping' accessibles containing 'outlineitem' accessible is
|
|
// its level.
|
|
level = 1;
|
|
|
|
if (!aFast) {
|
|
const Accessible* parent = this;
|
|
while ((parent = parent->Parent()) && !parent->IsDoc()) {
|
|
roles::Role parentRole = parent->Role();
|
|
|
|
if (parentRole == roles::OUTLINE) break;
|
|
if (parentRole == roles::GROUPING) ++level;
|
|
}
|
|
}
|
|
} else if (role == roles::LISTITEM && !aFast) {
|
|
// Expose 'level' attribute on nested lists. We support two hierarchies:
|
|
// a) list -> listitem -> list -> listitem (nested list is a last child
|
|
// of listitem of the parent list);
|
|
// b) list -> listitem -> group -> listitem (nested listitems are contained
|
|
// by group that is a last child of the parent listitem).
|
|
|
|
// Calculate 'level' attribute based on number of parent listitems.
|
|
level = 0;
|
|
const Accessible* parent = this;
|
|
while ((parent = parent->Parent()) && !parent->IsDoc()) {
|
|
roles::Role parentRole = parent->Role();
|
|
|
|
if (parentRole == roles::LISTITEM) {
|
|
++level;
|
|
} else if (parentRole != roles::LIST && parentRole != roles::GROUPING) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (level == 0) {
|
|
// If this listitem is on top of nested lists then expose 'level'
|
|
// attribute.
|
|
parent = Parent();
|
|
uint32_t siblingCount = parent->ChildCount();
|
|
for (uint32_t siblingIdx = 0; siblingIdx < siblingCount; siblingIdx++) {
|
|
Accessible* sibling = parent->ChildAt(siblingIdx);
|
|
|
|
Accessible* siblingChild = sibling->LastChild();
|
|
if (siblingChild) {
|
|
roles::Role lastChildRole = siblingChild->Role();
|
|
if (lastChildRole == roles::LIST ||
|
|
lastChildRole == roles::GROUPING) {
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
++level; // level is 1-index based
|
|
}
|
|
} else if (role == roles::OPTION || role == roles::COMBOBOX_OPTION) {
|
|
if (const Accessible* parent = Parent()) {
|
|
if (parent->IsHTMLOptGroup()) {
|
|
return 2;
|
|
}
|
|
|
|
if (parent->IsListControl() && !parent->ARIARoleMap()) {
|
|
// This is for HTML selects only.
|
|
if (aFast) {
|
|
return 1;
|
|
}
|
|
|
|
for (uint32_t i = 0, count = parent->ChildCount(); i < count; ++i) {
|
|
if (parent->ChildAt(i)->IsHTMLOptGroup()) {
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (role == roles::HEADING) {
|
|
const uint8_t level = HeadingLevel();
|
|
if (level) {
|
|
MOZ_ASSERT(level > 0 && level <= 9);
|
|
return level;
|
|
}
|
|
|
|
const nsRoleMapEntry* ariaRole = this->ARIARoleMap();
|
|
if (ariaRole && ariaRole->Is(nsGkAtoms::heading)) {
|
|
// An aria heading with no aria level has a default level of 2.
|
|
return 2;
|
|
}
|
|
} else if (role == roles::COMMENT) {
|
|
// For comments, count the ancestor elements with the same role to get the
|
|
// level.
|
|
level = 1;
|
|
|
|
if (!aFast) {
|
|
const Accessible* parent = this;
|
|
while ((parent = parent->Parent()) && !parent->IsDoc()) {
|
|
roles::Role parentRole = parent->Role();
|
|
if (parentRole == roles::COMMENT) {
|
|
++level;
|
|
}
|
|
}
|
|
}
|
|
} else if (role == roles::ROW) {
|
|
// It is a row inside flatten treegrid. Group level is always 1 until it
|
|
// is overriden by aria-level attribute.
|
|
const Accessible* parent = Parent();
|
|
if (parent->Role() == roles::TREE_TABLE) {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
return level;
|
|
}
|
|
|
|
void Accessible::GetPositionAndSetSize(int32_t* aPosInSet, int32_t* aSetSize) {
|
|
auto groupInfo = GetOrCreateGroupInfo();
|
|
if (groupInfo) {
|
|
*aPosInSet = groupInfo->PosInSet();
|
|
*aSetSize = groupInfo->SetSize();
|
|
}
|
|
}
|
|
|
|
bool Accessible::IsLinkValid() {
|
|
MOZ_ASSERT(IsLink(), "IsLinkValid is called on not hyper link!");
|
|
|
|
// XXX In order to implement this we would need to follow every link
|
|
// Perhaps we can get information about invalid links from the cache
|
|
// In the mean time authors can use role="link" aria-invalid="true"
|
|
// to force it for links they internally know to be invalid
|
|
return (0 == (State() & mozilla::a11y::states::INVALID));
|
|
}
|
|
|
|
uint32_t Accessible::AnchorCount() {
|
|
if (IsImageMap()) {
|
|
return ChildCount();
|
|
}
|
|
|
|
MOZ_ASSERT(IsLink(), "AnchorCount is called on not hyper link!");
|
|
return 1;
|
|
}
|
|
|
|
Accessible* Accessible::AnchorAt(uint32_t aAnchorIndex) const {
|
|
if (IsImageMap()) {
|
|
return ChildAt(aAnchorIndex);
|
|
}
|
|
|
|
MOZ_ASSERT(IsLink(), "GetAnchor is called on not hyper link!");
|
|
return aAnchorIndex == 0 ? const_cast<Accessible*>(this) : nullptr;
|
|
}
|
|
|
|
already_AddRefed<nsIURI> Accessible::AnchorURIAt(uint32_t aAnchorIndex) const {
|
|
Accessible* anchor = nullptr;
|
|
|
|
if (IsTextLeaf() || IsImage()) {
|
|
for (Accessible* parent = Parent(); parent && !parent->IsOuterDoc();
|
|
parent = parent->Parent()) {
|
|
if (parent->IsLink()) {
|
|
anchor = parent->AnchorAt(aAnchorIndex);
|
|
}
|
|
}
|
|
} else {
|
|
anchor = AnchorAt(aAnchorIndex);
|
|
}
|
|
|
|
if (anchor) {
|
|
RefPtr<nsIURI> uri;
|
|
nsAutoString spec;
|
|
anchor->Value(spec);
|
|
nsresult rv = NS_NewURI(getter_AddRefs(uri), spec);
|
|
if (NS_SUCCEEDED(rv)) {
|
|
return uri.forget();
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
#ifdef A11Y_LOG
|
|
void Accessible::DebugDescription(nsCString& aDesc) const {
|
|
aDesc.Truncate();
|
|
aDesc.AppendPrintf("%s", IsRemote() ? "Remote" : "Local");
|
|
aDesc.AppendPrintf("[%p] ", this);
|
|
nsAutoString role;
|
|
GetAccService()->GetStringRole(Role(), role);
|
|
aDesc.Append(NS_ConvertUTF16toUTF8(role));
|
|
|
|
if (nsAtom* tagAtom = TagName()) {
|
|
nsAutoCString tag;
|
|
tagAtom->ToUTF8String(tag);
|
|
aDesc.AppendPrintf(" %s", tag.get());
|
|
|
|
nsAutoString id;
|
|
DOMNodeID(id);
|
|
if (!id.IsEmpty()) {
|
|
aDesc.Append("#");
|
|
aDesc.Append(NS_ConvertUTF16toUTF8(id));
|
|
}
|
|
}
|
|
nsAutoString id;
|
|
|
|
nsAutoString name;
|
|
Name(name);
|
|
if (!name.IsEmpty()) {
|
|
aDesc.Append(" '");
|
|
aDesc.Append(NS_ConvertUTF16toUTF8(name));
|
|
aDesc.Append("'");
|
|
}
|
|
}
|
|
|
|
void Accessible::DebugPrint(const char* aPrefix,
|
|
const Accessible* aAccessible) {
|
|
nsAutoCString desc;
|
|
if (aAccessible) {
|
|
aAccessible->DebugDescription(desc);
|
|
} else {
|
|
desc.AssignLiteral("[null]");
|
|
}
|
|
# if defined(ANDROID) || defined(MOZ_WIDGET_UIKIT)
|
|
printf_stderr("%s %s\n", aPrefix, desc.get());
|
|
# else
|
|
printf("%s %s\n", aPrefix, desc.get());
|
|
# endif
|
|
}
|
|
|
|
#endif
|
|
|
|
void Accessible::TranslateString(const nsString& aKey, nsAString& aStringOut,
|
|
const nsTArray<nsString>& aParams) {
|
|
nsCOMPtr<nsIStringBundleService> stringBundleService =
|
|
components::StringBundle::Service();
|
|
if (!stringBundleService) return;
|
|
|
|
nsCOMPtr<nsIStringBundle> stringBundle;
|
|
stringBundleService->CreateBundle(
|
|
"chrome://global-platform/locale/accessible.properties",
|
|
getter_AddRefs(stringBundle));
|
|
if (!stringBundle) return;
|
|
|
|
nsAutoString xsValue;
|
|
nsresult rv = NS_OK;
|
|
if (aParams.IsEmpty()) {
|
|
rv = stringBundle->GetStringFromName(NS_ConvertUTF16toUTF8(aKey).get(),
|
|
xsValue);
|
|
} else {
|
|
rv = stringBundle->FormatStringFromName(NS_ConvertUTF16toUTF8(aKey).get(),
|
|
aParams, xsValue);
|
|
}
|
|
if (NS_SUCCEEDED(rv)) aStringOut.Assign(xsValue);
|
|
}
|
|
|
|
const Accessible* Accessible::ActionAncestor() const {
|
|
// We do want to consider a click handler on the document. However, we don't
|
|
// want to walk outside of this document, so we stop if we see an OuterDoc.
|
|
for (Accessible* parent = Parent(); parent && !parent->IsOuterDoc();
|
|
parent = parent->Parent()) {
|
|
if (parent->HasPrimaryAction()) {
|
|
return parent;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
nsStaticAtom* Accessible::LandmarkRole() const {
|
|
// For certain cases below (e.g. ARIA region, HTML <header>), whether it is
|
|
// actually a landmark is conditional. Rather than duplicating that
|
|
// conditional logic here, we check the Gecko role.
|
|
if (const nsRoleMapEntry* roleMapEntry = ARIARoleMap()) {
|
|
// Explicit ARIA role should take precedence.
|
|
if (roleMapEntry->Is(nsGkAtoms::region)) {
|
|
if (Role() == roles::REGION) {
|
|
return nsGkAtoms::region;
|
|
}
|
|
} else if (roleMapEntry->Is(nsGkAtoms::form)) {
|
|
if (Role() == roles::FORM) {
|
|
return nsGkAtoms::form;
|
|
}
|
|
} else if (roleMapEntry->IsOfType(eLandmark)) {
|
|
return roleMapEntry->roleAtom;
|
|
}
|
|
}
|
|
|
|
nsAtom* tagName = TagName();
|
|
if (!tagName) {
|
|
// Either no associated content, or no cache.
|
|
return nullptr;
|
|
}
|
|
|
|
if (tagName == nsGkAtoms::nav) {
|
|
return nsGkAtoms::navigation;
|
|
}
|
|
|
|
if (tagName == nsGkAtoms::aside) {
|
|
return nsGkAtoms::complementary;
|
|
}
|
|
|
|
if (tagName == nsGkAtoms::main) {
|
|
return nsGkAtoms::main;
|
|
}
|
|
|
|
if (tagName == nsGkAtoms::header) {
|
|
if (Role() == roles::LANDMARK) {
|
|
return nsGkAtoms::banner;
|
|
}
|
|
}
|
|
|
|
if (tagName == nsGkAtoms::footer) {
|
|
if (Role() == roles::LANDMARK) {
|
|
return nsGkAtoms::contentinfo;
|
|
}
|
|
}
|
|
|
|
if (tagName == nsGkAtoms::section) {
|
|
if (Role() == roles::REGION) {
|
|
return nsGkAtoms::region;
|
|
}
|
|
}
|
|
|
|
if (tagName == nsGkAtoms::form) {
|
|
if (Role() == roles::FORM_LANDMARK) {
|
|
return nsGkAtoms::form;
|
|
}
|
|
}
|
|
|
|
if (tagName == nsGkAtoms::search) {
|
|
return nsGkAtoms::search;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
nsStaticAtom* Accessible::ComputedARIARole() const {
|
|
const nsRoleMapEntry* roleMap = ARIARoleMap();
|
|
if (roleMap && roleMap->IsOfType(eDPub)) {
|
|
return roleMap->roleAtom;
|
|
}
|
|
if (roleMap && roleMap->roleAtom != nsGkAtoms::_empty &&
|
|
// region and form have their own Gecko roles and need to be handled
|
|
// specially.
|
|
roleMap->roleAtom != nsGkAtoms::region &&
|
|
roleMap->roleAtom != nsGkAtoms::form &&
|
|
(roleMap->roleRule == kUseNativeRole || roleMap->IsOfType(eLandmark) ||
|
|
roleMap->roleAtom == nsGkAtoms::alertdialog ||
|
|
roleMap->roleAtom == nsGkAtoms::feed)) {
|
|
// Explicit ARIA role (e.g. specified via the role attribute) which does not
|
|
// map to a unique Gecko role.
|
|
return roleMap->roleAtom;
|
|
}
|
|
role geckoRole = Role();
|
|
if (geckoRole == roles::LANDMARK) {
|
|
// Landmark role from native markup; e.g. <main>, <nav>.
|
|
return LandmarkRole();
|
|
}
|
|
// Role from native markup or layout.
|
|
#define ROLE(_geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
|
|
msaaRole, ia2Role, androidClass, iosIsElement, uiaControlType, \
|
|
nameRule) \
|
|
case roles::_geckoRole: \
|
|
return ariaRole;
|
|
switch (geckoRole) {
|
|
#include "RoleMap.inc"
|
|
}
|
|
#undef ROLE
|
|
MOZ_ASSERT_UNREACHABLE("Unknown role");
|
|
return nullptr;
|
|
}
|
|
|
|
void Accessible::ApplyImplicitState(uint64_t& aState) const {
|
|
// nsAccessibilityService (and thus FocusManager) can be shut down before
|
|
// RemoteAccessibles.
|
|
if (const auto* focusMgr = FocusMgr()) {
|
|
if (focusMgr->IsFocused(this)) {
|
|
aState |= states::FOCUSED;
|
|
}
|
|
}
|
|
|
|
// If this is an option, tab or treeitem and if it's focused and not marked
|
|
// unselected explicitly (i.e. aria-selected="false") then expose it as
|
|
// selected to make ARIA widget authors life easier.
|
|
const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
|
|
if (roleMapEntry &&
|
|
(roleMapEntry->Is(nsGkAtoms::option) ||
|
|
roleMapEntry->Is(nsGkAtoms::tab) ||
|
|
roleMapEntry->Is(nsGkAtoms::treeitem)) &&
|
|
!(aState & states::SELECTED) && ARIASelected().valueOr(true)) {
|
|
if (roleMapEntry->role == roles::PAGETAB && !(aState & states::FOCUSED)) {
|
|
// If focus is within the tab panel, this should mean the tab is selected.
|
|
// Note that we handle focus on the tab itself below.
|
|
Relation rel = RelationByType(RelationType::LABEL_FOR);
|
|
Accessible* relTarget = nullptr;
|
|
while ((relTarget = rel.Next())) {
|
|
if (relTarget->Role() == roles::PROPERTYPAGE &&
|
|
FocusMgr()->IsFocusWithin(relTarget)) {
|
|
aState |= states::SELECTED;
|
|
}
|
|
}
|
|
} else if (aState & states::FOCUSED) {
|
|
Accessible* container = nsAccUtils::GetSelectableContainer(this, aState);
|
|
AUTO_PROFILER_MARKER_TEXT(
|
|
"Accessible::ApplyImplicitState::ImplicitSelection", A11Y, {}, ""_ns);
|
|
auto HasExplicitSelection = [](Accessible* aAcc) {
|
|
Pivot p = Pivot(aAcc);
|
|
PivotARIASelectedRule rule;
|
|
return p.First(rule) != nullptr;
|
|
};
|
|
|
|
if (container && !(container->State() & states::MULTISELECTABLE) &&
|
|
!HasExplicitSelection(container)) {
|
|
aState |= states::SELECTED;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Opacity() == 1.0f && !(aState & states::INVISIBLE)) {
|
|
aState |= states::OPAQUE1;
|
|
}
|
|
|
|
if (aState & states::EXPANDABLE && !(aState & states::EXPANDED)) {
|
|
aState |= states::COLLAPSED;
|
|
}
|
|
|
|
if (!(aState & states::UNAVAILABLE)) {
|
|
aState |= states::ENABLED | states::SENSITIVE;
|
|
}
|
|
|
|
if (aState & states::FOCUSABLE && !(aState & states::UNAVAILABLE)) {
|
|
// Propagate UNAVAILABLE state from ancestors down to any focusable
|
|
// descendant.
|
|
for (auto ancestor = Parent(); ancestor; ancestor = ancestor->Parent()) {
|
|
if (ancestor->IsDoc() || ancestor->IsOuterDoc()) {
|
|
break;
|
|
}
|
|
|
|
if (ancestor->State() & states::UNAVAILABLE) {
|
|
aState |= states::UNAVAILABLE;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Accessible::NameIsEmpty() const {
|
|
nsAutoString name;
|
|
Name(name);
|
|
return name.IsEmpty();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// KeyBinding class
|
|
|
|
// static
|
|
uint32_t KeyBinding::AccelModifier() {
|
|
switch (WidgetInputEvent::AccelModifier()) {
|
|
case MODIFIER_ALT:
|
|
return kAlt;
|
|
case MODIFIER_CONTROL:
|
|
return kControl;
|
|
case MODIFIER_META:
|
|
return kMeta;
|
|
default:
|
|
MOZ_CRASH("Handle the new result of WidgetInputEvent::AccelModifier()");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
void KeyBinding::ToPlatformFormat(nsAString& aValue) const {
|
|
nsCOMPtr<nsIStringBundle> keyStringBundle;
|
|
nsCOMPtr<nsIStringBundleService> stringBundleService =
|
|
mozilla::components::StringBundle::Service();
|
|
if (stringBundleService) {
|
|
stringBundleService->CreateBundle(
|
|
"chrome://global-platform/locale/platformKeys.properties",
|
|
getter_AddRefs(keyStringBundle));
|
|
}
|
|
|
|
if (!keyStringBundle) return;
|
|
|
|
nsAutoString separator;
|
|
keyStringBundle->GetStringFromName("MODIFIER_SEPARATOR", separator);
|
|
|
|
nsAutoString modifierName;
|
|
if (mModifierMask & kControl) {
|
|
keyStringBundle->GetStringFromName("VK_CONTROL", modifierName);
|
|
|
|
aValue.Append(modifierName);
|
|
aValue.Append(separator);
|
|
}
|
|
|
|
if (mModifierMask & kAlt) {
|
|
keyStringBundle->GetStringFromName("VK_ALT", modifierName);
|
|
|
|
aValue.Append(modifierName);
|
|
aValue.Append(separator);
|
|
}
|
|
|
|
if (mModifierMask & kShift) {
|
|
keyStringBundle->GetStringFromName("VK_SHIFT", modifierName);
|
|
|
|
aValue.Append(modifierName);
|
|
aValue.Append(separator);
|
|
}
|
|
|
|
if (mModifierMask & kMeta) {
|
|
keyStringBundle->GetStringFromName("VK_META", modifierName);
|
|
|
|
aValue.Append(modifierName);
|
|
aValue.Append(separator);
|
|
}
|
|
|
|
aValue.Append(mKey);
|
|
}
|
|
|
|
void KeyBinding::ToAtkFormat(nsAString& aValue) const {
|
|
nsAutoString modifierName;
|
|
if (mModifierMask & kControl) aValue.AppendLiteral("<Control>");
|
|
|
|
if (mModifierMask & kAlt) aValue.AppendLiteral("<Alt>");
|
|
|
|
if (mModifierMask & kShift) aValue.AppendLiteral("<Shift>");
|
|
|
|
if (mModifierMask & kMeta) aValue.AppendLiteral("<Meta>");
|
|
|
|
aValue.Append(mKey);
|
|
}
|
|
|
|
role Accessible::FindNextValidARIARole(
|
|
std::initializer_list<nsStaticAtom*> aRolesToSkip) const {
|
|
const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
|
|
if (roleMapEntry) {
|
|
if (!ARIAAttrValueIs(nsGkAtoms::role, roleMapEntry->roleAtom)) {
|
|
nsAutoString roles;
|
|
GetStringARIAAttr(nsGkAtoms::role, roles);
|
|
// Get the next valid token that isn't in the list of roles to skip.
|
|
uint8_t roleMapIndex =
|
|
aria::GetFirstValidRoleMapIndexExcluding(roles, aRolesToSkip);
|
|
// If we don't find a valid token, fall back to the minimum role.
|
|
if (roleMapIndex == aria::NO_ROLE_MAP_ENTRY_INDEX ||
|
|
roleMapIndex == aria::LANDMARK_ROLE_MAP_ENTRY_INDEX) {
|
|
return NativeRole();
|
|
}
|
|
const nsRoleMapEntry* fallbackRoleMapEntry =
|
|
aria::GetRoleMapFromIndex(roleMapIndex);
|
|
if (!fallbackRoleMapEntry) {
|
|
return NativeRole();
|
|
}
|
|
// Return the next valid role, but validate that first, too.
|
|
return ARIATransformRole(fallbackRoleMapEntry->role);
|
|
}
|
|
}
|
|
// Fall back to the minimum role.
|
|
return NativeRole();
|
|
}
|
|
|
|
role Accessible::ARIATransformRole(role aRole) const {
|
|
// Beginning with ARIA 1.1, user agents are expected to use the native host
|
|
// language role of the element when the form or region roles are used without
|
|
// a name. Says the spec, "the user agent MUST treat such elements as if no
|
|
// role had been provided."
|
|
// https://w3c.github.io/aria/#document-handling_author-errors_roles
|
|
//
|
|
// XXX: While the name computation algorithm can be non-trivial in the general
|
|
// case, it should not be especially bad here: If the author hasn't used the
|
|
// region role, this calculation won't occur. And the region role's name
|
|
// calculation rule excludes name from content. That said, this use case is
|
|
// another example of why we should consider caching the accessible name. See:
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1378235.
|
|
if (aRole == roles::REGION || aRole == roles::FORM) {
|
|
if (NameIsEmpty()) {
|
|
// If we have a "form" or "region" role, but no accessible name, we need
|
|
// to search for the next valid role. First, we search through the role
|
|
// attribute value string - there might be a valid fallback there. Skip
|
|
// all "form" or "region" attributes; we know they're not valid since
|
|
// there's no accessible name. If we find a valid role that's not "form"
|
|
// or "region", fall back to it (but run it through ARIATransformRole
|
|
// first). Otherwise, fall back to the element's native role.
|
|
return FindNextValidARIARole({nsGkAtoms::region, nsGkAtoms::form});
|
|
}
|
|
return aRole;
|
|
}
|
|
|
|
// XXX: these unfortunate exceptions don't fit into the ARIA table. This is
|
|
// where the accessible role depends on both the role and ARIA state.
|
|
if (aRole == roles::PUSHBUTTON) {
|
|
if (HasARIAAttr(nsGkAtoms::aria_pressed)) {
|
|
// For simplicity, any existing pressed attribute except "" or "undefined"
|
|
// indicates a toggle.
|
|
return roles::TOGGLE_BUTTON;
|
|
}
|
|
|
|
if (ARIAAttrValueIs(nsGkAtoms::aria_haspopup, nsGkAtoms::_true)) {
|
|
// For button with aria-haspopup="true".
|
|
return roles::BUTTONMENU;
|
|
}
|
|
|
|
} else if (aRole == roles::LISTBOX) {
|
|
// A listbox inside of a combobox needs a special role because of ATK
|
|
// mapping to menu.
|
|
if (Parent() && Parent()->IsCombobox()) {
|
|
return roles::COMBOBOX_LIST;
|
|
}
|
|
|
|
} else if (aRole == roles::OPTION) {
|
|
const Accessible* listbox = FindAncestorIf([](const Accessible& aAcc) {
|
|
const role accRole = aAcc.Role();
|
|
return (accRole == roles::LISTBOX || accRole == roles::COMBOBOX_LIST)
|
|
? AncestorSearchOption::Found
|
|
: accRole == roles::GROUPING ? AncestorSearchOption::Continue
|
|
: AncestorSearchOption::NotFound;
|
|
});
|
|
if (listbox && listbox->Role() == roles::COMBOBOX_LIST) {
|
|
return roles::COMBOBOX_OPTION;
|
|
}
|
|
} else if (aRole == roles::MENUITEM) {
|
|
// Menuitem has a submenu.
|
|
if (ARIAAttrValueIs(nsGkAtoms::aria_haspopup, nsGkAtoms::_true)) {
|
|
return roles::PARENT_MENUITEM;
|
|
}
|
|
} else if (aRole == roles::CELL) {
|
|
// A cell inside an ancestor table element that has a grid role needs a
|
|
// gridcell role
|
|
// (https://www.w3.org/TR/html-aam-1.0/#html-element-role-mappings).
|
|
const Accessible* table = nsAccUtils::TableFor(this);
|
|
if (table && table->IsARIARole(nsGkAtoms::grid)) {
|
|
return roles::GRID_CELL;
|
|
}
|
|
}
|
|
|
|
return aRole;
|
|
}
|