chromium/ui/accessibility/platform/ax_platform_node_cocoa.mm

// 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/logging.h"
#include "base/mac/mac_util.h"
#include "base/memory/raw_ptr_exclusion.h"
#include "base/no_destructor.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/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;

@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*>;
using ActionList = std::vector<std::pair<ax::mojom::Action, 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));
}

ActionList BuildActionList() {
  const ActionList::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 ActionList(begin(entries), end(entries));
}

const ActionList& GetActionList() {
  static const base::NoDestructor<ActionList> action_map(BuildActionList());
  return *action_map;
}

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) {
  return action == ax::mojom::Action::kDoDefault &&
         node.GetData().IsClickable();
}

// 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

@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;

- (ui::AXPlatformNodeDelegate*)nodeDelegate {
  return _node ? _node->GetDelegate() : nil;
}

- (BOOL)instanceActive {
  return _node != nullptr;
}

- (BOOL)isIncludedInPlatformTree {
  // TODO(accessibility): Do we really need to have invisible objects in
  // the platform tree?
  return [self instanceActive] &&
         ![[self AXRole] isEqualToString:NSAccessibilityUnknownRole] &&
         !_node->IsInvisibleOrIgnored();
}

- (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();
}

- (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->GetDelegate()->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;
}

+ (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::kCaret:
      return NSAccessibilityUnknownRole;
    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 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::kImeCandidate:
      return NSAccessibilityUnknownRole;
    case ax::mojom::Role::kInlineTextBox:
      return NSAccessibilityStaticTextRole;
    case ax::mojom::Role::kInputTime:
      return @"AXTimeField";
    case ax::mojom::Role::kKeyboard:
      return NSAccessibilityUnknownRole;
    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::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::kRubyAnnotation:
      return NSAccessibilityUnknownRole;
    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:
      return NSAccessibilityUnknownRole;
    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::kDescriptionListTermDeprecated:
    case ax::mojom::Role::kDescriptionListDetailDeprecated:
    case ax::mojom::Role::kDirectoryDeprecated:
    case ax::mojom::Role::kPreDeprecated:
    case ax::mojom::Role::kPortalDeprecated:
      NOTREACHED();
  }
}

+ (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)detach {
  if (!_node)
    return;
  _node = nil;
  NSAccessibilityPostNotification(
      self, NSAccessibilityUIElementDestroyedNotification);
}

- (NSRect)boundsInScreen {
  if (!_node || !_node->GetDelegate())
    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 {
  if ([self instanceActive]) {
    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;
  }

  return ax::mojom::Role::kUnknown;
}

- (AXPlatformNodeCocoa*)fromNodeID:(ui::AXNodeID)id {
  ui::AXPlatformNode* cell = _node->GetDelegate()->GetFromNodeID(id);
  if (cell)
    return cell->GetNativeViewAccessible();
  return nil;
}

- (BOOL)isImage {
  bool has_image_semantics =
      ui::IsImage(_node->GetRole()) &&
      !_node->GetBoolAttribute(ax::mojom::BoolAttribute::kCanvasHasFallback) &&
      !_node->GetChildCount() &&
      _node->GetNameFrom() != ax::mojom::NameFrom::kAttributeExplicitlyEmpty;
#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.";

    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)];
    }

    // Add annotation information
    int leafTextLength = leafTextRange.GetText().length();
    DCHECK_LE(static_cast<unsigned long>(anchorStartOffset + leafTextLength),
              attributedString.length);
    NSRange leafRange = NSMakeRange(anchorStartOffset, leafTextLength);

    CollectAncestorRoles(*anchor, ancestor_roles);

    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 AXChildren] 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() : 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 {
  if (!_node)
    return @[];

  NSMutableArray* axActions = [NSMutableArray array];
  const ActionList& action_list = GetActionList();

  // 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;
}

- (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 ActionList::value_type& entry : GetActionList()) {
      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);
}

// This method, while deprecated, is still called internally by AppKit.
- (NSArray*)accessibilityAttributeNames {
  if (!_node)
    return @[];

  // 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;
}

- (NSArray*)accessibilityParameterizedAttributeNames {
  if (!_node)
    return @[];

  // General attributes.
  NSMutableArray* ret = [NSMutableArray
      arrayWithObjects:
          NSAccessibilityAttributedStringForTextMarkerRangeParameterizedAttribute,
          nil];

  if (_node->HasState(ax::mojom::State::kEditable)) {
    [ret addObjectsFromArray:@[
      NSAccessibilityAttributedStringForRangeParameterizedAttribute
    ]];
  }
  return ret;
}

// Despite it being deprecated, 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 ([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.

  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;

  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_IN_MIGRATION();
      return @"false";
    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_IN_MIGRATION();
  return @"false";
}

- (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();
  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() ];
}

- (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
      @(_node->GetData().GetRestriction() != ax::mojom::Restriction::kDisabled);
}

- (NSNumber*)AXFocused {
  return
      @(_node->GetDelegate()->GetFocus() == _node->GetNativeViewAccessible());
  return @NO;
}

- (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();
}

- (id)AXParent {
  if (!_node)
    return nil;
  return NSAccessibilityUnignoredAncestor(_node->GetParent());
}

- (NSArray*)AXChildren {
  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)) {
    ui::AXPlatformNodeDelegate* child = child_iterator_ptr->get();
    if (child && child->IsInvisibleOrIgnored()) {
      [children
          addObjectsFromArray:[child_iterator_ptr->GetNativeViewAccessible()
                                  accessibilityChildren]];
    } else {
      [children addObject:child_iterator_ptr->GetNativeViewAccessible()];
    }
  }
  return NSAccessibilityUnignoredChildren(children);
}

- (id)AXWindow {
  return _node->GetDelegate()->GetNSWindow();
}

- (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 {
  return [self accessibilityPlaceholderValue];
}

- (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.

- (NSString*)AXSelectedText {
  NSRange selectedTextRange;
  [[self AXSelectedTextRange] getValue:&selectedTextRange];
  return [[self getAXValueAsString] substringWithRange:selectedTextRange];
}

- (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)];
}

- (NSNumber*)AXNumberOfCharacters {
  return @([[self getAXValueAsString] length]);
}

- (NSValue*)AXVisibleCharacterRange {
  return [NSValue valueWithRange:{0, [[self getAXValueAsString] length]}];
}

- (NSNumber*)AXInsertionPointLineNumber {
  // TODO: multiline is not supported on views.
  return @0;
}

// Parameterized text-specific attributes.

- (id)AXLineForIndex:(id)parameter {
  // TODO: multiline is not supported on views.
  return @0;
}

- (id)AXRangeForLine:(id)parameter {
  if (![parameter isKindOfClass:[NSNumber class]] || [parameter intValue] != 0)
    return nil;

  return [NSValue valueWithRange:{0, [[self getAXValueAsString] length]}];
}

- (id)AXStringForRange:(id)parameter {
  if (![parameter isKindOfClass:[NSValue class]] ||
      (0 != strcmp([parameter objCType], @encode(NSRange))))
    return nil;

  return [[self getAXValueAsString] substringWithRange:[parameter rangeValue]];
}

- (id)AXRangeForPosition:(id)parameter {
  // TODO(tapted): Hit-test [parameter pointValue] and return an NSRange.
  NOTIMPLEMENTED();
  return nil;
}

- (id)AXRangeForIndex:(id)parameter {
  NOTIMPLEMENTED();
  return nil;
}

- (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 {
  if (![parameter isKindOfClass:[NSNumber class]])
    return nil;

  // TODO(crbug.com/41456329): Implement this for real.
  return [NSValue
      valueWithRange:NSMakeRange(0, [self accessibilityNumberOfCharacters])];
}

- (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 (![self instanceActive])
    return NO;

  return (![[[self class] nativeRoleFromAXRole:_node->GetRole()]
              isEqualToString:NSAccessibilityUnknownRole] &&
          !_node->GetDelegate()->IsIgnored());
}

- (BOOL)isAccessibilityEnabled {
  if (!_node)
    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 @"";
}

- (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;
  }

  // TODO(crbug.com/41115917): What about role-specific selectors?
  return [super isAccessibilitySelectorAllowed:selector];
}

// NSAccessibility: Determining Relationships.
- (NSArray*)accessibilityChildren {
  return [self AXChildren];
}

- (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();
  DCHECK(delegate);

  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();
  DCHECK(tableDelegate);
  for (ui::AXNodeID id : tableDelegate->GetColHeaderNodeIds(*column)) {
    AXPlatformNodeCocoa* colheader = [self fromNodeID:id];
    if (colheader) {
      [ret addObject:colheader];
    }
  }
  return [ret count] ? ret : nil;
}

- (NSArray*)accessibilityRows {
  // TODO(accessibility) accessibilityRows is defined in
  // browser_accessibility_cocoa.mm eventually that function definition should
  // be moved here.
  return nil;
}

// 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);
}

// 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).

- (NSInteger)accessibilityInsertionPointLineNumber {
  return [[self AXInsertionPointLineNumber] integerValue];
}

- (NSInteger)accessibilityNumberOfCharacters {
  if (!_node)
    return 0;

  return [[self AXNumberOfCharacters] integerValue];
}

- (NSString*)accessibilityPlaceholderValue {
  if (![self instanceActive])
    return nil;

  if (_node->GetNameFrom() == ax::mojom::NameFrom::kPlaceholder)
    return [self getName];

  return [self getStringAttribute:ax::mojom::StringAttribute::kPlaceholder];
}

- (NSString*)accessibilitySelectedText {
  if (!_node)
    return nil;

  return [self 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);
}

- (NSRange)accessibilitySelectedTextRange {
  if (!_node)
    return NSMakeRange(0, 0);

  NSRange r;
  [[self AXSelectedTextRange] getValue:&r];
  return r;
}

- (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] ];
}

- (NSRange)accessibilityVisibleCharacterRange {
  if (!_node)
    return NSMakeRange(0, 0);

  return [[self AXVisibleCharacterRange] rangeValue];
}

- (NSString*)accessibilityStringForRange:(NSRange)range {
  if (!_node)
    return nil;

  return (NSString*)[self AXStringForRange:[NSValue valueWithRange:range]];
}

- (NSAttributedString*)accessibilityAttributedStringForRange:(NSRange)range {
  if (!_node)
    return nil;

  return [self AXAttributedStringForRange:[NSValue valueWithRange:range]];
}

- (NSInteger)accessibilityLineForIndex:(NSInteger)index {
  if (!_node)
    return 0;

  return [[self AXLineForIndex:@(index)] integerValue];
}

- (NSRange)accessibilityRangeForIndex:(NSInteger)index {
  if (!_node)
    return NSMakeRange(0, 0);

  return [[self AXRangeForIndex:@(index)] rangeValue];
}

- (NSRange)accessibilityStyleRangeForIndex:(NSInteger)index {
  if (!_node)
    return NSMakeRange(0, 0);

  return [[self AXStyleRangeForIndex:@(index)] rangeValue];
}

- (NSRange)accessibilityRangeForLine:(NSInteger)line {
  if (!_node)
    return NSMakeRange(0, 0);

  return [[self AXRangeForLine:@(line)] rangeValue];
}

- (NSRange)accessibilityRangeForPosition:(NSPoint)point {
  return [[self AXRangeForPosition:[NSValue valueWithPoint:point]] rangeValue];
}

// 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))];
}

// NSAccessibility: configuring linkage elements.
- (id)accessibilityTitleUIElement {
  if (![self instanceActive])
    return nil;

  return [self titleUIElement];
}

//
// 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 AXChildren];
  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 AXChildren];
  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 AXChildren];
  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 AXChildren];
  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 AXChildren];
  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 AXChildren];
  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 AXChildren];
  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 AXChildren];
  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 AXChildren];
  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 AXChildren]) {
    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 AXChildren]) {
    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