mozSelectableElements.mm (11263B)
1 /* clang-format off */ 2 /* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 3 /* clang-format on */ 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/. */ 7 8 #import "mozSelectableElements.h" 9 #import "MOXWebAreaAccessible.h" 10 #import "MacUtils.h" 11 #include "LocalAccessible-inl.h" 12 #include "nsCocoaUtils.h" 13 14 using namespace mozilla::a11y; 15 16 @implementation mozSelectableAccessible 17 18 /** 19 * Return the mozAccessibles that are selectable. 20 */ 21 - (NSArray*)selectableChildren { 22 NSArray* toFilter; 23 if ([self isKindOfClass:[mozMenuAccessible class]]) { 24 // If we are a menu, our children are only selectable if they are visible 25 // so we filter this array instead of our unignored children list, which may 26 // contain invisible items. 27 toFilter = [static_cast<mozMenuAccessible*>(self) moxVisibleChildren]; 28 } else { 29 NSMutableArray* flattened = [[[NSMutableArray alloc] init] autorelease]; 30 void (^__block unnest)(NSArray*); 31 // Listboxes can contain nested groups, which then contain options. 32 // In order for VoiceOver to property advertise an option as 33 // "selected", it needs to appear in AXSelectableChildren, so 34 // we unnest any groups and present all "leaf" options as 35 // selectable here. We do this here, instead of relying solely on our 36 // `ignore` functions because multiple levels of groups could exist, and the 37 // `ignore` functions only analyse single-level parent-child relationships. 38 // Additionally, we don't want to override `moxUnignoredChildren` with this 39 // logic because groups should still be exposed in the tree. 40 unnest = ^(NSArray* children) { 41 for (mozAccessible* child : children) { 42 if ([[child moxRole] isEqualToString:@"AXGroup"]) { 43 unnest([child moxUnignoredChildren]); 44 } else { 45 [flattened addObject:child]; 46 } 47 } 48 }; 49 // This will do classic ignore-filtering, ensuring we don't expose accs that 50 // are invisible or defunct as selectable. 51 unnest([self moxUnignoredChildren]); 52 toFilter = flattened; 53 } 54 return [toFilter 55 filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL( 56 mozAccessible* child, 57 NSDictionary* bindings) { 58 return [child isKindOfClass:[mozSelectableChildAccessible class]]; 59 }]]; 60 } 61 62 - (void)moxSetSelectedChildren:(NSArray*)selectedChildren { 63 for (id child in [self selectableChildren]) { 64 BOOL selected = 65 [selectedChildren indexOfObjectIdenticalTo:child] != NSNotFound; 66 [child moxSetSelected:@(selected)]; 67 } 68 } 69 70 /** 71 * Return the mozAccessibles that are actually selected. 72 */ 73 - (NSArray*)moxSelectedChildren { 74 return [[self selectableChildren] 75 filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL( 76 mozAccessible* child, 77 NSDictionary* bindings) { 78 // Return mozSelectableChildAccessibles that have are selected (truthy 79 // value). 80 return [[(mozSelectableChildAccessible*)child moxSelected] boolValue]; 81 }]]; 82 } 83 84 @end 85 86 @implementation mozSelectableChildAccessible 87 88 - (NSNumber*)moxSelected { 89 return @([self stateWithMask:states::SELECTED] != 0); 90 } 91 92 - (void)moxSetSelected:(NSNumber*)selected { 93 // Get SELECTABLE and UNAVAILABLE state. 94 uint64_t state = 95 [self stateWithMask:(states::SELECTABLE | states::UNAVAILABLE)]; 96 if ((state & states::SELECTABLE) == 0 || (state & states::UNAVAILABLE) != 0) { 97 // The object is either not selectable or is unavailable. Don't do anything. 98 return; 99 } 100 101 mGeckoAccessible->SetSelected([selected boolValue]); 102 } 103 104 @end 105 106 @implementation mozTabGroupAccessible 107 108 - (NSArray*)moxTabs { 109 return [self selectableChildren]; 110 } 111 112 - (NSArray*)moxContents { 113 return [self moxUnignoredChildren]; 114 } 115 116 - (id)moxValue { 117 // The value of a tab group is its selected child. In the case 118 // of multiple selections this will return the first one. 119 return [[self moxSelectedChildren] firstObject]; 120 } 121 122 @end 123 124 @implementation mozTabAccessible 125 126 - (NSString*)moxRoleDescription { 127 return utils::LocalizedString(u"tab"_ns); 128 } 129 130 - (id)moxValue { 131 // Retuens 1 if item is selected, 0 if not. 132 return [self moxSelected]; 133 } 134 135 @end 136 137 @implementation mozListboxAccessible 138 139 - (BOOL)moxIgnoreChild:(mozAccessible*)child { 140 if (!child || [[child moxRole] isEqualToString:@"AXGroup"]) { 141 return YES; 142 } 143 144 return [super moxIgnoreChild:child]; 145 } 146 147 - (BOOL)disableChild:(mozAccessible*)child { 148 return ![child isKindOfClass:[mozSelectableChildAccessible class]]; 149 } 150 151 - (NSString*)moxOrientation { 152 return NSAccessibilityUnknownOrientationValue; 153 } 154 155 @end 156 157 @implementation mozOptionAccessible 158 159 - (NSString*)moxTitle { 160 return @""; 161 } 162 163 - (id)moxValue { 164 // Swap title and value of option so it behaves more like a AXStaticText. 165 return [super moxTitle]; 166 } 167 168 @end 169 170 @implementation mozMenuAccessible 171 172 - (NSString*)moxTitle { 173 return @""; 174 } 175 176 - (NSString*)moxLabel { 177 return @""; 178 } 179 180 - (BOOL)moxIgnoreWithParent:(mozAccessible*)parent { 181 // This helps us generate the correct moxChildren array for 182 // a sub menu -- that returned array should contain all 183 // menu items, regardless of if they are visible or not. 184 // Because moxChildren does ignore filtering, and because 185 // our base ignore method filters out invisible accessibles, 186 // we override this method. 187 if ([parent isKindOfClass:[MOXWebAreaAccessible class]] || 188 [parent isKindOfClass:[MOXRootGroup class]]) { 189 // We are a top level menu. Check our visibility the normal way 190 return [super moxIgnoreWithParent:parent]; 191 } 192 193 if ([parent isKindOfClass:[mozMenuItemAccessible class]] && 194 [parent geckoAccessible]->Role() == roles::PARENT_MENUITEM) { 195 // We are a submenu. If our parent menu item is in an open menu 196 // we should not be ignored 197 id grandparent = [parent moxParent]; 198 if ([grandparent isKindOfClass:[mozMenuAccessible class]]) { 199 mozMenuAccessible* parentMenu = 200 static_cast<mozMenuAccessible*>(grandparent); 201 return ![parentMenu isOpened]; 202 } 203 } 204 205 // Otherwise, we call into our superclass's ignore method 206 // to handle menus that are not submenus 207 return [super moxIgnoreWithParent:parent]; 208 } 209 210 - (NSArray*)moxVisibleChildren { 211 // VO expects us to expose two lists of children on menus: all children 212 // (done in moxUnignoredChildren), and children which are visible (here). 213 // We implement ignoreWithParent for both menus and menu items 214 // to ensure moxUnignoredChildren returns a complete list of children 215 // regardless of visibility, see comments in those methods for additional 216 // info. 217 return [[self moxChildren] 218 filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL( 219 mozAccessible* child, 220 NSDictionary* bindings) { 221 if (LocalAccessible* acc = [child geckoAccessible]->AsLocal()) { 222 if (acc->IsContent() && acc->GetContent()->IsXULElement()) { 223 return ((acc->VisibilityState() & states::INVISIBLE) == 0); 224 } 225 } 226 return true; 227 }]]; 228 } 229 230 - (id)moxTitleUIElement { 231 id parent = [self moxUnignoredParent]; 232 if (parent && [parent isKindOfClass:[mozAccessible class]]) { 233 return parent; 234 } 235 236 return nil; 237 } 238 239 - (void)moxPostNotification:(NSString*)notification { 240 if ([notification isEqualToString:@"AXMenuOpened"]) { 241 mIsOpened = YES; 242 } else if ([notification isEqualToString:@"AXMenuClosed"]) { 243 mIsOpened = NO; 244 } 245 246 [super moxPostNotification:notification]; 247 } 248 249 - (void)expire { 250 if (mIsOpened) { 251 // VO needs to receive a menu closed event when the menu goes away. 252 // If the menu is being destroyed, send a menu closed event first. 253 [self moxPostNotification:@"AXMenuClosed"]; 254 } 255 256 [super expire]; 257 } 258 259 - (BOOL)isOpened { 260 return mIsOpened; 261 } 262 263 @end 264 265 @implementation mozMenuItemAccessible 266 267 - (BOOL)moxIgnoreWithParent:(mozAccessible*)parent { 268 // This helps us generate the correct moxChildren array for 269 // a mozMenuAccessible; the returned array should contain all 270 // menu items, regardless of if they are visible or not. 271 // Because moxChildren does ignore filtering, and because 272 // our base ignore method filters out invisible accessibles, 273 // we override this method. 274 Accessible* parentAcc = [parent geckoAccessible]; 275 if (parentAcc) { 276 Accessible* grandparentAcc = parentAcc->Parent(); 277 if (mozAccessible* directGrandparent = 278 GetNativeFromGeckoAccessible(grandparentAcc)) { 279 if ([directGrandparent isKindOfClass:[MOXWebAreaAccessible class]]) { 280 return [parent moxIgnoreWithParent:directGrandparent]; 281 } 282 } 283 } 284 285 id grandparent = [parent moxParent]; 286 if ([grandparent isKindOfClass:[mozMenuItemAccessible class]]) { 287 mozMenuItemAccessible* acc = 288 static_cast<mozMenuItemAccessible*>(grandparent); 289 if ([acc geckoAccessible]->Role() == roles::PARENT_MENUITEM) { 290 mozMenuAccessible* parentMenu = static_cast<mozMenuAccessible*>(parent); 291 // if we are a menu item in a submenu, display only when 292 // parent menu item is open 293 return ![parentMenu isOpened]; 294 } 295 } 296 297 // Otherwise, we call into our superclass's method to handle 298 // menuitems that are not within submenus 299 return [super moxIgnoreWithParent:parent]; 300 } 301 302 - (NSString*)moxMenuItemMarkChar { 303 LocalAccessible* acc = mGeckoAccessible->AsLocal(); 304 if (acc && acc->IsContent() && 305 acc->GetContent()->IsXULElement(nsGkAtoms::menuitem)) { 306 // We need to provide a marker character. This is the visible "√" you see 307 // on dropdown menus. In our a11y tree this is a single child text node 308 // of the menu item. 309 // We do this only with XUL menuitems that conform to the native theme, and 310 // not with aria menu items that might have a pseudo element or something. 311 if (acc->ChildCount() == 1 && 312 acc->LocalFirstChild()->Role() == roles::STATICTEXT) { 313 nsAutoString marker; 314 acc->LocalFirstChild()->Name(marker); 315 if (marker.Length() == 1) { 316 return nsCocoaUtils::ToNSString(marker); 317 } 318 } 319 } 320 321 return nil; 322 } 323 324 - (NSNumber*)moxSelected { 325 // Our focused state is equivelent to native selected states for menus. 326 return @([self stateWithMask:states::FOCUSED] != 0); 327 } 328 329 - (void)handleAccessibleEvent:(uint32_t)eventType { 330 switch (eventType) { 331 case nsIAccessibleEvent::EVENT_FOCUS: 332 // Our focused state is equivelent to native selected states for menus. 333 mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent]; 334 [parent moxPostNotification: 335 NSAccessibilitySelectedChildrenChangedNotification]; 336 break; 337 } 338 339 [super handleAccessibleEvent:eventType]; 340 } 341 342 - (void)moxPerformPress { 343 [super moxPerformPress]; 344 // when a menu item is pressed (chosen), we need to tell 345 // VoiceOver about it, so we send this notification 346 [self moxPostNotification:@"AXMenuItemSelected"]; 347 } 348 349 @end