blob: 0b983a8e36f9ae4c91d72e06424ea4118ddb740c [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ui/accessibility/platform/ax_platform_node_cocoa.h"
#import <Cocoa/Cocoa.h>
#include <Foundation/Foundation.h>
#include "base/apple/foundation_util.h"
#include "base/compiler_specific.h"
#include "base/logging.h"
#include "base/mac/mac_util.h"
#include "base/memory/raw_ptr_exclusion.h"
#include "base/no_destructor.h"
#include "base/notimplemented.h"
#include "base/strings/sys_string_conversions.h"
#include "base/trace_event/trace_event.h"
#include "skia/ext/skia_utils_mac.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_range.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/accessibility/platform/ax_platform_node_mac.h"
#include "ui/accessibility/platform/ax_private_attributes_mac.h"
#include "ui/accessibility/platform/ax_private_roles_mac.h"
#include "ui/accessibility/platform/ax_utils_mac.h"
#include "ui/accessibility/platform/child_iterator.h"
#include "ui/base/l10n/l10n_util.h"
#import "ui/gfx/mac/coordinate_conversion.h"
#include "ui/strings/grit/ax_strings.h"
using AXRange = ui::AXPlatformNodeDelegate::AXRange;
// Not defined in current versions of library, but may be in the future:
#define NSAccessibilityChildrenInNavigationOrderAttribute \
@"AXChildrenInNavigationOrder"
@interface AXAnnouncementSpec ()
@property(nonatomic, strong) NSString* announcement;
@property(nonatomic, strong) NSWindow* window;
@property(nonatomic, assign) BOOL polite;
@end
@implementation AXAnnouncementSpec
@synthesize announcement = _announcement;
@synthesize window = _window;
@synthesize polite = _polite;
@end
namespace {
// Same length as web content/WebKit.
int kLiveRegionDebounceMillis = 20;
using RoleMap = std::map<ax::mojom::Role, NSString*>;
using EventMap = std::map<ax::mojom::Event, NSString*>;
RoleMap BuildSubroleMap() {
const RoleMap::value_type subroles[] = {
{ax::mojom::Role::kAlert, @"AXApplicationAlert"},
{ax::mojom::Role::kAlertDialog, @"AXApplicationAlertDialog"},
{ax::mojom::Role::kApplication, @"AXWebApplication"},
{ax::mojom::Role::kArticle, @"AXDocumentArticle"},
{ax::mojom::Role::kBanner, @"AXLandmarkBanner"},
{ax::mojom::Role::kCode, @"AXCodeStyleGroup"},
{ax::mojom::Role::kComplementary, @"AXLandmarkComplementary"},
{ax::mojom::Role::kContentDeletion, @"AXDeleteStyleGroup"},
{ax::mojom::Role::kContentInsertion, @"AXInsertStyleGroup"},
{ax::mojom::Role::kContentInfo, @"AXLandmarkContentInfo"},
{ax::mojom::Role::kDefinition, @"AXDefinition"},
{ax::mojom::Role::kDialog, @"AXApplicationDialog"},
{ax::mojom::Role::kDocument, @"AXDocument"},
{ax::mojom::Role::kEmphasis, @"AXEmphasisStyleGroup"},
{ax::mojom::Role::kFeed, @"AXApplicationGroup"},
{ax::mojom::Role::kFooter, @"AXLandmarkContentInfo"},
{ax::mojom::Role::kForm, @"AXLandmarkForm"},
{ax::mojom::Role::kGraphicsDocument, @"AXDocument"},
{ax::mojom::Role::kGroup, @"AXApplicationGroup"},
{ax::mojom::Role::kHeader, @"AXLandmarkBanner"},
{ax::mojom::Role::kLog, @"AXApplicationLog"},
{ax::mojom::Role::kMain, @"AXLandmarkMain"},
{ax::mojom::Role::kMarquee, @"AXApplicationMarquee"},
// https://w3c.github.io/mathml-aam/#mathml-element-mappings
{ax::mojom::Role::kMath, @"AXDocumentMath"},
{ax::mojom::Role::kMathMLFraction, @"AXMathFraction"},
{ax::mojom::Role::kMathMLIdentifier, @"AXMathIdentifier"},
{ax::mojom::Role::kMathMLMath, @"AXDocumentMath"},
{ax::mojom::Role::kMathMLMultiscripts, @"AXMathMultiscript"},
{ax::mojom::Role::kMathMLNoneScript, @"AXMathRow"},
{ax::mojom::Role::kMathMLNumber, @"AXMathNumber"},
{ax::mojom::Role::kMathMLOperator, @"AXMathOperator"},
{ax::mojom::Role::kMathMLOver, @"AXMathUnderOver"},
{ax::mojom::Role::kMathMLPrescriptDelimiter, @"AXMathRow"},
{ax::mojom::Role::kMathMLRoot, @"AXMathRoot"},
{ax::mojom::Role::kMathMLRow, @"AXMathRow"},
{ax::mojom::Role::kMathMLSquareRoot, @"AXMathSquareRoot"},
{ax::mojom::Role::kMathMLSub, @"AXMathSubscriptSuperscript"},
{ax::mojom::Role::kMathMLSubSup, @"AXMathSubscriptSuperscript"},
{ax::mojom::Role::kMathMLSup, @"AXMathSubscriptSuperscript"},
{ax::mojom::Role::kMathMLTable, @"AXMathTable"},
{ax::mojom::Role::kMathMLTableCell, @"AXMathTableCell"},
{ax::mojom::Role::kMathMLTableRow, @"AXMathTableRow"},
{ax::mojom::Role::kMathMLText, @"AXMathText"},
{ax::mojom::Role::kMathMLUnder, @"AXMathUnderOver"},
{ax::mojom::Role::kMathMLUnderOver, @"AXMathUnderOver"},
{ax::mojom::Role::kMeter, @"AXMeter"},
{ax::mojom::Role::kNavigation, @"AXLandmarkNavigation"},
{ax::mojom::Role::kNote, @"AXDocumentNote"},
{ax::mojom::Role::kRegion, @"AXLandmarkRegion"},
{ax::mojom::Role::kSearch, @"AXLandmarkSearch"},
{ax::mojom::Role::kSearchBox, @"AXSearchField"},
{ax::mojom::Role::kSectionFooter, @"AXSectionFooter"},
{ax::mojom::Role::kSectionHeader, @"AXSectionHeader"},
{ax::mojom::Role::kStatus, @"AXApplicationStatus"},
{ax::mojom::Role::kStrong, @"AXStrongStyleGroup"},
{ax::mojom::Role::kSubscript, @"AXSubscriptStyleGroup"},
{ax::mojom::Role::kSuperscript, @"AXSuperscriptStyleGroup"},
{ax::mojom::Role::kSwitch, @"AXSwitch"},
{ax::mojom::Role::kTab, @"AXTabButton"},
{ax::mojom::Role::kTabPanel, @"AXTabPanel"},
{ax::mojom::Role::kTerm, @"AXTerm"},
{ax::mojom::Role::kTime, @"AXTimeGroup"},
{ax::mojom::Role::kTimer, @"AXApplicationTimer"},
{ax::mojom::Role::kToggleButton, @"AXToggleButton"},
{ax::mojom::Role::kTooltip, @"AXUserInterfaceTooltip"},
{ax::mojom::Role::kTreeItem, NSAccessibilityOutlineRowSubrole},
};
return RoleMap(begin(subroles), end(subroles));
}
EventMap BuildEventMap() {
const EventMap::value_type events[] = {
{ax::mojom::Event::kCheckedStateChanged,
NSAccessibilityValueChangedNotification},
{ax::mojom::Event::kFocus,
NSAccessibilityFocusedUIElementChangedNotification},
{ax::mojom::Event::kFocusContext,
NSAccessibilityFocusedUIElementChangedNotification},
// Do not map kMenuStart/End to the Mac's opened/closed notifications.
// kMenuStart/End are fired at the start/end of menu interaction on the
// container of the menu; not the menu itself. All newly-opened/closed
// menus should fire kMenuPopupStart/End. See SubmenuView::ShowAt and
// SubmenuView::Hide.
{ax::mojom::Event::kMenuPopupStart, (NSString*)kAXMenuOpenedNotification},
{ax::mojom::Event::kMenuPopupEnd, (NSString*)kAXMenuClosedNotification},
{ax::mojom::Event::kTextChanged, NSAccessibilityTitleChangedNotification},
{ax::mojom::Event::kValueChanged,
NSAccessibilityValueChangedNotification},
{ax::mojom::Event::kTextSelectionChanged,
NSAccessibilitySelectedTextChangedNotification},
// TODO(patricialor): Add more events.
};
return EventMap(begin(events), end(events));
}
// Builds the pairings of accessibility actions and their Cocoa equivalents.
ui::CocoaActionList BuildActionList() {
const ui::CocoaActionList::value_type entries[] = {
// NSAccessibilityPressAction must come first in this list.
{ax::mojom::Action::kDoDefault, NSAccessibilityPressAction},
{ax::mojom::Action::kDecrement, NSAccessibilityDecrementAction},
{ax::mojom::Action::kIncrement, NSAccessibilityIncrementAction},
{ax::mojom::Action::kShowContextMenu, NSAccessibilityShowMenuAction},
};
return ui::CocoaActionList(begin(entries), end(entries));
}
// Returns a static vector of pairings of accessibility actions and their Cocoa
// equivalents.
const ui::CocoaActionList& GetCocoaActionList() {
static const base::NoDestructor<ui::CocoaActionList> action_list(
BuildActionList());
return *action_list;
}
void PostAnnouncementNotification(NSString* announcement,
NSWindow* window,
bool is_polite) {
NSAccessibilityPriorityLevel priority =
is_polite ? NSAccessibilityPriorityMedium : NSAccessibilityPriorityHigh;
NSDictionary* notification_info = @{
NSAccessibilityAnnouncementKey : announcement,
NSAccessibilityPriorityKey : @(priority)
};
// On Mojave, announcements from an inactive window aren't spoken.
NSAccessibilityPostNotificationWithUserInfo(
window, NSAccessibilityAnnouncementRequestedNotification,
notification_info);
}
// Returns true if |action| should be added implicitly for |data|.
bool HasImplicitAction(const ui::AXPlatformNodeBase& node,
ax::mojom::Action action) {
// TODO integrate the method into AXNodeData, see crrev.com/c/6115619
// for details.
switch (action) {
case ax::mojom::Action::kDoDefault:
return node.GetData().IsClickable();
case ax::mojom::Action::kDecrement:
case ax::mojom::Action::kIncrement:
return node.GetRole() == ax::mojom::Role::kSlider ||
node.GetRole() == ax::mojom::Role::kSpinButton;
default:
return false;
}
}
// For roles that show a menu for the default action, ensure "show menu" also
// appears in available actions, but only if that's not already used for a
// context menu. It will be mapped back to the default action when performed.
bool AlsoUseShowMenuActionForDefaultAction(const ui::AXPlatformNodeBase& node) {
return HasImplicitAction(node, ax::mojom::Action::kDoDefault) &&
!node.HasAction(ax::mojom::Action::kShowContextMenu) &&
(node.GetRole() == ax::mojom::Role::kPopUpButton ||
node.GetRole() == ax::mojom::Role::kComboBoxSelect);
}
// Check whether |selector| is an accessibility setter. This is a heuristic but
// seems to be a pretty good one.
bool IsAXSetter(SEL selector) {
return [NSStringFromSelector(selector) hasPrefix:@"setAccessibility"];
}
void CollectAncestorRoles(
const ui::AXNode& node,
std::map<ui::AXNodeID, std::set<ax::mojom::Role>>& out_ancestor_roles) {
if (out_ancestor_roles.contains(node.id()))
return;
out_ancestor_roles[node.id()] = {node.GetRole()};
if (!node.GetParent())
return;
CollectAncestorRoles(*node.GetParent(), out_ancestor_roles);
out_ancestor_roles[node.id()].insert(
out_ancestor_roles[node.GetParent()->id()].begin(),
out_ancestor_roles[node.GetParent()->id()].end());
}
} // namespace
namespace ui {
const ui::CocoaActionList& GetCocoaActionListForTesting() {
return GetCocoaActionList();
}
} // namespace ui
@interface AXPlatformNodeCocoa (Private)
// Helper function for string attributes that don't require extra processing.
- (NSString*)getStringAttribute:(ax::mojom::StringAttribute)attribute;
// Returns AXValue, or nil if AXValue isn't an NSString.
- (NSString*)getAXValueAsString;
// Returns the native wrapper for the given node id.
- (AXPlatformNodeCocoa*)fromNodeID:(ui::AXNodeID)id;
// Returns true if this object is an image.
- (BOOL)isImage;
@end
@implementation AXPlatformNodeCocoa {
// This field is not a raw_ptr<> because it requires @property rewrite.
RAW_PTR_EXCLUSION ui::AXPlatformNodeBase* _node; // Weak. Retains us.
AXAnnouncementSpec* __strong _pendingAnnouncement;
}
@synthesize node = _node;
// Required for AXCustomContentProvider, which defines the property.
@synthesize accessibilityCustomContent = _accessibilityCustomContent;
// The new NSAccessibility API is method-based, but the old NSAccessibility
// is attribute-based. For every method, there is a corresponding attribute.
// This function returns the map between the methods and the attributes
// for purposes of migrating to the new API.
+ (NSDictionary*)newAccessibilityAPIMethodToAttributeMap {
static NSDictionary* dict = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dict = @{
@"accessibilityCellForColumn:row:" :
NSAccessibilityCellForColumnAndRowParameterizedAttribute,
@"accessibilityChildrenInNavigationOrder" :
NSAccessibilityChildrenInNavigationOrderAttribute,
@"accessibilityColumns" : NSAccessibilityColumnsAttribute,
@"accessibilityColumnCount" : NSAccessibilityColumnCountAttribute,
@"accessibilityColumnIndexRange" :
NSAccessibilityColumnIndexRangeAttribute,
@"accessibilityDisclosedByRow" : NSAccessibilityDisclosedByRowAttribute,
@"accessibilityDisclosedRows" : NSAccessibilityDisclosedRowsAttribute,
@"accessibilityDisclosureLevel" : NSAccessibilityDisclosureLevelAttribute,
@"accessibilityHeader" : NSAccessibilityHeaderAttribute,
@"accessibilityHorizontalScrollBar" :
NSAccessibilityHorizontalScrollBarAttribute,
@"accessibilityIndex" : NSAccessibilityIndexAttribute,
@"accessibilityLinkedUIElements" :
NSAccessibilityLinkedUIElementsAttribute,
@"accessibilityRowCount" : NSAccessibilityRowCountAttribute,
@"accessibilityRowHeaderUIElements" :
NSAccessibilityRowHeaderUIElementsAttribute,
@"accessibilityRowIndexRange" : NSAccessibilityRowIndexRangeAttribute,
@"accessibilitySortDirection" : NSAccessibilitySortDirectionAttribute,
@"accessibilitySplitters" : NSAccessibilitySplittersAttribute,
@"accessibilityTabs" : NSAccessibilityTabsAttribute,
@"accessibilityToolbarButton" : NSAccessibilityToolbarButtonAttribute,
@"accessibilityVerticalScrollBar" :
NSAccessibilityVerticalScrollBarAttribute,
@"accessibilityVisibleColumns" : NSAccessibilityVisibleColumnsAttribute,
@"accessibilityVisibleCells" : NSAccessibilityVisibleCellsAttribute,
@"accessibilityVisibleRows" : NSAccessibilityVisibleRowsAttribute,
@"isAccessibilityDisclosed" : NSAccessibilityDisclosingAttribute,
@"isAccessibilityExpanded" : NSAccessibilityExpandedAttribute,
@"isAccessibilityFocused" : NSAccessibilityFocusedAttribute,
};
});
return dict;
}
// Similar to newAccessibilityAPIMethodToAttributeMap but for actions.
+ (NSDictionary*)newAccessibilityAPIMethodToActionMap {
static NSDictionary* dict = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dict = @{
@"accessibilityPerformConfirm" : NSAccessibilityConfirmAction,
@"accessibilityPerformPress" : NSAccessibilityPressAction,
@"accessibilityPerformShowMenu" : NSAccessibilityShowMenuAction,
@"accessibilityPerformDecrement" : NSAccessibilityDecrementAction,
@"accessibilityPerformIncrement" : NSAccessibilityIncrementAction,
};
});
return dict;
}
// Returns the set of attributes available through the new Cocoa
// accessibility API.
+ (NSSet<NSString*>*)attributesAvailableThroughNewAccessibilityAPI {
static NSSet<NSString*>* set = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
set = [NSSet<NSString*>
setWithArray:[[self newAccessibilityAPIMethodToAttributeMap]
allValues]];
});
return set;
}
// Returns the set of actions available through the new Cocoa
// accessibility API.
+ (NSSet<NSString*>*)actionsAvailableThroughNewAccessibilityAPI {
static NSSet<NSString*>* set = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
set = [NSSet<NSString*>
setWithArray:[[self newAccessibilityAPIMethodToActionMap] allValues]];
});
return set;
}
// Returns YES if `attribute` is available through a method implemented for
// the new accessibility API.
+ (BOOL)isAttributeAvailableThroughNewAccessibilityAPI:(NSString*)attribute {
if (features::IsMacAccessibilityAPIMigrationEnabled()) {
return [[self attributesAvailableThroughNewAccessibilityAPI]
containsObject:attribute];
}
return NO;
}
// Returns YES if `action` is available through a method implemented for
// the new accessibility API.
+ (BOOL)isActionAvailableThroughNewAccessibilityAPI:(NSString*)action {
if (features::IsMacAccessibilityAPIMigrationEnabled()) {
return [[self actionsAvailableThroughNewAccessibilityAPI]
containsObject:action];
}
return NO;
}
// Returns the set of methods implemented to support the new Cocoa
// accessibility API corresponding to old API attributes.
+ (NSSet<NSString*>*)newAccessibilityAPIMethods {
static NSSet<NSString*>* set = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
set = [NSSet<NSString*>
setWithArray:[[self newAccessibilityAPIMethodToAttributeMap] allKeys]];
});
return set;
}
// Returns YES if `method` has been implemented in the transition to the new
// accessibility API.
+ (BOOL)isMethodImplementedForNewAccessibilityAPI:(NSString*)method {
if (features::IsMacAccessibilityAPIMigrationEnabled()) {
return [[self newAccessibilityAPIMethods] containsObject:method];
}
return NO;
}
// Returns true if `method` has been implemented in the transition to the new
// accessibility API, and is supported by this node (based on its role).
- (BOOL)supportsNewAccessibilityAPIMethod:(NSString*)method {
if (!_node) {
return NO;
}
// Check whether the corresponding attribute is supported for this node.
NSString* attribute = [[[self class] newAccessibilityAPIMethodToAttributeMap]
objectForKey:method];
if (attribute) {
NSArray* attributeNames = [self internalAccessibilityAttributeNames];
if ([attributeNames containsObject:attribute]) {
return YES;
}
attributeNames = [self internalAccessibilityParameterizedAttributeNames];
return [attributeNames containsObject:attribute];
}
// Check whether the corresponding action is supported for this node.
NSString* action =
[[[self class] newAccessibilityAPIMethodToActionMap] objectForKey:method];
if (action) {
NSArray* actionNames = [self internalAccessibilityActionNames];
return [actionNames containsObject:action];
}
return NO;
}
- (BOOL)conditionallyRespondsToSelector:(SEL)selector {
static base::NoDestructor<std::unordered_set<SEL>> methodSelectorsForActions({
@selector(accessibilityPerformPress),
@selector(accessibilityPerformDecrement),
@selector(accessibilityPerformIncrement),
@selector(accessibilityPerformShowMenu),
@selector(accessibilityPerformConfirm)
});
static base::NoDestructor<std::unordered_set<SEL>>
methodSelectorsForParameterizedAttributes({
@selector(accessibilityCellForColumn:row:),
@selector(accessibilityRangeForIndex:),
@selector(accessibilityRangeForLine:),
@selector(accessibilityRangeForPosition:),
});
// See if the method is permitted by checking its corresponding parameterized
// attribute counterpart.
if (methodSelectorsForParameterizedAttributes->find(selector) !=
methodSelectorsForParameterizedAttributes->end()) {
NSString* selectorString = NSStringFromSelector(selector);
NSString* attribute =
[[AXPlatformNodeCocoa newAccessibilityAPIMethodToAttributeMap]
objectForKey:selectorString];
NSArray* attributes =
[self internalAccessibilityParameterizedAttributeNames];
if (![attributes containsObject:attribute]) {
return NO;
}
}
// See if the method is permitted by checking its corresponding action
// counterpart.
if (methodSelectorsForActions->find(selector) !=
methodSelectorsForActions->end()) {
NSString* selectorString = NSStringFromSelector(selector);
NSString* action =
[[AXPlatformNodeCocoa newAccessibilityAPIMethodToActionMap]
objectForKey:selectorString];
NSArray* actions = [self internalAccessibilityActionNames];
if (![actions containsObject:action]) {
return NO;
}
}
return YES;
}
- (BOOL)respondsToSelector:(SEL)selector {
// If we're in old-accessibility-API mode, disable methods that we've added
// to support the new API.
if (!features::IsMacAccessibilityAPIMigrationEnabled()) {
static base::NoDestructor<std::unordered_set<SEL>>
newAccessibilityAPISelectors;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSSet<NSString*>* methodNames =
[AXPlatformNodeCocoa newAccessibilityAPIMethods];
for (NSString* methodName in methodNames) {
SEL methodSelector = NSSelectorFromString(methodName);
newAccessibilityAPISelectors->insert(methodSelector);
}
});
if (newAccessibilityAPISelectors->find(selector) !=
newAccessibilityAPISelectors->end()) {
return NO;
}
} else {
// The following deprecated selectors had existing new-API implementations
// that are expected to continue to work independent of the flag. For any
// such API, ensure the corresponding old API is not available when the flag
// is enabled.
static base::NoDestructor<std::unordered_set<SEL>> deprecatedSelectors({
@selector(AXInsertionPointLineNumber), @selector(AXNumberOfCharacters),
@selector(AXPlaceholderValue), @selector(AXSelectedText),
@selector(AXSelectedTextRange), @selector(AXVisibleCharacterRange)
});
if (deprecatedSelectors->find(selector) != deprecatedSelectors->end()) {
return NO;
}
}
// Do not respond to the method if it's not supported by the node.
if (![self conditionallyRespondsToSelector:selector]) {
return NO;
}
return [super respondsToSelector:selector];
}
- (ui::AXPlatformNodeDelegate*)nodeDelegate {
return _node ? _node->GetDelegate() : nil;
}
- (BOOL)instanceActive {
return _node != nullptr;
}
- (id)titleUIElement {
// True only if it's a control, if there's a single label, and the label has
// nonempty text.
// VoiceOver ignores TitleUIElement if the element isn't a control.
if (!ui::IsControl(_node->GetRole()))
return nil;
if (!_node->HasNameFromOtherElement())
return nil;
std::vector<int32_t> labelledby_ids =
_node->GetIntListAttribute(ax::mojom::IntListAttribute::kLabelledbyIds);
if (labelledby_ids.size() != 1)
return nil;
ui::AXPlatformNode* label =
_node->GetDelegate()->GetFromNodeID(labelledby_ids[0]);
if (!label)
return nil;
// No title UI element if the label's name is empty.
std::string labelName = label->GetDelegate()->GetName();
if (labelName.empty())
return nil;
// In the case where we have a radio button or a checked box, no title UI
// element. This goes against Apple's documentation for AXTitleUIElement,
// but is consistent with Safari+Voiceover behavior.
// See crbug.com/1430419
ax::mojom::Role role = _node->GetRole();
if (ui::IsRadio(role) || ui::IsCheckBox(role))
return nil;
return label->GetNativeViewAccessible().Get();
}
- (BOOL)isNameFromLabel {
// Image annotations are not visible text, so they should be exposed
// as a description and not a title.
switch (_node->GetData().GetImageAnnotationStatus()) {
case ax::mojom::ImageAnnotationStatus::kEligibleForAnnotation:
case ax::mojom::ImageAnnotationStatus::kAnnotationPending:
case ax::mojom::ImageAnnotationStatus::kAnnotationEmpty:
case ax::mojom::ImageAnnotationStatus::kAnnotationAdult:
case ax::mojom::ImageAnnotationStatus::kAnnotationProcessFailed:
case ax::mojom::ImageAnnotationStatus::kAnnotationSucceeded:
return true;
case ax::mojom::ImageAnnotationStatus::kNone:
case ax::mojom::ImageAnnotationStatus::kWillNotAnnotateDueToScheme:
case ax::mojom::ImageAnnotationStatus::kIneligibleForAnnotation:
case ax::mojom::ImageAnnotationStatus::kSilentlyEligibleForAnnotation:
break;
}
// No label for windows or native dialogs.
ax::mojom::Role role = _node->GetRole();
if (ui::IsWindow(role) || (ui::IsDialog(role) && !_node->IsWebContent())) {
return false;
}
// VoiceOver computes the wrong description for a link.
if (ui::IsLink(role))
return true;
// If a radiobutton or checkbox has a single label, we are consistent
// with Safari+Voiceover and expose it via AccessibilityLabel.
// Note: Safari+Voiceover is inconsistent with Apple's documentation,
// which suggests this should be exposed via AXTitleUIElement. See
// crbug.com/1430419
if (ui::IsRadio(role) || ui::IsCheckBox(role)) {
std::vector<int32_t> labelledby_ids =
_node->GetIntListAttribute(ax::mojom::IntListAttribute::kLabelledbyIds);
if (labelledby_ids.size() == 1) {
ui::AXPlatformNode* label =
_node->GetDelegate()->GetFromNodeID(labelledby_ids[0]);
if (label) {
// No title UI element if the label's name is empty.
std::string labelName = label->GetDelegate()->GetName();
if (!labelName.empty())
return true;
}
}
}
// VoiceOver will not read the label of these roles unless it is
// exposed in the description instead of the title.
switch (role) {
case ax::mojom::Role::kGenericContainer:
case ax::mojom::Role::kGroup:
case ax::mojom::Role::kRadioGroup:
case ax::mojom::Role::kTabPanel:
return true;
default:
break;
}
// On macOS, the accessible name of an object is exposed as its title if it
// comes from visible text, and as its description otherwise, but never both.
//
// Note: a placeholder is often visible text, but since it aids in data entry
// it is similar to accessibilityValue, and thus cannot be exposed either in
// accessibilityTitle or in accessibilityLabel.
ax::mojom::NameFrom nameFrom = _node->GetNameFrom();
if (nameFrom == ax::mojom::NameFrom::kCaption ||
nameFrom == ax::mojom::NameFrom::kContents ||
nameFrom == ax::mojom::NameFrom::kPlaceholder ||
nameFrom == ax::mojom::NameFrom::kRelatedElement ||
nameFrom == ax::mojom::NameFrom::kValue) {
return false;
}
return true;
}
- (NSArray*)uiElementsForAttribute:(ax::mojom::IntListAttribute)attribute {
NSMutableArray* elements = [NSMutableArray array];
ui::AXPlatformNodeDelegate* delegate = [self nodeDelegate];
if (!delegate) {
return elements;
}
const std::vector<int32_t>& attributeValues =
delegate->GetIntListAttribute(attribute);
for (auto& attributeValue : attributeValues) {
ui::AXPlatformNode* node = delegate->GetFromNodeID(attributeValue);
if (node) {
[elements addObject:node->GetNativeViewAccessible().Get()];
}
}
return elements;
}
- (void)getTreeItemDescendantNodeIds:(std::vector<int32_t>*)treeItemIds {
for (auto childDelegateIterator = [self nodeDelegate]->ChildrenBegin();
*childDelegateIterator != *[self nodeDelegate]->ChildrenEnd();
++(*childDelegateIterator)) {
ui::AXPlatformNodeDelegate* childDelegate = childDelegateIterator->get();
if (childDelegate->GetRole() == ax::mojom::Role::kTreeItem) {
treeItemIds->push_back(childDelegate->GetId());
}
gfx::NativeViewAccessible child = childDelegate->GetNativeViewAccessible();
AXPlatformNodeCocoa* childCocoa =
base::apple::ObjCCastStrict<AXPlatformNodeCocoa>(child.Get());
[childCocoa getTreeItemDescendantNodeIds:treeItemIds];
}
}
+ (NSString*)nativeRoleFromAXRole:(ax::mojom::Role)role {
switch (role) {
case ax::mojom::Role::kAbbr:
case ax::mojom::Role::kAlert:
case ax::mojom::Role::kAlertDialog:
case ax::mojom::Role::kApplication:
case ax::mojom::Role::kArticle:
case ax::mojom::Role::kAudio:
case ax::mojom::Role::kBanner:
case ax::mojom::Role::kBlockquote:
case ax::mojom::Role::kCaption:
case ax::mojom::Role::kClient:
case ax::mojom::Role::kCode:
case ax::mojom::Role::kComment:
case ax::mojom::Role::kComplementary:
case ax::mojom::Role::kContentDeletion:
case ax::mojom::Role::kContentInsertion:
case ax::mojom::Role::kContentInfo:
case ax::mojom::Role::kDefinition:
case ax::mojom::Role::kDesktop:
case ax::mojom::Role::kDialog:
case ax::mojom::Role::kDetails:
case ax::mojom::Role::kDocAbstract:
case ax::mojom::Role::kDocAcknowledgments:
case ax::mojom::Role::kDocAfterword:
case ax::mojom::Role::kDocAppendix:
case ax::mojom::Role::kDocBiblioEntry:
case ax::mojom::Role::kDocBibliography:
case ax::mojom::Role::kDocChapter:
case ax::mojom::Role::kDocColophon:
case ax::mojom::Role::kDocConclusion:
case ax::mojom::Role::kDocCredit:
case ax::mojom::Role::kDocCredits:
case ax::mojom::Role::kDocDedication:
case ax::mojom::Role::kDocEndnote:
case ax::mojom::Role::kDocEndnotes:
case ax::mojom::Role::kDocEpigraph:
case ax::mojom::Role::kDocEpilogue:
case ax::mojom::Role::kDocErrata:
case ax::mojom::Role::kDocExample:
case ax::mojom::Role::kDocFootnote:
case ax::mojom::Role::kDocForeword:
case ax::mojom::Role::kDocGlossary:
case ax::mojom::Role::kDocIndex:
case ax::mojom::Role::kDocIntroduction:
case ax::mojom::Role::kDocNotice:
case ax::mojom::Role::kDocPageFooter:
case ax::mojom::Role::kDocPageHeader:
case ax::mojom::Role::kDocPageList:
case ax::mojom::Role::kDocPart:
case ax::mojom::Role::kDocPreface:
case ax::mojom::Role::kDocPrologue:
case ax::mojom::Role::kDocPullquote:
case ax::mojom::Role::kDocQna:
case ax::mojom::Role::kDocTip:
case ax::mojom::Role::kDocToc:
case ax::mojom::Role::kDocument:
case ax::mojom::Role::kEmbeddedObject:
case ax::mojom::Role::kEmphasis:
case ax::mojom::Role::kFeed:
case ax::mojom::Role::kFigcaption:
case ax::mojom::Role::kFigure:
case ax::mojom::Role::kFooter:
case ax::mojom::Role::kForm:
case ax::mojom::Role::kGenericContainer:
case ax::mojom::Role::kGraphicsDocument:
case ax::mojom::Role::kGraphicsObject:
case ax::mojom::Role::kGroup:
case ax::mojom::Role::kHeader:
case ax::mojom::Role::kIframe:
case ax::mojom::Role::kIframePresentational:
case ax::mojom::Role::kLabelText:
case ax::mojom::Role::kLayoutTable:
case ax::mojom::Role::kLayoutTableCell:
case ax::mojom::Role::kLayoutTableRow:
case ax::mojom::Role::kLegend:
case ax::mojom::Role::kLineBreak:
case ax::mojom::Role::kListItem:
case ax::mojom::Role::kLog:
case ax::mojom::Role::kMain:
case ax::mojom::Role::kMark:
case ax::mojom::Role::kMarquee:
case ax::mojom::Role::kMath:
case ax::mojom::Role::kMathMLFraction:
case ax::mojom::Role::kMathMLIdentifier:
case ax::mojom::Role::kMathMLMath:
case ax::mojom::Role::kMathMLMultiscripts:
case ax::mojom::Role::kMathMLNoneScript:
case ax::mojom::Role::kMathMLNumber:
case ax::mojom::Role::kMathMLOperator:
case ax::mojom::Role::kMathMLOver:
case ax::mojom::Role::kMathMLPrescriptDelimiter:
case ax::mojom::Role::kMathMLRoot:
case ax::mojom::Role::kMathMLRow:
case ax::mojom::Role::kMathMLSquareRoot:
case ax::mojom::Role::kMathMLStringLiteral:
case ax::mojom::Role::kMathMLSub:
case ax::mojom::Role::kMathMLSubSup:
case ax::mojom::Role::kMathMLSup:
case ax::mojom::Role::kMathMLTable:
case ax::mojom::Role::kMathMLTableCell:
case ax::mojom::Role::kMathMLTableRow:
case ax::mojom::Role::kMathMLText:
case ax::mojom::Role::kMathMLUnder:
case ax::mojom::Role::kMathMLUnderOver:
case ax::mojom::Role::kNavigation:
case ax::mojom::Role::kNone:
case ax::mojom::Role::kNote:
case ax::mojom::Role::kPane:
case ax::mojom::Role::kParagraph:
case ax::mojom::Role::kPdfRoot:
case ax::mojom::Role::kPluginObject:
case ax::mojom::Role::kRegion:
case ax::mojom::Role::kRowGroup:
case ax::mojom::Role::kRuby:
case ax::mojom::Role::kSearch:
case ax::mojom::Role::kSection:
case ax::mojom::Role::kSectionFooter:
case ax::mojom::Role::kSectionHeader:
case ax::mojom::Role::kSectionWithoutName:
case ax::mojom::Role::kStatus:
case ax::mojom::Role::kSubscript:
case ax::mojom::Role::kSuggestion:
case ax::mojom::Role::kSuperscript:
return NSAccessibilityGroupRole;
case ax::mojom::Role::kSvgRoot:
return NSAccessibilityImageRole;
case ax::mojom::Role::kStrong:
case ax::mojom::Role::kTableHeaderContainer:
case ax::mojom::Role::kTabPanel:
case ax::mojom::Role::kTerm:
case ax::mojom::Role::kTime:
case ax::mojom::Role::kTimer:
case ax::mojom::Role::kTooltip:
case ax::mojom::Role::kVideo:
case ax::mojom::Role::kWebView:
return NSAccessibilityGroupRole;
case ax::mojom::Role::kButton:
return NSAccessibilityButtonRole;
case ax::mojom::Role::kCanvas:
return NSAccessibilityImageRole;
case ax::mojom::Role::kCell:
return @"AXCell";
case ax::mojom::Role::kCheckBox:
return NSAccessibilityCheckBoxRole;
case ax::mojom::Role::kColorWell:
return NSAccessibilityColorWellRole;
case ax::mojom::Role::kColumn:
return NSAccessibilityColumnRole;
case ax::mojom::Role::kColumnHeader:
return @"AXCell";
case ax::mojom::Role::kComboBoxGrouping:
return NSAccessibilityComboBoxRole;
case ax::mojom::Role::kComboBoxMenuButton:
return NSAccessibilityComboBoxRole;
case ax::mojom::Role::kComboBoxSelect:
// TODO(crbug.com/40864556): Can this be NSAccessibilityComboBoxRole?
return NSAccessibilityPopUpButtonRole;
case ax::mojom::Role::kDate:
return @"AXDateField";
case ax::mojom::Role::kDateTime:
return @"AXDateField";
case ax::mojom::Role::kDescriptionList:
return NSAccessibilityListRole;
case ax::mojom::Role::kDisclosureTriangle:
case ax::mojom::Role::kDisclosureTriangleGrouped:
// If Mac supports AXExpandedChanged event with
// NSAccessibilityDisclosureTriangleRole, We should update
// ax::mojom::Role::kDisclosureTriangle mapping to
// NSAccessibilityDisclosureTriangleRole. http://crbug.com/558324
return features::IsAccessibilityExposeSummaryAsHeadingEnabled()
? NSAccessibilityDisclosureTriangleRole
: NSAccessibilityButtonRole;
case ax::mojom::Role::kDocBackLink:
case ax::mojom::Role::kDocBiblioRef:
case ax::mojom::Role::kDocGlossRef:
case ax::mojom::Role::kDocNoteRef:
return NSAccessibilityLinkRole;
case ax::mojom::Role::kDocCover:
return NSAccessibilityImageRole;
case ax::mojom::Role::kDocPageBreak:
return NSAccessibilitySplitterRole;
case ax::mojom::Role::kDocSubtitle:
return @"AXHeading";
case ax::mojom::Role::kGraphicsSymbol:
return NSAccessibilityImageRole;
case ax::mojom::Role::kGrid:
// Should be NSAccessibilityGridRole but VoiceOver treating it like
// a list as of 10.12.6, so following WebKit and using table role:
// crbug.com/753925
return NSAccessibilityTableRole;
case ax::mojom::Role::kGridCell:
return @"AXCell";
case ax::mojom::Role::kHeading:
return @"AXHeading";
case ax::mojom::Role::kImage:
return NSAccessibilityImageRole;
case ax::mojom::Role::kInlineTextBox:
return NSAccessibilityStaticTextRole;
case ax::mojom::Role::kInputTime:
return @"AXTimeField";
case ax::mojom::Role::kLink:
return NSAccessibilityLinkRole;
case ax::mojom::Role::kList:
return NSAccessibilityListRole;
case ax::mojom::Role::kListBox:
return NSAccessibilityListRole;
case ax::mojom::Role::kListBoxOption:
return NSAccessibilityStaticTextRole;
case ax::mojom::Role::kListGrid:
return NSAccessibilityTableRole;
case ax::mojom::Role::kListMarker:
return @"AXListMarker";
case ax::mojom::Role::kMenu:
return NSAccessibilityMenuRole;
case ax::mojom::Role::kMenuBar:
return NSAccessibilityMenuBarRole;
case ax::mojom::Role::kMenuItem:
return NSAccessibilityMenuItemRole;
case ax::mojom::Role::kMenuItemCheckBox:
return NSAccessibilityMenuItemRole;
case ax::mojom::Role::kMenuItemRadio:
return NSAccessibilityMenuItemRole;
case ax::mojom::Role::kMenuItemSeparator:
return NSAccessibilityMenuItemRole;
case ax::mojom::Role::kMenuListOption:
return NSAccessibilityMenuItemRole;
case ax::mojom::Role::kMenuListPopup:
return NSAccessibilityMenuRole;
case ax::mojom::Role::kMeter:
return NSAccessibilityLevelIndicatorRole;
case ax::mojom::Role::kPdfActionableHighlight:
return NSAccessibilityButtonRole;
case ax::mojom::Role::kPopUpButton:
return NSAccessibilityPopUpButtonRole;
case ax::mojom::Role::kProgressIndicator:
return NSAccessibilityProgressIndicatorRole;
case ax::mojom::Role::kRadioButton:
return NSAccessibilityRadioButtonRole;
case ax::mojom::Role::kRadioGroup:
return NSAccessibilityRadioGroupRole;
case ax::mojom::Role::kRootWebArea:
return NSAccessibilityWebAreaRole;
case ax::mojom::Role::kRow:
return NSAccessibilityRowRole;
case ax::mojom::Role::kRowHeader:
return @"AXCell";
case ax::mojom::Role::kScrollBar:
return NSAccessibilityScrollBarRole;
case ax::mojom::Role::kScrollView:
return NSAccessibilityScrollAreaRole;
case ax::mojom::Role::kSearchBox:
return NSAccessibilityTextFieldRole;
case ax::mojom::Role::kSlider:
return NSAccessibilitySliderRole;
case ax::mojom::Role::kSpinButton:
return NSAccessibilityIncrementorRole;
case ax::mojom::Role::kSplitter:
return NSAccessibilitySplitterRole;
case ax::mojom::Role::kStaticText:
return NSAccessibilityStaticTextRole;
case ax::mojom::Role::kSwitch:
return NSAccessibilityCheckBoxRole;
case ax::mojom::Role::kTab:
return NSAccessibilityRadioButtonRole;
case ax::mojom::Role::kTable:
return NSAccessibilityTableRole;
case ax::mojom::Role::kTabList:
return NSAccessibilityTabGroupRole;
case ax::mojom::Role::kTextField:
return NSAccessibilityTextFieldRole;
case ax::mojom::Role::kTextFieldWithComboBox:
return NSAccessibilityComboBoxRole;
case ax::mojom::Role::kTitleBar:
return NSAccessibilityStaticTextRole;
case ax::mojom::Role::kToggleButton:
return NSAccessibilityCheckBoxRole;
case ax::mojom::Role::kToolbar:
return NSAccessibilityToolbarRole;
case ax::mojom::Role::kTree:
return NSAccessibilityOutlineRole;
case ax::mojom::Role::kTreeGrid:
return NSAccessibilityTableRole;
case ax::mojom::Role::kTreeItem:
return NSAccessibilityRowRole;
case ax::mojom::Role::kUnknown:
// This occurs in the case where a View has no widget, and while this will
// not be exposed to users, it allows isAccessibilityElement() to have
// fewer rules.
return NSAccessibilityGroupRole;
case ax::mojom::Role::kWindow:
// Use the group role as the BrowserNativeWidgetWindow already provides
// a kWindow role, and having extra window roles, which are treated
// specially by screen readers, can break their ability to find the
// content window. See http://crbug.com/875843 for more information.
return NSAccessibilityGroupRole;
case ax::mojom::Role::kCaret:
case ax::mojom::Role::kDescriptionListTermDeprecated:
case ax::mojom::Role::kDescriptionListDetailDeprecated:
case ax::mojom::Role::kDirectoryDeprecated:
case ax::mojom::Role::kImeCandidate:
case ax::mojom::Role::kKeyboard:
case ax::mojom::Role::kPreDeprecated:
case ax::mojom::Role::kPortalDeprecated:
case ax::mojom::Role::kRubyAnnotation:
NOTREACHED() << "The following role should not be present: " << role;
}
}
+ (NSString*)nativeSubroleFromAXRole:(ax::mojom::Role)role {
static const base::NoDestructor<RoleMap> subrole_map(BuildSubroleMap());
RoleMap::const_iterator it = subrole_map->find(role);
return it != subrole_map->end() ? it->second : nil;
}
+ (NSString*)nativeNotificationFromAXEvent:(ax::mojom::Event)event {
static const base::NoDestructor<EventMap> event_map(BuildEventMap());
EventMap::const_iterator it = event_map->find(event);
return it != event_map->end() ? it->second : nil;
}
- (instancetype)initWithNode:(ui::AXPlatformNodeBase*)node {
if ((self = [super init])) {
_node = node;
}
return self;
}
- (void)detachAndNotifyDestroyed:(BOOL)shouldNotify {
if (!_node)
return;
_node = nil;
if (shouldNotify) {
NSAccessibilityPostNotification(
self, NSAccessibilityUIElementDestroyedNotification);
}
}
- (NSRect)boundsInScreen {
if (!_node) {
return NSZeroRect;
}
return gfx::ScreenRectToNSRect(_node->GetDelegate()->GetBoundsRect(
ui::AXCoordinateSystem::kScreenDIPs, ui::AXClippingBehavior::kClipped));
}
- (NSString*)getStringAttribute:(ax::mojom::StringAttribute)attribute {
std::string attributeValue;
if (_node->GetStringAttribute(attribute, &attributeValue))
return base::SysUTF8ToNSString(attributeValue);
return nil;
}
- (NSString*)getAXValueAsString {
id value = [self AXValue];
return [value isKindOfClass:[NSString class]] ? value : nil;
}
- (ax::mojom::Role)internalRole {
ax::mojom::Role role = static_cast<ax::mojom::Role>(_node->GetRole());
// Make sure to use Role::kPopupButton instead of Role::kButton for all
// values of kHasPopup. This is normally already true, but the default
// implementation does not use kPopupButton if aria-haspopup="dialog".
if (role == ax::mojom::Role::kButton &&
_node->HasIntAttribute(ax::mojom::IntAttribute::kHasPopup)) {
return ax::mojom::Role::kPopUpButton;
}
return role;
}
- (BOOL)hasAction:(ax::mojom::Action)action {
return _node->HasAction(action) || HasImplicitAction(*_node, action);
}
- (BOOL)performAction:(ax::mojom::Action)action {
if (![self hasAction:action]) {
return NO;
}
ui::AXActionData data;
data.action = action;
_node->GetDelegate()->AccessibilityPerformAction(data);
return YES;
}
- (AXPlatformNodeCocoa*)fromNodeID:(ui::AXNodeID)id {
ui::AXPlatformNode* cell = _node->GetDelegate()->GetFromNodeID(id);
if (cell) {
return base::apple::ObjCCast<AXPlatformNodeCocoa>(
cell->GetNativeViewAccessible().Get());
}
return nil;
}
- (BOOL)isImage {
bool has_image_semantics =
ui::IsImage(_node->GetRole()) &&
!_node->GetBoolAttribute(ax::mojom::BoolAttribute::kCanvasHasFallback) &&
!_node->GetChildCount();
#if DCHECK_IS_ON()
bool is_native_image =
[[self accessibilityRole] isEqualToString:NSAccessibilityImageRole];
DCHECK_EQ(is_native_image, has_image_semantics)
<< "\nPresence/lack of native image role do not match the expected "
"internal semantics:"
<< "\n* Chrome role: " << ui::ToString(_node->GetRole())
<< "\n* NSAccessibility role: " << [self accessibilityRole]
<< "\n* AXNode: " << *_node;
#endif
return has_image_semantics;
}
- (void)addTextAnnotationsIn:(const AXRange*)axRange
to:(NSMutableAttributedString*)attributedString {
int anchorStartOffset = 0;
std::map<ui::AXNodeID, std::set<ax::mojom::Role>> ancestor_roles;
[attributedString beginEditing];
for (const AXRange& leafTextRange : *axRange) {
DCHECK(!leafTextRange.IsNull());
DCHECK_EQ(leafTextRange.anchor()->GetAnchor(),
leafTextRange.focus()->GetAnchor())
<< "An anchor range should only span a single object.";
int leafTextLength = leafTextRange.GetText().length();
NSRange leafRange = NSMakeRange(anchorStartOffset, leafTextLength);
// As we iterate over the attributed string's string using leaf ranges,
// double check that the next leaf string actually matches the text in the
// attributed string. If it doesn't, the leaf text is "extra" text that's
// not included in the attributed string.
if (leafRange.location >= attributedString.length ||
base::SysNSStringToUTF16([attributedString.string
substringWithRange:leafRange]) != leafTextRange.GetText()) {
continue;
}
ui::AXNode* anchor = leafTextRange.focus()->GetAnchor();
DCHECK(anchor) << "A non-null position should have a non-null anchor node.";
// Add misspelling information
const std::vector<int32_t>& markerTypes =
anchor->GetIntListAttribute(ax::mojom::IntListAttribute::kMarkerTypes);
const std::vector<int>& markerStarts =
anchor->GetIntListAttribute(ax::mojom::IntListAttribute::kMarkerStarts);
const std::vector<int>& markerEnds =
anchor->GetIntListAttribute(ax::mojom::IntListAttribute::kMarkerEnds);
DCHECK_EQ(markerTypes.size(), markerStarts.size());
DCHECK_EQ(markerTypes.size(), markerEnds.size());
for (size_t i = 0; i < markerTypes.size(); ++i) {
if (!(markerTypes[i] &
static_cast<int32_t>(ax::mojom::MarkerType::kSpelling))) {
continue;
}
int misspellingStart = anchorStartOffset + markerStarts[i];
int misspellingEnd = anchorStartOffset + markerEnds[i];
int misspellingLength = misspellingEnd - misspellingStart;
DCHECK_LE(static_cast<unsigned long>(misspellingEnd),
[attributedString length]);
DCHECK_GT(misspellingLength, 0);
[attributedString
addAttribute:NSAccessibilityMarkedMisspelledTextAttribute
value:@YES
range:NSMakeRange(misspellingStart, misspellingLength)];
}
CollectAncestorRoles(*anchor, ancestor_roles);
// Add annotation information
if (ancestor_roles[anchor->id()].contains(ax::mojom::Role::kMark)) {
[attributedString addAttribute:@"AXHighlight" value:@YES range:leafRange];
}
if (ancestor_roles[anchor->id()].contains(ax::mojom::Role::kSuggestion)) {
[attributedString addAttribute:@"AXIsSuggestion"
value:@YES
range:leafRange];
}
if (ancestor_roles[anchor->id()].contains(
ax::mojom::Role::kContentDeletion)) {
[attributedString addAttribute:@"AXIsSuggestedDeletion"
value:@YES
range:leafRange];
}
if (ancestor_roles[anchor->id()].contains(
ax::mojom::Role::kContentInsertion)) {
[attributedString addAttribute:@"AXIsSuggestedInsertion"
value:@YES
range:leafRange];
}
ui::AXTextAttributes text_attrs =
leafTextRange.anchor()->GetTextAttributes();
NSMutableDictionary* fontAttributes = [NSMutableDictionary dictionary];
// TODO(crbug.com/41456329): Implement NSAccessibilityFontFamilyKey.
// TODO(crbug.com/41456329): Implement NSAccessibilityFontNameKey.
// TODO(crbug.com/41456329): Implement NSAccessibilityVisibleNameKey.
if (text_attrs.font_size != ui::AXTextAttributes::kUnsetValue) {
fontAttributes[NSAccessibilityFontSizeKey] = @(text_attrs.font_size);
}
if (text_attrs.HasTextStyle(ax::mojom::TextStyle::kBold)) {
fontAttributes[@"AXFontBold"] = @YES;
}
if (text_attrs.HasTextStyle(ax::mojom::TextStyle::kItalic)) {
fontAttributes[@"AXFontItalic"] = @YES;
}
[attributedString addAttribute:NSAccessibilityFontTextAttribute
value:fontAttributes
range:leafRange];
if (text_attrs.color != ui::AXTextAttributes::kUnsetValue) {
[attributedString addAttribute:NSAccessibilityForegroundColorTextAttribute
value:(__bridge id)skia::SkColorToSRGBNSColor(
SkColor(text_attrs.color))
.CGColor
range:leafRange];
} else {
[attributedString
removeAttribute:NSAccessibilityForegroundColorTextAttribute
range:leafRange];
}
if (text_attrs.background_color != ui::AXTextAttributes::kUnsetValue) {
[attributedString addAttribute:NSAccessibilityBackgroundColorTextAttribute
value:(__bridge id)skia::SkColorToSRGBNSColor(
SkColor(text_attrs.background_color))
.CGColor
range:leafRange];
} else {
[attributedString
removeAttribute:NSAccessibilityBackgroundColorTextAttribute
range:leafRange];
}
// TODO(crbug.com/41456329): Implement
// NSAccessibilitySuperscriptTextAttribute.
// TODO(crbug.com/41456329): Implement NSAccessibilityShadowTextAttribute.
if (text_attrs.underline_style != ui::AXTextAttributes::kUnsetValue) {
[attributedString addAttribute:NSAccessibilityUnderlineTextAttribute
value:@YES
range:leafRange];
} else {
[attributedString removeAttribute:NSAccessibilityUnderlineTextAttribute
range:leafRange];
}
// TODO(crbug.com/41456329): Implement
// NSAccessibilityUnderlineColorTextAttribute.
if (text_attrs.strikethrough_style != ui::AXTextAttributes::kUnsetValue) {
[attributedString addAttribute:NSAccessibilityStrikethroughTextAttribute
value:@YES
range:leafRange];
} else {
[attributedString
removeAttribute:NSAccessibilityStrikethroughTextAttribute
range:leafRange];
}
// TODO(crbug.com/41456329): Implement
// NSAccessibilityStrikethroughColorTextAttribute.
// TODO(crbug.com/41456329): Implement NSAccessibilityLinkTextAttribute.
// TODO(crbug.com/41456329): Implement
// NSAccessibilityAutocorrectedTextAttribute.
anchorStartOffset += leafTextLength;
}
[attributedString endEditing];
}
- (BOOL)descriptionIsFromAriaDescription {
ax::mojom::DescriptionFrom descFrom = static_cast<ax::mojom::DescriptionFrom>(
_node->GetIntAttribute(ax::mojom::IntAttribute::kDescriptionFrom));
return descFrom == ax::mojom::DescriptionFrom::kAriaDescription ||
descFrom == ax::mojom::DescriptionFrom::kRelatedElement;
}
- (NSString*)getName {
return base::SysUTF8ToNSString(_node->GetName());
}
- (AXAnnouncementSpec*)announcementForEvent:(ax::mojom::Event)eventType {
// Only alerts and live region changes should be announced.
DCHECK(eventType == ax::mojom::Event::kAlert ||
eventType == ax::mojom::Event::kLiveRegionChanged);
std::string liveStatus =
_node->GetStringAttribute(ax::mojom::StringAttribute::kLiveStatus);
// If live status is explicitly set to off, don't announce.
if (liveStatus == "off") {
return nil;
}
NSString* name = [self getName];
NSString* announcementText =
name.length > 0 ? name
: base::SysUTF16ToNSString(_node->GetTextContentUTF16());
if (announcementText.length == 0) {
return nil;
}
const std::string& description =
_node->GetStringAttribute(ax::mojom::StringAttribute::kDescription);
if (!description.empty()) {
// Concatenating name and description, with a newline in between to create a
// pause to avoid treating the concatenation as a single sentence.
announcementText =
[NSString stringWithFormat:@"%@\n%@", announcementText,
base::SysUTF8ToNSString(description)];
}
AXAnnouncementSpec* spec = [[AXAnnouncementSpec alloc] init];
spec.announcement = announcementText;
spec.window = [self AXWindow];
spec.polite = liveStatus != "assertive";
return spec;
}
- (void)scheduleLiveRegionAnnouncement:(AXAnnouncementSpec*)announcement {
if (_pendingAnnouncement) {
// An announcement is already in flight, so just reset the contents. This is
// threadsafe because the dispatch is on the main queue.
_pendingAnnouncement = announcement;
return;
}
_pendingAnnouncement = announcement;
dispatch_after(
kLiveRegionDebounceMillis * NSEC_PER_MSEC, dispatch_get_main_queue(), ^{
if (!self->_pendingAnnouncement) {
return;
}
PostAnnouncementNotification(self->_pendingAnnouncement.announcement,
self->_pendingAnnouncement.window,
self->_pendingAnnouncement.polite);
self->_pendingAnnouncement = nil;
});
}
//
// NSAccessibility legacy informal protocol implementation (deprecated).
// https://developer.apple.com/documentation/appkit/deprecated_symbols/nsaccessibility
//
- (BOOL)accessibilityIsIgnored {
return ![self isAccessibilityElement];
}
- (id)accessibilityHitTest:(NSPoint)point {
if (!NSPointInRect(point, self.boundsInScreen)) {
return nil;
}
for (id child in [[self accessibilityChildren] reverseObjectEnumerator]) {
if (!NSPointInRect(point, [child accessibilityFrame]))
continue;
if (id foundChild = [child accessibilityHitTest:point])
return foundChild;
}
// Hit self, but not any child.
return NSAccessibilityUnignoredAncestor(self);
}
- (BOOL)accessibilityNotifiesWhenDestroyed {
return YES;
}
- (id)accessibilityFocusedUIElement {
return _node ? _node->GetDelegate()->GetFocus().Get() : nil;
}
// This function and accessibilityPerformAction:, while deprecated, are a) still
// called by AppKit internally and b) not implemented by NSAccessibilityElement,
// so this class needs its own implementations.
- (NSArray*)accessibilityActionNames {
TRACE_EVENT1("accessibility", "AXPlatformNodeCocoa::accessibilityActionNames",
"role=", ui::ToString([self internalRole]));
// Exclude actions available through the new accessibility API.
NSMutableArray* actions = [self internalAccessibilityActionNames];
if (features::IsMacAccessibilityAPIMigrationEnabled()) {
[actions
filterUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
id evaluatedObject,
NSDictionary* bindings) {
return ![[[self class] actionsAvailableThroughNewAccessibilityAPI]
containsObject:evaluatedObject];
}]];
}
return actions;
}
- (NSMutableArray*)internalAccessibilityActionNames {
if (![self instanceActive]) {
return [NSMutableArray array];
}
NSMutableArray* axActions = [NSMutableArray array];
const ui::CocoaActionList& action_list = GetCocoaActionList();
// VoiceOver expects the "press" action to be first. Note that some roles
// should be given a press action implicitly.
DCHECK([action_list[0].second isEqualToString:NSAccessibilityPressAction]);
for (const auto& item : action_list) {
if ((_node->HasAction(item.first) ||
HasImplicitAction(*_node, item.first))) {
[axActions addObject:item.second];
}
}
if (AlsoUseShowMenuActionForDefaultAction(*_node))
[axActions addObject:NSAccessibilityShowMenuAction];
return axActions;
}
// This API is deprecated.
- (void)accessibilityPerformAction:(NSString*)action {
// Actions are performed asynchronously, so it's always possible for an object
// to change its mind after previously reporting an action as available.
if (![[self accessibilityActionNames] containsObject:action]) {
return;
}
ui::AXActionData data;
if ([action isEqualToString:NSAccessibilityShowMenuAction] &&
AlsoUseShowMenuActionForDefaultAction(*_node)) {
data.action = ax::mojom::Action::kDoDefault;
} else {
for (const ui::CocoaActionList::value_type& entry : GetCocoaActionList()) {
if ([action isEqualToString:entry.second]) {
data.action = entry.first;
break;
}
}
}
// Note ui::AX_ACTIONs which are just overwriting an accessibility attribute
// are already implemented in -accessibilitySetValue:forAttribute:, so ignore
// those here.
if (data.action != ax::mojom::Action::kNone)
_node->GetDelegate()->AccessibilityPerformAction(data);
}
- (BOOL)accessibilityPerformPress {
if (![self instanceActive]) {
return NO;
}
return [self performAction:ax::mojom::Action::kDoDefault];
}
- (BOOL)accessibilityPerformShowMenu {
if (![self instanceActive]) {
return NO;
}
if (AlsoUseShowMenuActionForDefaultAction(*_node)) {
return [self accessibilityPerformPress];
}
if ([self performAction:ax::mojom::Action::kShowContextMenu]) {
return YES;
}
return NO;
}
- (BOOL)accessibilityPerformDecrement {
if (![self instanceActive]) {
return NO;
}
return [self performAction:ax::mojom::Action::kDecrement];
}
- (BOOL)accessibilityPerformIncrement {
if (![self instanceActive]) {
return NO;
}
return [self performAction:ax::mojom::Action::kIncrement];
}
- (BOOL)accessibilityPerformConfirm {
// Placeholder for the future. Needs to implement Return press key action.
return NO;
}
- (NSMutableArray*)internalAccessibilityAttributeNames {
if (!_node)
return [NSMutableArray array];
// These attributes are required on all accessibility objects.
NSArray* const kAllRoleAttributes = @[
NSAccessibilityBlockQuoteLevelAttribute, NSAccessibilityChildrenAttribute,
NSAccessibilityDOMClassList, NSAccessibilityDOMIdentifierAttribute,
NSAccessibilityDescriptionAttribute, NSAccessibilityElementBusyAttribute,
NSAccessibilityParentAttribute, NSAccessibilityPositionAttribute,
NSAccessibilityRoleAttribute, NSAccessibilitySizeAttribute,
NSAccessibilitySelectedAttribute, NSAccessibilitySizeAttribute,
NSAccessibilitySubroleAttribute,
// Title is required for most elements. Cocoa asks for the value even if it
// is omitted here, but won't present it to accessibility APIs without this.
NSAccessibilityTitleAttribute,
// Attributes which are not required, but are general to all roles.
NSAccessibilityRoleDescriptionAttribute, NSAccessibilityEnabledAttribute,
NSAccessibilityFocusedAttribute, NSAccessibilityHelpAttribute,
NSAccessibilityTopLevelUIElementAttribute, NSAccessibilityVisitedAttribute,
NSAccessibilityWindowAttribute, NSAccessibilityChromeAXNodeIdAttribute
];
// Attributes required for user-editable controls.
NSArray* const kValueAttributes = @[ NSAccessibilityValueAttribute ];
// Attributes required for unprotected textfields and labels.
NSArray* const kUnprotectedTextAttributes = @[
NSAccessibilityInsertionPointLineNumberAttribute,
NSAccessibilityNumberOfCharactersAttribute,
NSAccessibilitySelectedTextAttribute,
NSAccessibilitySelectedTextRangeAttribute,
NSAccessibilityVisibleCharacterRangeAttribute
];
// Required for all text, including protected textfields.
NSString* const kTextAttributes = NSAccessibilityPlaceholderValueAttribute;
NSMutableArray* axAttributes =
[NSMutableArray arrayWithArray:kAllRoleAttributes];
ax::mojom::Role role = _node->GetRole();
switch (role) {
case ax::mojom::Role::kTextField:
case ax::mojom::Role::kTextFieldWithComboBox:
[axAttributes addObject:NSAccessibilityOwnsAttribute];
break;
case ax::mojom::Role::kStaticText:
[axAttributes addObject:kTextAttributes];
if (!_node->HasState(ax::mojom::State::kProtected))
[axAttributes addObjectsFromArray:kUnprotectedTextAttributes];
[[fallthrough]];
case ax::mojom::Role::kCheckBox:
case ax::mojom::Role::kComboBoxMenuButton:
case ax::mojom::Role::kMenuItemCheckBox:
case ax::mojom::Role::kMenuItemRadio:
case ax::mojom::Role::kRadioButton:
case ax::mojom::Role::kSearchBox:
case ax::mojom::Role::kSlider:
case ax::mojom::Role::kToggleButton:
[axAttributes addObjectsFromArray:kValueAttributes];
break;
case ax::mojom::Role::kMathMLFraction:
[axAttributes addObjectsFromArray:@[
NSAccessibilityMathFractionNumeratorAttribute,
NSAccessibilityMathFractionDenominatorAttribute
]];
break;
case ax::mojom::Role::kMathMLSquareRoot:
[axAttributes addObject:NSAccessibilityMathRootRadicandAttribute];
break;
case ax::mojom::Role::kMathMLRoot:
[axAttributes addObjectsFromArray:@[
NSAccessibilityMathRootRadicandAttribute,
NSAccessibilityMathRootIndexAttribute
]];
break;
case ax::mojom::Role::kMathMLSub:
[axAttributes addObjectsFromArray:@[
NSAccessibilityMathBaseAttribute, NSAccessibilityMathSubscriptAttribute
]];
break;
case ax::mojom::Role::kMathMLSup:
[axAttributes addObjectsFromArray:@[
NSAccessibilityMathBaseAttribute,
NSAccessibilityMathSuperscriptAttribute
]];
break;
case ax::mojom::Role::kMathMLSubSup:
[axAttributes addObjectsFromArray:@[
NSAccessibilityMathBaseAttribute, NSAccessibilityMathSubscriptAttribute,
NSAccessibilityMathSuperscriptAttribute
]];
break;
case ax::mojom::Role::kMathMLUnder:
[axAttributes addObjectsFromArray:@[
NSAccessibilityMathBaseAttribute, NSAccessibilityMathUnderAttribute
]];
break;
case ax::mojom::Role::kMathMLOver:
[axAttributes addObjectsFromArray:@[
NSAccessibilityMathBaseAttribute, NSAccessibilityMathOverAttribute
]];
break;
case ax::mojom::Role::kMathMLUnderOver:
[axAttributes addObjectsFromArray:@[
NSAccessibilityMathBaseAttribute, NSAccessibilityMathUnderAttribute,
NSAccessibilityMathOverAttribute
]];
break;
case ax::mojom::Role::kMathMLMultiscripts:
[axAttributes addObjectsFromArray:@[
NSAccessibilityMathBaseAttribute,
NSAccessibilityMathPostscriptsAttribute,
NSAccessibilityMathPrescriptsAttribute
]];
break;
// TODO(tapted): Add additional attributes based on role.
default:
break;
}
if (ui::IsMenuItem(role))
[axAttributes addObject:@"AXMenuItemMarkChar"];
if (ui::IsItemLike(role))
[axAttributes addObjectsFromArray:@[ @"AXARIAPosInSet", @"AXARIASetSize" ]];
if (ui::IsSetLike(role))
[axAttributes addObject:@"AXARIASetSize"];
if ([[self accessibilityRole] isEqualToString:NSAccessibilityWebAreaRole]) {
[axAttributes addObjectsFromArray:@[
NSAccessibilityLoadedAttribute, NSAccessibilityLoadingProgressAttribute
]];
}
// Caret navigation and text selection attributes.
if (!ui::IsPlatformDocument(_node->GetRole())) {
[axAttributes addObject:NSAccessibilityFocusableAncestorAttribute];
if (_node->HasState(ax::mojom::State::kEditable)) {
[axAttributes addObjectsFromArray:@[
NSAccessibilityEditableAncestorAttribute,
NSAccessibilityHighestEditableAncestorAttribute
]];
}
}
// Live regions.
if (_node->HasStringAttribute(ax::mojom::StringAttribute::kLiveStatus))
[axAttributes addObject:NSAccessibilityARIALiveAttribute];
if (_node->HasStringAttribute(ax::mojom::StringAttribute::kLiveRelevant))
[axAttributes addObject:NSAccessibilityARIARelevantAttribute];
if (_node->HasBoolAttribute(ax::mojom::BoolAttribute::kLiveAtomic))
[axAttributes addObject:NSAccessibilityARIAAtomicAttribute];
if (_node->HasBoolAttribute(ax::mojom::BoolAttribute::kBusy))
[axAttributes addObject:NSAccessibilityARIABusyAttribute];
if (_node->HasIntAttribute(ax::mojom::IntAttribute::kAriaCurrentState))
[axAttributes addObject:NSAccessibilityARIACurrentAttribute];
// Control element.
if (ui::IsControl(role)) {
[axAttributes addObjectsFromArray:@[
NSAccessibilityAccessKeyAttribute,
NSAccessibilityInvalidAttribute,
]];
}
// Autocomplete.
if (_node->HasStringAttribute(ax::mojom::StringAttribute::kAutoComplete))
[axAttributes addObject:NSAccessibilityAutocompleteValueAttribute];
// AriaBrailleLabel.
if (_node->HasStringAttribute(ax::mojom::StringAttribute::kAriaBrailleLabel))
[axAttributes addObject:NSAccessibilityBrailleLabelAttribute];
// AriaBrailleRoleDescription.
if (_node->HasStringAttribute(
ax::mojom::StringAttribute::kAriaBrailleRoleDescription))
[axAttributes addObject:NSAccessibilityBrailleRoleDescription];
// Details.
if (_node->HasIntListAttribute(ax::mojom::IntListAttribute::kDetailsIds)) {
[axAttributes addObject:NSAccessibilityDetailsElementsAttribute];
}
// Error messages.
if (_node->HasIntListAttribute(
ax::mojom::IntListAttribute::kErrormessageIds)) {
[axAttributes addObject:NSAccessibilityErrorMessageElementsAttribute];
}
if (ui::SupportsRequired(role)) {
[axAttributes addObject:NSAccessibilityRequiredAttribute];
}
// Url: add the url attribute only if the object has a valid url.
if ([self accessibilityURL])
[axAttributes addObject:NSAccessibilityURLAttribute];
// Table and grid.
if (ui::IsTableLike(role)) {
[axAttributes addObjectsFromArray:@[
NSAccessibilityColumnHeaderUIElementsAttribute,
NSAccessibilityARIAColumnCountAttribute,
NSAccessibilityARIARowCountAttribute,
]];
}
if (ui::IsCellOrTableHeader(role)) {
[axAttributes addObjectsFromArray:@[
NSAccessibilityARIAColumnIndexAttribute,
NSAccessibilityARIARowIndexAttribute,
]];
}
if (ui::IsCellOrTableHeader(role) && role != ax::mojom::Role::kColumnHeader) {
[axAttributes addObject:NSAccessibilityColumnHeaderUIElementsAttribute];
}
// Tree and grid (Outline role in Mac accessibility)
if (ui::IsGridLike(role))
[axAttributes addObject:NSAccessibilitySelectedRowsAttribute];
// Popup
if (_node->HasIntAttribute(ax::mojom::IntAttribute::kHasPopup)) {
[axAttributes addObjectsFromArray:@[
NSAccessibilityHasPopupAttribute, NSAccessibilityPopupValueAttribute
]];
}
// KeyShortcuts
if (_node->HasStringAttribute(ax::mojom::StringAttribute::kKeyShortcuts))
[axAttributes addObject:NSAccessibilityKeyShortcutsValueAttribute];
// TitleUIElement
if ([self titleUIElement])
[axAttributes addObject:NSAccessibilityTitleUIElementAttribute];
return axAttributes;
}
// This API is deprecated.
// This method, while deprecated, is still called internally by AppKit.
- (NSArray*)accessibilityAttributeNames {
TRACE_EVENT1("accessibility",
"AXPlatformNodeCocoa::accessibilityAttributeNames",
"role=", ui::ToString([self internalRole]));
if (![self instanceActive]) {
LOG(ERROR) << "Stale object in tree, no AXPlatformNode.";
return @[];
}
if (!_node->GetDelegate()) {
LOG(ERROR) << "Stale object in tree, no delegate.";
return @[];
}
// No need to compute attribute names for ignored nodes.
if (![self isAccessibilityElement]) {
return @[];
}
// Exclude attributes available through the new accessibility API.
NSMutableArray* attributes = [self internalAccessibilityAttributeNames];
if (features::IsMacAccessibilityAPIMigrationEnabled()) {
[attributes
filterUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
id evaluatedObject,
NSDictionary* bindings) {
return ![[[self class] attributesAvailableThroughNewAccessibilityAPI]
containsObject:evaluatedObject];
}]];
}
return attributes;
}
- (NSArray*)accessibilityParameterizedAttributeNames {
TRACE_EVENT1("accessibility",
"AXPlatformNodeCocoa::accessibilityParameterizedAttributeNames",
"role=", ui::ToString([self internalRole]));
// Exclude attributes available through the new accessibility API.
NSMutableArray* attributes =
[self internalAccessibilityParameterizedAttributeNames];
if (features::IsMacAccessibilityAPIMigrationEnabled()) {
[attributes
filterUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
id evaluatedObject,
NSDictionary* bindings) {
return ![[[self class] attributesAvailableThroughNewAccessibilityAPI]
containsObject:evaluatedObject];
}]];
}
return attributes;
}
- (NSMutableArray*)internalAccessibilityParameterizedAttributeNames {
if (![self instanceActive]) {
return [NSMutableArray array];
}
// General attributes.
NSMutableArray* attributeNames = [NSMutableArray
arrayWithObjects:
NSAccessibilityAttributedStringForTextMarkerRangeParameterizedAttribute,
nil];
if (_node->HasState(ax::mojom::State::kEditable)) {
[attributeNames addObjectsFromArray:@[
NSAccessibilityAttributedStringForRangeParameterizedAttribute
]];
}
return attributeNames;
}
// This API is deprecated.
// Despite its deprecation, the AppKit internally calls this function sometimes
// in unclear circumstances. It is implemented in terms of the new a11y API
// here.
- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute {
if (!_node)
return;
if ([[self class] isAttributeAvailableThroughNewAccessibilityAPI:attribute]) {
return;
}
if ([attribute isEqualToString:NSAccessibilityValueAttribute]) {
[self setAccessibilityValue:value];
} else if ([attribute isEqualToString:NSAccessibilitySelectedTextAttribute]) {
[self setAccessibilitySelectedText:base::apple::ObjCCastStrict<NSString>(
value)];
} else if ([attribute
isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) {
[self
setAccessibilitySelectedTextRange:base::apple::ObjCCastStrict<NSValue>(
value)
.rangeValue];
} else if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) {
[self setAccessibilityFocused:base::apple::ObjCCastStrict<NSNumber>(value)
.boolValue];
}
}
// This method, while deprecated, is still called internally by AppKit.
- (id)accessibilityAttributeValue:(NSString*)attribute {
if (!_node)
return nil; // Return nil when detached. Even for ax::mojom::Role.
if ([[self class] isAttributeAvailableThroughNewAccessibilityAPI:attribute]) {
// TODO(crbug.com/376723178): We should be able to add a NOTREACHED()
// here, but at the moment, test infrastructure still directly calls this
// api endpoint.
return nil;
}
SEL selector = NSSelectorFromString(attribute);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([self respondsToSelector:selector])
return [self performSelector:selector];
#pragma clang diagnostic pop
return nil;
}
- (id)accessibilityAttributeValue:(NSString*)attribute
forParameter:(id)parameter {
if (!_node)
return nil;
if ([[self class] isAttributeAvailableThroughNewAccessibilityAPI:attribute]) {
// TODO(crbug.com/376723178): We should be able to add a NOTREACHED()
// here, but at the moment, test infrastructure still directly calls this
// api endpoint.
return nil;
}
SEL selector = NSSelectorFromString([attribute stringByAppendingString:@":"]);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([self respondsToSelector:selector])
return [self performSelector:selector withObject:parameter];
#pragma clang diagnostic pop
return nil;
}
//
// End of legacy deprecated NSAccessibility informal protocol.
//
// NSAccessibility (key-based) attributes. Order them according to
// NSAccessibilityConstants.h, or see https://crbug.com/678898.
- (NSString*)AXAccessKey {
if (![self instanceActive])
return nil;
return [self getStringAttribute:ax::mojom::StringAttribute::kAccessKey];
}
- (NSNumber*)AXARIAAtomic {
if (![self instanceActive])
return nil;
return @(_node->GetBoolAttribute(ax::mojom::BoolAttribute::kLiveAtomic));
}
- (NSNumber*)AXARIABusy {
if (![self instanceActive])
return nil;
return @(_node->GetBoolAttribute(ax::mojom::BoolAttribute::kBusy));
}
- (NSString*)AXARIACurrent {
if (![self instanceActive])
return nil;
int ariaCurrent;
if (!_node->GetIntAttribute(ax::mojom::IntAttribute::kAriaCurrentState,
&ariaCurrent))
return nil;
switch (static_cast<ax::mojom::AriaCurrentState>(ariaCurrent)) {
case ax::mojom::AriaCurrentState::kNone:
NOTREACHED();
case ax::mojom::AriaCurrentState::kFalse:
return @"false";
case ax::mojom::AriaCurrentState::kTrue:
return @"true";
case ax::mojom::AriaCurrentState::kPage:
return @"page";
case ax::mojom::AriaCurrentState::kStep:
return @"step";
case ax::mojom::AriaCurrentState::kLocation:
return @"location";
case ax::mojom::AriaCurrentState::kDate:
return @"date";
case ax::mojom::AriaCurrentState::kTime:
return @"time";
}
NOTREACHED();
}
- (NSNumber*)AXARIAColumnCount {
if (![self instanceActive])
return nil;
std::optional<int> ariaColCount =
_node->GetDelegate()->GetTableAriaColCount();
if (!ariaColCount)
return nil;
return @(*ariaColCount);
}
- (NSNumber*)AXARIAColumnIndex {
if (![self instanceActive])
return nil;
std::optional<int> ariaColIndex =
_node->GetDelegate()->GetTableCellAriaColIndex();
if (!ariaColIndex)
return nil;
return @(*ariaColIndex);
}
- (NSString*)AXARIALive {
if (![self instanceActive])
return nil;
return [self getStringAttribute:ax::mojom::StringAttribute::kLiveStatus];
}
- (NSString*)AXARIARelevant {
if (![self instanceActive])
return nil;
return [self getStringAttribute:ax::mojom::StringAttribute::kLiveRelevant];
}
- (NSNumber*)AXARIARowCount {
if (![self instanceActive])
return nil;
std::optional<int> ariaRowCount =
_node->GetDelegate()->GetTableAriaRowCount();
if (!ariaRowCount)
return nil;
return @(*ariaRowCount);
}
- (NSNumber*)AXARIARowIndex {
if (![self instanceActive])
return nil;
std::optional<int> ariaRowIndex =
_node->GetDelegate()->GetTableCellAriaRowIndex();
if (!ariaRowIndex)
return nil;
return @(*ariaRowIndex);
}
- (NSString*)AXAutocompleteValue {
if (![self instanceActive])
return nil;
return [self getStringAttribute:ax::mojom::StringAttribute::kAutoComplete];
}
- (NSString*)AXBrailleLabel {
if (![self instanceActive])
return nil;
return
[self getStringAttribute:ax::mojom::StringAttribute::kAriaBrailleLabel];
}
- (NSString*)AXBrailleRoleDescription {
if (![self instanceActive])
return nil;
return [self getStringAttribute:ax::mojom::StringAttribute::
kAriaBrailleRoleDescription];
}
- (id)AXBlockQuoteLevel {
if (![self instanceActive])
return nil;
// This is for the number of ancestors that are a <blockquote>, including
// self, useful for tracking replies to replies etc. in an email.
int level = 0;
for (ui::AXPlatformNodeBase* ancestor = _node; ancestor;
ancestor = ancestor->GetPlatformParent()) {
// Do not cross document boundaries.
if (ui::IsPlatformDocument(ancestor->GetRole()))
break;
if (ancestor->GetRole() == ax::mojom::Role::kBlockquote)
++level;
}
return @(level);
}
- (NSArray*)AXColumnHeaderUIElements {
return [self accessibilityColumnHeaderUIElements];
}
- (NSArray*)AXDetailsElements {
if (![self instanceActive])
return nil;
NSMutableArray* elements = [NSMutableArray array];
for (ui::AXNodeID id :
_node->GetIntListAttribute(ax::mojom::IntListAttribute::kDetailsIds)) {
AXPlatformNodeCocoa* node = [self fromNodeID:id];
if (node)
[elements addObject:node];
}
return elements.count ? elements : nil;
}
- (NSArray*)AXDOMClassList {
if (![self instanceActive])
return nil;
NSMutableArray* ret = [NSMutableArray array];
std::string classes;
if (_node->GetStringAttribute(ax::mojom::StringAttribute::kClassName,
&classes)) {
std::vector<std::string> split_classes = base::SplitString(
classes, " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
for (const auto& className : split_classes)
[ret addObject:(base::SysUTF8ToNSString(className))];
}
return ret;
}
- (NSString*)AXDOMIdentifier {
if (![self instanceActive])
return nil;
std::string id;
if (_node->GetStringAttribute(ax::mojom::StringAttribute::kHtmlId, &id)) {
return base::SysUTF8ToNSString(id);
}
return @"";
}
- (id)AXEditableAncestor {
if (![self instanceActive])
return nil;
ui::AXPlatformNodeBase* text_field_ancestor =
_node->GetPlatformTextFieldAncestor();
if (text_field_ancestor)
return text_field_ancestor->GetNativeViewAccessible().Get();
return nil;
}
- (NSNumber*)AXElementBusy {
if (![self instanceActive])
return nil;
return @(_node->GetBoolAttribute(ax::mojom::BoolAttribute::kBusy));
}
- (NSArray*)AXErrorMessageElements {
if (![self instanceActive]) {
return nil;
}
NSMutableArray* elements = [NSMutableArray array];
for (ui::AXNodeID id : _node->GetIntListAttribute(
ax::mojom::IntListAttribute::kErrormessageIds)) {
AXPlatformNodeCocoa* node = [self fromNodeID:id];
if (node) {
[elements addObject:node];
}
}
return elements.count ? elements : nil;
}
- (NSNumber*)AXGrabbed {
return @NO;
}
- (NSNumber*)AXHasPopup {
if (![self instanceActive])
return nil;
return @(_node->HasIntAttribute(ax::mojom::IntAttribute::kHasPopup));
}
- (id)AXHighestEditableAncestor {
if (![self instanceActive])
return nil;
AXPlatformNodeCocoa* highestEditableAncestor = [self AXEditableAncestor];
while (highestEditableAncestor) {
AXPlatformNodeCocoa* ancestorParent = [highestEditableAncestor AXParent];
if (!ancestorParent || ![ancestorParent isKindOfClass:[self class]])
break;
AXPlatformNodeCocoa* higherAncestor = [ancestorParent AXEditableAncestor];
if (!higherAncestor)
break;
highestEditableAncestor = higherAncestor;
}
return highestEditableAncestor;
}
- (NSString*)AXInvalid {
if (![self instanceActive])
return nil;
switch (_node->GetData().GetInvalidState()) {
case ax::mojom::InvalidState::kNone:
case ax::mojom::InvalidState::kFalse:
return @"false";
case ax::mojom::InvalidState::kTrue:
return @"true";
}
}
- (NSNumber*)AXIsMultiSelectable {
if (![self instanceActive])
return nil;
return @(_node->HasState(ax::mojom::State::kMultiselectable));
}
- (NSString*)AXKeyShortcutsValue {
if (![self instanceActive])
return nil;
return [self getStringAttribute:ax::mojom::StringAttribute::kKeyShortcuts];
}
- (NSNumber*)AXLoaded {
if (![self instanceActive])
return nil;
return @(_node->GetDelegate()->GetTreeData().loaded);
}
- (NSNumber*)AXLoadingProgress {
if (![self instanceActive])
return nil;
double doubleValue = _node->GetDelegate()->GetTreeData().loading_progress;
return @(doubleValue);
}
- (id)AXOwns {
if (![self instanceActive])
return nil;
ui::AXPlatformNodeBase* activeDescendant = _node->GetActiveDescendant();
if (!activeDescendant)
return nil;
ui::AXPlatformNodeBase* container = activeDescendant->GetSelectionContainer();
if (!container)
return nil;
return @[ container->GetNativeViewAccessible().Get() ];
}
- (NSString*)AXPopupValue {
if (![self instanceActive])
return nil;
int hasPopup = _node->GetIntAttribute(ax::mojom::IntAttribute::kHasPopup);
switch (static_cast<ax::mojom::HasPopup>(hasPopup)) {
case ax::mojom::HasPopup::kFalse:
return @"false";
case ax::mojom::HasPopup::kTrue:
return @"true";
case ax::mojom::HasPopup::kMenu:
return @"menu";
case ax::mojom::HasPopup::kListbox:
return @"listbox";
case ax::mojom::HasPopup::kTree:
return @"tree";
case ax::mojom::HasPopup::kGrid:
return @"grid";
case ax::mojom::HasPopup::kDialog:
return @"dialog";
}
}
- (NSNumber*)AXRequired {
return [self isAccessibilityRequired] ? @YES : @NO;
}
- (NSString*)AXRole {
if (!_node)
return nil;
return [[self class] nativeRoleFromAXRole:_node->GetRole()];
}
- (NSString*)AXRoleDescription {
return [self accessibilityRoleDescription];
}
- (NSNumber*)AXSelected {
return [self accessibilitySelected];
}
- (NSArray*)AXSelectedRows {
return [self accessibilitySelectedRows];
}
- (NSString*)AXSubrole {
ax::mojom::Role role = _node->GetRole();
switch (role) {
case ax::mojom::Role::kTextField:
if (_node->HasState(ax::mojom::State::kProtected))
return NSAccessibilitySecureTextFieldSubrole;
break;
default:
break;
}
return [AXPlatformNodeCocoa nativeSubroleFromAXRole:role];
}
- (NSURL*)AXURL {
return [self accessibilityURL];
}
- (NSNumber*)AXVisited {
if (![self instanceActive])
return nil;
return @(_node->HasState(ax::mojom::State::kVisited));
}
- (NSString*)AXHelp {
if (![self instanceActive]) {
return nil;
}
// ARIA descriptions are returned as AXCustomContent (see
// -accessibilityCustomContent below), so if the description is from ARIA,
// don't provide it as AXHelp, and return nothing.
if ([self descriptionIsFromAriaDescription]) {
return nil;
}
// Otherwise, it's a non-ARIA description, which is returned as AXHelp.
return [self getStringAttribute:ax::mojom::StringAttribute::kDescription];
}
- (id)AXValue {
ax::mojom::Role role = _node->GetRole();
if (role == ax::mojom::Role::kTab)
return [self AXSelected];
if (ui::IsNameExposedInAXValueForRole(role))
return [self getName];
if (_node->IsPlatformCheckable()) {
// Mixed checkbox state not currently supported in views, but could be.
// See browser_accessibility_cocoa.mm for details.
const auto checkedState = static_cast<ax::mojom::CheckedState>(
_node->GetIntAttribute(ax::mojom::IntAttribute::kCheckedState));
return checkedState == ax::mojom::CheckedState::kTrue ? @1 : @0;
}
return base::SysUTF16ToNSString(_node->GetValueForControl());
}
- (NSNumber*)AXEnabled {
return @([self isAccessibilityEnabled]);
}
- (BOOL)isAccessibilityExpanded {
// Keep logic consistent with `-[BrowserAccessibilityCocoa expanded]`
if (![self instanceActive]) {
return NO;
}
return _node->HasState(ax::mojom::State::kExpanded);
}
- (NSNumber*)AXFocused {
return @([self isAccessibilityFocused]);
}
- (BOOL)isAccessibilityFocused {
if (![self instanceActive]) {
return NO;
}
return _node->GetDelegate()->GetFocus() == _node->GetNativeViewAccessible();
}
- (id)AXFocusableAncestor {
if (![self instanceActive])
return nil;
ui::AXPlatformNodeBase* ancestor = _node;
for (; ancestor; ancestor = ancestor->GetPlatformParent()) {
// Do not cross document boundaries.
if (ui::IsPlatformDocument(ancestor->GetRole()))
return nil;
if (ancestor->IsFocusable())
break;
}
// The assignment to ancestor may be null.
if (!ancestor)
return nil;
return ancestor->GetNativeViewAccessible().Get();
}
- (id)AXParent {
if (!_node)
return nil;
return NSAccessibilityUnignoredAncestor(_node->GetParent().Get());
}
- (NSArray*)accessibilityChildren {
if (!_node)
return @[];
int count = _node->GetChildCount();
NSMutableArray* children = [NSMutableArray arrayWithCapacity:count];
for (auto child_iterator_ptr = _node->GetDelegate()->ChildrenBegin();
*child_iterator_ptr != *_node->GetDelegate()->ChildrenEnd();
++(*child_iterator_ptr)) {
[children addObject:child_iterator_ptr->GetNativeViewAccessible().Get()];
}
return NSAccessibilityUnignoredChildren(children);
}
- (NSArray*)accessibilityChildrenInNavigationOrder {
// We follow Webkit's implementation here.
return [self accessibilityChildren];
}
- (id)AXWindow {
return _node->GetDelegate()->GetNSWindow().Get();
}
- (id)AXTopLevelUIElement {
return [self AXWindow];
}
- (NSValue*)AXPosition {
return [NSValue valueWithPoint:self.boundsInScreen.origin];
}
- (NSValue*)AXSize {
return [NSValue valueWithSize:self.boundsInScreen.size];
}
- (NSString*)AXTitle {
return [self accessibilityTitle];
}
- (id)AXTitleUIElement {
return [self accessibilityTitleUIElement];
}
- (NSString*)AXDescription {
return [self accessibilityLabel];
}
// Misc attributes.
- (NSString*)AXPlaceholderValue {
if (![self instanceActive]) {
return nil;
}
if (_node->GetNameFrom() == ax::mojom::NameFrom::kPlaceholder) {
return [self getName];
}
return [self getStringAttribute:ax::mojom::StringAttribute::kPlaceholder];
}
- (NSString*)AXMenuItemMarkChar {
if (!ui::IsMenuItem(_node->GetRole()))
return nil;
const auto checkedState = static_cast<ax::mojom::CheckedState>(
_node->GetIntAttribute(ax::mojom::IntAttribute::kCheckedState));
if (checkedState == ax::mojom::CheckedState::kTrue) {
return @"\u2713"; // "check mark"
}
return @"";
}
- (NSNumber*)AXARIAPosInSet {
if (![self instanceActive])
return nil;
std::optional<int> posInSet = _node->GetPosInSet();
if (!posInSet)
return nil;
return @(*posInSet);
}
- (NSNumber*)AXARIASetSize {
if (![self instanceActive])
return nil;
std::optional<int> setSize = _node->GetSetSize();
if (!setSize)
return nil;
return @(*setSize);
}
// Text-specific attributes.
// LINT.IfChange
- (NSString*)AXSelectedText {
NSRange selectedTextRange = [[self AXSelectedTextRange] rangeValue];
return [[self getAXValueAsString] substringWithRange:selectedTextRange];
}
// LINT.ThenChange(accessibilitySelectedText)
// LINT.IfChange
- (NSValue*)AXSelectedTextRange {
int start = 0, end = 0;
if (_node->IsAtomicTextField() &&
_node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart, &start) &&
_node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd, &end)) {
// NSRange cannot represent the direction the text was selected in.
return
[NSValue valueWithRange:{static_cast<NSUInteger>(std::min(start, end)),
static_cast<NSUInteger>(abs(end - start))}];
}
return [NSValue valueWithRange:NSMakeRange(0, 0)];
}
// LINT.ThenChange(accessibilitySelectedTextRange)
// LINT.IfChange
- (NSNumber*)AXNumberOfCharacters {
return @([[self getAXValueAsString] length]);
}
// LINT.ThenChange(accessibilityNumberOfCharacters)
// LINT.IfChange
- (NSValue*)AXVisibleCharacterRange {
return [NSValue valueWithRange:{0, [[self getAXValueAsString] length]}];
}
// LINT.ThenChange(accessibilityVisibleCharacterRange)
// LINT.IfChange
- (NSNumber*)AXInsertionPointLineNumber {
// TODO: multiline is not supported on views.
return @0;
}
// LINT.ThenChange(accessibilityInsertionPointLineNumber)
// Parameterized text-specific attributes.
- (id)AXRangeForLine:(id)parameter {
NSNumber* lineNumber = base::apple::ObjCCast<NSNumber>(parameter);
if (!lineNumber) {
return nil;
}
int lineIndex = [lineNumber intValue];
if (lineIndex != 0) {
return nil;
}
return [NSValue valueWithRange:[self accessibilityRangeForLine:lineIndex]];
}
- (id)AXStringForRange:(id)parameter {
if (![parameter isKindOfClass:[NSValue class]] ||
(0 != UNSAFE_TODO(strcmp([parameter objCType], @encode(NSRange))))) {
return nil;
}
return [self accessibilityStringForRange:[parameter rangeValue]];
}
- (id)AXRangeForPosition:(id)parameter {
NSValue* positionValue = base::apple::ObjCCast<NSValue>(parameter);
if (!positionValue) {
return nil;
}
NSPoint point = [positionValue pointValue];
return [NSValue valueWithRange:[self accessibilityRangeForPosition:point]];
}
- (id)AXRangeForIndex:(id)parameter {
NSNumber* indexNumber = base::apple::ObjCCast<NSNumber>(parameter);
if (!indexNumber) {
return nil;
}
NSInteger index = [indexNumber intValue];
return [NSValue valueWithRange:[self accessibilityRangeForIndex:index]];
}
- (id)AXBoundsForRange:(id)parameter {
// TODO(tapted): Provide an accessor on AXPlatformNodeDelegate to obtain this
// from ui::TextInputClient::GetCompositionCharacterBounds().
NOTIMPLEMENTED();
return nil;
}
- (id)AXRTFForRange:(id)parameter {
NOTIMPLEMENTED();
return nil;
}
- (id)AXStyleRangeForIndex:(id)parameter {
NSNumber* indexNumber = base::apple::ObjCCast<NSNumber>(parameter);
if (!indexNumber) {
return nil;
}
return [NSValue
valueWithRange:[self accessibilityStyleRangeForIndex:[indexNumber
intValue]]];
}
- (id)AXAttributedStringForRange:(id)parameter {
if (![parameter isKindOfClass:[NSValue class]])
return nil;
// TODO(crbug.com/41456329): Finish implementation.
// Currently, we only decorate the attributed string with misspelling
// information.
// TODO(tapted): views::WordLookupClient has a way to obtain the actual
// decorations, and BridgedContentView has a conversion function that creates
// an NSAttributedString. Refactor things so they can be used here.
NSRange range = [(NSValue*)parameter rangeValue];
std::u16string textContent = _node->GetTextContentUTF16();
if (NSMaxRange(range) > textContent.length())
return nil;
// We potentially need to add text attributes to the whole text content
// because a spelling mistake might start or end outside the given range.
NSMutableAttributedString* attributedTextContent =
[[NSMutableAttributedString alloc]
initWithString:base::SysUTF16ToNSString(textContent)];
if (!_node->IsText()) {
AXRange axRange(_node->GetDelegate()->CreateTextPositionAt(0),
_node->GetDelegate()->CreateTextPositionAt(
static_cast<int>(textContent.length())));
[self addTextAnnotationsIn:&axRange to:attributedTextContent];
}
return [attributedTextContent attributedSubstringFromRange:range];
}
- (NSAttributedString*)AXAttributedStringForTextMarkerRange:(id)markerRange {
AXRange axRange = ui::AXTextMarkerRangeToAXRange(markerRange);
if (axRange.IsNull())
return nil;
NSString* text = base::SysUTF16ToNSString(axRange.GetText());
if (text.length == 0) {
return nil;
}
NSMutableAttributedString* attributedText =
[[NSMutableAttributedString alloc] initWithString:text];
// Currently, we only decorate the attributed string with misspelling
// and annotation information.
[self addTextAnnotationsIn:&axRange to:attributedText];
return attributedText;
}
- (NSString*)ChromeAXNodeId {
return [@(_node->GetNodeId()) stringValue];
}
- (NSString*)description {
return [NSString stringWithFormat:@"%@ - %@ (%@)", [super description],
[self accessibilityTitle], [self AXRole]];
}
//
// End of key-based attributes.
//
//
// NSAccessibility protocol.
// https://developer.apple.com/documentation/appkit/nsaccessibilityprotocol
//
// These methods appear to be the minimum needed to avoid AppKit refusing to
// handle the element or crashing internally. Most of the remaining old API
// methods (the ones from NSObject) are implemented in terms of the new
// NSAccessibility methods.
//
// TODO(crbug.com/41115917): Does this class need to implement the various
// accessibilityPerformFoo methods, or are the stub implementations from
// NSAccessibilityElement sufficient?
// NSAccessibility: Configuring Accessibility.
- (BOOL)isAccessibilityElement {
if (!_node) {
return NO;
}
DCHECK(_node->GetDelegate());
DCHECK([self instanceActive]);
// After ViewsAX lands, we should be able to add this DCHECK.
// DCHECK(!_node->GetDelegate()->IsIgnored())
// << "Ignored nodes should be removed by PlatformGet*() methods:"
// << _node->GetDelegate()->ToString();
if (_node->GetDelegate()->IsInvisibleOrIgnored()) {
return NO;
}
if ([self internalRole] == ax::mojom::Role::kImage &&
_node->GetData().GetNameFrom() ==
ax::mojom::NameFrom::kAttributeExplicitlyEmpty) {
return NO;
}
return YES;
}
- (BOOL)isAccessibilityEnabled {
if (!_node)
return NO;
// Native menus expose separators as disabled menu items. Chromium mirrors
// this behavior.
if (_node->GetRole() == ax::mojom::Role::kMenuItemSeparator) {
return NO;
}
return _node->GetData().GetRestriction() != ax::mojom::Restriction::kDisabled;
}
- (NSRect)accessibilityFrame {
return [self boundsInScreen];
}
- (NSString*)accessibilityHelp {
return [self AXHelp];
}
- (NSString*)accessibilityLabel {
if (![self instanceActive])
return nil;
// macOS wants static text exposed in AXValue.
if (ui::IsNameExposedInAXValueForRole([self internalRole]))
return @"";
// If we're exposing the title in TitleUIElement, don't also redundantly
// expose it in accessibilityLabel.
if ([self titleUIElement])
return @"";
if (![self isNameFromLabel])
return @"";
std::string name = _node->GetName();
if (!name.empty())
return base::SysUTF8ToNSString(name);
// Given an image where there's no other title, return the base part
// of the filename as the description.
if ([self isImage]) {
std::string url;
if (_node->GetStringAttribute(ax::mojom::StringAttribute::kUrl, &url)) {
// Given a url like http://foo.com/bar/baz.png, just return the
// base name, e.g., "baz.png".
size_t leftIndex = url.rfind('/');
std::string basename =
leftIndex != std::string::npos ? url.substr(leftIndex) : url;
return base::SysUTF8ToNSString(basename);
}
}
return @"";
}
// LINT.IfChange(accessibilityLinkedUIElements)
- (NSArray*)accessibilityLinkedUIElements {
if (![self instanceActive]) {
return nil;
}
ui::AXPlatformNodeDelegate* delegate = [self nodeDelegate];
if (!delegate) {
return nil;
}
NSMutableArray* elements = [[NSMutableArray alloc] init];
[elements
addObjectsFromArray:[self uiElementsForAttribute:
ax::mojom::IntListAttribute::kControlsIds]];
[elements
addObjectsFromArray:[self uiElementsForAttribute:
ax::mojom::IntListAttribute::kFlowtoIds]];
int targetId;
if (delegate->GetIntAttribute(ax::mojom::IntAttribute::kInPageLinkTargetId,
&targetId)) {
ui::AXPlatformNode* target = delegate->GetFromNodeID(targetId);
if (target) {
[elements addObject:target->GetNativeViewAccessible().Get()];
}
}
[elements
addObjectsFromArray:[self
uiElementsForAttribute:
ax::mojom::IntListAttribute::kRadioGroupIds]];
return elements;
}
// LINT.ThenChange(ui/accessibility/platform/browser_accessibility_cocoa.mm:accessibilityLinkedUIElements)
- (NSString*)accessibilityTitle {
if (![self instanceActive])
return nil;
if (ui::IsNameExposedInAXValueForRole(_node->GetRole()))
return @"";
if ([self isNameFromLabel])
return @"";
// If we're exposing the title in TitleUIElement, don't also redundantly
// expose it in AXDescription.
if ([self titleUIElement])
return @"";
ax::mojom::NameFrom nameFrom = _node->GetNameFrom();
// The accessible name, which is exposed via accessibilityTitle, should not
// contain any placeholder text because an HTML or an ARIA placeholder refers
// to a sample value that is usually found in a text field and is used to aid
// the user in data entry. It is similar to a replacement for the value
// attribute, not the title.
if (nameFrom == ax::mojom::NameFrom::kPlaceholder)
return @"";
// Cell titles are empty if they came from content.
if (nameFrom == ax::mojom::NameFrom::kContents) {
NSString* role = [self accessibilityRole];
if ([role isEqualToString:NSAccessibilityCellRole])
return @"";
}
return [self getName];
}
- (id)accessibilityValue {
return [self AXValue];
}
- (void)setAccessibilityValue:(id)value {
if (!_node) {
return;
}
ui::AXActionData data;
data.action = _node->GetRole() == ax::mojom::Role::kTab
? ax::mojom::Action::kSetSelection
: ax::mojom::Action::kSetValue;
if ([value isKindOfClass:[NSString class]]) {
data.value = base::SysNSStringToUTF8(value);
} else if ([value isKindOfClass:[NSValue class]]) {
// TODO(crbug.com/41115917): Is this case actually needed? The
// NSObject accessibility implementation supported this, but can it actually
// occur?
NSRange range = [value rangeValue];
data.anchor_offset = range.location;
data.focus_offset = NSMaxRange(range);
}
_node->GetDelegate()->AccessibilityPerformAction(data);
}
- (BOOL)isAccessibilitySelectorAllowed:(SEL)selector {
TRACE_EVENT1(
"accessibility", "AXPlatformNodeCocoa::isAccessibilitySelectorAllowed",
"selector=", base::SysNSStringToUTF8(NSStringFromSelector(selector)));
if (!_node) {
return NO;
}
if (selector == @selector(setAccessibilityFocused:)) {
return _node->IsFocusable();
}
if (selector == @selector(setAccessibilityValue:)) {
switch (_node->GetRole()) {
case ax::mojom::Role::kSlider:
// When VoiceOver performs an increment/decrement action, it immediately
// calls upon success of the action the selector setAccessibilityValue
// on the slider that was just updated. The value passed to this
// function is always equals to 5% of the slider's value range, so
// actually setting that value to our slider would:
// 1. render the increment/decrement action performed a moment before
// useless as it would override the modified value;
// 2. make the slider value stuck in place, at 5% of its range.
//
// I haven't found much on the topic online, so the following is at best
// a conjecture: I believe that VoiceOver "suggests" us to
// increment/decrement the value by 5%. There might be a setting I'm not
// aware of that allows the VO users to modify this value by a different
// one, which would allow them to always increment/decrement sliders by
// the same amount on all apps.
//
// However, in Chromium, we handle the increment and decrement actions
// on the blink side and the step value is computed over there. That
// way, the experience for changing the value of a slider by increments
// is the same for all different inputs: whether it's the keyboard arrow
// keys, an AT, etc.
//
// TL;DR: setAccessibilityValue, when called on sliders, is breaking our
// increment and decrement AX actions, so don't allow it.
return NO;
case ax::mojom::Role::kTab:
// Tabs use the radio button role on Mac, so they are selected by
// calling setSelected on an individual tab, rather than by setting the
// selected element on the tabstrip as a whole.
return !_node->GetBoolAttribute(ax::mojom::BoolAttribute::kSelected);
default:
break;
}
}
// Don't allow calling AX setters on disabled elements.
// TODO(crbug.com/41301942): Once the underlying bug in
// views::Textfield::SetSelectionRange() described in that bug is fixed,
// remove the check here when the selector is setAccessibilitySelectedText*;
// right now, this check serves to prevent accessibility clients from trying
// to set the selection range, which won't work because of 692362.
if (_node->GetDelegate() && _node->GetDelegate()->IsReadOnlyOrDisabled() &&
IsAXSetter(selector)) {
return NO;
}
NSString* selectorString = NSStringFromSelector(selector);
if ([[self class] isMethodImplementedForNewAccessibilityAPI:selectorString] &&
![self supportsNewAccessibilityAPIMethod:selectorString]) {
return NO;
}
// TODO(crbug.com/41115917): What about role-specific selectors?
return [super isAccessibilitySelectorAllowed:selector];
}
// NSAccessibility: Determining Relationships.
- (NSArray*)AXChildren {
return [self accessibilityChildren];
}
- (id)accessibilityParent {
return [self AXParent];
}
// NSAccessibility: Assigning Roles.
- (BOOL)isAccessibilityRequired {
TRACE_EVENT1("accessibility", "accessibilityRequired",
"role=", ui::ToString([self internalRole]));
if (![self instanceActive]) {
return NO;
}
return _node->HasState(ax::mojom::State::kRequired);
}
- (NSAccessibilityRole)accessibilityRole {
return [self AXRole];
}
- (NSAccessibilitySubrole)accessibilitySubrole {
return [self AXSubrole];
}
- (NSString*)accessibilityRoleDescription {
TRACE_EVENT1("accessibility", "accessibilityRoleDescription",
"role=", ui::ToString([self internalRole]));
if (![self instanceActive]) {
return nil;
}
// Image annotations.
if (_node->GetData().GetImageAnnotationStatus() ==
ax::mojom::ImageAnnotationStatus::kEligibleForAnnotation ||
_node->GetData().GetImageAnnotationStatus() ==
ax::mojom::ImageAnnotationStatus::kSilentlyEligibleForAnnotation) {
return base::SysUTF16ToNSString(
_node->GetDelegate()->GetLocalizedRoleDescriptionForUnlabeledImage());
}
// ARIA role description.
std::string roleDescription;
if (_node->GetStringAttribute(ax::mojom::StringAttribute::kRoleDescription,
&roleDescription)) {
return [base::SysUTF8ToNSString(_node->GetStringAttribute(
ax::mojom::StringAttribute::kRoleDescription)) lowercaseString];
}
NSString* role = [self accessibilityRole];
switch ([self internalRole]) {
case ax::mojom::Role::kColorWell: // Use platform's "color well"
case ax::mojom::Role::kImage: // Default: IDS_AX_ROLE_GRAPHIC
case ax::mojom::Role::kInputTime: // Use platform's "time field"
case ax::mojom::Role::kMeter: // Use platform's "level indicator"
case ax::mojom::Role::kPopUpButton: // Use platform's "popup button"
case ax::mojom::Role::kTabList: // Use platform's "tab group"
case ax::mojom::Role::kTree: // Use platform's "outline"
case ax::mojom::Role::kTreeItem: // Use platform's "outline row"
break;
case ax::mojom::Role::kHeader: // Default: IDS_AX_ROLE_HEADER
return l10n_util::GetNSString(IDS_AX_ROLE_BANNER);
case ax::mojom::Role::kRootWebArea: {
if ([role isEqualToString:NSAccessibilityWebAreaRole]) {
return l10n_util::GetNSString(IDS_AX_ROLE_WEB_AREA);
}
// Preserve platform default of "group" in the case of the child
// of a presentational <iframe> which has the internal role of
// kRootWebArea.
break;
}
default: {
std::u16string result =
_node->GetDelegate()->GetLocalizedStringForRoleDescription();
if (!result.empty()) {
return base::SysUTF16ToNSString(result);
}
}
}
return NSAccessibilityRoleDescription(role, [self accessibilitySubrole]);
}
// NSAccessibility: Configuring Table and Outline Views.
- (NSArray*)accessibilitySelectedRows {
if (![self instanceActive]) {
return nil;
}
NSArray* rows = [self accessibilityRows];
// accessibilityRows returns an empty array unless instanceActive does,
// not exist, so we do not need to check if rows is nil at this time.
NSMutableArray* selectedRows = [NSMutableArray array];
for (id row in rows) {
if ([[row accessibilitySelected] boolValue]) {
[selectedRows addObject:row];
}
}
return selectedRows;
}
- (NSArray*)accessibilityColumnHeaderUIElements {
if (![self instanceActive]) {
return nil;
}
ui::AXPlatformNodeDelegate* delegate = _node->GetDelegate();
NSMutableArray* ret = [NSMutableArray array];
// If this is a table, return all column headers.
ax::mojom::Role role = _node->GetRole();
if (ui::IsTableLike(role)) {
for (ui::AXNodeID id : delegate->GetColHeaderNodeIds()) {
AXPlatformNodeCocoa* colheader = [self fromNodeID:id];
if (colheader) {
[ret addObject:colheader];
}
}
return [ret count] ? ret : nil;
}
// Otherwise if this is a cell or a header cell, return the column headers for
// it.
if (!ui::IsCellOrTableHeader(role)) {
return nil;
}
ui::AXPlatformNodeBase* table = _node->GetTable();
if (!table) {
return nil;
}
std::optional<int> column = delegate->GetTableCellColIndex();
if (!column) {
return nil;
}
ui::AXPlatformNodeDelegate* tableDelegate = table->GetDelegate();
for (ui::AXNodeID id : tableDelegate->GetColHeaderNodeIds(*column)) {
AXPlatformNodeCocoa* colheader = [self fromNodeID:id];
if (colheader) {
[ret addObject:colheader];
}
}
return [ret count] ? ret : nil;
}
- (id)accessibilityHeader {
// Keep logic consistent with `-[BrowserAccessibilityCocoa header]`
if (![self instanceActive]) {
return nil;
}
if (ui::IsTableLike(_node->GetRole())) {
ui::AXPlatformNodeDelegate* delegate = _node->GetDelegate();
// The table header container is a special node in the accessibility tree
// only used on macOS. It has all of the table headers as its children, even
// though those cells are also children of rows in the table. Internally
// this is implemented using `AXTableInfo` and `indirect_child_ids` with the
// result retrievable via `AXNode::GetExtraMacNodes()`.
const std::vector<raw_ptr<ui::AXNode, VectorExperimental>>* nodes =
delegate->node()->GetExtraMacNodes();
if (nodes && !nodes->empty()) {
ui::AXNode* lastChild = nodes->back();
if (lastChild->GetRole() == ax::mojom::Role::kTableHeaderContainer) {
// TODO(crbug.com/363275809): This works for `BrowserAccessibilityCocoa`
// nodes but will otherwise fail with `-fromNodeID` returning nil. This
// is due to the fact that `BrowserAccessibilityMac` ensures that the
// internal "extra Mac nodes" are included in the platform accessibility
// tree. See `BrowserAccessibilityMac::PlatformChildCount` and
// `BrowserAccessibilityMac::PlatformGetChild` as examples.
return [self fromNodeID:lastChild->id()];
}
}
return nil;
}
int headerElementId = -1;
if ([self internalRole] == ax::mojom::Role::kColumn) {
_node->GetIntAttribute(ax::mojom::IntAttribute::kTableColumnHeaderId,
&headerElementId);
} else if ([self internalRole] == ax::mojom::Role::kRow) {
_node->GetIntAttribute(ax::mojom::IntAttribute::kTableRowHeaderId,
&headerElementId);
}
return headerElementId > 0 ? [self fromNodeID:headerElementId] : nil;
}
- (NSInteger)accessibilityColumnCount {
if (![self instanceActive]) {
return NSNotFound;
}
if (!ui::IsTableLike(_node->GetRole())) {
return NSNotFound;
}
ui::AXPlatformNodeDelegate* delegate = _node->GetDelegate();
std::optional<int> count = delegate->GetTableColCount();
if (count.has_value()) {
return *count;
}
return -1;
}
- (NSInteger)accessibilityRowCount {
if (![self instanceActive]) {
return NSNotFound;
}
if (!ui::IsTableLike(_node->GetRole())) {
return NSNotFound;
}
ui::AXPlatformNodeDelegate* delegate = _node->GetDelegate();
std::optional<int> count = delegate->GetTableRowCount();
if (count.has_value()) {
return *count;
}
return -1;
}
// LINT.IfChange(accessibilityRowHeaderUIElements)
- (NSArray*)accessibilityRowHeaderUIElements {
if (![self instanceActive]) {
return nil;
}
ax::mojom::Role role = [self internalRole];
bool isCellOrTableHeader = ui::IsCellOrTableHeader(role);
bool isTableLike = ui::IsTableLike(role);
if (!isTableLike && !isCellOrTableHeader) {
return nil;
}
ui::AXPlatformNodeDelegate* delegate = [self nodeDelegate];
gfx::NativeViewAccessible table = delegate->GetTableAncestor();
if (!table) {
return nil;
}
ui::AXPlatformNode* tableNode =
ui::AXPlatformNode::FromNativeViewAccessible(table);
if (!tableNode) {
return nil;
}
ui::AXPlatformNodeDelegate* tableDelegate = tableNode->GetDelegate();
// A table with no row headers.
if (isTableLike && !tableDelegate->GetTableRowCount().has_value()) {
return nil;
}
NSMutableArray* rowHeaders = [[NSMutableArray alloc] init];
if (isTableLike) {
// Return the table's row headers.
std::set<int32_t> headerIds;
int numberOfRows = tableDelegate->GetTableRowCount().value();
// Rows can have more than one row header cell. Also, we apparently need
// to guard against duplicate row header ids. Storing in a set dedups.
for (int i = 0; i < numberOfRows; i++) {
std::vector<int32_t> rowHeaderIds = tableDelegate->GetRowHeaderNodeIds(i);
for (int32_t rowHeaderId : rowHeaderIds) {
headerIds.insert(rowHeaderId);
}
}
for (int32_t headerId : headerIds) {
ui::AXPlatformNode* cellNode = tableDelegate->GetFromNodeID(headerId);
if (cellNode) {
[rowHeaders addObject:cellNode->GetNativeViewAccessible().Get()];
}
}
} else {
// Otherwise this is a cell, return the row headers for this cell.
for (int32_t nodeId : delegate->GetRowHeaderNodeIds()) {
ui::AXPlatformNode* cellNode = delegate->GetFromNodeID(nodeId);
if (cellNode) {
[rowHeaders addObject:cellNode->GetNativeViewAccessible().Get()];
}
}
}
return [rowHeaders count] ? rowHeaders : nil;
}
// LINT.ThenChange(ui/accessibility/platform/browser_accessibility_cocoa.mm:accessibilityRowHeaderUIElements)
// LINT.IfChange(accessibilityColumns)
- (NSArray*)accessibilityColumns {
if (![self instanceActive]) {
return nil;
}
NSMutableArray* columns = [[NSMutableArray alloc] init];
for (AXPlatformNodeCocoa* child in [self accessibilityChildren]) {
if ([[child accessibilityRole] isEqualToString:NSAccessibilityColumnRole]) {
[columns addObject:child];
}
}
return columns;
}
// LINT.ThenChange(ui/accessibility/platform/browser_accessibility_cocoa.mm:accessibilityColumns)
- (NSArray*)accessibilityRows {
if (![self instanceActive]) {
return nil;
}
ui::AXPlatformNodeDelegate* delegate = [self nodeDelegate];
if (!delegate) {
return nil;
}
NSMutableArray* rows = [[NSMutableArray alloc] init];
ax::mojom::Role role = [self internalRole];
std::vector<int32_t> nodeIds;
if (role == ax::mojom::Role::kTree) {
[self getTreeItemDescendantNodeIds:&nodeIds];
} else if (ui::IsTableLike(role)) {
nodeIds = delegate->GetRowNodeIds();
} else if (role == ax::mojom::Role::kColumn) {
// Rows attribute for a column is the list of all the elements in that
// column at each row.
nodeIds = delegate->GetIntListAttribute(
ax::mojom::IntListAttribute::kIndirectChildIds);
}
for (int32_t nodeId : nodeIds) {
ui::AXPlatformNode* rowNode = delegate->GetFromNodeID(nodeId);
if (rowNode) {
[rows addObject:rowNode->GetNativeViewAccessible().Get()];
}
}
return rows;
}
- (NSAccessibilitySortDirection)accessibilitySortDirection {
// Keep logic consistent with `-[BrowserAccessibilityCocoa sortDirection]`
if (![self instanceActive]) {
return NSAccessibilitySortDirectionUnknown;
}
// If this object should not support `accessibilitySortDirection`, treat
// the sort direction as unknown regardless of what's in the `AXNodeData`.
if ([self internalRole] != ax::mojom::Role::kRowHeader &&
[self internalRole] != ax::mojom::Role::kColumnHeader) {
return NSAccessibilitySortDirectionUnknown;
}
// The Core-AAM states that `aria-sort=none` is "not mapped".
int sortDirection;
if (!_node->GetIntAttribute(ax::mojom::IntAttribute::kSortDirection,
&sortDirection) ||
static_cast<ax::mojom::SortDirection>(sortDirection) ==
ax::mojom::SortDirection::kUnsorted) {
return NSAccessibilitySortDirectionUnknown;
}
switch (static_cast<ax::mojom::SortDirection>(sortDirection)) {
case ax::mojom::SortDirection::kAscending:
return NSAccessibilitySortDirectionAscending;
case ax::mojom::SortDirection::kDescending:
return NSAccessibilitySortDirectionDescending;
case ax::mojom::SortDirection::kOther:
return NSAccessibilitySortDirectionUnknown;
default:
NOTREACHED();
}
}
- (id)accessibilityDisclosedByRow {
if (![self instanceActive]) {
return nil;
}
// The row that contains this row.
// It should be the same as the first parent that is a treeitem.
return nil;
}
- (id)accessibilityDisclosedRows {
if (![self instanceActive]) {
return nil;
}
// The rows that are considered inside this row.
return nil;
}
- (NSInteger)accessibilityDisclosureLevel {
if (![self instanceActive]) {
return 0;
}
ax::mojom::Role role = [self internalRole];
if (role == ax::mojom::Role::kRow || role == ax::mojom::Role::kTreeItem ||
role == ax::mojom::Role::kHeading) {
int level =
_node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel);
// Mac disclosureLevel is 0-based, but web levels are 1-based.
if (level > 0) {
level--;
}
return level;
}
return 0;
}
- (BOOL)isAccessibilityDisclosed {
if (![self instanceActive]) {
return NO;
}
if ([self internalRole] == ax::mojom::Role::kTreeItem) {
return _node->HasState(ax::mojom::State::kExpanded);
}
return NO;
}
// NSAccessibility: Setting the Focus.
- (void)setAccessibilityFocused:(BOOL)isFocused {
if (!_node)
return;
ui::AXActionData data;
data.action =
isFocused ? ax::mojom::Action::kFocus : ax::mojom::Action::kBlur;
_node->GetDelegate()->AccessibilityPerformAction(data);
}
- (NSNumber*)treeItemRowIndex {
// TODO(crbug.com/363275809): `-[BrowserAccessibilityCocoa treeItemRowIndex]`
// and related logic such as `-[BrowswerAccessibilityCocoa findRowIndex]`
// should be moved here unless doing so has some impact on view tree items.
return nil;
}
- (NSInteger)accessibilityIndex {
// Keep logic consistent with `-[BrowserAccessibilityCocoa index]`
if (![self instanceActive]) {
return NSNotFound;
}
if ([self internalRole] == ax::mojom::Role::kTreeItem) {
return [[self treeItemRowIndex] integerValue];
} else if ([self internalRole] == ax::mojom::Role::kColumn) {
DCHECK(_node);
std::optional<int> col_index =
_node->GetDelegate()->node()->GetTableColColIndex();
if (col_index.has_value()) {
return *col_index;
}
} else if ([self internalRole] == ax::mojom::Role::kRow) {
DCHECK(_node);
std::optional<int> row_index =
_node->GetDelegate()->node()->GetTableRowRowIndex();
if (row_index.has_value()) {
return *row_index;
}
}
return NSNotFound;
}
// NSAccessibility: Configuring Text Elements.
// These are all "required" methods, although in practice the ones that are left
// NOTIMPLEMENTED() seem to not be called anywhere (and were NOTIMPLEMENTED in
// the old API as well).
// LINT.IfChange
- (NSInteger)accessibilityInsertionPointLineNumber {
if (![self instanceActive]) {
return NSNotFound;
}
// TODO(crbug.com/363275809): According to the comment in the old API code,
// "multiline is not supported on views." If that is no longer the case, we
// need an implementation here. Also the old API code in `AXPlatformNodeCocoa`
// doesn't do any of the work done in by `BrowserAccessibilityCocoa`.
return 0;
}
// LINT.ThenChange(AXInsertionPointLineNumber)
// LINT.IfChange
- (NSInteger)accessibilityNumberOfCharacters {
if (![self instanceActive]) {
return 0;
}
return [[self getAXValueAsString] length];
}
// LINT.ThenChange(AXNumberOfCharacters)
- (NSString*)accessibilityPlaceholderValue {
if (![self instanceActive])
return nil;
if (_node->GetNameFrom() == ax::mojom::NameFrom::kPlaceholder)
return [self getName];
return [self getStringAttribute:ax::mojom::StringAttribute::kPlaceholder];
}
// LINT.IfChange
- (NSString*)accessibilitySelectedText {
if (![self instanceActive]) {
return nil;
}
NSRange selectedTextRange = [self accessibilitySelectedTextRange];
return [[self getAXValueAsString] substringWithRange:selectedTextRange];
}
// LINT.ThenChange(AXSelectedText)
- (void)setAccessibilitySelectedText:(NSString*)text {
if (!_node) {
return;
}
ui::AXActionData data;
data.action = ax::mojom::Action::kReplaceSelectedText;
data.value = base::SysNSStringToUTF8(text);
_node->GetDelegate()->AccessibilityPerformAction(data);
}
// LINT.IfChange
- (NSRange)accessibilitySelectedTextRange {
if (![self instanceActive]) {
return NSMakeRange(0, 0);
}
int start = 0, end = 0;
if (_node->IsAtomicTextField() &&
_node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart, &start) &&
_node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd, &end)) {
// NSRange cannot represent the direction the text was selected in.
return NSMakeRange(static_cast<NSUInteger>(std::min(start, end)),
static_cast<NSUInteger>(abs(end - start)));
}
return NSMakeRange(0, 0);
}
// LINT.ThenChange(AXSelectedTextRange)
- (void)setAccessibilitySelectedTextRange:(NSRange)range {
if (!_node) {
return;
}
ui::AXActionData data;
data.action = ax::mojom::Action::kSetSelection;
data.anchor_offset = range.location;
data.anchor_node_id = _node->GetData().id;
data.focus_offset = NSMaxRange(range);
data.focus_node_id = _node->GetData().id;
_node->GetDelegate()->AccessibilityPerformAction(data);
}
- (NSArray*)accessibilitySelectedTextRanges {
if (!_node)
return nil;
return @[ [self AXSelectedTextRange] ];
}
// LINT.IfChange
- (NSRange)accessibilityVisibleCharacterRange {
if (![self instanceActive]) {
return NSMakeRange(0, 0);
}
return NSMakeRange(0, [[self getAXValueAsString] length]);
}
// LINT.ThenChange(AXVisibleCharacterRange)
- (NSString*)accessibilityStringForRange:(NSRange)range {
if (![self instanceActive]) {
return nil;
}
return [[self getAXValueAsString] substringWithRange:range];
}
- (NSInteger)accessibilityLineForIndex:(NSInteger)index {
// TODO: multiline is not supported on views.
return 0;
}
- (NSAttributedString*)accessibilityAttributedStringForRange:(NSRange)range {
if (!_node)
return nil;
return [self AXAttributedStringForRange:[NSValue valueWithRange:range]];
}
- (id)AXLineForIndex:(id)parameter {
NSNumber* lineNumber = base::apple::ObjCCast<NSNumber>(parameter);
if (!lineNumber) {
return nil;
}
return @([self accessibilityLineForIndex:[lineNumber intValue]]);
}
- (NSRange)accessibilityRangeForIndex:(NSInteger)index {
NOTIMPLEMENTED();
return NSMakeRange(0, 0);
}
- (NSRange)accessibilityStyleRangeForIndex:(NSInteger)index {
if (![self instanceActive]) {
return NSMakeRange(0, 0);
}
// TODO(crbug.com/41456329): Implement this for real.
return NSMakeRange(0, [self accessibilityNumberOfCharacters]);
}
- (NSRange)accessibilityRangeForLine:(NSInteger)line {
if (![self instanceActive]) {
return NSMakeRange(0, 0);
}
return NSMakeRange(0, [[self getAXValueAsString] length]);
}
- (NSRange)accessibilityRangeForPosition:(NSPoint)point {
// TODO(tapted): Hit-test [parameter pointValue] and return an NSRange.
NOTIMPLEMENTED();
return NSMakeRange(0, 0);
}
// NSAccessibility: setting content and values.
- (NSNumber*)accessibilitySelected {
if (![self instanceActive])
return nil;
return @(_node->GetBoolAttribute(ax::mojom::BoolAttribute::kSelected));
}
- (NSURL*)accessibilityURL {
TRACE_EVENT1("accessibility", "accessibilityURL",
"role=", ui::ToString([self internalRole]));
if (![self instanceActive])
return nil;
std::string url;
if ([[self accessibilityRole] isEqualToString:NSAccessibilityWebAreaRole]) {
url = _node->GetDelegate()->GetTreeData().url;
} else {
url = _node->GetStringAttribute(ax::mojom::StringAttribute::kUrl);
}
if (url.empty())
return nil;
return [NSURL URLWithString:(base::SysUTF8ToNSString(url))];
}
// LINT.IfChange(accessibilityTabs)
- (id)accessibilityTabs {
if (![self instanceActive]) {
return nil;
}
NSMutableArray* tabSubtree = [[NSMutableArray alloc] init];
if ([self internalRole] == ax::mojom::Role::kTab) {
[tabSubtree addObject:self];
}
for (AXPlatformNodeCocoa* child in [self accessibilityChildren]) {
NSArray* tabChildren = [child accessibilityTabs];
if ([tabChildren count] > 0) {
[tabSubtree addObjectsFromArray:tabChildren];
}
}
return tabSubtree;
}
// LINT.ThenChange(ui/accessibility/platform/browser_accessibility_cocoa.mm:accessibilityTabs)
- (id)accessibilitySplitters {
// Chromium windows do not have NSSplitViews or anything similar.
return nil;
}
- (id)accessibilityToolbarButton {
// Chromium windows do not have a toolbar button.
return nil;
}
- (id)accessibilityScrollBar:(ax::mojom::State)state {
if (![self instanceActive]) {
return nil;
}
// TODO(crbug.com/363275809): For this to work for `ScrollView`, `ScrollView`
// should add `kControlsIds` on its horizontal and vertical scrollbars.
std::vector<ui::AXPlatformNode*> targets =
_node->GetDelegate()->GetSourceNodesForReverseRelations(
ax::mojom::IntListAttribute::kControlsIds);
for (auto target : targets) {
if (auto* delegate = target->GetDelegate()) {
if (delegate->GetRole() == ax::mojom::Role::kScrollBar &&
delegate->HasState(state)) {
return target->GetNativeViewAccessible().Get();
}
}
}
return nil;
}
- (id)accessibilityHorizontalScrollBar {
return [self accessibilityScrollBar:ax::mojom::State::kHorizontal];
}
- (id)accessibilityVerticalScrollBar {
return [self accessibilityScrollBar:ax::mojom::State::kVertical];
}
// NSAccessibility: configuring linkage elements.
- (id)accessibilityTitleUIElement {
if (![self instanceActive])
return nil;
return [self titleUIElement];
}
// LINT.IfChange(accessibilityCellForColumn)
- (id)accessibilityCellForColumn:(NSInteger)column row:(NSInteger)row {
if (![self instanceActive] || ![self nodeDelegate]) {
return nil;
}
if (!ui::IsTableLike([self internalRole])) {
return nil;
}
std::optional<int32_t> cellId = [self nodeDelegate]->GetCellId(row, column);
if (!cellId) {
return nil;
}
ui::AXPlatformNode* cell = [self nodeDelegate]->GetFromNodeID(*cellId);
if (!cell) {
return nil;
}
return cell->GetNativeViewAccessible().Get();
}
// LINT.ThenChange(ui/accessibility/platform/browser_accessibility_cocoa.mm:accessibilityCellForColumn)
- (NSRange)accessibilityColumnIndexRange {
if (![self instanceActive] || ![self nodeDelegate]) {
return NSMakeRange(0, 0);
}
std::optional<int> column = [self nodeDelegate]->GetTableCellColIndex();
std::optional<int> columnSpan = [self nodeDelegate]->GetTableCellColSpan();
if (column && columnSpan) {
return NSMakeRange(*column, *columnSpan);
}
return NSMakeRange(0, 0);
}
- (NSRange)accessibilityRowIndexRange {
if (![self instanceActive] || ![self nodeDelegate]) {
return NSMakeRange(0, 0);
}
std::optional<int> row = [self nodeDelegate]->GetTableCellRowIndex();
std::optional<int> rowSpan = [self nodeDelegate]->GetTableCellRowSpan();
if (row && rowSpan) {
return NSMakeRange(*row, *rowSpan);
}
return NSMakeRange(0, 0);
}
// LINT.IfChange(accessibilityVisibleColumns)
- (NSArray*)accessibilityVisibleColumns {
if (![self instanceActive]) {
return nil;
}
NSMutableArray* columns = [[NSMutableArray alloc] init];
for (AXPlatformNodeCocoa* child in [self accessibilityChildren]) {
if ([[child accessibilityRole] isEqualToString:NSAccessibilityColumnRole]) {
[columns addObject:child];
}
}
return columns;
}
// LINT.ThenChange(ui/accessibility/platform/browser_accessibility_cocoa.mm:accessibilityVisibleColumns)
// LINT.IfChange(accessibilityVisibleCells)
- (NSArray*)accessibilityVisibleCells {
if (![self instanceActive]) {
return nil;
}
ui::AXPlatformNodeDelegate* table = [self nodeDelegate];
if (!table) {
return nil;
}
NSMutableArray* cells = [[NSMutableArray alloc] init];
for (int32_t id : table->GetTableUniqueCellIds()) {
ui::AXPlatformNode* cell = table->GetFromNodeID(id);
if (cell) {
[cells addObject:cell->GetNativeViewAccessible().Get()];
}
}
return cells;
}
// LINT.ThenChange(ui/accessibility/platform/browser_accessibility_cocoa.mm:accessibilityVisibleCells)
// LINT.IfChange(accessibilityVisibleRows)
- (NSArray*)accessibilityVisibleRows {
return [self accessibilityRows];
}
// LINT.ThenChange(ui/accessibility/platform/browser_accessibility_cocoa.mm:accessibilityVisibleRows)
//
// End of NSAccessibility protocol.
//
//
// AXCustomContentProvider
// https://developer.apple.com/documentation/accessibility/axcustomcontentprovider/3600104-accessibilitycustomcontent
//
- (NSArray*)accessibilityCustomContent {
if (![self instanceActive]) {
return nil;
}
// Only descriptions originating from ARIA are returned as custom content.
// (Non-ARIA descriptions are returned as AXHelp.)
if (![self descriptionIsFromAriaDescription]) {
return nil;
}
NSString* description =
[self getStringAttribute:ax::mojom::StringAttribute::kDescription];
AXCustomContent* contentItem =
[AXCustomContent customContentWithLabel:@"description" value:description];
// A custom content importance of high causes it to be spoken
// automatically, rather than "More content available".
contentItem.importance = AXCustomContentImportanceHigh;
return @[ contentItem ];
}
// MathML attributes.
// TODO(crbug.com/40673555): The MathML aam considers only in-flow children.
// TODO(crbug.com/40673555): When/if it is needed to expose this for other a11y
// APIs, then some of the logic below should probably be moved to the
// platform-independent classes.
- (id)AXMathFractionNumerator {
if (![self instanceActive] ||
_node->GetRole() != ax::mojom::Role::kMathMLFraction) {
return nil;
}
NSArray* children = [self accessibilityChildren];
if ([children count] >= 1)
return children[0];
return nil;
}
- (id)AXMathFractionDenominator {
if (![self instanceActive] ||
_node->GetRole() != ax::mojom::Role::kMathMLFraction) {
return nil;
}
NSArray* children = [self accessibilityChildren];
if ([children count] >= 2)
return children[1];
return nil;
}
- (id)AXMathRootRadicand {
if (![self instanceActive] ||
!(_node->GetRole() == ax::mojom::Role::kMathMLRoot ||
_node->GetRole() == ax::mojom::Role::kMathMLSquareRoot)) {
return nil;
}
NSArray* children = [self accessibilityChildren];
if (_node->GetRole() == ax::mojom::Role::kMathMLRoot) {
if ([children count] >= 1)
return [NSArray arrayWithObjects:children[0], nil];
return nil;
}
return children;
}
- (id)AXMathRootIndex {
if (![self instanceActive] ||
_node->GetRole() != ax::mojom::Role::kMathMLRoot) {
return nil;
}
NSArray* children = [self accessibilityChildren];
if ([children count] >= 2)
return children[1];
return nil;
}
- (id)AXMathBase {
if (![self instanceActive] ||
!(_node->GetRole() == ax::mojom::Role::kMathMLSub ||
_node->GetRole() == ax::mojom::Role::kMathMLSup ||
_node->GetRole() == ax::mojom::Role::kMathMLSubSup ||
_node->GetRole() == ax::mojom::Role::kMathMLUnder ||
_node->GetRole() == ax::mojom::Role::kMathMLOver ||
_node->GetRole() == ax::mojom::Role::kMathMLUnderOver ||
_node->GetRole() == ax::mojom::Role::kMathMLMultiscripts)) {
return nil;
}
NSArray* children = [self accessibilityChildren];
if ([children count] >= 1)
return children[0];
return nil;
}
- (id)AXMathUnder {
if (![self instanceActive] ||
!(_node->GetRole() == ax::mojom::Role::kMathMLUnder ||
_node->GetRole() == ax::mojom::Role::kMathMLUnderOver)) {
return nil;
}
NSArray* children = [self accessibilityChildren];
if ([children count] >= 2)
return children[1];
return nil;
}
- (id)AXMathOver {
if (![self instanceActive] ||
!(_node->GetRole() == ax::mojom::Role::kMathMLOver ||
_node->GetRole() == ax::mojom::Role::kMathMLUnderOver)) {
return nil;
}
NSArray* children = [self accessibilityChildren];
if (_node->GetRole() == ax::mojom::Role::kMathMLOver &&
[children count] >= 2) {
return children[1];
}
if (_node->GetRole() == ax::mojom::Role::kMathMLUnderOver &&
[children count] >= 3) {
return children[2];
}
return nil;
}
- (id)AXMathSubscript {
if (![self instanceActive] ||
!(_node->GetRole() == ax::mojom::Role::kMathMLSub ||
_node->GetRole() == ax::mojom::Role::kMathMLSubSup)) {
return nil;
}
NSArray* children = [self accessibilityChildren];
if ([children count] >= 2)
return children[1];
return nil;
}
- (id)AXMathSuperscript {
if (![self instanceActive] ||
!(_node->GetRole() == ax::mojom::Role::kMathMLSup ||
_node->GetRole() == ax::mojom::Role::kMathMLSubSup)) {
return nil;
}
NSArray* children = [self accessibilityChildren];
if (_node->GetRole() == ax::mojom::Role::kMathMLSup &&
[children count] >= 2) {
return children[1];
}
if (_node->GetRole() == ax::mojom::Role::kMathMLSubSup &&
[children count] >= 3) {
return children[2];
}
return nil;
}
namespace {
NSDictionary* CreateMathSubSupScriptsPair(AXPlatformNodeCocoa* subscript,
AXPlatformNodeCocoa* superscript) {
NSMutableDictionary* dictionary = [NSMutableDictionary dictionary];
if (subscript) {
dictionary[NSAccessibilityMathSubscriptAttribute] = subscript;
}
if (superscript) {
dictionary[NSAccessibilityMathSuperscriptAttribute] = superscript;
}
return dictionary;
}
} // namespace
- (NSArray*)AXMathPostscripts {
if (![self instanceActive] ||
_node->GetRole() != ax::mojom::Role::kMathMLMultiscripts)
return nil;
NSMutableArray* ret = [NSMutableArray array];
bool foundBaseElement = false;
AXPlatformNodeCocoa* subscript = nullptr;
for (AXPlatformNodeCocoa* child in [self accessibilityChildren]) {
if ([child internalRole] == ax::mojom::Role::kMathMLPrescriptDelimiter)
break;
if (!foundBaseElement) {
foundBaseElement = true;
continue;
}
if (!subscript) {
subscript = child;
continue;
}
AXPlatformNodeCocoa* superscript = child;
[ret addObject:CreateMathSubSupScriptsPair(subscript, superscript)];
subscript = nullptr;
}
return [ret count] ? ret : nil;
}
- (NSArray*)AXMathPrescripts {
if (![self instanceActive] ||
_node->GetRole() != ax::mojom::Role::kMathMLMultiscripts)
return nil;
NSMutableArray* ret = [NSMutableArray array];
bool foundPrescriptDelimiter = false;
AXPlatformNodeCocoa* subscript = nullptr;
for (AXPlatformNodeCocoa* child in [self accessibilityChildren]) {
if (!foundPrescriptDelimiter) {
foundPrescriptDelimiter =
([child internalRole] == ax::mojom::Role::kMathMLPrescriptDelimiter);
continue;
}
if (!subscript) {
subscript = child;
continue;
}
AXPlatformNodeCocoa* superscript = child;
[ret addObject:CreateMathSubSupScriptsPair(subscript, superscript)];
subscript = nullptr;
}
return [ret count] ? ret : nil;
}
@end