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 "mozAccessible.h"
9 #include "MOXAccessibleBase.h"
13 #import "MOXSearchInfo.h"
14 #import "MOXTextMarkerDelegate.h"
15 #import "MOXWebAreaAccessible.h"
16 #import "mozTextAccessible.h"
17 #import "mozRootAccessible.h"
19 #include "LocalAccessible-inl.h"
20 #include "nsAccUtils.h"
21 #include "DocAccessibleParent.h"
23 #include "mozilla/a11y/Role.h"
24 #include "RootAccessible.h"
25 #include "mozilla/a11y/PDocAccessible.h"
26 #include "mozilla/dom/BrowserParent.h"
27 #include "OuterDocAccessible.h"
28 #include "nsChildView.h"
29 #include "xpcAccessibleMacInterface.h"
32 #include "nsCocoaUtils.h"
34 #include "nsObjCExceptions.h"
35 #include "nsWhitespaceTokenizer.h"
38 using namespace mozilla;
39 using namespace mozilla::a11y;
43 @interface mozAccessible ()
44 - (BOOL)providesLabelNotTitle;
46 - (void)maybePostLiveRegionChanged;
47 - (void)maybePostA11yUtilNotification;
50 @implementation mozAccessible
52 - (id)initWithAccessible:(Accessible*)aAcc {
53 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
54 MOZ_ASSERT(aAcc, "Cannot init mozAccessible with null");
55 if ((self = [super init])) {
56 mGeckoAccessible = aAcc;
62 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
66 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
70 NS_OBJC_END_TRY_IGNORE_BLOCK;
73 #pragma mark - mozAccessible widget
75 - (BOOL)hasRepresentedView {
79 - (id)representedView {
89 - (BOOL)moxIgnoreWithParent:(mozAccessible*)parent {
90 if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
91 if (acc->IsContent() && acc->GetContent()->IsXULElement()) {
92 if (acc->VisibilityState() & states::INVISIBLE) {
98 return [parent moxIgnoreChild:self];
101 - (BOOL)moxIgnoreChild:(mozAccessible*)child {
102 return nsAccUtils::MustPrune(mGeckoAccessible);
105 - (id)childAt:(uint32_t)i {
106 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
108 Accessible* child = mGeckoAccessible->ChildAt(i);
109 return child ? GetNativeFromGeckoAccessible(child) : nil;
111 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
115 return mGeckoAccessible->State();
118 - (uint64_t)stateWithMask:(uint64_t)mask {
119 return [self state] & mask;
122 - (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled {
123 if (state == states::BUSY) {
124 [self moxPostNotification:@"AXElementBusyChanged"];
128 - (BOOL)providesLabelNotTitle {
129 // These accessible types are the exception to the rule of label vs. title:
130 // They may be named explicitly, but they still provide a label not a title.
131 return mRole == roles::GROUPING || mRole == roles::RADIO_GROUP ||
132 mRole == roles::FIGURE || mRole == roles::GRAPHIC ||
133 mRole == roles::DOCUMENT || mRole == roles::OUTLINE ||
134 mRole == roles::ARTICLE || mRole == roles::ENTRY ||
135 mRole == roles::SPINBUTTON;
138 - (mozilla::a11y::Accessible*)geckoAccessible {
139 return mGeckoAccessible;
142 #pragma mark - MOXAccessible protocol
144 - (BOOL)moxBlockSelector:(SEL)selector {
145 if (selector == @selector(moxPerformPress)) {
146 uint8_t actionCount = mGeckoAccessible->ActionCount();
148 // If we have no action, we don't support press, so return YES.
149 return actionCount == 0;
152 if (selector == @selector(moxSetFocused:)) {
153 return [self stateWithMask:states::FOCUSABLE] == 0;
156 if (selector == @selector(moxARIALive) ||
157 selector == @selector(moxARIAAtomic) ||
158 selector == @selector(moxARIARelevant)) {
159 return ![self moxIsLiveRegion];
162 if (selector == @selector(moxARIAPosInSet) || selector == @selector
164 GroupPos groupPos = mGeckoAccessible->GroupPosition();
165 return groupPos.setSize == 0;
168 if (selector == @selector(moxExpanded)) {
169 return [self stateWithMask:states::EXPANDABLE] == 0;
172 return [super moxBlockSelector:selector];
175 - (id)moxFocusedUIElement {
176 MOZ_ASSERT(mGeckoAccessible);
177 // This only gets queried on the web area or the root group
178 // so just use the doc's focused child instead of trying to get
179 // the focused child of mGeckoAccessible.
180 Accessible* doc = nsAccUtils::DocumentFor(mGeckoAccessible);
181 mozAccessible* focusedChild =
182 GetNativeFromGeckoAccessible(doc->FocusedChild());
184 if ([focusedChild isAccessibilityElement]) {
188 // return ourself if we can't get a native focused child.
192 - (id<MOXTextMarkerSupport>)moxTextMarkerDelegate {
193 MOZ_ASSERT(mGeckoAccessible);
195 return [MOXTextMarkerDelegate
196 getOrCreateForDoc:nsAccUtils::DocumentFor(mGeckoAccessible)];
199 - (BOOL)moxIsLiveRegion {
200 return mIsLiveRegion;
203 - (id)moxHitTest:(NSPoint)point {
204 MOZ_ASSERT(mGeckoAccessible);
206 // Convert the given screen-global point in the cocoa coordinate system (with
207 // origin in the bottom-left corner of the screen) into point in the Gecko
208 // coordinate system (with origin in a top-left screen point).
209 NSScreen* mainView = [[NSScreen screens] objectAtIndex:0];
211 NSMakePoint(point.x, [mainView frame].size.height - point.y);
212 LayoutDeviceIntPoint geckoPoint = nsCocoaUtils::CocoaPointsToDevPixels(
213 tmpPoint, nsCocoaUtils::GetBackingScaleFactor(mainView));
215 Accessible* child = mGeckoAccessible->ChildAtPoint(
216 geckoPoint.x, geckoPoint.y, Accessible::EWhichChildAtPoint::DeepestChild);
219 mozAccessible* nativeChild = GetNativeFromGeckoAccessible(child);
220 return [nativeChild isAccessibilityElement]
222 : [nativeChild moxUnignoredParent];
225 // if we didn't find anything, return ourself or child view.
229 - (id<mozAccessible>)moxParent {
230 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
231 if ([self isExpired]) {
235 Accessible* parent = mGeckoAccessible->Parent();
241 id nativeParent = GetNativeFromGeckoAccessible(parent);
242 if ([nativeParent isKindOfClass:[MOXWebAreaAccessible class]]) {
243 // Before returning a WebArea as parent, check to see if
244 // there is a generated root group that is an intermediate container.
245 if (id<mozAccessible> rootGroup = [nativeParent rootGroup]) {
246 nativeParent = rootGroup;
250 if (!nativeParent && mGeckoAccessible->IsLocal()) {
251 // Return native of root accessible if we have no direct parent.
252 // XXX: need to return a sensible fallback in proxy case as well
253 nativeParent = GetNativeFromGeckoAccessible(
254 mGeckoAccessible->AsLocal()->RootAccessible());
257 return GetObjectOrRepresentedView(nativeParent);
259 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
262 // gets all our native children lazily, including those that are ignored.
263 - (NSArray*)moxChildren {
264 MOZ_ASSERT(mGeckoAccessible);
266 NSMutableArray* children = [[[NSMutableArray alloc]
267 initWithCapacity:mGeckoAccessible->ChildCount()] autorelease];
269 for (uint32_t childIdx = 0; childIdx < mGeckoAccessible->ChildCount();
271 Accessible* child = mGeckoAccessible->ChildAt(childIdx);
272 mozAccessible* nativeChild = GetNativeFromGeckoAccessible(child);
277 [children addObject:nativeChild];
283 - (NSValue*)moxPosition {
284 CGRect frame = [[self moxFrame] rectValue];
286 return [NSValue valueWithPoint:NSMakePoint(frame.origin.x, frame.origin.y)];
289 - (NSValue*)moxSize {
290 CGRect frame = [[self moxFrame] rectValue];
293 [NSValue valueWithSize:NSMakeSize(frame.size.width, frame.size.height)];
296 - (NSString*)moxRole {
297 #define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
298 msaaRole, ia2Role, androidClass, nameRule) \
299 case roles::geckoRole: \
305 MOZ_ASSERT_UNREACHABLE("Unknown role.");
306 return NSAccessibilityUnknownRole;
312 - (nsStaticAtom*)ARIARole {
313 MOZ_ASSERT(mGeckoAccessible);
315 if (mGeckoAccessible->HasARIARole()) {
316 const nsRoleMapEntry* roleMap = mGeckoAccessible->ARIARoleMap();
317 return roleMap->roleAtom;
320 return nsGkAtoms::_empty;
323 - (NSString*)moxSubrole {
324 MOZ_ASSERT(mGeckoAccessible);
326 // Deal with landmarks first
327 // macOS groups the specific landmark types of DPub ARIA into two broad
328 // categories with corresponding subroles: Navigation and region/container.
329 if (mRole == roles::LANDMARK) {
330 nsAtom* landmark = mGeckoAccessible->LandmarkRole();
331 // HTML Elements treated as landmarks, and ARIA landmarks.
333 if (landmark == nsGkAtoms::banner) return @"AXLandmarkBanner";
334 if (landmark == nsGkAtoms::complementary)
335 return @"AXLandmarkComplementary";
336 if (landmark == nsGkAtoms::contentinfo) return @"AXLandmarkContentInfo";
337 if (landmark == nsGkAtoms::main) return @"AXLandmarkMain";
338 if (landmark == nsGkAtoms::navigation) return @"AXLandmarkNavigation";
339 if (landmark == nsGkAtoms::search) return @"AXLandmarkSearch";
342 // None of the above, so assume DPub ARIA.
343 return @"AXLandmarkRegion";
346 // Now, deal with widget roles
347 nsStaticAtom* roleAtom = nullptr;
349 if (mRole == roles::DIALOG) {
350 roleAtom = [self ARIARole];
352 if (roleAtom == nsGkAtoms::alertdialog) {
353 return @"AXApplicationAlertDialog";
355 if (roleAtom == nsGkAtoms::dialog) {
356 return @"AXApplicationDialog";
360 if (mRole == roles::FORM) {
361 roleAtom = [self ARIARole];
363 if (roleAtom == nsGkAtoms::form) {
364 return @"AXLandmarkForm";
368 #define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
369 msaaRole, ia2Role, androidClass, nameRule) \
370 case roles::geckoRole: \
371 if (![macSubrole isEqualToString:NSAccessibilityUnknownSubrole]) { \
381 // These are special. They map to roles::NOTHING
382 // and are instructed by the ARIA map to use the native host role.
383 roleAtom = [self ARIARole];
385 if (roleAtom == nsGkAtoms::log_) {
386 return @"AXApplicationLog";
389 if (roleAtom == nsGkAtoms::timer) {
390 return @"AXApplicationTimer";
392 // macOS added an AXSubrole value to distinguish generic AXGroup objects
393 // from those which are AXGroups as a result of an explicit ARIA role,
394 // such as the non-landmark, non-listitem text containers in DPub ARIA.
395 if (mRole == roles::FOOTNOTE || mRole == roles::SECTION) {
396 return @"AXApplicationGroup";
399 return NSAccessibilityUnknownSubrole;
404 struct RoleDescrMap {
406 const nsString description;
409 static const RoleDescrMap sRoleDescrMap[] = {
410 {@"AXApplicationAlert", u"alert"_ns},
411 {@"AXApplicationAlertDialog", u"alertDialog"_ns},
412 {@"AXApplicationDialog", u"dialog"_ns},
413 {@"AXApplicationLog", u"log"_ns},
414 {@"AXApplicationMarquee", u"marquee"_ns},
415 {@"AXApplicationStatus", u"status"_ns},
416 {@"AXApplicationTimer", u"timer"_ns},
417 {@"AXContentSeparator", u"separator"_ns},
418 {@"AXDefinition", u"definition"_ns},
419 {@"AXDetails", u"details"_ns},
420 {@"AXDocument", u"document"_ns},
421 {@"AXDocumentArticle", u"article"_ns},
422 {@"AXDocumentMath", u"math"_ns},
423 {@"AXDocumentNote", u"note"_ns},
424 {@"AXLandmarkApplication", u"application"_ns},
425 {@"AXLandmarkBanner", u"banner"_ns},
426 {@"AXLandmarkComplementary", u"complementary"_ns},
427 {@"AXLandmarkContentInfo", u"content"_ns},
428 {@"AXLandmarkMain", u"main"_ns},
429 {@"AXLandmarkNavigation", u"navigation"_ns},
430 {@"AXLandmarkRegion", u"region"_ns},
431 {@"AXLandmarkSearch", u"search"_ns},
432 {@"AXSearchField", u"searchTextField"_ns},
433 {@"AXSummary", u"summary"_ns},
434 {@"AXTabPanel", u"tabPanel"_ns},
435 {@"AXTerm", u"term"_ns},
436 {@"AXUserInterfaceTooltip", u"tooltip"_ns}};
438 struct RoleDescrComparator {
439 const NSString* mRole;
440 explicit RoleDescrComparator(const NSString* aRole) : mRole(aRole) {}
441 int operator()(const RoleDescrMap& aEntry) const {
442 return [mRole compare:aEntry.role];
446 - (NSString*)moxRoleDescription {
447 if (NSString* ariaRoleDescription =
448 utils::GetAccAttr(self, nsGkAtoms::aria_roledescription)) {
449 if ([ariaRoleDescription length]) {
450 return ariaRoleDescription;
454 if (mRole == roles::FIGURE) return utils::LocalizedString(u"figure"_ns);
456 if (mRole == roles::HEADING) return utils::LocalizedString(u"heading"_ns);
458 if (mRole == roles::MARK) {
459 return utils::LocalizedString(u"highlight"_ns);
462 NSString* subrole = [self moxSubrole];
466 if (BinarySearchIf(sRoleDescrMap, 0, ArrayLength(sRoleDescrMap),
467 RoleDescrComparator(subrole), &idx)) {
468 return utils::LocalizedString(sRoleDescrMap[idx].description);
472 return NSAccessibilityRoleDescription([self moxRole], subrole);
475 - (NSString*)moxLabel {
476 if ([self isExpired]) {
482 /* If our accessible is:
483 * 1. Named by invisible text, or
484 * 2. Has more than one labeling relation, or
485 * 3. Is a special role defined in providesLabelNotTitle
486 * ... return its name as a label (AXDescription).
488 ENameValueFlag flag = mGeckoAccessible->Name(name);
489 if (flag == eNameFromSubtree) {
493 if (![self providesLabelNotTitle]) {
494 NSArray* relations = [self getRelationsByType:RelationType::LABELLED_BY];
495 if ([relations count] == 1) {
500 return nsCocoaUtils::ToNSString(name);
503 - (NSString*)moxTitle {
504 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
506 // In some special cases we provide the name in the label (AXDescription).
507 if ([self providesLabelNotTitle]) {
512 mGeckoAccessible->Name(title);
513 if (nsCoreUtils::IsWhitespaceString(title)) {
517 return nsCocoaUtils::ToNSString(title);
519 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
523 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
526 mGeckoAccessible->Value(value);
528 return nsCocoaUtils::ToNSString(value);
530 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
533 - (NSString*)moxHelp {
534 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
536 // What needs to go here is actually the accDescription of an item.
537 // The MSAA acc_help method has nothing to do with this one.
538 nsAutoString helpText;
539 mGeckoAccessible->Description(helpText);
541 return nsCocoaUtils::ToNSString(helpText);
543 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
546 - (NSWindow*)moxWindow {
547 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
549 // Get a pointer to the native window (NSWindow) we reside in.
550 NSWindow* nativeWindow = nil;
551 DocAccessible* docAcc = nullptr;
552 if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
553 docAcc = acc->Document();
555 RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
556 LocalAccessible* outerDoc = proxy->OuterDocOfRemoteBrowser();
557 if (outerDoc) docAcc = outerDoc->Document();
560 if (docAcc) nativeWindow = static_cast<NSWindow*>(docAcc->GetNativeWindow());
562 MOZ_ASSERT(nativeWindow || gfxPlatform::IsHeadless(),
563 "Couldn't get native window");
566 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
569 - (NSNumber*)moxEnabled {
570 if ([self stateWithMask:states::UNAVAILABLE]) {
574 if (![self isRoot]) {
575 mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent];
576 if (![parent isRoot]) {
577 return @(![parent disableChild:self]);
584 - (NSNumber*)moxFocused {
585 return @([self stateWithMask:states::FOCUSED] != 0);
588 - (NSNumber*)moxSelected {
592 - (NSNumber*)moxExpanded {
593 return @([self stateWithMask:states::EXPANDED] != 0);
596 - (NSValue*)moxFrame {
597 MOZ_ASSERT(mGeckoAccessible);
599 LayoutDeviceIntRect rect = mGeckoAccessible->Bounds();
600 NSScreen* mainView = [[NSScreen screens] objectAtIndex:0];
601 CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mainView);
604 valueWithRect:NSMakeRect(
605 static_cast<CGFloat>(rect.x) / scaleFactor,
606 [mainView frame].size.height -
607 static_cast<CGFloat>(rect.y + rect.height) /
609 static_cast<CGFloat>(rect.width) / scaleFactor,
610 static_cast<CGFloat>(rect.height) / scaleFactor)];
613 - (NSString*)moxARIACurrent {
614 if (![self stateWithMask:states::CURRENT]) {
618 return utils::GetAccAttr(self, nsGkAtoms::aria_current);
621 - (NSNumber*)moxARIAAtomic {
622 return @(utils::GetAccAttr(self, nsGkAtoms::aria_atomic) != nil);
625 - (NSString*)moxARIALive {
626 return utils::GetAccAttr(self, nsGkAtoms::aria_live);
629 - (NSNumber*)moxARIAPosInSet {
630 GroupPos groupPos = mGeckoAccessible->GroupPosition();
631 return @(groupPos.posInSet);
634 - (NSNumber*)moxARIASetSize {
635 GroupPos groupPos = mGeckoAccessible->GroupPosition();
636 return @(groupPos.setSize);
639 - (NSString*)moxARIARelevant {
640 if (NSString* relevant =
641 utils::GetAccAttr(self, nsGkAtoms::containerRelevant)) {
645 // Default aria-relevant value
646 return @"additions text";
649 - (id)moxTitleUIElement {
650 MOZ_ASSERT(mGeckoAccessible);
652 NSArray* relations = [self getRelationsByType:RelationType::LABELLED_BY];
653 if ([relations count] == 1) {
654 return [relations firstObject];
660 - (NSString*)moxDOMIdentifier {
661 MOZ_ASSERT(mGeckoAccessible);
664 mGeckoAccessible->DOMNodeID(id);
666 return nsCocoaUtils::ToNSString(id);
669 - (NSNumber*)moxRequired {
670 return @([self stateWithMask:states::REQUIRED] != 0);
673 - (NSNumber*)moxElementBusy {
674 return @([self stateWithMask:states::BUSY] != 0);
677 - (NSArray*)moxLinkedUIElements {
678 return [self getRelationsByType:RelationType::FLOWS_TO];
681 - (NSArray*)moxARIAControls {
682 return [self getRelationsByType:RelationType::CONTROLLER_FOR];
685 - (mozAccessible*)topWebArea {
686 Accessible* doc = nsAccUtils::DocumentFor(mGeckoAccessible);
688 if (doc->IsLocal()) {
689 DocAccessible* docAcc = doc->AsLocal()->AsDoc();
690 if (docAcc->DocumentNode()->GetBrowsingContext()->IsTopContent()) {
691 return GetNativeFromGeckoAccessible(docAcc);
694 doc = docAcc->ParentDocument();
696 DocAccessibleParent* docProxy = doc->AsRemote()->AsDoc();
697 if (docProxy->IsTopLevel()) {
698 return GetNativeFromGeckoAccessible(docProxy);
700 doc = docProxy->ParentDoc();
707 - (void)handleRoleChanged:(mozilla::a11y::role)newRole {
711 // For testing purposes
712 [self moxPostNotification:@"AXMozRoleChanged"];
715 - (id)moxEditableAncestor {
716 return [self moxFindAncestor:^BOOL(id moxAcc, BOOL* stop) {
717 return [moxAcc isKindOfClass:[mozTextAccessible class]];
721 - (id)moxHighestEditableAncestor {
722 id highestAncestor = [self moxEditableAncestor];
723 while ([highestAncestor conformsToProtocol:@protocol(MOXAccessible)]) {
724 id ancestorParent = [highestAncestor moxUnignoredParent];
725 if (![ancestorParent conformsToProtocol:@protocol(MOXAccessible)]) {
729 id higherAncestor = [ancestorParent moxEditableAncestor];
731 if (!higherAncestor) {
735 highestAncestor = higherAncestor;
738 return highestAncestor;
741 - (id)moxFocusableAncestor {
742 // XXX: Checking focusable state up the chain can be expensive. For now,
743 // we can just return AXEditableAncestor since the main use case for this
744 // is rich text editing with links.
745 return [self moxEditableAncestor];
748 #ifndef RELEASE_OR_BETA
749 - (NSString*)moxMozDebugDescription {
750 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
752 if (!mGeckoAccessible) {
753 return [NSString stringWithFormat:@"<%@: %p mGeckoAccessible=null>",
754 NSStringFromClass([self class]), self];
757 NSMutableString* domInfo = [NSMutableString string];
758 if (NSString* tagName = utils::GetAccAttr(self, nsGkAtoms::tag)) {
759 [domInfo appendFormat:@" %@", tagName];
760 NSString* domID = [self moxDOMIdentifier];
761 if ([domID length]) {
762 [domInfo appendFormat:@"#%@", domID];
764 if (NSString* className = utils::GetAccAttr(self, nsGkAtoms::_class)) {
767 [className stringByReplacingOccurrencesOfString:@" "
772 return [NSString stringWithFormat:@"<%@: %p %@%@>",
773 NSStringFromClass([self class]), self,
774 [self moxRole], domInfo];
776 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
780 - (NSArray*)moxUIElementsForSearchPredicate:(NSDictionary*)searchPredicate {
781 // Create our search object and set it up with the searchPredicate
782 // params. The init function does additional parsing. We pass a
783 // reference to the web area to use as a start element if one is not
785 MOXSearchInfo* search =
786 [[[MOXSearchInfo alloc] initWithParameters:searchPredicate
787 andRoot:self] autorelease];
789 return [search performSearch];
792 - (NSNumber*)moxUIElementCountForSearchPredicate:
793 (NSDictionary*)searchPredicate {
795 numberWithDouble:[[self moxUIElementsForSearchPredicate:searchPredicate]
799 - (void)moxSetFocused:(NSNumber*)focused {
800 MOZ_ASSERT(mGeckoAccessible);
802 if ([focused boolValue]) {
803 mGeckoAccessible->TakeFocus();
807 - (void)moxPerformScrollToVisible {
808 MOZ_ASSERT(mGeckoAccessible);
809 mGeckoAccessible->ScrollTo(nsIAccessibleScrollType::SCROLL_TYPE_ANYWHERE);
812 - (void)moxPerformShowMenu {
813 MOZ_ASSERT(mGeckoAccessible);
815 // We don't need to convert this rect into mac coordinates because the
816 // mouse event synthesizer expects layout (gecko) coordinates.
817 LayoutDeviceIntRect bounds = mGeckoAccessible->Bounds();
819 LocalAccessible* rootAcc = mGeckoAccessible->IsLocal()
820 ? mGeckoAccessible->AsLocal()->RootAccessible()
821 : mGeckoAccessible->AsRemote()
822 ->OuterDocOfRemoteBrowser()
825 GetObjectOrRepresentedView(GetNativeFromGeckoAccessible(rootAcc));
827 LayoutDeviceIntPoint p = LayoutDeviceIntPoint(
828 bounds.X() + (bounds.Width() / 2), bounds.Y() + (bounds.Height() / 2));
829 nsIWidget* widget = [objOrView widget];
830 widget->SynthesizeNativeMouseEvent(
831 p, nsIWidget::NativeMouseMessage::ButtonDown, MouseButton::eSecondary,
832 nsIWidget::Modifiers::NO_MODIFIERS, nullptr);
835 - (void)moxPerformPress {
836 MOZ_ASSERT(mGeckoAccessible);
838 mGeckoAccessible->DoAction(0);
843 - (BOOL)disableChild:(mozAccessible*)child {
847 - (void)maybePostLiveRegionChanged {
848 id<MOXAccessible> liveRegion =
849 [self moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) {
850 return [moxAcc moxIsLiveRegion];
854 [liveRegion moxPostNotification:@"AXLiveRegionChanged"];
858 - (void)maybePostA11yUtilNotification {
859 MOZ_ASSERT(mGeckoAccessible);
860 // Sometimes we use a special live region to make announcements to the user.
861 // This region is a child of the root document, but doesn't contain any
862 // content. If we try to fire regular AXLiveRegion changed events through it,
863 // VoiceOver clips the notifications because it (rightfully) doesn't detect
864 // focus within the region. We get around this by firing an
865 // AXAnnouncementRequested notification here instead.
866 // Verify we're trying to send a notification for the a11yUtils alert (and not
867 // a random acc with the same ID) by checking:
868 // - The gecko acc is local, our a11y-announcement lives in browser.xhtml
869 // - The ID of the gecko acc is "a11y-announcement"
870 // - The native acc is a direct descendent of the root
871 if (mGeckoAccessible->IsLocal() &&
872 [[self moxDOMIdentifier] isEqualToString:@"a11y-announcement"] &&
873 [[self moxParent] isKindOfClass:[mozRootAccessible class]]) {
874 // Our actual announcement should be stored as a child of the alert,
875 // so we verify a child exists, and then query that child below.
876 NSArray* children = [self moxChildren];
877 MOZ_ASSERT([children count] == 1 && children[0],
878 "A11yUtil event received, but no announcement found?");
880 mozAccessible* announcement = children[0];
882 if ([announcement providesLabelNotTitle]) {
883 key = [announcement moxLabel];
885 key = [announcement moxTitle];
888 NSDictionary* info = @{
889 NSAccessibilityAnnouncementKey : key ? key : @(""),
890 NSAccessibilityPriorityKey : @(NSAccessibilityPriorityMedium)
893 id window = [self moxWindow];
895 // This sends events via nsIObserverService to be consumed by our
896 // mochitests. Normally we'd fire these events through moxPostNotification
897 // which takes care of this, but because the window we fetch above isn't
898 // derrived from MOXAccessibleBase, we do this (and post the notification)
900 xpcAccessibleMacEvent::FireEvent(
901 window, NSAccessibilityAnnouncementRequestedNotification, info);
902 NSAccessibilityPostNotificationWithUserInfo(
903 window, NSAccessibilityAnnouncementRequestedNotification, info);
907 - (NSArray<mozAccessible*>*)getRelationsByType:(RelationType)relationType {
908 NSMutableArray<mozAccessible*>* relations =
909 [[[NSMutableArray alloc] init] autorelease];
910 Relation rel = mGeckoAccessible->RelationByType(relationType);
911 while (Accessible* relAcc = rel.Next()) {
912 if (mozAccessible* relNative = GetNativeFromGeckoAccessible(relAcc)) {
913 [relations addObject:relNative];
920 - (void)handleAccessibleTextChangeEvent:(NSString*)change
921 inserted:(BOOL)isInserted
922 inContainer:(Accessible*)container
926 - (void)handleAccessibleEvent:(uint32_t)eventType {
928 case nsIAccessibleEvent::EVENT_ALERT:
929 [self maybePostA11yUtilNotification];
931 case nsIAccessibleEvent::EVENT_FOCUS:
932 [self moxPostNotification:
933 NSAccessibilityFocusedUIElementChangedNotification];
935 case nsIAccessibleEvent::EVENT_MENUPOPUP_START:
936 [self moxPostNotification:@"AXMenuOpened"];
938 case nsIAccessibleEvent::EVENT_MENUPOPUP_END:
939 [self moxPostNotification:@"AXMenuClosed"];
941 case nsIAccessibleEvent::EVENT_SELECTION:
942 case nsIAccessibleEvent::EVENT_SELECTION_ADD:
943 case nsIAccessibleEvent::EVENT_SELECTION_REMOVE:
944 case nsIAccessibleEvent::EVENT_SELECTION_WITHIN:
945 [self moxPostNotification:
946 NSAccessibilitySelectedChildrenChangedNotification];
948 case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
949 if (![self stateWithMask:states::SELECTABLE_TEXT]) {
952 // We consider any caret move event to be a selected text change event.
953 // So dispatching an event for EVENT_TEXT_SELECTION_CHANGED would be
955 MOXTextMarkerDelegate* delegate =
956 static_cast<MOXTextMarkerDelegate*>([self moxTextMarkerDelegate]);
957 NSMutableDictionary* userInfo =
958 [[[delegate selectionChangeInfo] mutableCopy] autorelease];
959 userInfo[@"AXTextChangeElement"] = self;
961 mozAccessible* webArea = [self topWebArea];
963 moxPostNotification:NSAccessibilitySelectedTextChangedNotification
964 withUserInfo:userInfo];
965 [self moxPostNotification:NSAccessibilitySelectedTextChangedNotification
966 withUserInfo:userInfo];
969 case nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED:
970 mIsLiveRegion = true;
971 [self moxPostNotification:@"AXLiveRegionCreated"];
973 case nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED:
974 mIsLiveRegion = false;
976 case nsIAccessibleEvent::EVENT_REORDER:
977 [self maybePostLiveRegionChanged];
979 case nsIAccessibleEvent::EVENT_NAME_CHANGE: {
980 if (![self providesLabelNotTitle]) {
981 [self moxPostNotification:NSAccessibilityTitleChangedNotification];
983 [self maybePostLiveRegionChanged];
990 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
992 mGeckoAccessible = nullptr;
994 [self moxPostNotification:NSAccessibilityUIElementDestroyedNotification];
996 NS_OBJC_END_TRY_IGNORE_BLOCK;
1000 return !mGeckoAccessible;