summaryrefslogtreecommitdiffstats
path: root/accessible/mac/mozSelectableElements.mm
diff options
context:
space:
mode:
Diffstat (limited to 'accessible/mac/mozSelectableElements.mm')
-rw-r--r--accessible/mac/mozSelectableElements.mm336
1 files changed, 336 insertions, 0 deletions
diff --git a/accessible/mac/mozSelectableElements.mm b/accessible/mac/mozSelectableElements.mm
new file mode 100644
index 0000000000..693514e405
--- /dev/null
+++ b/accessible/mac/mozSelectableElements.mm
@@ -0,0 +1,336 @@
+/* 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 "mozSelectableElements.h"
+#import "MOXWebAreaAccessible.h"
+#import "MacUtils.h"
+#include "LocalAccessible-inl.h"
+#include "nsCocoaUtils.h"
+
+using namespace mozilla::a11y;
+
+@implementation mozSelectableAccessible
+
+/**
+ * Return the mozAccessibles that are selectable.
+ */
+- (NSArray*)selectableChildren {
+ NSArray* toFilter;
+ if ([self isKindOfClass:[mozMenuAccessible class]]) {
+ // If we are a menu, our children are only selectable if they are visible
+ // so we filter this array instead of our unignored children list, which may
+ // contain invisible items.
+ toFilter = [static_cast<mozMenuAccessible*>(self) moxVisibleChildren];
+ } else {
+ toFilter = [self moxUnignoredChildren];
+ }
+ return [toFilter
+ filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
+ mozAccessible* child,
+ NSDictionary* bindings) {
+ return [child isKindOfClass:[mozSelectableChildAccessible class]];
+ }]];
+}
+
+- (void)moxSetSelectedChildren:(NSArray*)selectedChildren {
+ for (id child in [self selectableChildren]) {
+ BOOL selected =
+ [selectedChildren indexOfObjectIdenticalTo:child] != NSNotFound;
+ [child moxSetSelected:@(selected)];
+ }
+}
+
+/**
+ * Return the mozAccessibles that are actually selected.
+ */
+- (NSArray*)moxSelectedChildren {
+ return [[self selectableChildren]
+ filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
+ mozAccessible* child,
+ NSDictionary* bindings) {
+ // Return mozSelectableChildAccessibles that have are selected (truthy
+ // value).
+ return [[(mozSelectableChildAccessible*)child moxSelected] boolValue];
+ }]];
+}
+
+@end
+
+@implementation mozSelectableChildAccessible
+
+- (NSNumber*)moxSelected {
+ return @([self stateWithMask:states::SELECTED] != 0);
+}
+
+- (void)moxSetSelected:(NSNumber*)selected {
+ // Get SELECTABLE and UNAVAILABLE state.
+ uint64_t state =
+ [self stateWithMask:(states::SELECTABLE | states::UNAVAILABLE)];
+ if ((state & states::SELECTABLE) == 0 || (state & states::UNAVAILABLE) != 0) {
+ // The object is either not selectable or is unavailable. Don't do anything.
+ return;
+ }
+
+ mGeckoAccessible->SetSelected([selected boolValue]);
+
+ // We need to invalidate the state because the accessibility service
+ // may check the selected attribute synchornously and not wait for
+ // selection events.
+ [self invalidateState];
+}
+
+@end
+
+@implementation mozTabGroupAccessible
+
+- (NSArray*)moxTabs {
+ return [self selectableChildren];
+}
+
+- (NSArray*)moxContents {
+ return [self moxUnignoredChildren];
+}
+
+- (id)moxValue {
+ // The value of a tab group is its selected child. In the case
+ // of multiple selections this will return the first one.
+ return [[self moxSelectedChildren] firstObject];
+}
+
+@end
+
+@implementation mozTabAccessible
+
+- (NSString*)moxRoleDescription {
+ return utils::LocalizedString(u"tab"_ns);
+}
+
+- (id)moxValue {
+ // Retuens 1 if item is selected, 0 if not.
+ return [self moxSelected];
+}
+
+@end
+
+@implementation mozListboxAccessible
+
+- (BOOL)moxIgnoreChild:(mozAccessible*)child {
+ if (!child || child->mRole == roles::GROUPING) {
+ return YES;
+ }
+
+ return [super moxIgnoreChild:child];
+}
+
+- (BOOL)disableChild:(mozAccessible*)child {
+ return ![child isKindOfClass:[mozSelectableChildAccessible class]];
+}
+
+- (NSString*)moxOrientation {
+ return NSAccessibilityUnknownOrientationValue;
+}
+
+@end
+
+@implementation mozOptionAccessible
+
+- (NSString*)moxTitle {
+ return @"";
+}
+
+- (id)moxValue {
+ // Swap title and value of option so it behaves more like a AXStaticText.
+ return [super moxTitle];
+}
+
+@end
+
+@implementation mozMenuAccessible
+
+- (NSString*)moxTitle {
+ return @"";
+}
+
+- (NSString*)moxLabel {
+ return @"";
+}
+
+- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent {
+ // This helps us generate the correct moxChildren array for
+ // a sub menu -- that returned array should contain all
+ // menu items, regardless of if they are visible or not.
+ // Because moxChildren does ignore filtering, and because
+ // our base ignore method filters out invisible accessibles,
+ // we override this method.
+ if ([parent isKindOfClass:[MOXWebAreaAccessible class]] ||
+ [parent isKindOfClass:[MOXRootGroup class]]) {
+ // We are a top level menu. Check our visibility the normal way
+ return [super moxIgnoreWithParent:parent];
+ }
+
+ if ([parent isKindOfClass:[mozMenuItemAccessible class]] &&
+ [parent geckoAccessible]->Role() == roles::PARENT_MENUITEM) {
+ // We are a submenu. If our parent menu item is in an open menu
+ // we should not be ignored
+ id grandparent = [parent moxParent];
+ if ([grandparent isKindOfClass:[mozMenuAccessible class]]) {
+ mozMenuAccessible* parentMenu =
+ static_cast<mozMenuAccessible*>(grandparent);
+ return ![parentMenu isOpened];
+ }
+ }
+
+ // Otherwise, we call into our superclass's ignore method
+ // to handle menus that are not submenus
+ return [super moxIgnoreWithParent:parent];
+}
+
+- (NSArray*)moxVisibleChildren {
+ // VO expects us to expose two lists of children on menus: all children
+ // (done in moxUnignoredChildren), and children which are visible (here).
+ // We implement ignoreWithParent for both menus and menu items
+ // to ensure moxUnignoredChildren returns a complete list of children
+ // regardless of visibility, see comments in those methods for additional
+ // info.
+ return [[self moxChildren]
+ filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
+ mozAccessible* child,
+ NSDictionary* bindings) {
+ if (LocalAccessible* acc = [child geckoAccessible]->AsLocal()) {
+ if (acc->IsContent() && acc->GetContent()->IsXULElement()) {
+ return ((acc->VisibilityState() & states::INVISIBLE) == 0);
+ }
+ }
+ return true;
+ }]];
+}
+
+- (id)moxTitleUIElement {
+ id parent = [self moxUnignoredParent];
+ if (parent && [parent isKindOfClass:[mozAccessible class]]) {
+ return parent;
+ }
+
+ return nil;
+}
+
+- (void)moxPostNotification:(NSString*)notification {
+ if ([notification isEqualToString:@"AXMenuOpened"]) {
+ mIsOpened = YES;
+ } else if ([notification isEqualToString:@"AXMenuClosed"]) {
+ mIsOpened = NO;
+ }
+
+ [super moxPostNotification:notification];
+}
+
+- (void)expire {
+ if (mIsOpened) {
+ // VO needs to receive a menu closed event when the menu goes away.
+ // If the menu is being destroyed, send a menu closed event first.
+ [self moxPostNotification:@"AXMenuClosed"];
+ }
+
+ [super expire];
+}
+
+- (BOOL)isOpened {
+ return mIsOpened;
+}
+
+@end
+
+@implementation mozMenuItemAccessible
+
+- (NSString*)moxLabel {
+ return @"";
+}
+
+- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent {
+ // This helps us generate the correct moxChildren array for
+ // a mozMenuAccessible; the returned array should contain all
+ // menu items, regardless of if they are visible or not.
+ // Because moxChildren does ignore filtering, and because
+ // our base ignore method filters out invisible accessibles,
+ // we override this method.
+ Accessible* parentAcc = [parent geckoAccessible];
+ if (parentAcc) {
+ Accessible* grandparentAcc = parentAcc->Parent();
+ if (mozAccessible* directGrandparent =
+ GetNativeFromGeckoAccessible(grandparentAcc)) {
+ if ([directGrandparent isKindOfClass:[MOXWebAreaAccessible class]]) {
+ return [parent moxIgnoreWithParent:directGrandparent];
+ }
+ }
+ }
+
+ id grandparent = [parent moxParent];
+ if ([grandparent isKindOfClass:[mozMenuItemAccessible class]]) {
+ mozMenuItemAccessible* acc =
+ static_cast<mozMenuItemAccessible*>(grandparent);
+ if ([acc geckoAccessible]->Role() == roles::PARENT_MENUITEM) {
+ mozMenuAccessible* parentMenu = static_cast<mozMenuAccessible*>(parent);
+ // if we are a menu item in a submenu, display only when
+ // parent menu item is open
+ return ![parentMenu isOpened];
+ }
+ }
+
+ // Otherwise, we call into our superclass's method to handle
+ // menuitems that are not within submenus
+ return [super moxIgnoreWithParent:parent];
+}
+
+- (NSString*)moxMenuItemMarkChar {
+ LocalAccessible* acc = mGeckoAccessible->AsLocal();
+ if (acc && acc->IsContent() &&
+ acc->GetContent()->IsXULElement(nsGkAtoms::menuitem)) {
+ // We need to provide a marker character. This is the visible "√" you see
+ // on dropdown menus. In our a11y tree this is a single child text node
+ // of the menu item.
+ // We do this only with XUL menuitems that conform to the native theme, and
+ // not with aria menu items that might have a pseudo element or something.
+ if (acc->ChildCount() == 1 &&
+ acc->LocalFirstChild()->Role() == roles::STATICTEXT) {
+ nsAutoString marker;
+ acc->LocalFirstChild()->Name(marker);
+ if (marker.Length() == 1) {
+ return nsCocoaUtils::ToNSString(marker);
+ }
+ }
+ }
+
+ return nil;
+}
+
+- (NSNumber*)moxSelected {
+ // Our focused state is equivelent to native selected states for menus.
+ return @([self stateWithMask:states::FOCUSED] != 0);
+}
+
+- (void)handleAccessibleEvent:(uint32_t)eventType {
+ switch (eventType) {
+ case nsIAccessibleEvent::EVENT_FOCUS:
+ [self invalidateState];
+ // Our focused state is equivelent to native selected states for menus.
+ mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent];
+ [parent moxPostNotification:
+ NSAccessibilitySelectedChildrenChangedNotification];
+ break;
+ }
+
+ [super handleAccessibleEvent:eventType];
+}
+
+- (void)moxPerformPress {
+ [super moxPerformPress];
+ // when a menu item is pressed (chosen), we need to tell
+ // VoiceOver about it, so we send this notification
+ [self moxPostNotification:@"AXMenuItemSelected"];
+}
+
+@end