2 /* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
4 /* This Source Code Form is subject to the terms of the Mozilla Public
5 * License, v. 2.0. If a copy of the MPL was not distributed with this
6 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
8 #import "MUIAccessible.h"
11 #include "RootAccessibleWrap.h"
13 using namespace mozilla;
14 using namespace mozilla::a11y;
21 static NSString* ToNSString(const nsACString& aCString) {
22 if (aCString.IsEmpty()) {
23 return [NSString string];
25 return [[[NSString alloc] initWithBytes:aCString.BeginReading()
26 length:aCString.Length()
27 encoding:NSUTF8StringEncoding] autorelease];
31 static NSString* ToNSString(const nsAString& aString) {
32 if (aString.IsEmpty()) {
33 return [NSString string];
35 return [NSString stringWithCharacters:reinterpret_cast<const unichar*>(
36 aString.BeginReading())
37 length:aString.Length()];
40 // These rules offer conditions for whether a gecko accessible
41 // should be considered a UIKit accessibility element. Each role is mapped to a
43 enum class IsAccessibilityElementRule {
48 // If the accessible has no children. For example an empty header
51 // If the accessible has no children and it is named and focusable.
52 IfChildlessWithNameAndFocusable,
53 // If this accessible isn't a child of an accessibility element. For example,
54 // a text leaf child of a button.
55 IfParentIsntElementWithName,
56 // If this accessible has multiple leafs that should functionally be
57 // united, for example a link with span elements.
63 static const uint64_t None = 0;
64 static const uint64_t Button = ((uint64_t)0x1) << 0;
65 static const uint64_t Link = ((uint64_t)0x1) << 1;
66 static const uint64_t Image = ((uint64_t)0x1) << 2;
67 static const uint64_t Selected = ((uint64_t)0x1) << 3;
68 static const uint64_t PlaysSound = ((uint64_t)0x1) << 4;
69 static const uint64_t KeyboardKey = ((uint64_t)0x1) << 5;
70 static const uint64_t StaticText = ((uint64_t)0x1) << 6;
71 static const uint64_t SummaryElement = ((uint64_t)0x1) << 7;
72 static const uint64_t NotEnabled = ((uint64_t)0x1) << 8;
73 static const uint64_t UpdatesFrequently = ((uint64_t)0x1) << 9;
74 static const uint64_t SearchField = ((uint64_t)0x1) << 10;
75 static const uint64_t StartsMediaSession = ((uint64_t)0x1) << 11;
76 static const uint64_t Adjustable = ((uint64_t)0x1) << 12;
77 static const uint64_t AllowsDirectInteraction = ((uint64_t)0x1) << 13;
78 static const uint64_t CausesPageTurn = ((uint64_t)0x1) << 14;
79 static const uint64_t TabBar = ((uint64_t)0x1) << 15;
80 static const uint64_t Header = ((uint64_t)0x1) << 16;
81 static const uint64_t WebContent = ((uint64_t)0x1) << 17;
82 static const uint64_t TextEntry = ((uint64_t)0x1) << 18;
83 static const uint64_t PickerElement = ((uint64_t)0x1) << 19;
84 static const uint64_t RadioButton = ((uint64_t)0x1) << 20;
85 static const uint64_t IsEditing = ((uint64_t)0x1) << 21;
86 static const uint64_t LaunchIcon = ((uint64_t)0x1) << 22;
87 static const uint64_t StatusBarElement = ((uint64_t)0x1) << 23;
88 static const uint64_t SecureTextField = ((uint64_t)0x1) << 24;
89 static const uint64_t Inactive = ((uint64_t)0x1) << 25;
90 static const uint64_t Footer = ((uint64_t)0x1) << 26;
91 static const uint64_t BackButton = ((uint64_t)0x1) << 27;
92 static const uint64_t TabButton = ((uint64_t)0x1) << 28;
93 static const uint64_t AutoCorrectCandidate = ((uint64_t)0x1) << 29;
94 static const uint64_t DeleteKey = ((uint64_t)0x1) << 30;
95 static const uint64_t SelectionDismissesItem = ((uint64_t)0x1) << 31;
96 static const uint64_t Visited = ((uint64_t)0x1) << 32;
97 static const uint64_t Scrollable = ((uint64_t)0x1) << 33;
98 static const uint64_t Spacer = ((uint64_t)0x1) << 34;
99 static const uint64_t TableIndex = ((uint64_t)0x1) << 35;
100 static const uint64_t Map = ((uint64_t)0x1) << 36;
101 static const uint64_t TextOperationsAvailable = ((uint64_t)0x1) << 37;
102 static const uint64_t Draggable = ((uint64_t)0x1) << 38;
103 static const uint64_t GesturePracticeRegion = ((uint64_t)0x1) << 39;
104 static const uint64_t PopupButton = ((uint64_t)0x1) << 40;
105 static const uint64_t AllowsNativeSliding = ((uint64_t)0x1) << 41;
106 static const uint64_t MathEquation = ((uint64_t)0x1) << 42;
107 static const uint64_t ContainedByTable = ((uint64_t)0x1) << 43;
108 static const uint64_t ContainedByList = ((uint64_t)0x1) << 44;
109 static const uint64_t TouchContainer = ((uint64_t)0x1) << 45;
110 static const uint64_t SupportsZoom = ((uint64_t)0x1) << 46;
111 static const uint64_t TextArea = ((uint64_t)0x1) << 47;
112 static const uint64_t BookContent = ((uint64_t)0x1) << 48;
113 static const uint64_t ContainedByLandmark = ((uint64_t)0x1) << 49;
114 static const uint64_t FolderIcon = ((uint64_t)0x1) << 50;
115 static const uint64_t ReadOnly = ((uint64_t)0x1) << 51;
116 static const uint64_t MenuItem = ((uint64_t)0x1) << 52;
117 static const uint64_t Toggle = ((uint64_t)0x1) << 53;
118 static const uint64_t IgnoreItemChooser = ((uint64_t)0x1) << 54;
119 static const uint64_t SupportsTrackingDetail = ((uint64_t)0x1) << 55;
120 static const uint64_t Alert = ((uint64_t)0x1) << 56;
121 static const uint64_t ContainedByFieldset = ((uint64_t)0x1) << 57;
122 static const uint64_t AllowsLayoutChangeInStatusBar = ((uint64_t)0x1) << 58;
127 @interface NSObject (AccessibilityPrivate)
128 - (void)_accessibilityUnregister;
131 @implementation MUIAccessible
133 - (id)initWithAccessible:(Accessible*)aAcc {
134 MOZ_ASSERT(aAcc, "Cannot init MUIAccessible with null");
135 if ((self = [super init])) {
136 mGeckoAccessible = aAcc;
142 - (mozilla::a11y::Accessible*)geckoAccessible {
143 return mGeckoAccessible;
147 mGeckoAccessible = nullptr;
148 if ([self respondsToSelector:@selector(_accessibilityUnregister)]) {
149 [self _accessibilityUnregister];
157 static bool isAccessibilityElementInternal(Accessible* aAccessible) {
158 MOZ_ASSERT(aAccessible);
159 IsAccessibilityElementRule rule = IsAccessibilityElementRule::No;
161 #define ROLE(_geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
162 msaaRole, ia2Role, androidClass, iosIsElement, uiaControlType, \
164 case roles::_geckoRole: \
165 rule = iosIsElement; \
167 switch (aAccessible->Role()) {
172 case IsAccessibilityElementRule::Yes:
174 case IsAccessibilityElementRule::No:
176 case IsAccessibilityElementRule::IfChildless:
177 return aAccessible->ChildCount() == 0;
178 case IsAccessibilityElementRule::IfParentIsntElementWithName: {
180 aAccessible->Name(name);
181 name.CompressWhitespace();
182 if (name.IsEmpty()) {
186 if (isAccessibilityElementInternal(aAccessible->Parent())) {
187 // This is a text leaf that needs to be pruned from a button or the
188 // likes. It should also be ignored in the event of its parent being a
195 case IsAccessibilityElementRule::IfChildlessWithNameAndFocusable:
196 if (aAccessible->ChildCount() == 0 &&
197 (aAccessible->State() & states::FOCUSABLE)) {
199 aAccessible->Name(name);
200 name.CompressWhitespace();
201 return !name.IsEmpty();
204 case IsAccessibilityElementRule::IfBrokenUp: {
205 uint32_t childCount = aAccessible->ChildCount();
206 if (childCount == 1) {
207 // If this is a single child container just use the text leaf and its
208 // traits will be inherited.
212 for (uint32_t idx = 0; idx < childCount; idx++) {
213 Accessible* child = aAccessible->ChildAt(idx);
214 role accRole = child->Role();
215 if (accRole != roles::STATICTEXT && accRole != roles::TEXT_LEAF &&
216 accRole != roles::GRAPHIC) {
217 // If this container contains anything but text leafs and images
218 // ignore this accessible. Its descendants will inherit the
219 // container's traits.
230 MOZ_ASSERT_UNREACHABLE("Unhandled IsAccessibilityElementRule");
235 - (BOOL)isAccessibilityElement {
236 if (!mGeckoAccessible) {
240 return isAccessibilityElementInternal(mGeckoAccessible) ? YES : NO;
243 - (NSString*)accessibilityLabel {
244 if (!mGeckoAccessible) {
249 mGeckoAccessible->Name(name);
251 return ToNSString(name);
254 - (NSString*)accessibilityHint {
255 if (!mGeckoAccessible) {
260 // Just put in a debug description as the label so we get a clue about which
261 // accessible ends up where.
263 mGeckoAccessible->DebugDescription(desc);
264 return ToNSString(desc);
270 - (CGRect)accessibilityFrame {
271 RootAccessibleWrap* rootAcc = static_cast<RootAccessibleWrap*>(
272 mGeckoAccessible->IsLocal()
273 ? mGeckoAccessible->AsLocal()->RootAccessible()
274 : mGeckoAccessible->AsRemote()
275 ->OuterDocOfRemoteBrowser()
279 return CGRectMake(0, 0, 0, 0);
282 LayoutDeviceIntRect rect = mGeckoAccessible->Bounds();
283 return rootAcc->DevPixelsRectToUIKit(rect);
286 - (NSString*)accessibilityValue {
287 if (!mGeckoAccessible) {
291 uint64_t state = mGeckoAccessible->State();
292 if (state & states::LINKED) {
293 // Value returns the URL. We don't want to expose that as the value on iOS.
297 if (state & states::CHECKABLE) {
298 if (state & states::CHECKED) {
301 if (state & states::MIXED) {
307 if (mGeckoAccessible->IsPassword()) {
308 // Accessible::Value returns an empty string. On iOS, we need to return the
309 // masked password so that AT knows how many characters are in the password.
310 Accessible* leaf = mGeckoAccessible->FirstChild();
315 leaf->AppendTextTo(masked);
316 return ToNSString(masked);
319 // If there is a heading ancestor, self has the header trait, so value should
320 // be the heading level.
321 for (Accessible* acc = mGeckoAccessible; acc; acc = acc->Parent()) {
322 if (acc->Role() == roles::HEADING) {
323 return [NSString stringWithFormat:@"%d", acc->GroupPosition().level];
328 mGeckoAccessible->Value(value);
329 return ToNSString(value);
332 static uint64_t GetAccessibilityTraits(Accessible* aAccessible) {
333 uint64_t state = aAccessible->State();
334 uint64_t traits = Trait::WebContent;
335 switch (aAccessible->Role()) {
337 traits |= Trait::Link;
340 traits |= Trait::Image;
343 traits |= Trait::TabButton;
345 case roles::PUSHBUTTON:
347 case roles::COMBOBOX:
348 case roles::BUTTONMENU:
349 case roles::TOGGLE_BUTTON:
350 case roles::CHECKBUTTON:
352 traits |= Trait::Button;
354 case roles::RADIOBUTTON:
355 traits |= Trait::RadioButton;
358 traits |= Trait::Header;
360 case roles::STATICTEXT:
361 case roles::TEXT_LEAF:
362 traits |= Trait::StaticText;
365 case roles::SPINBUTTON:
366 traits |= Trait::Adjustable;
368 case roles::MENUITEM:
369 case roles::PARENT_MENUITEM:
370 case roles::CHECK_MENU_ITEM:
371 case roles::RADIO_MENU_ITEM:
372 traits |= Trait::MenuItem;
374 case roles::PASSWORD_TEXT:
375 traits |= Trait::SecureTextField;
381 if ((traits & Trait::Link) && (state & states::TRAVERSED)) {
382 traits |= Trait::Visited;
385 if ((traits & Trait::Button) && (state & states::HASPOPUP)) {
386 traits |= Trait::PopupButton;
389 if (state & states::SELECTED) {
390 traits |= Trait::Selected;
393 if (state & states::CHECKABLE) {
394 traits |= Trait::Toggle;
397 if (!(state & states::ENABLED)) {
398 traits |= Trait::NotEnabled;
401 if (state & states::EDITABLE) {
402 traits |= Trait::TextEntry;
403 if (state & states::FOCUSED) {
404 // XXX: Also add "has text cursor" trait
405 traits |= Trait::IsEditing | Trait::TextOperationsAvailable;
408 if (aAccessible->IsSearchbox()) {
409 traits |= Trait::SearchField;
412 if (state & states::MULTI_LINE) {
413 traits |= Trait::TextArea;
420 - (uint64_t)accessibilityTraits {
421 if (!mGeckoAccessible) {
425 uint64_t traits = GetAccessibilityTraits(mGeckoAccessible);
427 for (Accessible* parent = mGeckoAccessible->Parent(); parent;
428 parent = parent->Parent()) {
429 traits |= GetAccessibilityTraits(parent);
435 - (NSInteger)accessibilityElementCount {
436 return mGeckoAccessible ? mGeckoAccessible->ChildCount() : 0;
439 - (nullable id)accessibilityElementAtIndex:(NSInteger)index {
440 if (!mGeckoAccessible) {
444 Accessible* child = mGeckoAccessible->ChildAt(index);
445 return GetNativeFromGeckoAccessible(child);
448 - (NSInteger)indexOfAccessibilityElement:(id)element {
449 Accessible* acc = [(MUIAccessible*)element geckoAccessible];
450 if (!acc || mGeckoAccessible != acc->Parent()) {
454 return acc->IndexInParent();
457 - (NSArray* _Nullable)accessibilityElements {
458 NSMutableArray* children = [[[NSMutableArray alloc] init] autorelease];
459 uint32_t childCount = mGeckoAccessible->ChildCount();
460 for (uint32_t i = 0; i < childCount; i++) {
461 if (MUIAccessible* child =
462 GetNativeFromGeckoAccessible(mGeckoAccessible->ChildAt(i))) {
463 [children addObject:child];
470 - (UIAccessibilityContainerType)accessibilityContainerType {
471 return UIAccessibilityContainerTypeNone;
474 - (NSRange)_accessibilitySelectedTextRange {
475 if (!mGeckoAccessible || !mGeckoAccessible->IsHyperText()) {
476 return NSMakeRange(NSNotFound, 0);
478 // XXX This will only work in simple plain text boxes. It will break horribly
479 // if there are any embedded objects. Also, it only supports caret, not
481 int32_t caret = mGeckoAccessible->AsHyperTextBase()->CaretOffset();
483 return NSMakeRange(caret, 0);
485 return NSMakeRange(NSNotFound, 0);
488 - (void)_accessibilitySetSelectedTextRange:(NSRange)range {
489 if (!mGeckoAccessible || !mGeckoAccessible->IsHyperText()) {
492 // XXX This will only work in simple plain text boxes. It will break horribly
493 // if there are any embedded objects. Also, it only supports caret, not
495 mGeckoAccessible->AsHyperTextBase()->SetCaretOffset(range.location);