1. Return this role as appropriate in HTMLTextFieldAccessible and update ARIAMap. 2. Remove Accessible::IsSearchbox, which is no longer required. 3. Remove Mac mozTextAccessible moxSubrole override. This was only used for password inputs and searchboxes, but we can now specify these subroles in the RoleMap instead. 4. Tweak the iOS code for the removal of IsSearchbox. Differential Revision: https://phabricator.services.mozilla.com/D233230
498 lines
15 KiB
Plaintext
498 lines
15 KiB
Plaintext
/* clang-format off */
|
|
/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* clang-format on */
|
|
/* 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/. */
|
|
|
|
#import "MUIAccessible.h"
|
|
|
|
#include "nsString.h"
|
|
#include "RootAccessibleWrap.h"
|
|
|
|
using namespace mozilla;
|
|
using namespace mozilla::a11y;
|
|
|
|
#ifdef A11Y_LOG
|
|
# define DEBUG_HINTS
|
|
#endif
|
|
|
|
#ifdef DEBUG_HINTS
|
|
static NSString* ToNSString(const nsACString& aCString) {
|
|
if (aCString.IsEmpty()) {
|
|
return [NSString string];
|
|
}
|
|
return [[[NSString alloc] initWithBytes:aCString.BeginReading()
|
|
length:aCString.Length()
|
|
encoding:NSUTF8StringEncoding] autorelease];
|
|
}
|
|
#endif
|
|
|
|
static NSString* ToNSString(const nsAString& aString) {
|
|
if (aString.IsEmpty()) {
|
|
return [NSString string];
|
|
}
|
|
return [NSString stringWithCharacters:reinterpret_cast<const unichar*>(
|
|
aString.BeginReading())
|
|
length:aString.Length()];
|
|
}
|
|
|
|
// These rules offer conditions for whether a gecko accessible
|
|
// should be considered a UIKit accessibility element. Each role is mapped to a
|
|
// rule.
|
|
enum class IsAccessibilityElementRule {
|
|
// Always yes
|
|
Yes,
|
|
// Always no
|
|
No,
|
|
// If the accessible has no children. For example an empty header
|
|
// which is labeled.
|
|
IfChildless,
|
|
// If the accessible has no children and it is named and focusable.
|
|
IfChildlessWithNameAndFocusable,
|
|
// If this accessible isn't a child of an accessibility element. For example,
|
|
// a text leaf child of a button.
|
|
IfParentIsntElementWithName,
|
|
// If this accessible has multiple leafs that should functionally be
|
|
// united, for example a link with span elements.
|
|
IfBrokenUp,
|
|
};
|
|
|
|
class Trait {
|
|
public:
|
|
static const uint64_t None = 0;
|
|
static const uint64_t Button = ((uint64_t)0x1) << 0;
|
|
static const uint64_t Link = ((uint64_t)0x1) << 1;
|
|
static const uint64_t Image = ((uint64_t)0x1) << 2;
|
|
static const uint64_t Selected = ((uint64_t)0x1) << 3;
|
|
static const uint64_t PlaysSound = ((uint64_t)0x1) << 4;
|
|
static const uint64_t KeyboardKey = ((uint64_t)0x1) << 5;
|
|
static const uint64_t StaticText = ((uint64_t)0x1) << 6;
|
|
static const uint64_t SummaryElement = ((uint64_t)0x1) << 7;
|
|
static const uint64_t NotEnabled = ((uint64_t)0x1) << 8;
|
|
static const uint64_t UpdatesFrequently = ((uint64_t)0x1) << 9;
|
|
static const uint64_t SearchField = ((uint64_t)0x1) << 10;
|
|
static const uint64_t StartsMediaSession = ((uint64_t)0x1) << 11;
|
|
static const uint64_t Adjustable = ((uint64_t)0x1) << 12;
|
|
static const uint64_t AllowsDirectInteraction = ((uint64_t)0x1) << 13;
|
|
static const uint64_t CausesPageTurn = ((uint64_t)0x1) << 14;
|
|
static const uint64_t TabBar = ((uint64_t)0x1) << 15;
|
|
static const uint64_t Header = ((uint64_t)0x1) << 16;
|
|
static const uint64_t WebContent = ((uint64_t)0x1) << 17;
|
|
static const uint64_t TextEntry = ((uint64_t)0x1) << 18;
|
|
static const uint64_t PickerElement = ((uint64_t)0x1) << 19;
|
|
static const uint64_t RadioButton = ((uint64_t)0x1) << 20;
|
|
static const uint64_t IsEditing = ((uint64_t)0x1) << 21;
|
|
static const uint64_t LaunchIcon = ((uint64_t)0x1) << 22;
|
|
static const uint64_t StatusBarElement = ((uint64_t)0x1) << 23;
|
|
static const uint64_t SecureTextField = ((uint64_t)0x1) << 24;
|
|
static const uint64_t Inactive = ((uint64_t)0x1) << 25;
|
|
static const uint64_t Footer = ((uint64_t)0x1) << 26;
|
|
static const uint64_t BackButton = ((uint64_t)0x1) << 27;
|
|
static const uint64_t TabButton = ((uint64_t)0x1) << 28;
|
|
static const uint64_t AutoCorrectCandidate = ((uint64_t)0x1) << 29;
|
|
static const uint64_t DeleteKey = ((uint64_t)0x1) << 30;
|
|
static const uint64_t SelectionDismissesItem = ((uint64_t)0x1) << 31;
|
|
static const uint64_t Visited = ((uint64_t)0x1) << 32;
|
|
static const uint64_t Scrollable = ((uint64_t)0x1) << 33;
|
|
static const uint64_t Spacer = ((uint64_t)0x1) << 34;
|
|
static const uint64_t TableIndex = ((uint64_t)0x1) << 35;
|
|
static const uint64_t Map = ((uint64_t)0x1) << 36;
|
|
static const uint64_t TextOperationsAvailable = ((uint64_t)0x1) << 37;
|
|
static const uint64_t Draggable = ((uint64_t)0x1) << 38;
|
|
static const uint64_t GesturePracticeRegion = ((uint64_t)0x1) << 39;
|
|
static const uint64_t PopupButton = ((uint64_t)0x1) << 40;
|
|
static const uint64_t AllowsNativeSliding = ((uint64_t)0x1) << 41;
|
|
static const uint64_t MathEquation = ((uint64_t)0x1) << 42;
|
|
static const uint64_t ContainedByTable = ((uint64_t)0x1) << 43;
|
|
static const uint64_t ContainedByList = ((uint64_t)0x1) << 44;
|
|
static const uint64_t TouchContainer = ((uint64_t)0x1) << 45;
|
|
static const uint64_t SupportsZoom = ((uint64_t)0x1) << 46;
|
|
static const uint64_t TextArea = ((uint64_t)0x1) << 47;
|
|
static const uint64_t BookContent = ((uint64_t)0x1) << 48;
|
|
static const uint64_t ContainedByLandmark = ((uint64_t)0x1) << 49;
|
|
static const uint64_t FolderIcon = ((uint64_t)0x1) << 50;
|
|
static const uint64_t ReadOnly = ((uint64_t)0x1) << 51;
|
|
static const uint64_t MenuItem = ((uint64_t)0x1) << 52;
|
|
static const uint64_t Toggle = ((uint64_t)0x1) << 53;
|
|
static const uint64_t IgnoreItemChooser = ((uint64_t)0x1) << 54;
|
|
static const uint64_t SupportsTrackingDetail = ((uint64_t)0x1) << 55;
|
|
static const uint64_t Alert = ((uint64_t)0x1) << 56;
|
|
static const uint64_t ContainedByFieldset = ((uint64_t)0x1) << 57;
|
|
static const uint64_t AllowsLayoutChangeInStatusBar = ((uint64_t)0x1) << 58;
|
|
};
|
|
|
|
#pragma mark -
|
|
|
|
@interface NSObject (AccessibilityPrivate)
|
|
- (void)_accessibilityUnregister;
|
|
@end
|
|
|
|
@implementation MUIAccessible
|
|
|
|
- (id)initWithAccessible:(Accessible*)aAcc {
|
|
MOZ_ASSERT(aAcc, "Cannot init MUIAccessible with null");
|
|
if ((self = [super init])) {
|
|
mGeckoAccessible = aAcc;
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (mozilla::a11y::Accessible*)geckoAccessible {
|
|
return mGeckoAccessible;
|
|
}
|
|
|
|
- (void)expire {
|
|
mGeckoAccessible = nullptr;
|
|
if ([self respondsToSelector:@selector(_accessibilityUnregister)]) {
|
|
[self _accessibilityUnregister];
|
|
}
|
|
}
|
|
|
|
- (void)dealloc {
|
|
[super dealloc];
|
|
}
|
|
|
|
static bool isAccessibilityElementInternal(Accessible* aAccessible) {
|
|
MOZ_ASSERT(aAccessible);
|
|
IsAccessibilityElementRule rule = IsAccessibilityElementRule::No;
|
|
|
|
#define ROLE(_geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
|
|
msaaRole, ia2Role, androidClass, iosIsElement, uiaControlType, \
|
|
nameRule) \
|
|
case roles::_geckoRole: \
|
|
rule = iosIsElement; \
|
|
break;
|
|
switch (aAccessible->Role()) {
|
|
#include "RoleMap.h"
|
|
}
|
|
|
|
switch (rule) {
|
|
case IsAccessibilityElementRule::Yes:
|
|
return true;
|
|
case IsAccessibilityElementRule::No:
|
|
return false;
|
|
case IsAccessibilityElementRule::IfChildless:
|
|
return aAccessible->ChildCount() == 0;
|
|
case IsAccessibilityElementRule::IfParentIsntElementWithName: {
|
|
nsAutoString name;
|
|
aAccessible->Name(name);
|
|
name.CompressWhitespace();
|
|
if (name.IsEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
if (isAccessibilityElementInternal(aAccessible->Parent())) {
|
|
// This is a text leaf that needs to be pruned from a button or the
|
|
// likes. It should also be ignored in the event of its parent being a
|
|
// pruned link.
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
case IsAccessibilityElementRule::IfChildlessWithNameAndFocusable:
|
|
if (aAccessible->ChildCount() == 0 &&
|
|
(aAccessible->State() & states::FOCUSABLE)) {
|
|
nsAutoString name;
|
|
aAccessible->Name(name);
|
|
name.CompressWhitespace();
|
|
return !name.IsEmpty();
|
|
}
|
|
return false;
|
|
case IsAccessibilityElementRule::IfBrokenUp: {
|
|
uint32_t childCount = aAccessible->ChildCount();
|
|
if (childCount == 1) {
|
|
// If this is a single child container just use the text leaf and its
|
|
// traits will be inherited.
|
|
return false;
|
|
}
|
|
|
|
for (uint32_t idx = 0; idx < childCount; idx++) {
|
|
Accessible* child = aAccessible->ChildAt(idx);
|
|
role accRole = child->Role();
|
|
if (accRole != roles::STATICTEXT && accRole != roles::TEXT_LEAF &&
|
|
accRole != roles::GRAPHIC) {
|
|
// If this container contains anything but text leafs and images
|
|
// ignore this accessible. Its descendants will inherit the
|
|
// container's traits.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
MOZ_ASSERT_UNREACHABLE("Unhandled IsAccessibilityElementRule");
|
|
|
|
return false;
|
|
}
|
|
|
|
- (BOOL)isAccessibilityElement {
|
|
if (!mGeckoAccessible) {
|
|
return NO;
|
|
}
|
|
|
|
return isAccessibilityElementInternal(mGeckoAccessible) ? YES : NO;
|
|
}
|
|
|
|
- (NSString*)accessibilityLabel {
|
|
if (!mGeckoAccessible) {
|
|
return @"";
|
|
}
|
|
|
|
nsAutoString name;
|
|
mGeckoAccessible->Name(name);
|
|
|
|
return ToNSString(name);
|
|
}
|
|
|
|
- (NSString*)accessibilityHint {
|
|
if (!mGeckoAccessible) {
|
|
return @"";
|
|
}
|
|
|
|
#ifdef DEBUG_HINTS
|
|
// Just put in a debug description as the label so we get a clue about which
|
|
// accessible ends up where.
|
|
nsAutoCString desc;
|
|
mGeckoAccessible->DebugDescription(desc);
|
|
return ToNSString(desc);
|
|
#else
|
|
return @"";
|
|
#endif
|
|
}
|
|
|
|
- (CGRect)accessibilityFrame {
|
|
RootAccessibleWrap* rootAcc = static_cast<RootAccessibleWrap*>(
|
|
mGeckoAccessible->IsLocal()
|
|
? mGeckoAccessible->AsLocal()->RootAccessible()
|
|
: mGeckoAccessible->AsRemote()
|
|
->OuterDocOfRemoteBrowser()
|
|
->RootAccessible());
|
|
|
|
if (!rootAcc) {
|
|
return CGRectMake(0, 0, 0, 0);
|
|
}
|
|
|
|
LayoutDeviceIntRect rect = mGeckoAccessible->Bounds();
|
|
return rootAcc->DevPixelsRectToUIKit(rect);
|
|
}
|
|
|
|
- (NSString*)accessibilityValue {
|
|
if (!mGeckoAccessible) {
|
|
return nil;
|
|
}
|
|
|
|
uint64_t state = mGeckoAccessible->State();
|
|
if (state & states::LINKED) {
|
|
// Value returns the URL. We don't want to expose that as the value on iOS.
|
|
return nil;
|
|
}
|
|
|
|
if (state & states::CHECKABLE) {
|
|
if (state & states::CHECKED) {
|
|
return @"1";
|
|
}
|
|
if (state & states::MIXED) {
|
|
return @"2";
|
|
}
|
|
return @"0";
|
|
}
|
|
|
|
if (mGeckoAccessible->IsPassword()) {
|
|
// Accessible::Value returns an empty string. On iOS, we need to return the
|
|
// masked password so that AT knows how many characters are in the password.
|
|
Accessible* leaf = mGeckoAccessible->FirstChild();
|
|
if (!leaf) {
|
|
return nil;
|
|
}
|
|
nsAutoString masked;
|
|
leaf->AppendTextTo(masked);
|
|
return ToNSString(masked);
|
|
}
|
|
|
|
// If there is a heading ancestor, self has the header trait, so value should
|
|
// be the heading level.
|
|
for (Accessible* acc = mGeckoAccessible; acc; acc = acc->Parent()) {
|
|
if (acc->Role() == roles::HEADING) {
|
|
return [NSString stringWithFormat:@"%d", acc->GroupPosition().level];
|
|
}
|
|
}
|
|
|
|
nsAutoString value;
|
|
mGeckoAccessible->Value(value);
|
|
return ToNSString(value);
|
|
}
|
|
|
|
static uint64_t GetAccessibilityTraits(Accessible* aAccessible) {
|
|
uint64_t state = aAccessible->State();
|
|
uint64_t traits = Trait::WebContent;
|
|
switch (aAccessible->Role()) {
|
|
case roles::LINK:
|
|
traits |= Trait::Link;
|
|
break;
|
|
case roles::GRAPHIC:
|
|
traits |= Trait::Image;
|
|
break;
|
|
case roles::PAGETAB:
|
|
traits |= Trait::TabButton;
|
|
break;
|
|
case roles::PUSHBUTTON:
|
|
case roles::SUMMARY:
|
|
case roles::COMBOBOX:
|
|
case roles::BUTTONMENU:
|
|
case roles::TOGGLE_BUTTON:
|
|
case roles::CHECKBUTTON:
|
|
case roles::SWITCH:
|
|
traits |= Trait::Button;
|
|
break;
|
|
case roles::RADIOBUTTON:
|
|
traits |= Trait::RadioButton;
|
|
break;
|
|
case roles::HEADING:
|
|
traits |= Trait::Header;
|
|
break;
|
|
case roles::STATICTEXT:
|
|
case roles::TEXT_LEAF:
|
|
traits |= Trait::StaticText;
|
|
break;
|
|
case roles::SLIDER:
|
|
case roles::SPINBUTTON:
|
|
traits |= Trait::Adjustable;
|
|
break;
|
|
case roles::MENUITEM:
|
|
case roles::PARENT_MENUITEM:
|
|
case roles::CHECK_MENU_ITEM:
|
|
case roles::RADIO_MENU_ITEM:
|
|
traits |= Trait::MenuItem;
|
|
break;
|
|
case roles::PASSWORD_TEXT:
|
|
traits |= Trait::SecureTextField;
|
|
break;
|
|
case roles::SEARCHBOX:
|
|
traits |= Trait::SearchField;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if ((traits & Trait::Link) && (state & states::TRAVERSED)) {
|
|
traits |= Trait::Visited;
|
|
}
|
|
|
|
if ((traits & Trait::Button) && (state & states::HASPOPUP)) {
|
|
traits |= Trait::PopupButton;
|
|
}
|
|
|
|
if (state & states::SELECTED) {
|
|
traits |= Trait::Selected;
|
|
}
|
|
|
|
if (state & states::CHECKABLE) {
|
|
traits |= Trait::Toggle;
|
|
}
|
|
|
|
if (!(state & states::ENABLED)) {
|
|
traits |= Trait::NotEnabled;
|
|
}
|
|
|
|
if (state & states::EDITABLE) {
|
|
traits |= Trait::TextEntry;
|
|
if (state & states::FOCUSED) {
|
|
// XXX: Also add "has text cursor" trait
|
|
traits |= Trait::IsEditing | Trait::TextOperationsAvailable;
|
|
}
|
|
|
|
if (state & states::MULTI_LINE) {
|
|
traits |= Trait::TextArea;
|
|
}
|
|
}
|
|
|
|
return traits;
|
|
}
|
|
|
|
- (uint64_t)accessibilityTraits {
|
|
if (!mGeckoAccessible) {
|
|
return Trait::None;
|
|
}
|
|
|
|
uint64_t traits = GetAccessibilityTraits(mGeckoAccessible);
|
|
|
|
for (Accessible* parent = mGeckoAccessible->Parent(); parent;
|
|
parent = parent->Parent()) {
|
|
traits |= GetAccessibilityTraits(parent);
|
|
}
|
|
|
|
return traits;
|
|
}
|
|
|
|
- (NSInteger)accessibilityElementCount {
|
|
return mGeckoAccessible ? mGeckoAccessible->ChildCount() : 0;
|
|
}
|
|
|
|
- (nullable id)accessibilityElementAtIndex:(NSInteger)index {
|
|
if (!mGeckoAccessible) {
|
|
return nil;
|
|
}
|
|
|
|
Accessible* child = mGeckoAccessible->ChildAt(index);
|
|
return GetNativeFromGeckoAccessible(child);
|
|
}
|
|
|
|
- (NSInteger)indexOfAccessibilityElement:(id)element {
|
|
Accessible* acc = [(MUIAccessible*)element geckoAccessible];
|
|
if (!acc || mGeckoAccessible != acc->Parent()) {
|
|
return -1;
|
|
}
|
|
|
|
return acc->IndexInParent();
|
|
}
|
|
|
|
- (NSArray* _Nullable)accessibilityElements {
|
|
NSMutableArray* children = [[[NSMutableArray alloc] init] autorelease];
|
|
uint32_t childCount = mGeckoAccessible->ChildCount();
|
|
for (uint32_t i = 0; i < childCount; i++) {
|
|
if (MUIAccessible* child =
|
|
GetNativeFromGeckoAccessible(mGeckoAccessible->ChildAt(i))) {
|
|
[children addObject:child];
|
|
}
|
|
}
|
|
|
|
return children;
|
|
}
|
|
|
|
- (UIAccessibilityContainerType)accessibilityContainerType {
|
|
return UIAccessibilityContainerTypeNone;
|
|
}
|
|
|
|
- (NSRange)_accessibilitySelectedTextRange {
|
|
if (!mGeckoAccessible || !mGeckoAccessible->IsHyperText()) {
|
|
return NSMakeRange(NSNotFound, 0);
|
|
}
|
|
// XXX This will only work in simple plain text boxes. It will break horribly
|
|
// if there are any embedded objects. Also, it only supports caret, not
|
|
// selection.
|
|
int32_t caret = mGeckoAccessible->AsHyperTextBase()->CaretOffset();
|
|
if (caret != -1) {
|
|
return NSMakeRange(caret, 0);
|
|
}
|
|
return NSMakeRange(NSNotFound, 0);
|
|
}
|
|
|
|
- (void)_accessibilitySetSelectedTextRange:(NSRange)range {
|
|
if (!mGeckoAccessible || !mGeckoAccessible->IsHyperText()) {
|
|
return;
|
|
}
|
|
// XXX This will only work in simple plain text boxes. It will break horribly
|
|
// if there are any embedded objects. Also, it only supports caret, not
|
|
// selection.
|
|
mGeckoAccessible->AsHyperTextBase()->SetCaretOffset(range.location);
|
|
}
|
|
|
|
@end
|