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

#include <ApplicationServices/ApplicationServices.h>
#import <Cocoa/Cocoa.h>

#include <optional>

#include "base/apple/bridging.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/notimplemented.h"
#include "base/strings/sys_string_conversions.h"
#include "base/trace_event/trace_event.h"
#include "skia/ext/skia_utils_mac.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_range.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/accessibility/platform/ax_platform_node_mac.h"
#include "ui/accessibility/platform/ax_private_attributes_mac.h"
#include "ui/accessibility/platform/ax_private_roles_mac.h"
#include "ui/accessibility/platform/ax_private_webkit_constants_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;
using base::apple::CFToNSPtrCast;
using base::apple::ObjCCast;
using base::apple::ObjCCastStrict;

@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.
constexpr int kLiveRegionDebounceMillis = 20;

using RoleMap = std::map<ax::mojom::Role, NSString*>;
using EventMap = std::map<ax::mojom::Event, NSString*>;

RoleMap BuildSubroleMap() {
  const RoleMap::value_type subroles[] = {
      {ax::mojom::Role::kAlert, CFToNSPtrCast(kAXApplicationAlertSubrole)},
      {ax::mojom::Role::kAlertDialog,
       CFToNSPtrCast(kAXApplicationAlertDialogSubrole)},
      {ax::mojom::Role::kApplication, CFToNSPtrCast(kAXWebApplicationSubrole)},
      {ax::mojom::Role::kArticle, CFToNSPtrCast(kAXDocumentArticleSubrole)},
      {ax::mojom::Role::kBanner, CFToNSPtrCast(kAXLandmarkBannerSubrole)},
      {ax::mojom::Role::kCode, CFToNSPtrCast(kAXCodeStyleGroupSubrole)},
      {ax::mojom::Role::kComplementary,
       CFToNSPtrCast(kAXLandmarkComplementarySubrole)},
      {ax::mojom::Role::kContentDeletion,
       CFToNSPtrCast(kAXDeleteStyleGroupSubrole)},
      {ax::mojom::Role::kContentInsertion,
       CFToNSPtrCast(kAXInsertStyleGroupSubrole)},
      {ax::mojom::Role::kContentInfo,
       CFToNSPtrCast(kAXLandmarkContentInfoSubrole)},
      {ax::mojom::Role::kDefinition, CFToNSPtrCast(kAXDefinitionSubrole)},
      {ax::mojom::Role::kDialog, CFToNSPtrCast(kAXApplicationDialogSubrole)},
      {ax::mojom::Role::kDocument, CFToNSPtrCast(kAXDocumentAttribute)},
      {ax::mojom::Role::kEmphasis, @"AXEmphasisStyleGroup"},
      {ax::mojom::Role::kFeed, CFToNSPtrCast(kAXApplicationGroupSubrole)},
      {ax::mojom::Role::kFooter, CFToNSPtrCast(kAXLandmarkContentInfoSubrole)},
      {ax::mojom::Role::kForm, @"AXLandmarkForm"},
      {ax::mojom::Role::kGraphicsDocument, CFToNSPtrCast(kAXDocumentAttribute)},
      {ax::mojom::Role::kGroup, CFToNSPtrCast(kAXApplicationGroupSubrole)},
      {ax::mojom::Role::kHeader, CFToNSPtrCast(kAXLandmarkBannerSubrole)},
      {ax::mojom::Role::kLog, CFToNSPtrCast(kAXApplicationLogSubrole)},
      {ax::mojom::Role::kMain, CFToNSPtrCast(kAXLandmarkMainSubrole)},
      {ax::mojom::Role::kMarquee, CFToNSPtrCast(kAXApplicationMarqueeSubrole)},
      // https://w3c.github.io/mathml-aam/#mathml-element-mappings
      {ax::mojom::Role::kMath, CFToNSPtrCast(kAXDocumentMathSubrole)},
      {ax::mojom::Role::kMathMLFraction, CFToNSPtrCast(kAXMathFractionSubrole)},
      {ax::mojom::Role::kMathMLIdentifier,
       CFToNSPtrCast(kAXMathIdentifierSubrole)},
      {ax::mojom::Role::kMathMLMath, CFToNSPtrCast(kAXDocumentMathSubrole)},
      {ax::mojom::Role::kMathMLMultiscripts,
       CFToNSPtrCast(kAXMathMultiscriptSubrole)},
      {ax::mojom::Role::kMathMLNoneScript, CFToNSPtrCast(kAXMathRowSubrole)},
      {ax::mojom::Role::kMathMLNumber, CFToNSPtrCast(kAXMathNumberSubrole)},
      {ax::mojom::Role::kMathMLOperator, CFToNSPtrCast(kAXMathOperatorSubrole)},
      {ax::mojom::Role::kMathMLOver, CFToNSPtrCast(kAXMathUnderOverSubrole)},
      {ax::mojom::Role::kMathMLPrescriptDelimiter,
       CFToNSPtrCast(kAXMathRowSubrole)},
      {ax::mojom::Role::kMathMLRoot, CFToNSPtrCast(kAXMathRootSubrole)},
      {ax::mojom::Role::kMathMLRow, CFToNSPtrCast(kAXMathRowSubrole)},
      {ax::mojom::Role::kMathMLSquareRoot,
       CFToNSPtrCast(kAXMathSquareRootSubrole)},
      {ax::mojom::Role::kMathMLSub,
       CFToNSPtrCast(kAXMathSubscriptSuperscriptSubrole)},
      {ax::mojom::Role::kMathMLSubSup,
       CFToNSPtrCast(kAXMathSubscriptSuperscriptSubrole)},
      {ax::mojom::Role::kMathMLSup,
       CFToNSPtrCast(kAXMathSubscriptSuperscriptSubrole)},
      {ax::mojom::Role::kMathMLTable, CFToNSPtrCast(kAXMathTableSubrole)},
      {ax::mojom::Role::kMathMLTableCell,
       CFToNSPtrCast(kAXMathTableCellSubrole)},
      {ax::mojom::Role::kMathMLTableRow, CFToNSPtrCast(kAXMathTableRowSubrole)},
      {ax::mojom::Role::kMathMLText, CFToNSPtrCast(kAXMathTextSubrole)},
      {ax::mojom::Role::kMathMLUnder, CFToNSPtrCast(kAXMathUnderOverSubrole)},
      {ax::mojom::Role::kMathMLUnderOver,
       CFToNSPtrCast(kAXMathUnderOverSubrole)},
      {ax::mojom::Role::kMeter, CFToNSPtrCast(kAXMeterSubrole)},
      {ax::mojom::Role::kNavigation,
       CFToNSPtrCast(kAXLandmarkNavigationSubrole)},
      {ax::mojom::Role::kNote, CFToNSPtrCast(kAXDocumentNoteSubrole)},
      {ax::mojom::Role::kRegion, CFToNSPtrCast(kAXLandmarkRegionSubrole)},
      {ax::mojom::Role::kSearch, CFToNSPtrCast(kAXLandmarkSearchSubrole)},
      {ax::mojom::Role::kSearchBox, CFToNSPtrCast(kAXSearchFieldSubrole)},
      {ax::mojom::Role::kSectionFooter, @"AXSectionFooter"},
      {ax::mojom::Role::kSectionHeader, @"AXSectionHeader"},
      {ax::mojom::Role::kStatus, CFToNSPtrCast(kAXApplicationStatusSubrole)},
      {ax::mojom::Role::kStrong, @"AXStrongStyleGroup"},
      {ax::mojom::Role::kSubscript,
       CFToNSPtrCast(kAXSubscriptStyleGroupSubrole)},
      {ax::mojom::Role::kSuperscript,
       CFToNSPtrCast(kAXSuperscriptStyleGroupSubrole)},
      {ax::mojom::Role::kSwitch, CFToNSPtrCast(kAXSwitchSubrole)},
      {ax::mojom::Role::kTab, NSAccessibilityTabButtonSubrole},
      {ax::mojom::Role::kTabPanel, CFToNSPtrCast(kAXTabPanelSubrole)},
      {ax::mojom::Role::kTerm, CFToNSPtrCast(kAXTermSubrole)},
      {ax::mojom::Role::kTime, CFToNSPtrCast(kAXTimeGroupSubrole)},
      {ax::mojom::Role::kTimer, CFToNSPtrCast(kAXApplicationTimerSubrole)},
      {ax::mojom::Role::kToggleButton, @"AXToggleButton"},
      {ax::mojom::Role::kTooltip,
       CFToNSPtrCast(kAXUserInterfaceTooltipSubrole)},
      {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},

      // 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,
       CFToNSPtrCast(kAXMenuOpenedNotification)},
      {ax::mojom::Event::kMenuPopupEnd,
       CFToNSPtrCast(kAXMenuClosedNotification)},

      {ax::mojom::Event::kTextChanged, NSAccessibilityTitleChangedNotification},
      {ax::mojom::Event::kValueChanged,
       NSAccessibilityValueChangedNotification},
      {ax::mojom::Event::kTextSelectionChanged,
       NSAccessibilitySelectedTextChangedNotification},
      // TODO(patricialor): Add more events.
  };

  return EventMap(begin(events), end(events));
}

// Builds the pairings of accessibility actions and their Cocoa equivalents.
ui::CocoaActionList BuildActionList() {
  const ui::CocoaActionList::value_type entries[] = {
      // NSAccessibilityPressAction must come first in this list.
      {ax::mojom::Action::kDoDefault, NSAccessibilityPressAction},
      {ax::mojom::Action::kDecrement, NSAccessibilityDecrementAction},
      {ax::mojom::Action::kIncrement, NSAccessibilityIncrementAction},
      {ax::mojom::Action::kShowContextMenu, NSAccessibilityShowMenuAction},
  };
  return ui::CocoaActionList(begin(entries), end(entries));
}

// Returns a static vector of pairings of accessibility actions and their Cocoa
// equivalents.
const ui::CocoaActionList& GetCocoaActionList() {
  static const base::NoDestructor<ui::CocoaActionList> action_list(
      BuildActionList());
  return *action_list;
}

void PostAnnouncementNotification(NSString* announcement,
                                  NSWindow* window,
                                  bool is_polite) {
  NSAccessibilityPriorityLevel priority =
      is_polite ? NSAccessibilityPriorityMedium : NSAccessibilityPriorityHigh;
  NSDictionary* notification_info = @{
    NSAccessibilityAnnouncementKey : announcement,
    NSAccessibilityPriorityKey : @(priority)
  };
  // On Mojave, announcements from an inactive window aren't spoken.
  NSAccessibilityPostNotificationWithUserInfo(
      window, NSAccessibilityAnnouncementRequestedNotification,
      notification_info);
}

// Returns true if |action| should be added implicitly for |data|.
bool HasImplicitAction(const ui::AXPlatformNodeBase& node,
                       ax::mojom::Action action) {
  // TODO integrate the method into AXNodeData, see crrev.com/c/6115619
  // for details.
  switch (action) {
    case ax::mojom::Action::kDoDefault:
      return node.GetData().IsClickable();
    case ax::mojom::Action::kDecrement:
    case ax::mojom::Action::kIncrement:
      return node.GetRole() == ax::mojom::Role::kSlider ||
             node.GetRole() == ax::mojom::Role::kSpinButton;
    default:
      return false;
  }
}

// For roles that show a menu for the default action, ensure "show menu" also
// appears in available actions, but only if that's not already used for a
// context menu. It will be mapped back to the default action when performed.
bool AlsoUseShowMenuActionForDefaultAction(const ui::AXPlatformNodeBase& node) {
  return HasImplicitAction(node, ax::mojom::Action::kDoDefault) &&
         !node.HasAction(ax::mojom::Action::kShowContextMenu) &&
         (node.GetRole() == ax::mojom::Role::kPopUpButton ||
          node.GetRole() == ax::mojom::Role::kComboBoxSelect);
}

// Check whether |selector| is an accessibility setter. This is a heuristic but
// seems to be a pretty good one.
bool IsAXSetter(SEL selector) {
  return [NSStringFromSelector(selector) hasPrefix:@"setAccessibility"];
}

void CollectAncestorRoles(
    const ui::AXNode& node,
    std::map<ui::AXNodeID, std::set<ax::mojom::Role>>& out_ancestor_roles) {
  if (out_ancestor_roles.contains(node.id()))
    return;
  out_ancestor_roles[node.id()] = {node.GetRole()};
  if (!node.GetParent())
    return;
  CollectAncestorRoles(*node.GetParent(), out_ancestor_roles);
  out_ancestor_roles[node.id()].insert(
      out_ancestor_roles[node.GetParent()->id()].begin(),
      out_ancestor_roles[node.GetParent()->id()].end());
}

}  // namespace

namespace ui {
const ui::CocoaActionList& GetCocoaActionListForTesting() {
  return GetCocoaActionList();
}
}  // namespace ui

@interface AXPlatformNodeCocoa (Private)
// Helper function for string attributes that don't require extra processing.
- (NSString*)getStringAttribute:(ax::mojom::StringAttribute)attribute;

// Returns AXValue, or nil if AXValue isn't an NSString.
- (NSString*)getAXValueAsString;

// Returns the native wrapper for the given node id.
- (AXPlatformNodeCocoa*)fromNodeID:(ui::AXNodeID)id;

// Returns true if this object is an image.
- (BOOL)isImage;

@end

@implementation AXPlatformNodeCocoa {
  // This field is not a raw_ptr<> because it requires @property rewrite.
  RAW_PTR_EXCLUSION ui::AXPlatformNodeBase* _node;  // Weak. Retains us.
  AXAnnouncementSpec* __strong _pendingAnnouncement;
}

@synthesize node = _node;
// Required for AXCustomContentProvider, which defines the property.
@synthesize accessibilityCustomContent = _accessibilityCustomContent;

// The new NSAccessibility API is method-based, but the old NSAccessibility
// is attribute-based. For every method, there is a corresponding attribute.
// This function returns the map between the methods and the attributes
// for purposes of migrating to the new API.
+ (NSDictionary*)newAccessibilityAPIMethodToAttributeMap {
  static NSDictionary* dict = nil;
  static dispatch_once_t onceToken;

  dispatch_once(&onceToken, ^{
    dict = @{
      @"accessibilityCellForColumn:row:" :
          NSAccessibilityCellForColumnAndRowParameterizedAttribute,
      @"accessibilityChildrenInNavigationOrder" :
          NSAccessibilityChildrenInNavigationOrderAttribute,
      @"accessibilityColumns" : NSAccessibilityColumnsAttribute,
      @"accessibilityColumnCount" : NSAccessibilityColumnCountAttribute,
      @"accessibilityColumnIndexRange" :
          NSAccessibilityColumnIndexRangeAttribute,
      @"accessibilityDisclosedByRow" : NSAccessibilityDisclosedByRowAttribute,
      @"accessibilityDisclosedRows" : NSAccessibilityDisclosedRowsAttribute,
      @"accessibilityDisclosureLevel" : NSAccessibilityDisclosureLevelAttribute,
      @"accessibilityHeader" : NSAccessibilityHeaderAttribute,
      @"accessibilityHorizontalScrollBar" :
          NSAccessibilityHorizontalScrollBarAttribute,
      @"accessibilityIndex" : NSAccessibilityIndexAttribute,
      @"accessibilityLinkedUIElements" :
          NSAccessibilityLinkedUIElementsAttribute,
      @"accessibilityRowCount" : NSAccessibilityRowCountAttribute,
      @"accessibilityRowHeaderUIElements" :
          NSAccessibilityRowHeaderUIElementsAttribute,
      @"accessibilityRowIndexRange" : NSAccessibilityRowIndexRangeAttribute,
      @"accessibilitySortDirection" : NSAccessibilitySortDirectionAttribute,
      @"accessibilitySplitters" : NSAccessibilitySplittersAttribute,
      @"accessibilityTabs" : NSAccessibilityTabsAttribute,
      @"accessibilityToolbarButton" : NSAccessibilityToolbarButtonAttribute,
      @"accessibilityVerticalScrollBar" :
          NSAccessibilityVerticalScrollBarAttribute,
      @"accessibilityVisibleColumns" : NSAccessibilityVisibleColumnsAttribute,
      @"accessibilityVisibleCells" : NSAccessibilityVisibleCellsAttribute,
      @"accessibilityVisibleRows" : NSAccessibilityVisibleRowsAttribute,
      @"isAccessibilityDisclosed" : NSAccessibilityDisclosingAttribute,
      @"isAccessibilityExpanded" : NSAccessibilityExpandedAttribute,
      @"isAccessibilityFocused" : NSAccessibilityFocusedAttribute,
    };
  });
  return dict;
}

// Similar to newAccessibilityAPIMethodToAttributeMap but for actions.
+ (NSDictionary*)newAccessibilityAPIMethodToActionMap {
  static NSDictionary* dict = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    dict = @{
      @"accessibilityPerformConfirm" : NSAccessibilityConfirmAction,
      @"accessibilityPerformPress" : NSAccessibilityPressAction,
      @"accessibilityPerformShowMenu" : NSAccessibilityShowMenuAction,
      @"accessibilityPerformDecrement" : NSAccessibilityDecrementAction,
      @"accessibilityPerformIncrement" : NSAccessibilityIncrementAction,
    };
  });
  return dict;
}

// Returns the set of attributes available through the new Cocoa
// accessibility API.
+ (NSSet<NSString*>*)attributesAvailableThroughNewAccessibilityAPI {
  static NSSet<NSString*>* set = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    set = [NSSet<NSString*>
        setWithArray:[[self newAccessibilityAPIMethodToAttributeMap]
                         allValues]];
  });
  return set;
}

// Returns the set of actions available through the new Cocoa
// accessibility API.
+ (NSSet<NSString*>*)actionsAvailableThroughNewAccessibilityAPI {
  static NSSet<NSString*>* set = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    set = [NSSet<NSString*>
        setWithArray:[[self newAccessibilityAPIMethodToActionMap] allValues]];
  });
  return set;
}

// Returns YES if `attribute` is available through a method implemented for
// the new accessibility API.
+ (BOOL)isAttributeAvailableThroughNewAccessibilityAPI:(NSString*)attribute {
  if (features::IsMacAccessibilityAPIMigrationEnabled()) {
    return [[self attributesAvailableThroughNewAccessibilityAPI]
        containsObject:attribute];
  }
  return NO;
}

// Returns YES if `action` is available through a method implemented for
// the new accessibility API.
+ (BOOL)isActionAvailableThroughNewAccessibilityAPI:(NSString*)action {
  if (features::IsMacAccessibilityAPIMigrationEnabled()) {
    return [[self actionsAvailableThroughNewAccessibilityAPI]
        containsObject:action];
  }
  return NO;
}

// Returns the set of methods implemented to support the new Cocoa
// accessibility API corresponding to old API attributes.
+ (NSSet<NSString*>*)newAccessibilityAPIMethods {
  static NSSet<NSString*>* set = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    set = [NSSet<NSString*>
        setWithArray:[[self newAccessibilityAPIMethodToAttributeMap] allKeys]];
  });
  return set;
}

// Returns YES if `method` has been implemented in the transition to the new
// accessibility API.
+ (BOOL)isMethodImplementedForNewAccessibilityAPI:(NSString*)method {
  if (features::IsMacAccessibilityAPIMigrationEnabled()) {
    return [[self newAccessibilityAPIMethods] containsObject:method];
  }
  return NO;
}

// Returns true if `method` has been implemented in the transition to the new
// accessibility API, and is supported by this node (based on its role).
- (BOOL)supportsNewAccessibilityAPIMethod:(NSString*)method {
  if (!_node) {
    return NO;
  }

  // Check whether the corresponding attribute is supported for this node.
  NSString* attribute = [[[self class] newAccessibilityAPIMethodToAttributeMap]
      objectForKey:method];
  if (attribute) {
    NSArray* attributeNames = [self internalAccessibilityAttributeNames];
    if ([attributeNames containsObject:attribute]) {
      return YES;
    }

    attributeNames = [self internalAccessibilityParameterizedAttributeNames];
    return [attributeNames containsObject:attribute];
  }

  // Check whether the corresponding action is supported for this node.
  NSString* action =
      [[[self class] newAccessibilityAPIMethodToActionMap] objectForKey:method];
  if (action) {
    NSArray* actionNames = [self internalAccessibilityActionNames];
    return [actionNames containsObject:action];
  }

  return NO;
}

- (BOOL)conditionallyRespondsToSelector:(SEL)selector {
  static base::NoDestructor<std::unordered_set<SEL>> methodSelectorsForActions({
    @selector(accessibilityPerformPress),
        @selector(accessibilityPerformDecrement),
        @selector(accessibilityPerformIncrement),
        @selector(accessibilityPerformShowMenu),
        @selector(accessibilityPerformConfirm)
  });

  static base::NoDestructor<std::unordered_set<SEL>>
      methodSelectorsForParameterizedAttributes({
        @selector(accessibilityCellForColumn:row:),
            @selector(accessibilityRangeForIndex:),
            @selector(accessibilityRangeForLine:),
            @selector(accessibilityRangeForPosition:),
      });

  // See if the method is permitted by checking its corresponding parameterized
  // attribute counterpart.
  if (methodSelectorsForParameterizedAttributes->find(selector) !=
      methodSelectorsForParameterizedAttributes->end()) {
    NSString* selectorString = NSStringFromSelector(selector);
    NSString* attribute =
        [[AXPlatformNodeCocoa newAccessibilityAPIMethodToAttributeMap]
            objectForKey:selectorString];
    NSArray* attributes =
        [self internalAccessibilityParameterizedAttributeNames];
    if (![attributes containsObject:attribute]) {
      return NO;
    }
  }

  // See if the method is permitted by checking its corresponding action
  // counterpart.
  if (methodSelectorsForActions->find(selector) !=
      methodSelectorsForActions->end()) {
    NSString* selectorString = NSStringFromSelector(selector);
    NSString* action =
        [[AXPlatformNodeCocoa newAccessibilityAPIMethodToActionMap]
            objectForKey:selectorString];
    NSArray* actions = [self internalAccessibilityActionNames];
    if (![actions containsObject:action]) {
      return NO;
    }
  }
  return YES;
}

- (BOOL)respondsToSelector:(SEL)selector {
  // If we're in old-accessibility-API mode, disable methods that we've added
  // to support the new API.
  if (!features::IsMacAccessibilityAPIMigrationEnabled()) {
    static base::NoDestructor<std::unordered_set<SEL>>
        newAccessibilityAPISelectors;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
      NSSet<NSString*>* methodNames =
          [AXPlatformNodeCocoa newAccessibilityAPIMethods];
      for (NSString* methodName in methodNames) {
        SEL methodSelector = NSSelectorFromString(methodName);
        newAccessibilityAPISelectors->insert(methodSelector);
      }
    });

    if (newAccessibilityAPISelectors->find(selector) !=
        newAccessibilityAPISelectors->end()) {
      return NO;
    }
  } else {
    // The following deprecated selectors had existing new-API implementations
    // that are expected to continue to work independent of the flag. For any
    // such API, ensure the corresponding old API is not available when the flag
    // is enabled.
    static base::NoDestructor<std::unordered_set<SEL>> deprecatedSelectors({
      @selector(AXInsertionPointLineNumber), @selector(AXNumberOfCharacters),
          @selector(AXPlaceholderValue), @selector(AXSelectedText),
          @selector(AXSelectedTextRange), @selector(AXVisibleCharacterRange)
    });
    if (deprecatedSelectors->find(selector) != deprecatedSelectors->end()) {
      return NO;
    }
  }

  // Do not respond to the method if it's not supported by the node.
  if (![self conditionallyRespondsToSelector:selector]) {
    return NO;
  }

  return [super respondsToSelector:selector];
}

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

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

- (id)titleUIElement {
  // True only if it's a control, if there's a single label, and the label has
  // nonempty text.

  // VoiceOver ignores TitleUIElement if the element isn't a control.
  if (!ui::IsControl(_node->GetRole()))
    return nil;

  if (!_node->HasNameFromOtherElement())
    return nil;

  // TODO(crbug.com/535058209): When aria-labelledby refers to two elements in
  // the DOM and one is aria-hidden, even though the name uses the content that
  // is aria-hidden, this int attribute only has one value: the non-aria-hidden
  // one. In this case, titleUIElement should not be returned because the name
  // came from two places.
  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;

  // If the label itself gets its name from a source that we would normally
  // expose as a description (like aria-label), don't create a titleUIElement
  // link. The button should expose the name directly in its own title.
  if (label->GetDelegate()->GetNameFrom() == ax::mojom::NameFrom::kAttribute)
    return nil;

  // No title UI element if the label's name is empty.
  std::string labelName = label->GetDelegate()->GetName();
  if (labelName.empty())
    return nil;

  // In the case where we have a radio button or a checked box, no title UI
  // element. This goes against Apple's documentation for AXTitleUIElement,
  // but is consistent with Safari+Voiceover behavior.
  // See crbug.com/1430419
  ax::mojom::Role role = _node->GetRole();
  if (ui::IsRadio(role) || ui::IsCheckBox(role))
    return nil;

  return label->GetNativeViewAccessible().Get();
}

- (BOOL)isNameFromLabel {
  // Image annotations are not visible text, so they should be exposed
  // as a description and not a title.
  switch (_node->GetData().GetImageAnnotationStatus()) {
    case ax::mojom::ImageAnnotationStatus::kEligibleForAnnotation:
    case ax::mojom::ImageAnnotationStatus::kAnnotationPending:
    case ax::mojom::ImageAnnotationStatus::kAnnotationEmpty:
    case ax::mojom::ImageAnnotationStatus::kAnnotationAdult:
    case ax::mojom::ImageAnnotationStatus::kAnnotationProcessFailed:
    case ax::mojom::ImageAnnotationStatus::kAnnotationSucceeded:
      return true;

    case ax::mojom::ImageAnnotationStatus::kNone:
    case ax::mojom::ImageAnnotationStatus::kWillNotAnnotateDueToScheme:
    case ax::mojom::ImageAnnotationStatus::kIneligibleForAnnotation:
    case ax::mojom::ImageAnnotationStatus::kSilentlyEligibleForAnnotation:
      break;
  }

  // No label for windows or native dialogs.
  ax::mojom::Role role = _node->GetRole();
  if (ui::IsWindow(role) || (ui::IsDialog(role) && !_node->IsWebContent())) {
    return false;
  }

  // VoiceOver computes the wrong description for a link.
  if (ui::IsLink(role))
    return true;

  // If a radiobutton or checkbox has a single label, we are consistent
  // with Safari+Voiceover and expose it via AccessibilityLabel.
  // Note: Safari+Voiceover is inconsistent with Apple's documentation,
  // which suggests this should be exposed via AXTitleUIElement. See
  // crbug.com/1430419
  if (ui::IsRadio(role) || ui::IsCheckBox(role)) {
    std::vector<int32_t> labelledby_ids =
        _node->GetIntListAttribute(ax::mojom::IntListAttribute::kLabelledbyIds);
    if (labelledby_ids.size() == 1) {
      ui::AXPlatformNode* label =
          _node->GetDelegate()->GetFromNodeID(labelledby_ids[0]);
      if (label) {
        // No title UI element if the label's name is empty.
        std::string labelName = label->GetDelegate()->GetName();
        if (!labelName.empty())
          return true;
      }
    }
  }

  // VoiceOver will not read the label of these roles unless it is
  // exposed in the description instead of the title.
  switch (role) {
    case ax::mojom::Role::kGenericContainer:
    case ax::mojom::Role::kGroup:
    case ax::mojom::Role::kRadioGroup:
    case ax::mojom::Role::kTabPanel:
      return true;
    default:
      break;
  }

  // On macOS, the accessible name of an object is exposed as its title if it
  // comes from visible text, and as its description otherwise, but never both.
  //
  // Note: a placeholder is often visible text, but since it aids in data entry
  // it is similar to accessibilityValue, and thus cannot be exposed either in
  // accessibilityTitle or in accessibilityLabel.
  ax::mojom::NameFrom nameFrom = _node->GetNameFrom();
  if (nameFrom == ax::mojom::NameFrom::kCaption ||
      nameFrom == ax::mojom::NameFrom::kContents ||
      nameFrom == ax::mojom::NameFrom::kPlaceholder ||
      nameFrom == ax::mojom::NameFrom::kRelatedElement ||
      nameFrom == ax::mojom::NameFrom::kValue) {
    return false;
  }

  return true;
}

- (NSArray*)uiElementsForAttribute:(ax::mojom::IntListAttribute)attribute {
  NSMutableArray* elements = [NSMutableArray array];
  ui::AXPlatformNodeDelegate* delegate = [self nodeDelegate];
  if (!delegate) {
    return elements;
  }

  const std::vector<int32_t>& attributeValues =
      delegate->GetIntListAttribute(attribute);
  for (auto& attributeValue : attributeValues) {
    ui::AXPlatformNode* node = delegate->GetFromNodeID(attributeValue);
    if (node) {
      [elements addObject:node->GetNativeViewAccessible().Get()];
    }
  }
  return elements;
}

- (void)getTreeItemDescendantNodeIds:(std::vector<int32_t>*)treeItemIds {
  for (auto childDelegateIterator = [self nodeDelegate]->ChildrenBegin();
       *childDelegateIterator != *[self nodeDelegate]->ChildrenEnd();
       ++(*childDelegateIterator)) {
    ui::AXPlatformNodeDelegate* childDelegate = childDelegateIterator->get();
    if (childDelegate->GetRole() == ax::mojom::Role::kTreeItem) {
      treeItemIds->push_back(childDelegate->GetId());
    }
    gfx::NativeViewAccessible child = childDelegate->GetNativeViewAccessible();
    AXPlatformNodeCocoa* childCocoa =
        ObjCCastStrict<AXPlatformNodeCocoa>(child.Get());
    [childCocoa getTreeItemDescendantNodeIds:treeItemIds];
  }
}

+ (NSString*)nativeRoleFromAXRole:(ax::mojom::Role)role {
  switch (role) {
    case ax::mojom::Role::kAbbr:
    case ax::mojom::Role::kAlert:
    case ax::mojom::Role::kAlertDialog:
    case ax::mojom::Role::kApplication:
    case ax::mojom::Role::kArticle:
    case ax::mojom::Role::kAudio:
    case ax::mojom::Role::kBanner:
    case ax::mojom::Role::kBlockquote:
    case ax::mojom::Role::kCaption:
    case ax::mojom::Role::kClient:
    case ax::mojom::Role::kCode:
    case ax::mojom::Role::kComment:
    case ax::mojom::Role::kComplementary:
    case ax::mojom::Role::kContentDeletion:
    case ax::mojom::Role::kContentInsertion:
    case ax::mojom::Role::kContentInfo:
    case ax::mojom::Role::kDefinition:
    case ax::mojom::Role::kDesktop:
    case ax::mojom::Role::kDialog:
    case ax::mojom::Role::kDetails:
    case ax::mojom::Role::kDocAbstract:
    case ax::mojom::Role::kDocAcknowledgments:
    case ax::mojom::Role::kDocAfterword:
    case ax::mojom::Role::kDocAppendix:
    case ax::mojom::Role::kDocBiblioEntry:
    case ax::mojom::Role::kDocBibliography:
    case ax::mojom::Role::kDocChapter:
    case ax::mojom::Role::kDocColophon:
    case ax::mojom::Role::kDocConclusion:
    case ax::mojom::Role::kDocCredit:
    case ax::mojom::Role::kDocCredits:
    case ax::mojom::Role::kDocDedication:
    case ax::mojom::Role::kDocEndnote:
    case ax::mojom::Role::kDocEndnotes:
    case ax::mojom::Role::kDocEpigraph:
    case ax::mojom::Role::kDocEpilogue:
    case ax::mojom::Role::kDocErrata:
    case ax::mojom::Role::kDocExample:
    case ax::mojom::Role::kDocFootnote:
    case ax::mojom::Role::kDocForeword:
    case ax::mojom::Role::kDocGlossary:
    case ax::mojom::Role::kDocIndex:
    case ax::mojom::Role::kDocIntroduction:
    case ax::mojom::Role::kDocNotice:
    case ax::mojom::Role::kDocPageFooter:
    case ax::mojom::Role::kDocPageHeader:
    case ax::mojom::Role::kDocPageList:
    case ax::mojom::Role::kDocPart:
    case ax::mojom::Role::kDocPreface:
    case ax::mojom::Role::kDocPrologue:
    case ax::mojom::Role::kDocPullquote:
    case ax::mojom::Role::kDocQna:
    case ax::mojom::Role::kDocTip:
    case ax::mojom::Role::kDocToc:
    case ax::mojom::Role::kDocument:
    case ax::mojom::Role::kEmbeddedObject:
    case ax::mojom::Role::kEmphasis:
    case ax::mojom::Role::kFeed:
    case ax::mojom::Role::kFigcaption:
    case ax::mojom::Role::kFigure:
    case ax::mojom::Role::kFooter:
    case ax::mojom::Role::kForm:
    case ax::mojom::Role::kGenericContainer:
    case ax::mojom::Role::kGraphicsDocument:
    case ax::mojom::Role::kGraphicsObject:
    case ax::mojom::Role::kGroup:
    case ax::mojom::Role::kHeader:
    case ax::mojom::Role::kIframe:
    case ax::mojom::Role::kIframePresentational:
    case ax::mojom::Role::kLabelText:
    case ax::mojom::Role::kLayoutTable:
    case ax::mojom::Role::kLayoutTableCell:
    case ax::mojom::Role::kLayoutTableRow:
    case ax::mojom::Role::kLegend:
    case ax::mojom::Role::kLineBreak:
    case ax::mojom::Role::kListItem:
    case ax::mojom::Role::kLog:
    case ax::mojom::Role::kMain:
    case ax::mojom::Role::kMark:
    case ax::mojom::Role::kMarquee:
    case ax::mojom::Role::kMath:
    case ax::mojom::Role::kMathMLFraction:
    case ax::mojom::Role::kMathMLIdentifier:
    case ax::mojom::Role::kMathMLMath:
    case ax::mojom::Role::kMathMLMultiscripts:
    case ax::mojom::Role::kMathMLNoneScript:
    case ax::mojom::Role::kMathMLNumber:
    case ax::mojom::Role::kMathMLOperator:
    case ax::mojom::Role::kMathMLOver:
    case ax::mojom::Role::kMathMLPrescriptDelimiter:
    case ax::mojom::Role::kMathMLRoot:
    case ax::mojom::Role::kMathMLRow:
    case ax::mojom::Role::kMathMLSquareRoot:
    case ax::mojom::Role::kMathMLStringLiteral:
    case ax::mojom::Role::kMathMLSub:
    case ax::mojom::Role::kMathMLSubSup:
    case ax::mojom::Role::kMathMLSup:
    case ax::mojom::Role::kMathMLTable:
    case ax::mojom::Role::kMathMLTableCell:
    case ax::mojom::Role::kMathMLTableRow:
    case ax::mojom::Role::kMathMLText:
    case ax::mojom::Role::kMathMLUnder:
    case ax::mojom::Role::kMathMLUnderOver:
    case ax::mojom::Role::kNavigation:
    case ax::mojom::Role::kNone:
    case ax::mojom::Role::kNote:
    case ax::mojom::Role::kPane:
    case ax::mojom::Role::kParagraph:
    case ax::mojom::Role::kPdfRoot:
    case ax::mojom::Role::kPluginObject:
    case ax::mojom::Role::kRegion:
    case ax::mojom::Role::kRowGroup:
    case ax::mojom::Role::kRuby:
    case ax::mojom::Role::kSearch:
    case ax::mojom::Role::kSection:
    case ax::mojom::Role::kSectionFooter:
    case ax::mojom::Role::kSectionHeader:
    case ax::mojom::Role::kSectionWithoutName:
    case ax::mojom::Role::kStatus:
    case ax::mojom::Role::kSubscript:
    case ax::mojom::Role::kSuggestion:
    case ax::mojom::Role::kSuperscript:
      return NSAccessibilityGroupRole;
    case ax::mojom::Role::kSvgRoot:
      return NSAccessibilityImageRole;
    case ax::mojom::Role::kStrong:
    case ax::mojom::Role::kTableHeaderContainer:
    case ax::mojom::Role::kTabPanel:
    case ax::mojom::Role::kTerm:
    case ax::mojom::Role::kTime:
    case ax::mojom::Role::kTimer:
    case ax::mojom::Role::kTooltip:
    case ax::mojom::Role::kVideo:
    case ax::mojom::Role::kWebView:
      return NSAccessibilityGroupRole;
    case ax::mojom::Role::kButton:
      return NSAccessibilityButtonRole;
    case ax::mojom::Role::kCanvas:
      return NSAccessibilityImageRole;
    case ax::mojom::Role::kCell:
      return CFToNSPtrCast(kAXCellRole);
    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 CFToNSPtrCast(kAXCellRole);
    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 CFToNSPtrCast(kAXDateFieldRole);
    case ax::mojom::Role::kDateTime:
      return CFToNSPtrCast(kAXDateFieldRole);
    case ax::mojom::Role::kDescriptionList:
      return NSAccessibilityListRole;
    case ax::mojom::Role::kDisclosureTriangle:
    case ax::mojom::Role::kDisclosureTriangleGrouped:
      // If Mac supports AXExpandedChanged event with
      // NSAccessibilityDisclosureTriangleRole, We should update
      // ax::mojom::Role::kDisclosureTriangle mapping to
      // NSAccessibilityDisclosureTriangleRole. http://crbug.com/558324
      return features::IsAccessibilityExposeSummaryAsHeadingEnabled()
                 ? NSAccessibilityDisclosureTriangleRole
                 : NSAccessibilityButtonRole;
    case ax::mojom::Role::kDocBackLink:
    case ax::mojom::Role::kDocBiblioRef:
    case ax::mojom::Role::kDocGlossRef:
    case ax::mojom::Role::kDocNoteRef:
      return NSAccessibilityLinkRole;
    case ax::mojom::Role::kDocCover:
      return NSAccessibilityImageRole;
    case ax::mojom::Role::kDocPageBreak:
      return NSAccessibilitySplitterRole;
    case ax::mojom::Role::kDocSubtitle:
      return CFToNSPtrCast(kAXHeadingRole);
    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 CFToNSPtrCast(kAXCellRole);
    case ax::mojom::Role::kHeading:
      return CFToNSPtrCast(kAXHeadingRole);
    case ax::mojom::Role::kImage:
      return NSAccessibilityImageRole;
    case ax::mojom::Role::kInlineTextBox:
      return NSAccessibilityStaticTextRole;
    case ax::mojom::Role::kInputTime:
      return CFToNSPtrCast(kAXTimeFieldRole);
    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 NSAccessibilityListMarkerRole;
    case ax::mojom::Role::kMenu:
      return NSAccessibilityMenuRole;
    case ax::mojom::Role::kMenuBar:
      return NSAccessibilityMenuBarRole;
    case ax::mojom::Role::kMenuItem:
      return NSAccessibilityMenuItemRole;
    case ax::mojom::Role::kMenuItemCheckBox:
      return NSAccessibilityMenuItemRole;
    case ax::mojom::Role::kMenuItemRadio:
      return NSAccessibilityMenuItemRole;
    case ax::mojom::Role::kMenuItemSeparator:
      return NSAccessibilityMenuItemRole;
    case ax::mojom::Role::kMenuListOption:
      return NSAccessibilityMenuItemRole;
    case ax::mojom::Role::kMenuListPopup:
      return NSAccessibilityMenuRole;
    case ax::mojom::Role::kMeter:
      return NSAccessibilityLevelIndicatorRole;
    case ax::mojom::Role::kPdfActionableHighlight:
      return NSAccessibilityButtonRole;
    case ax::mojom::Role::kPopUpButton:
      return NSAccessibilityPopUpButtonRole;
    case ax::mojom::Role::kProgressIndicator:
      return NSAccessibilityProgressIndicatorRole;
    case ax::mojom::Role::kRadioButton:
      return NSAccessibilityRadioButtonRole;
    case ax::mojom::Role::kRadioGroup:
      return NSAccessibilityRadioGroupRole;
    case ax::mojom::Role::kRootWebArea:
      return NSAccessibilityWebAreaRole;
    case ax::mojom::Role::kRow:
      return NSAccessibilityRowRole;
    case ax::mojom::Role::kRowHeader:
      return CFToNSPtrCast(kAXCellRole);
    case ax::mojom::Role::kScrollBar:
      return NSAccessibilityScrollBarRole;
    case ax::mojom::Role::kScrollView:
      return NSAccessibilityScrollAreaRole;
    case ax::mojom::Role::kSearchBox:
      return NSAccessibilityTextFieldRole;
    case ax::mojom::Role::kSlider:
      return NSAccessibilitySliderRole;
    case ax::mojom::Role::kSpinButton:
      return NSAccessibilityIncrementorRole;
    case ax::mojom::Role::kSplitter:
      return NSAccessibilitySplitterRole;
    case ax::mojom::Role::kStaticText:
      return NSAccessibilityStaticTextRole;
    case ax::mojom::Role::kSwitch:
      return NSAccessibilityCheckBoxRole;
    case ax::mojom::Role::kTab:
      return NSAccessibilityRadioButtonRole;
    case ax::mojom::Role::kTable:
      return NSAccessibilityTableRole;
    case ax::mojom::Role::kTabList:
      return NSAccessibilityTabGroupRole;
    case ax::mojom::Role::kTextField:
      return NSAccessibilityTextFieldRole;
    case ax::mojom::Role::kTextFieldWithComboBox:
      return NSAccessibilityComboBoxRole;
    case ax::mojom::Role::kTitleBar:
      return NSAccessibilityStaticTextRole;
    case ax::mojom::Role::kToggleButton:
      return NSAccessibilityCheckBoxRole;
    case ax::mojom::Role::kToolbar:
      return NSAccessibilityToolbarRole;
    case ax::mojom::Role::kTree:
      return NSAccessibilityOutlineRole;
    case ax::mojom::Role::kTreeGrid:
      return NSAccessibilityTableRole;
    case ax::mojom::Role::kTreeItem:
      return NSAccessibilityRowRole;
    case ax::mojom::Role::kUnknown:
      // This occurs in the case where a View has no widget, and while this will
      // not be exposed to users, it allows isAccessibilityElement() to have
      // fewer rules.
      return NSAccessibilityGroupRole;
    case ax::mojom::Role::kWindow:
      // Use the group role as the BrowserNativeWidgetWindow already provides
      // a kWindow role, and having extra window roles, which are treated
      // specially by screen readers, can break their ability to find the
      // content window. See http://crbug.com/875843 for more information.
      return NSAccessibilityGroupRole;
    case ax::mojom::Role::kCaret:
    case ax::mojom::Role::kDescriptionListTermDeprecated:
    case ax::mojom::Role::kDescriptionListDetailDeprecated:
    case ax::mojom::Role::kDirectoryDeprecated:
    case ax::mojom::Role::kImeCandidate:
    case ax::mojom::Role::kKeyboard:
    case ax::mojom::Role::kPreDeprecated:
    case ax::mojom::Role::kPortalDeprecated:
    case ax::mojom::Role::kRubyAnnotation:
      NOTREACHED() << "The following role should not be present: " << role;
  }
}

+ (NSString*)nativeSubroleFromAXRole:(ax::mojom::Role)role {
  static const base::NoDestructor<RoleMap> subrole_map(BuildSubroleMap());
  RoleMap::const_iterator it = subrole_map->find(role);
  return it != subrole_map->end() ? it->second : nil;
}

+ (NSString*)nativeNotificationFromAXEvent:(ax::mojom::Event)event {
  static const base::NoDestructor<EventMap> event_map(BuildEventMap());
  EventMap::const_iterator it = event_map->find(event);
  return it != event_map->end() ? it->second : nil;
}

+ (NSString*)nativeNotificationForExpandedChangedWithRole:(ax::mojom::Role)role
                                               isExpanded:(BOOL)isExpanded {
  if (role == ax::mojom::Role::kRow ||
      role == ax::mojom::Role::kTreeItem) {
    return isExpanded ? NSAccessibilityRowExpandedNotification
                      : NSAccessibilityRowCollapsedNotification;
  }
  return CFToNSPtrCast(kAXExpandedChangedNotification);
}

- (instancetype)initWithNode:(ui::AXPlatformNodeBase*)node {
  if ((self = [super init])) {
    _node = node;
  }
  return self;
}

- (void)detachAndNotifyDestroyed:(BOOL)shouldNotify {
  if (!_node)
    return;
  _node = nil;
  if (shouldNotify) {
    NSAccessibilityPostNotification(
        self, NSAccessibilityUIElementDestroyedNotification);
  }
}

- (NSRect)boundsInScreen {
  if (!_node) {
    return NSZeroRect;
  }
  return gfx::ScreenRectToNSRect(_node->GetDelegate()->GetBoundsRect(
      ui::AXCoordinateSystem::kScreenDIPs, ui::AXClippingBehavior::kClipped));
}

- (NSString*)getStringAttribute:(ax::mojom::StringAttribute)attribute {
  std::string attributeValue;
  if (_node->GetStringAttribute(attribute, &attributeValue))
    return base::SysUTF8ToNSString(attributeValue);
  return nil;
}

- (NSString*)getAXValueAsString {
  id value = [self AXValue];
  return ObjCCast<NSString>(value);
}

- (ax::mojom::Role)internalRole {
  ax::mojom::Role role = static_cast<ax::mojom::Role>(_node->GetRole());
  // Make sure to use Role::kPopupButton instead of Role::kButton for all
  // values of kHasPopup. This is normally already true, but the default
  // implementation does not use kPopupButton if aria-haspopup="dialog".
  if (role == ax::mojom::Role::kButton &&
      _node->HasIntAttribute(ax::mojom::IntAttribute::kHasPopup)) {
    return ax::mojom::Role::kPopUpButton;
  }
  return role;
}

- (BOOL)hasAction:(ax::mojom::Action)action {
  return _node->HasAction(action) || HasImplicitAction(*_node, action);
}

- (BOOL)performAction:(ax::mojom::Action)action {
  if (![self hasAction:action]) {
    return NO;
  }

  ui::AXActionData data;
  data.action = action;
  _node->GetDelegate()->AccessibilityPerformAction(data);
  return YES;
}

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

- (BOOL)isImage {
  bool has_image_semantics =
      ui::IsImage(_node->GetRole()) &&
      !_node->GetBoolAttribute(ax::mojom::BoolAttribute::kCanvasHasFallback) &&
      !_node->GetChildCount();
#if DCHECK_IS_ON()
  bool is_native_image =
      [[self accessibilityRole] isEqualToString:NSAccessibilityImageRole];
  DCHECK_EQ(is_native_image, has_image_semantics)
      << "\nPresence/lack of native image role do not match the expected "
         "internal semantics:"
      << "\n* Chrome role: " << ui::ToString(_node->GetRole())
      << "\n* NSAccessibility role: " << [self accessibilityRole]
      << "\n* AXNode: " << *_node;
#endif
  return has_image_semantics;
}

- (void)addTextAnnotationsIn:(const AXRange*)axRange
                          to:(NSMutableAttributedString*)attributedString {
  int anchorStartOffset = 0;
  std::map<ui::AXNodeID, std::set<ax::mojom::Role>> ancestor_roles;

  [attributedString beginEditing];
  for (const AXRange& leafTextRange : *axRange) {
    DCHECK(!leafTextRange.IsNull());
    DCHECK_EQ(leafTextRange.anchor()->GetAnchor(),
              leafTextRange.focus()->GetAnchor())
        << "An anchor range should only span a single object.";

    int leafTextLength = leafTextRange.GetText().length();
    NSRange leafRange = NSMakeRange(anchorStartOffset, leafTextLength);

    // As we iterate over the attributed string's string using leaf ranges,
    // double check that the next leaf string actually matches the text in the
    // attributed string. If it doesn't, the leaf text is "extra" text that's
    // not included in the attributed string.
    if (leafRange.location >= attributedString.length ||
        base::SysNSStringToUTF16([attributedString.string
            substringWithRange:leafRange]) != leafTextRange.GetText()) {
      continue;
    }

    ui::AXNode* anchor = leafTextRange.focus()->GetAnchor();
    DCHECK(anchor) << "A non-null position should have a non-null anchor node.";

    // Document markers are stored on the static text parent of an inline text
    // box. If this node is an inline text box, create equivalent positions in
    // its parent static text node so that the markers can be retrieved.
    AXRange markersTextRange(leafTextRange.anchor()->Clone(),
                             leafTextRange.focus()->Clone());
    ui::AXNode* markers_anchor = leafTextRange.anchor()->GetAnchor();
    if (leafTextRange.focus()->GetAnchor()->GetRole() ==
        ax::mojom::Role::kInlineTextBox) {
      markersTextRange = AXRange(leafTextRange.anchor()->CreateParentPosition(),
                                 leafTextRange.focus()->CreateParentPosition());
      markers_anchor = markersTextRange.anchor()->GetAnchor();

      DCHECK(markersTextRange.anchor()->GetAnchor() ==
             markersTextRange.focus()->GetAnchor());
      DCHECK(markers_anchor) << "Markers anchor should not be null.";
      DCHECK(markers_anchor->GetRole() == ax::mojom::Role::kStaticText ||
             markers_anchor->GetRole() == ax::mojom::Role::kLineBreak);
    }

    // Add misspelling information
    const std::vector<int32_t>& markerTypes =
        markers_anchor->GetIntListAttribute(
            ax::mojom::IntListAttribute::kMarkerTypes);
    const std::vector<int>& markerStarts = markers_anchor->GetIntListAttribute(
        ax::mojom::IntListAttribute::kMarkerStarts);
    const std::vector<int>& markerEnds = markers_anchor->GetIntListAttribute(
        ax::mojom::IntListAttribute::kMarkerEnds);
    const std::vector<int32_t>& highlightTypes =
        markers_anchor->GetIntListAttribute(
            ax::mojom::IntListAttribute::kHighlightTypes);

    DCHECK_EQ(markerTypes.size(), markerStarts.size());
    DCHECK_EQ(markerTypes.size(), markerEnds.size());
    DCHECK_EQ(markerTypes.size(), highlightTypes.size());

    for (size_t i = 0; i < markerTypes.size(); ++i) {
      const bool is_spelling_marker =
          (markerTypes[i] &
           static_cast<int32_t>(ax::mojom::MarkerType::kSpelling)) ||
          ((markerTypes[i] &
            static_cast<int32_t>(ax::mojom::MarkerType::kHighlight)) &&
           (highlightTypes[i] ==
            static_cast<int32_t>(ax::mojom::HighlightType::kSpellingError)));

      const bool is_custom_highlight_marker =
          (markerTypes[i] &
           static_cast<int32_t>(ax::mojom::MarkerType::kHighlight)) &&
          (highlightTypes[i] ==
           static_cast<int32_t>(ax::mojom::HighlightType::kHighlight));

      NSString* attribute;
      if (is_spelling_marker) {
        attribute = NSAccessibilityMarkedMisspelledTextAttribute;
      } else if (is_custom_highlight_marker) {
        attribute = CFToNSPtrCast(kAXHighlightStringAttribute);
      } else {
        continue;
      }

      // Calculate the intersection of the marker range and the current text
      // range. These offsets are relative to the static text node, not the leaf
      // inline text.
      int markerStartOffset =
          std::max(markerStarts[i], markersTextRange.anchor()->text_offset());
      int markerEndOffset =
          std::min(markerEnds[i], markersTextRange.focus()->text_offset());
      if (markerEndOffset <= markerStartOffset) {
        continue;
      }

      // Convert the intersection so it's relative to the text range of the
      // attributed string we're building.
      int rangeStart = markerStartOffset -
                       markersTextRange.anchor()->text_offset() +
                       anchorStartOffset;
      int rangeEnd = markerEndOffset -
                     markersTextRange.anchor()->text_offset() +
                     anchorStartOffset;
      int rangeLength = rangeEnd - rangeStart;
      [attributedString addAttribute:attribute
                               value:@YES
                               range:NSMakeRange(rangeStart, rangeLength)];
    }

    CollectAncestorRoles(*anchor, ancestor_roles);

    // Add annotation information
    if (ancestor_roles[anchor->id()].contains(ax::mojom::Role::kMark)) {
      [attributedString addAttribute:CFToNSPtrCast(kAXHighlightStringAttribute)
                               value:@YES
                               range:leafRange];
    }
    if (ancestor_roles[anchor->id()].contains(ax::mojom::Role::kSuggestion)) {
      [attributedString
          addAttribute:CFToNSPtrCast(kAXIsSuggestionStringAttribute)
                 value:@YES
                 range:leafRange];
    }
    if (ancestor_roles[anchor->id()].contains(
            ax::mojom::Role::kContentDeletion)) {
      [attributedString
          addAttribute:CFToNSPtrCast(kAXIsSuggestedDeletionStringAttribute)
                 value:@YES
                 range:leafRange];
    }
    if (ancestor_roles[anchor->id()].contains(
            ax::mojom::Role::kContentInsertion)) {
      [attributedString
          addAttribute:CFToNSPtrCast(kAXIsSuggestedInsertionStringAttribute)
                 value:@YES
                 range:leafRange];
    }

    ui::AXTextAttributes text_attrs =
        leafTextRange.anchor()->GetTextAttributes();

    NSMutableDictionary* fontAttributes = [NSMutableDictionary dictionary];

    // TODO(crbug.com/41456329): Implement NSAccessibilityFontFamilyKey.
    // TODO(crbug.com/41456329): Implement NSAccessibilityFontNameKey.
    // TODO(crbug.com/41456329): Implement NSAccessibilityVisibleNameKey.

    if (text_attrs.font_size != ui::AXTextAttributes::kUnsetValue) {
      fontAttributes[NSAccessibilityFontSizeKey] = @(text_attrs.font_size);
    }

    if (text_attrs.HasTextStyle(ax::mojom::TextStyle::kBold)) {
      fontAttributes[@"AXFontBold"] = @YES;
    }

    if (text_attrs.HasTextStyle(ax::mojom::TextStyle::kItalic)) {
      fontAttributes[@"AXFontItalic"] = @YES;
    }

    [attributedString addAttribute:NSAccessibilityFontTextAttribute
                             value:fontAttributes
                             range:leafRange];

    if (text_attrs.color != ui::AXTextAttributes::kUnsetValue) {
      [attributedString addAttribute:NSAccessibilityForegroundColorTextAttribute
                               value:(__bridge id)skia::SkColorToSRGBNSColor(
                                         SkColor(text_attrs.color))
                                         .CGColor
                               range:leafRange];
    } else {
      [attributedString
          removeAttribute:NSAccessibilityForegroundColorTextAttribute
                    range:leafRange];
    }

    if (text_attrs.background_color != ui::AXTextAttributes::kUnsetValue) {
      [attributedString addAttribute:NSAccessibilityBackgroundColorTextAttribute
                               value:(__bridge id)skia::SkColorToSRGBNSColor(
                                         SkColor(text_attrs.background_color))
                                         .CGColor
                               range:leafRange];
    } else {
      [attributedString
          removeAttribute:NSAccessibilityBackgroundColorTextAttribute
                    range:leafRange];
    }

    // TODO(crbug.com/41456329): Implement
    // NSAccessibilitySuperscriptTextAttribute.
    // TODO(crbug.com/41456329): Implement NSAccessibilityShadowTextAttribute.

    if (text_attrs.underline_style != ui::AXTextAttributes::kUnsetValue) {
      [attributedString addAttribute:NSAccessibilityUnderlineTextAttribute
                               value:@YES
                               range:leafRange];
    } else {
      [attributedString removeAttribute:NSAccessibilityUnderlineTextAttribute
                                  range:leafRange];
    }

    // TODO(crbug.com/41456329): Implement
    // NSAccessibilityUnderlineColorTextAttribute.

    if (text_attrs.strikethrough_style != ui::AXTextAttributes::kUnsetValue) {
      [attributedString addAttribute:NSAccessibilityStrikethroughTextAttribute
                               value:@YES
                               range:leafRange];
    } else {
      [attributedString
          removeAttribute:NSAccessibilityStrikethroughTextAttribute
                    range:leafRange];
    }

    // TODO(crbug.com/41456329): Implement
    // NSAccessibilityStrikethroughColorTextAttribute.

    // TODO(crbug.com/41456329): Implement NSAccessibilityLinkTextAttribute.
    // TODO(crbug.com/41456329): Implement
    // NSAccessibilityAutocorrectedTextAttribute.

    anchorStartOffset += leafTextLength;
  }
  [attributedString endEditing];
}

- (BOOL)descriptionIsFromAriaDescription {
  ax::mojom::DescriptionFrom descFrom = static_cast<ax::mojom::DescriptionFrom>(
      _node->GetIntAttribute(ax::mojom::IntAttribute::kDescriptionFrom));
  return descFrom == ax::mojom::DescriptionFrom::kAriaDescription ||
         descFrom == ax::mojom::DescriptionFrom::kRelatedElement;
}

- (NSString*)getName {
  return base::SysUTF8ToNSString(_node->GetName());
}

- (AXAnnouncementSpec*)announcementForEvent:(ax::mojom::Event)eventType {
  // Only alerts and live region changes should be announced.
  DCHECK(eventType == ax::mojom::Event::kAlert ||
         eventType == ax::mojom::Event::kLiveRegionChanged);
  std::string liveStatus =
      _node->GetStringAttribute(ax::mojom::StringAttribute::kLiveStatus);
  // If live status is explicitly set to off, don't announce.
  if (liveStatus == "off") {
    return nil;
  }

  NSString* name = [self getName];
  NSString* announcementText =
      name.length > 0 ? name
                      : base::SysUTF16ToNSString(_node->GetTextContentUTF16());
  if (announcementText.length == 0) {
    return nil;
  }

  const std::string& description =
      _node->GetStringAttribute(ax::mojom::StringAttribute::kDescription);
  if (!description.empty()) {
    // Concatenating name and description, with a newline in between to create a
    // pause to avoid treating the concatenation as a single sentence.
    announcementText =
        [NSString stringWithFormat:@"%@\n%@", announcementText,
                                   base::SysUTF8ToNSString(description)];
  }

  AXAnnouncementSpec* spec = [[AXAnnouncementSpec alloc] init];
  spec.announcement = announcementText;
  spec.window = [self AXWindow];
  spec.polite = liveStatus != "assertive";
  return spec;
}

- (void)scheduleLiveRegionAnnouncement:(AXAnnouncementSpec*)announcement {
  if (_pendingAnnouncement) {
    // An announcement is already in flight, so just reset the contents. This is
    // threadsafe because the dispatch is on the main queue.
    _pendingAnnouncement = announcement;
    return;
  }

  _pendingAnnouncement = announcement;
  dispatch_after(
      kLiveRegionDebounceMillis * NSEC_PER_MSEC, dispatch_get_main_queue(), ^{
        if (!self->_pendingAnnouncement) {
          return;
        }
        PostAnnouncementNotification(self->_pendingAnnouncement.announcement,
                                     self->_pendingAnnouncement.window,
                                     self->_pendingAnnouncement.polite);
        self->_pendingAnnouncement = nil;
      });
}

//
// NSAccessibility legacy informal protocol implementation (deprecated).
// https://developer.apple.com/documentation/appkit/deprecated_symbols/nsaccessibility
//

- (BOOL)accessibilityIsIgnored {
  return ![self isAccessibilityElement];
}

- (id)accessibilityHitTest:(NSPoint)point {
  if (!NSPointInRect(point, self.boundsInScreen)) {
    return nil;
  }

  for (id child in [[self accessibilityChildren] reverseObjectEnumerator]) {
    if (!NSPointInRect(point, [child accessibilityFrame]))
      continue;
    if (id foundChild = [child accessibilityHitTest:point])
      return foundChild;
  }

  // Hit self, but not any child.
  return NSAccessibilityUnignoredAncestor(self);
}

- (BOOL)accessibilityNotifiesWhenDestroyed {
  return YES;
}

- (id)accessibilityFocusedUIElement {
  return _node ? _node->GetDelegate()->GetFocus().Get() : nil;
}

// This function and accessibilityPerformAction:, while deprecated, are a) still
// called by AppKit internally and b) not implemented by NSAccessibilityElement,
// so this class needs its own implementations.
- (NSArray*)accessibilityActionNames {
  TRACE_EVENT1("accessibility", "AXPlatformNodeCocoa::accessibilityActionNames",
               "role=", ui::ToString([self internalRole]));

  // Exclude actions available through the new accessibility API.
  NSMutableArray* actions = [self internalAccessibilityActionNames];
  if (features::IsMacAccessibilityAPIMigrationEnabled()) {
    [actions
        filterUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
                                              id evaluatedObject,
                                              NSDictionary* bindings) {
          return ![[[self class] actionsAvailableThroughNewAccessibilityAPI]
              containsObject:evaluatedObject];
        }]];
  }

  return actions;
}

- (NSMutableArray*)internalAccessibilityActionNames {
  if (![self instanceActive]) {
    return [NSMutableArray array];
  }

  NSMutableArray* axActions = [NSMutableArray array];
  const ui::CocoaActionList& action_list = GetCocoaActionList();

  // VoiceOver expects the "press" action to be first. Note that some roles
  // should be given a press action implicitly.
  DCHECK([action_list[0].second isEqualToString:NSAccessibilityPressAction]);
  for (const auto& item : action_list) {
    if ((_node->HasAction(item.first) ||
         HasImplicitAction(*_node, item.first))) {
      [axActions addObject:item.second];
    }
  }

  if (AlsoUseShowMenuActionForDefaultAction(*_node))
    [axActions addObject:NSAccessibilityShowMenuAction];

  return axActions;
}

// This API is deprecated.
- (void)accessibilityPerformAction:(NSString*)action {
  // Actions are performed asynchronously, so it's always possible for an object
  // to change its mind after previously reporting an action as available.

  if (![[self accessibilityActionNames] containsObject:action]) {
    return;
  }

  ui::AXActionData data;
  if ([action isEqualToString:NSAccessibilityShowMenuAction] &&
      AlsoUseShowMenuActionForDefaultAction(*_node)) {
    data.action = ax::mojom::Action::kDoDefault;
  } else {
    for (const ui::CocoaActionList::value_type& entry : GetCocoaActionList()) {
      if ([action isEqualToString:entry.second]) {
        data.action = entry.first;
        break;
      }
    }
  }

  // Note ui::AX_ACTIONs which are just overwriting an accessibility attribute
  // are already implemented in -accessibilitySetValue:forAttribute:, so ignore
  // those here.

  if (data.action != ax::mojom::Action::kNone)
    _node->GetDelegate()->AccessibilityPerformAction(data);
}

- (BOOL)accessibilityPerformPress {
  if (![self instanceActive]) {
    return NO;
  }
  return [self performAction:ax::mojom::Action::kDoDefault];
}

- (BOOL)accessibilityPerformShowMenu {
  if (![self instanceActive]) {
    return NO;
  }

  if (AlsoUseShowMenuActionForDefaultAction(*_node)) {
    return [self accessibilityPerformPress];
  }

  if ([self performAction:ax::mojom::Action::kShowContextMenu]) {
    return YES;
  }
  return NO;
}

- (BOOL)accessibilityPerformDecrement {
  if (![self instanceActive]) {
    return NO;
  }
  return [self performAction:ax::mojom::Action::kDecrement];
}

- (BOOL)accessibilityPerformIncrement {
  if (![self instanceActive]) {
    return NO;
  }
  return [self performAction:ax::mojom::Action::kIncrement];
}

- (BOOL)accessibilityPerformConfirm {
  // Placeholder for the future. Needs to implement Return press key action.
  return NO;
}

- (NSMutableArray*)internalAccessibilityAttributeNames {
  if (!_node) {
    return [NSMutableArray array];
  }

  // These attributes are required on all accessibility objects.
  // clang-format off
  NSArray* const kAllRoleAttributes = @[
    NSAccessibilityBlockQuoteLevelAttribute,
    NSAccessibilityChildrenAttribute,
    CFToNSPtrCast(kAXDOMClassListAttribute),
    NSAccessibilityDescriptionAttribute,
    CFToNSPtrCast(kAXDOMIdentifierAttribute),
    CFToNSPtrCast(kAXElementBusyAttribute),
    NSAccessibilityParentAttribute,
    NSAccessibilityPositionAttribute,
    NSAccessibilityRoleAttribute,
    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.
    NSAccessibilityEnabledAttribute,
    NSAccessibilityFocusedAttribute,
    NSAccessibilityHelpAttribute,
    NSAccessibilityRoleDescriptionAttribute,
    NSAccessibilityTopLevelUIElementAttribute,
    NSAccessibilityVisitedAttribute,
    NSAccessibilityWindowAttribute,

    // Always add the node ID for round-tripping.
    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.
  NSArray* const kTextAttributes = @[
    NSAccessibilityPlaceholderValueAttribute
  ];
  // clang-format on

  NSMutableArray* axAttributes = [kAllRoleAttributes mutableCopy];
  ax::mojom::Role role = _node->GetRole();
  switch (role) {
    case ax::mojom::Role::kTextField:
    case ax::mojom::Role::kTextFieldWithComboBox:
      [axAttributes addObject:CFToNSPtrCast(kAXOwnsAttribute)];
      break;
    case ax::mojom::Role::kStaticText:
      [axAttributes addObjectsFromArray: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:@[
        CFToNSPtrCast(kAXMathFractionNumeratorAttribute),
        CFToNSPtrCast(kAXMathFractionDenominatorAttribute)
      ]];
      break;
    case ax::mojom::Role::kMathMLSquareRoot:
      [axAttributes addObject:CFToNSPtrCast(kAXMathRootRadicandAttribute)];
      break;
    case ax::mojom::Role::kMathMLRoot:
      [axAttributes addObjectsFromArray:@[
        CFToNSPtrCast(kAXMathRootIndexAttribute),
        CFToNSPtrCast(kAXMathRootRadicandAttribute)
      ]];
      break;
    case ax::mojom::Role::kMathMLSub:
      [axAttributes addObjectsFromArray:@[
        CFToNSPtrCast(kAXMathBaseAttribute),
        CFToNSPtrCast(kAXMathSubscriptAttribute)
      ]];
      break;
    case ax::mojom::Role::kMathMLSup:
      [axAttributes addObjectsFromArray:@[
        CFToNSPtrCast(kAXMathBaseAttribute),
        CFToNSPtrCast(kAXMathSuperscriptAttribute)
      ]];
      break;
    case ax::mojom::Role::kMathMLSubSup:
      [axAttributes addObjectsFromArray:@[
        CFToNSPtrCast(kAXMathBaseAttribute),
        CFToNSPtrCast(kAXMathSubscriptAttribute),
        CFToNSPtrCast(kAXMathSuperscriptAttribute)
      ]];
      break;
    case ax::mojom::Role::kMathMLUnder:
      [axAttributes addObjectsFromArray:@[
        CFToNSPtrCast(kAXMathBaseAttribute),
        CFToNSPtrCast(kAXMathUnderAttribute)
      ]];
      break;
    case ax::mojom::Role::kMathMLOver:
      [axAttributes addObjectsFromArray:@[
        CFToNSPtrCast(kAXMathBaseAttribute), CFToNSPtrCast(kAXMathOverAttribute)
      ]];
      break;
    case ax::mojom::Role::kMathMLUnderOver:
      [axAttributes addObjectsFromArray:@[
        CFToNSPtrCast(kAXMathBaseAttribute),
        CFToNSPtrCast(kAXMathOverAttribute),
        CFToNSPtrCast(kAXMathUnderAttribute)
      ]];
      break;
    case ax::mojom::Role::kMathMLMultiscripts:
      [axAttributes addObjectsFromArray:@[
        CFToNSPtrCast(kAXMathBaseAttribute),
        CFToNSPtrCast(kAXMathPostscriptsAttribute),
        CFToNSPtrCast(kAXMathPrescriptsAttribute)
      ]];
      break;
      // TODO(tapted): Add additional attributes based on role.
    default:
      break;
  }
  if (ui::IsMenuItem(role)) {
    [axAttributes addObject:CFToNSPtrCast(kAXMenuItemMarkCharAttribute)];
  }
  // Only expose AXARIAPosInSet/AXARIASetSize when explicit ARIA attributes
  // are present. Exposing computed values for plain HTML items (e.g. <li>)
  // causes VoiceOver to use its ARIA code path instead of its native
  // children-counting logic, which results in the position announcement
  // being dropped for the last item in the list. WebKit/Safari only exposes
  // these attributes when explicit aria-posinset/aria-setsize are set.
  if (ui::IsItemLike(role)) {
    if (_node->HasIntAttribute(ax::mojom::IntAttribute::kSetSize)) {
      [axAttributes addObject:CFToNSPtrCast(kAXARIASetSizeAttribute)];
    }

    if (_node->HasIntAttribute(ax::mojom::IntAttribute::kPosInSet)) {
      [axAttributes addObject:CFToNSPtrCast(kAXARIAPosInSetAttribute)];
    }
  }

  if ([[self accessibilityRole] isEqualToString:NSAccessibilityWebAreaRole]) {
    [axAttributes addObjectsFromArray:@[
      CFToNSPtrCast(kAXLoadedAttribute),
      CFToNSPtrCast(kAXLoadingProgressAttribute)
    ]];
  }

  // Caret navigation and text selection attributes.
  if (!ui::IsPlatformDocument(_node->GetRole())) {
    [axAttributes addObject:CFToNSPtrCast(kAXFocusableAncestorAttribute)];

    if (_node->HasState(ax::mojom::State::kEditable)) {
      [axAttributes addObjectsFromArray:@[
        CFToNSPtrCast(kAXEditableAncestorAttribute),
        CFToNSPtrCast(kAXHighestEditableAncestorAttribute)
      ]];
    }
  }

  // Live regions.
  if (_node->HasStringAttribute(ax::mojom::StringAttribute::kLiveStatus)) {
    [axAttributes addObject:CFToNSPtrCast(kAXARIALiveAttribute)];
  }
  if (_node->HasStringAttribute(ax::mojom::StringAttribute::kLiveRelevant)) {
    [axAttributes addObject:CFToNSPtrCast(kAXARIARelevantAttribute)];
  }
  if (_node->HasBoolAttribute(ax::mojom::BoolAttribute::kLiveAtomic)) {
    [axAttributes addObject:CFToNSPtrCast(kAXARIAAtomicAttribute)];
  }
  if (_node->HasBoolAttribute(ax::mojom::BoolAttribute::kBusy)) {
    [axAttributes addObject:NSAccessibilityARIABusyAttribute];
  }
  if (_node->HasIntAttribute(ax::mojom::IntAttribute::kAriaCurrentState)) {
    [axAttributes addObject:CFToNSPtrCast(kAXARIACurrentAttribute)];
  }

  // Control element.
  if (ui::IsControl(role)) {
    [axAttributes addObjectsFromArray:@[
      CFToNSPtrCast(kAXAccessKeyAttribute),
      CFToNSPtrCast(kAXInvalidAttribute),
    ]];
  }

  // Autocomplete.
  if (_node->HasStringAttribute(ax::mojom::StringAttribute::kAutoComplete)) {
    [axAttributes addObject:NSAccessibilityAutocompleteValueAttribute];
  }

  // AriaBrailleLabel.
  if (_node->HasStringAttribute(
          ax::mojom::StringAttribute::kAriaBrailleLabel)) {
    [axAttributes addObject:CFToNSPtrCast(kAXBrailleLabelAttribute)];
  }

  // AriaBrailleRoleDescription.
  if (_node->HasStringAttribute(
          ax::mojom::StringAttribute::kAriaBrailleRoleDescription)) {
    [axAttributes addObject:CFToNSPtrCast(kAXBrailleRoleDescriptionAttribute)];
  }

  // Details.
  if (_node->HasIntListAttribute(ax::mojom::IntListAttribute::kDetailsIds)) {
    [axAttributes addObject:NSAccessibilityDetailsElementsAttribute];
  }

  // Error messages.
  if (_node->HasIntListAttribute(
          ax::mojom::IntListAttribute::kErrormessageIds)) {
    [axAttributes addObject:CFToNSPtrCast(kAXErrorMessageElementsAttribute)];
  }

  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,
      CFToNSPtrCast(kAXARIAColumnCountAttribute),
      CFToNSPtrCast(kAXARIARowCountAttribute),
    ]];
  }
  if (ui::IsCellOrTableHeader(role)) {
    [axAttributes addObjectsFromArray:@[
      CFToNSPtrCast(kAXARIAColumnIndexAttribute),
      CFToNSPtrCast(kAXARIARowIndexAttribute),
    ]];
  }
  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:@[
      CFToNSPtrCast(kAXHasPopupAttribute), CFToNSPtrCast(kAXPopupValueAttribute)
    ]];
  }

  // KeyShortcuts
  if (_node->HasStringAttribute(ax::mojom::StringAttribute::kKeyShortcuts)) {
    [axAttributes addObject:CFToNSPtrCast(kAXKeyShortcutsAttribute)];
  }

  // TitleUIElement
  if ([self titleUIElement]) {
    [axAttributes addObject:NSAccessibilityTitleUIElementAttribute];
  }

  return axAttributes;
}

// This API is deprecated.
// This method, while deprecated, is still called internally by AppKit.
- (NSArray*)accessibilityAttributeNames {
  TRACE_EVENT1("accessibility",
               "AXPlatformNodeCocoa::accessibilityAttributeNames",
               "role=", ui::ToString([self internalRole]));

  if (![self instanceActive]) {
    LOG(ERROR) << "Stale object in tree, no AXPlatformNode.";
    return @[];
  }

  if (!_node->GetDelegate()) {
    LOG(ERROR) << "Stale object in tree, no delegate.";
    return @[];
  }

  // No need to compute attribute names for ignored nodes.
  if (![self isAccessibilityElement]) {
    return @[];
  }

  // Exclude attributes available through the new accessibility API.
  NSMutableArray* attributes = [self internalAccessibilityAttributeNames];
  if (features::IsMacAccessibilityAPIMigrationEnabled()) {
    [attributes
        filterUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
                                              id evaluatedObject,
                                              NSDictionary* bindings) {
          return ![[[self class] attributesAvailableThroughNewAccessibilityAPI]
              containsObject:evaluatedObject];
        }]];
  }
  return attributes;
}

- (NSArray*)accessibilityParameterizedAttributeNames {
  TRACE_EVENT1("accessibility",
               "AXPlatformNodeCocoa::accessibilityParameterizedAttributeNames",
               "role=", ui::ToString([self internalRole]));

  // Exclude attributes available through the new accessibility API.
  NSMutableArray* attributes =
      [self internalAccessibilityParameterizedAttributeNames];
  if (features::IsMacAccessibilityAPIMigrationEnabled()) {
    [attributes
        filterUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
                                              id evaluatedObject,
                                              NSDictionary* bindings) {
          return ![[[self class] attributesAvailableThroughNewAccessibilityAPI]
              containsObject:evaluatedObject];
        }]];
  }
  return attributes;
}

- (NSMutableArray*)internalAccessibilityParameterizedAttributeNames {
  NSMutableArray* attributeNames = [NSMutableArray array];

  if (![self instanceActive]) {
    return attributeNames;
  }

  // General attributes.
  [attributeNames addObjectsFromArray:@[
    CFToNSPtrCast(kAXAttributedStringForTextMarkerRangeParameterizedAttribute)
  ]];

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

// This API is deprecated.
// Despite its deprecation, the AppKit internally calls this function sometimes
// in unclear circumstances. It is implemented in terms of the new a11y API
// here.
- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute {
  if (!_node)
    return;

  if ([[self class] isAttributeAvailableThroughNewAccessibilityAPI:attribute]) {
    return;
  }

  if ([attribute isEqualToString:NSAccessibilityValueAttribute]) {
    [self setAccessibilityValue:value];
  } else if ([attribute isEqualToString:NSAccessibilitySelectedTextAttribute]) {
    [self setAccessibilitySelectedText:ObjCCastStrict<NSString>(value)];
  } else if ([attribute
                 isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) {
    [self setAccessibilitySelectedTextRange:ObjCCastStrict<NSValue>(value)
                                                .rangeValue];
  } else if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) {
    [self setAccessibilityFocused:ObjCCastStrict<NSNumber>(value).boolValue];
  }
}

// This method, while deprecated, is still called internally by AppKit.
- (id)accessibilityAttributeValue:(NSString*)attribute {
  if (!_node)
    return nil;  // Return nil when detached. Even for ax::mojom::Role.

  if ([[self class] isAttributeAvailableThroughNewAccessibilityAPI:attribute]) {
    // TODO(crbug.com/376723178): We should be able to add a NOTREACHED()
    // here, but at the moment, test infrastructure still directly calls this
    // api endpoint.
    return nil;
  }

  SEL selector = NSSelectorFromString(attribute);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
  if ([self respondsToSelector:selector])
    return [self performSelector:selector];
#pragma clang diagnostic pop
  return nil;
}

- (id)accessibilityAttributeValue:(NSString*)attribute
                     forParameter:(id)parameter {
  if (!_node)
    return nil;

  if ([[self class] isAttributeAvailableThroughNewAccessibilityAPI:attribute]) {
    // TODO(crbug.com/376723178): We should be able to add a NOTREACHED()
    // here, but at the moment, test infrastructure still directly calls this
    // api endpoint.
    return nil;
  }

  SEL selector = NSSelectorFromString([attribute stringByAppendingString:@":"]);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
  if ([self respondsToSelector:selector])
    return [self performSelector:selector withObject:parameter];
#pragma clang diagnostic pop
  return nil;
}

//
// End of legacy deprecated NSAccessibility informal protocol.
//

// NSAccessibility (key-based) attributes. Order them according to
// NSAccessibilityConstants.h, or see https://crbug.com/678898.

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

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

- (NSNumber*)AXARIAAtomic {
  if (![self instanceActive])
    return nil;

  return @(_node->GetBoolAttribute(ax::mojom::BoolAttribute::kLiveAtomic));
}

- (NSNumber*)AXARIABusy {
  if (![self instanceActive])
    return nil;

  return @(_node->GetBoolAttribute(ax::mojom::BoolAttribute::kBusy));
}

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

  int ariaCurrent;
  if (!_node->GetIntAttribute(ax::mojom::IntAttribute::kAriaCurrentState,
                              &ariaCurrent))
    return nil;

  switch (static_cast<ax::mojom::AriaCurrentState>(ariaCurrent)) {
    case ax::mojom::AriaCurrentState::kNone:
      NOTREACHED();
    case ax::mojom::AriaCurrentState::kFalse:
      return @"false";
    case ax::mojom::AriaCurrentState::kTrue:
      return @"true";
    case ax::mojom::AriaCurrentState::kPage:
      return @"page";
    case ax::mojom::AriaCurrentState::kStep:
      return @"step";
    case ax::mojom::AriaCurrentState::kLocation:
      return @"location";
    case ax::mojom::AriaCurrentState::kDate:
      return @"date";
    case ax::mojom::AriaCurrentState::kTime:
      return @"time";
  }

  NOTREACHED();
}

- (NSNumber*)AXARIAColumnCount {
  if (![self instanceActive])
    return nil;
  std::optional<int> ariaColCount =
      _node->GetDelegate()->GetTableAriaColCount();
  if (!ariaColCount)
    return nil;
  return @(*ariaColCount);
}

- (NSNumber*)AXARIAColumnIndex {
  if (![self instanceActive])
    return nil;

  std::optional<int> ariaColIndex =
      _node->GetDelegate()->GetTableCellAriaColIndex();
  if (!ariaColIndex)

    return nil;
  return @(*ariaColIndex);
}

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

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

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

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

- (NSNumber*)AXARIARowCount {
  if (![self instanceActive])
    return nil;
  std::optional<int> ariaRowCount =
      _node->GetDelegate()->GetTableAriaRowCount();
  if (!ariaRowCount)
    return nil;
  return @(*ariaRowCount);
}

- (NSNumber*)AXARIARowIndex {
  if (![self instanceActive])
    return nil;
  std::optional<int> ariaRowIndex =
      _node->GetDelegate()->GetTableCellAriaRowIndex();
  if (!ariaRowIndex)
    return nil;
  return @(*ariaRowIndex);
}

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

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

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

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

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

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

- (id)AXBlockQuoteLevel {
  if (![self instanceActive])
    return nil;
  // This is for the number of ancestors that are a <blockquote>, including
  // self, useful for tracking replies to replies etc. in an email.
  int level = 0;

  for (ui::AXPlatformNodeBase* ancestor = _node; ancestor;
       ancestor = ancestor->GetPlatformParent()) {
    // Do not cross document boundaries.
    if (ui::IsPlatformDocument(ancestor->GetRole()))
      break;
    if (ancestor->GetRole() == ax::mojom::Role::kBlockquote)
      ++level;
  }
  return @(level);
}

- (NSArray*)AXColumnHeaderUIElements {
  return [self accessibilityColumnHeaderUIElements];
}

- (NSArray*)AXDetailsElements {
  if (![self instanceActive])
    return nil;

  NSMutableArray* elements = [NSMutableArray array];
  for (ui::AXNodeID id :
       _node->GetIntListAttribute(ax::mojom::IntListAttribute::kDetailsIds)) {
    AXPlatformNodeCocoa* node = [self fromNodeID:id];
    if (node)
      [elements addObject:node];
  }

  return elements.count ? elements : nil;
}

- (NSArray*)AXDOMClassList {
  if (![self instanceActive])
    return nil;

  NSMutableArray* ret = [NSMutableArray array];

  std::string classes;
  if (_node->GetStringAttribute(ax::mojom::StringAttribute::kClassName,
                                &classes)) {
    std::vector<std::string> split_classes = base::SplitString(
        classes, " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
    for (const auto& className : split_classes)
      [ret addObject:(base::SysUTF8ToNSString(className))];
  }
  return ret;
}

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

  std::string id;
  if (_node->GetStringAttribute(ax::mojom::StringAttribute::kHtmlId, &id)) {
    return base::SysUTF8ToNSString(id);
  }

  return @"";
}

- (id)AXEditableAncestor {
  if (![self instanceActive])
    return nil;
  ui::AXPlatformNodeBase* text_field_ancestor =
      _node->GetPlatformTextFieldAncestor();
  if (text_field_ancestor)
    return text_field_ancestor->GetNativeViewAccessible().Get();
  return nil;
}

- (NSNumber*)AXElementBusy {
  if (![self instanceActive])
    return nil;
  return @(_node->GetBoolAttribute(ax::mojom::BoolAttribute::kBusy));
}

- (NSArray*)AXErrorMessageElements {
  if (![self instanceActive]) {
    return nil;
  }

  NSMutableArray* elements = [NSMutableArray array];
  for (ui::AXNodeID id : _node->GetIntListAttribute(
           ax::mojom::IntListAttribute::kErrormessageIds)) {
    AXPlatformNodeCocoa* node = [self fromNodeID:id];
    if (node) {
      [elements addObject:node];
    }
  }

  return elements.count ? elements : nil;
}

- (NSNumber*)AXGrabbed {
  return @NO;
}

- (NSNumber*)AXHasPopup {
  if (![self instanceActive])
    return nil;
  return @(_node->HasIntAttribute(ax::mojom::IntAttribute::kHasPopup));
}

- (id)AXHighestEditableAncestor {
  if (![self instanceActive])
    return nil;

  AXPlatformNodeCocoa* highestEditableAncestor = [self AXEditableAncestor];

  while (highestEditableAncestor) {
    AXPlatformNodeCocoa* ancestorParent = [highestEditableAncestor AXParent];
    if (!ancestorParent || ![ancestorParent isKindOfClass:[self class]])
      break;
    AXPlatformNodeCocoa* higherAncestor = [ancestorParent AXEditableAncestor];
    if (!higherAncestor)
      break;
    highestEditableAncestor = higherAncestor;
  }
  return highestEditableAncestor;
}

- (NSString*)AXInvalid {
  if (![self instanceActive])
    return nil;
  switch (_node->GetData().GetInvalidState()) {
    case ax::mojom::InvalidState::kNone:
    case ax::mojom::InvalidState::kFalse:
      return @"false";
    case ax::mojom::InvalidState::kTrue:
      return @"true";
  }
  NOTREACHED();
}

- (NSNumber*)AXIsMultiSelectable {
  if (![self instanceActive])
    return nil;
  return @(_node->HasState(ax::mojom::State::kMultiselectable));
}

- (NSString*)AXKeyShortcutsValue {
  if (![self instanceActive])
    return nil;
  return [self getStringAttribute:ax::mojom::StringAttribute::kKeyShortcuts];
}

- (NSNumber*)AXLoaded {
  if (![self instanceActive])
    return nil;
  return @(_node->GetDelegate()->GetTreeData().loaded);
}

- (NSNumber*)AXLoadingProgress {
  if (![self instanceActive])
    return nil;
  double doubleValue = _node->GetDelegate()->GetTreeData().loading_progress;
  return @(doubleValue);
}

- (id)AXOwns {
  if (![self instanceActive])
    return nil;

  ui::AXPlatformNodeBase* activeDescendant = _node->GetActiveDescendant();
  if (!activeDescendant)
    return nil;

  ui::AXPlatformNodeBase* container = activeDescendant->GetSelectionContainer();
  if (!container)
    return nil;

  return @[ container->GetNativeViewAccessible().Get() ];
}

- (NSString*)AXPopupValue {
  if (![self instanceActive])
    return nil;
  int hasPopup = _node->GetIntAttribute(ax::mojom::IntAttribute::kHasPopup);
  switch (static_cast<ax::mojom::HasPopup>(hasPopup)) {
    case ax::mojom::HasPopup::kFalse:
      return @"false";
    case ax::mojom::HasPopup::kTrue:
      return @"true";
    case ax::mojom::HasPopup::kMenu:
      return @"menu";
    case ax::mojom::HasPopup::kListbox:
      return @"listbox";
    case ax::mojom::HasPopup::kTree:
      return @"tree";
    case ax::mojom::HasPopup::kGrid:
      return @"grid";
    case ax::mojom::HasPopup::kDialog:
      return @"dialog";
  }
  NOTREACHED();
}

- (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::kForm:
      // Per HTML-AAM and Core-AAM, unnamed forms should not be exposed as
      // landmarks. Only named forms keep the AXLandmarkForm subrole.
      if (!_node->HasStringAttribute(ax::mojom::StringAttribute::kName)) {
        return nil;
      }
      break;
    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]) {
    // VoiceOver does not announce AXCustomContent for fieldsets, so we fall
    // back to exposing the ARIA description as AXHelp for them.
    if ([[self getStringAttribute:ax::mojom::StringAttribute::kHtmlTag]
            isEqualToString:@"fieldset"]) {
      return [self getStringAttribute:ax::mojom::StringAttribute::kDescription];
    }
    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;
  }

  // Must be kept in sync with BrowserAccessibilityCocoa::value.
  if (_node->GetData().IsRangeValueSupported()) {
    float floatValue;
    if (_node->GetFloatAttribute(ax::mojom::FloatAttribute::kValueForRange,
                                 &floatValue)) {
      return @(floatValue);
    }
  }

  return base::SysUTF16ToNSString(_node->GetValueForControl());
}

- (NSNumber*)AXEnabled {
  return @([self isAccessibilityEnabled]);
}

- (BOOL)isAccessibilityExpanded {
  // Keep logic consistent with `-[BrowserAccessibilityCocoa expanded]`
  if (![self instanceActive]) {
    return NO;
  }
  return _node->HasState(ax::mojom::State::kExpanded);
}

- (NSNumber*)AXFocused {
  return @([self isAccessibilityFocused]);
}

- (BOOL)isAccessibilityFocused {
  if (![self instanceActive]) {
    return NO;
  }

  return _node->GetDelegate()->GetFocus() == _node->GetNativeViewAccessible();
}

- (id)AXFocusableAncestor {
  if (![self instanceActive])
    return nil;

  ui::AXPlatformNodeBase* ancestor = _node;
  for (; ancestor; ancestor = ancestor->GetPlatformParent()) {
    // Do not cross document boundaries.
    if (ui::IsPlatformDocument(ancestor->GetRole()))
      return nil;
    if (ancestor->IsFocusable())
      break;
  }
  // The assignment to ancestor may be null.
  if (!ancestor)
    return nil;
  return ancestor->GetNativeViewAccessible().Get();
}

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

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

  int count = _node->GetChildCount();
  NSMutableArray* children = [NSMutableArray arrayWithCapacity:count];
  for (auto child_iterator_ptr = _node->GetDelegate()->ChildrenBegin();
       *child_iterator_ptr != *_node->GetDelegate()->ChildrenEnd();
       ++(*child_iterator_ptr)) {
    [children addObject:child_iterator_ptr->GetNativeViewAccessible().Get()];
  }
  return NSAccessibilityUnignoredChildren(children);
}

- (NSArray*)accessibilityChildrenInNavigationOrder {
  // We follow Webkit's implementation here.
  return [self accessibilityChildren];
}

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

- (id)AXTopLevelUIElement {
  return [self AXWindow];
}

- (NSValue*)AXPosition {
  return [NSValue valueWithPoint:self.boundsInScreen.origin];
}

- (NSValue*)AXSize {
  return [NSValue valueWithSize:self.boundsInScreen.size];
}

- (NSString*)AXTitle {
  return [self accessibilityTitle];
}

- (id)AXTitleUIElement {
  return [self accessibilityTitleUIElement];
}

- (NSString*)AXDescription {
  return [self accessibilityLabel];
}

// Misc attributes.

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

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

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

- (NSString*)AXMenuItemMarkChar {
  if (!ui::IsMenuItem(_node->GetRole()))
    return nil;

  const auto checkedState = static_cast<ax::mojom::CheckedState>(
      _node->GetIntAttribute(ax::mojom::IntAttribute::kCheckedState));
  if (checkedState == ax::mojom::CheckedState::kTrue) {
    return @"\u2713";  // "check mark"
  }

  return @"";
}

- (NSNumber*)AXARIAPosInSet {
  if (![self instanceActive])
    return nil;
  std::optional<int> posInSet = _node->GetPosInSet();
  if (!posInSet)
    return nil;
  return @(*posInSet);
}

- (NSNumber*)AXARIASetSize {
  if (![self instanceActive])
    return nil;
  std::optional<int> setSize = _node->GetSetSize();
  if (!setSize)
    return nil;
  return @(*setSize);
}

// Text-specific attributes.

// LINT.IfChange
- (NSString*)AXSelectedText {
  NSRange selectedTextRange = [[self AXSelectedTextRange] rangeValue];
  return [[self getAXValueAsString] substringWithRange:selectedTextRange];
}
// LINT.ThenChange(accessibilitySelectedText)

// LINT.IfChange
- (NSValue*)AXSelectedTextRange {
  int start = 0, end = 0;
  if (_node->IsAtomicTextField() &&
      _node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart, &start) &&
      _node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd, &end)) {
    // NSRange cannot represent the direction the text was selected in.
    return
        [NSValue valueWithRange:{static_cast<NSUInteger>(std::min(start, end)),
                                 static_cast<NSUInteger>(abs(end - start))}];
  }

  return [NSValue valueWithRange:NSMakeRange(0, 0)];
}
// LINT.ThenChange(accessibilitySelectedTextRange)

// LINT.IfChange
- (NSNumber*)AXNumberOfCharacters {
  return @([[self getAXValueAsString] length]);
}
// LINT.ThenChange(accessibilityNumberOfCharacters)

// LINT.IfChange
- (NSValue*)AXVisibleCharacterRange {
  return [NSValue valueWithRange:{0, [[self getAXValueAsString] length]}];
}
// LINT.ThenChange(accessibilityVisibleCharacterRange)

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

// Parameterized text-specific attributes.

- (id)AXRangeForLine:(id)parameter {
  NSNumber* lineNumber = ObjCCast<NSNumber>(parameter);
  if (!lineNumber) {
    return nil;
  }

  return [NSValue
      valueWithRange:[self accessibilityRangeForLine:[lineNumber intValue]]];
}

- (id)AXStringForRange:(id)parameter {
  if (std::optional<NSRange> range = ui::NSValueGetRange(parameter)) {
    return [self accessibilityStringForRange:range.value()];
  }

  return nil;
}

- (id)AXRangeForPosition:(id)parameter {
  NSValue* positionValue = ObjCCast<NSValue>(parameter);
  if (!positionValue) {
    return nil;
  }

  NSPoint point = [positionValue pointValue];
  return [NSValue valueWithRange:[self accessibilityRangeForPosition:point]];
}

- (id)AXRangeForIndex:(id)parameter {
  NSNumber* indexNumber = ObjCCast<NSNumber>(parameter);
  if (!indexNumber) {
    return nil;
  }

  NSInteger index = [indexNumber intValue];
  return [NSValue valueWithRange:[self accessibilityRangeForIndex:index]];
}

- (id)AXBoundsForRange:(id)parameter {
  // TODO(tapted): Provide an accessor on AXPlatformNodeDelegate to obtain this
  // from ui::TextInputClient::GetCompositionCharacterBounds().
  NOTIMPLEMENTED();
  return nil;
}

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

- (id)AXStyleRangeForIndex:(id)parameter {
  NSNumber* indexNumber = ObjCCast<NSNumber>(parameter);
  if (!indexNumber) {
    return nil;
  }
  return [NSValue
      valueWithRange:[self accessibilityStyleRangeForIndex:[indexNumber
                                                               intValue]]];
}

- (id)AXAttributedStringForRange:(id)parameter {
  NSValue* value = ObjCCast<NSValue>(parameter);
  if (!value) {
    return nil;
  }
  NSRange range = value.rangeValue;

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

  std::u16string textContent;
  if (ui::IsNameExposedInAXValueForRole(_node->GetRole())) {
    textContent = base::SysNSStringToUTF16([self getAXValueAsString]);
  } else {
    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() &&
      !ui::IsNameExposedInAXValueForRole(_node->GetRole())) {
    AXRange axRange(_node->GetDelegate()->CreateTextPositionAt(0),
                    _node->GetDelegate()->CreateTextPositionAt(
                        static_cast<int>(textContent.length())));
    [self addTextAnnotationsIn:&axRange to:attributedTextContent];
  }

  return [attributedTextContent attributedSubstringFromRange:range];
}

- (NSAttributedString*)AXAttributedStringForTextMarkerRange:(id)markerRange {
  AXRange axRange = ui::AXTextMarkerRangeToAXRange(markerRange);
  if (axRange.IsNull())
    return nil;

  NSString* text = base::SysUTF16ToNSString(axRange.GetText());
  if (text.length == 0) {
    return nil;
  }

  NSMutableAttributedString* attributedText =
      [[NSMutableAttributedString alloc] initWithString:text];
  // Currently, we only decorate the attributed string with misspelling
  // and annotation information.
  [self addTextAnnotationsIn:&axRange to:attributedText];
  return attributedText;
}

- (NSString*)ChromeAXNodeId {
  return [@(_node->GetNodeId()) stringValue];
}

- (NSString*)description {
  return [NSString stringWithFormat:@"%@ - %@ (%@)", [super description],
                                    [self accessibilityTitle], [self AXRole]];
}

//
// End of key-based attributes.
//

//
// NSAccessibility protocol.
// https://developer.apple.com/documentation/appkit/nsaccessibilityprotocol
//

// These methods appear to be the minimum needed to avoid AppKit refusing to
// handle the element or crashing internally. Most of the remaining old API
// methods (the ones from NSObject) are implemented in terms of the new
// NSAccessibility methods.
//
// TODO(crbug.com/41115917): Does this class need to implement the various
// accessibilityPerformFoo methods, or are the stub implementations from
// NSAccessibilityElement sufficient?

// NSAccessibility: Configuring Accessibility.
- (BOOL)isAccessibilityElement {
  if (!_node) {
    return NO;
  }
  DCHECK(_node->GetDelegate());
  DCHECK([self instanceActive]);

  // After ViewsAX lands, we should be able to add this DCHECK.
  // DCHECK(!_node->GetDelegate()->IsIgnored())
  //     << "Ignored nodes should be removed by PlatformGet*() methods:"
  //     << _node->GetDelegate()->ToString();

  if (_node->GetDelegate()->IsInvisibleOrIgnored()) {
    return NO;
  }

  if ([self internalRole] == ax::mojom::Role::kImage &&
      _node->GetData().GetNameFrom() ==
          ax::mojom::NameFrom::kAttributeExplicitlyEmpty) {
    return NO;
  }

  if ([self internalRole] == ax::mojom::Role::kLineBreak) {
    return NO;
  }

  return YES;
}

- (BOOL)isAccessibilityEnabled {
  if (!_node)
    return NO;

  // Native menus expose separators as disabled menu items. Chromium mirrors
  // this behavior.
  if (_node->GetRole() == ax::mojom::Role::kMenuItemSeparator) {
    return NO;
  }

  return _node->GetData().GetRestriction() != ax::mojom::Restriction::kDisabled;
}

- (NSRect)accessibilityFrame {
  return [self boundsInScreen];
}

- (NSString*)accessibilityHelp {
  return [self AXHelp];
}

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

  // macOS wants static text exposed in AXValue.
  if (ui::IsNameExposedInAXValueForRole([self internalRole])) {
    return @"";
  }

  // If we're exposing the title in TitleUIElement, don't also redundantly
  // expose it in accessibilityLabel.
  if ([self titleUIElement])
    return @"";

  // For web dialogs whose name comes from aria-labelledby
  // (NameFrom::kRelatedElement), additionally expose the name as
  // AXDescription. accessibilityTitle still publishes it as AXTitle, so
  // the dialog ends up with both attributes populated, matching Safari.
  // Without AXDescription, VoiceOver does not announce the AXTitle of
  // AXApplicationDialog or AXApplicationAlertDialog subroles, leaving
  // dialogs labelled via aria-labelledby silent on focus. See
  // https://issues.chromium.org/issues/41487406 and
  // https://github.com/w3c/core-aam/issues/213.
  ax::mojom::Role role = _node->GetRole();
  bool isWebDialogLabelledBy =
      (role == ax::mojom::Role::kDialog ||
       role == ax::mojom::Role::kAlertDialog) &&
      _node->IsWebContent() &&
      _node->GetNameFrom() == ax::mojom::NameFrom::kRelatedElement;

  if (![self isNameFromLabel] && !isWebDialogLabelledBy) {
    return @"";
  }

  std::string name = _node->GetName();

  if (!name.empty())
    return base::SysUTF8ToNSString(name);

  // Given an image where there's no other title, return the base part
  // of the filename as the description.
  if ([self isImage]) {
    std::string url;
    if (_node->GetStringAttribute(ax::mojom::StringAttribute::kUrl, &url)) {
      // Given a url like http://foo.com/bar/baz.png, just return the
      // base name, e.g., "baz.png".
      size_t leftIndex = url.rfind('/');
      std::string basename =
          leftIndex != std::string::npos ? url.substr(leftIndex) : url;
      return base::SysUTF8ToNSString(basename);
    }
  }

  return @"";
}

// LINT.IfChange(accessibilityLinkedUIElements)
- (NSArray*)accessibilityLinkedUIElements {
  if (![self instanceActive]) {
    return nil;
  }

  ui::AXPlatformNodeDelegate* delegate = [self nodeDelegate];
  if (!delegate) {
    return nil;
  }

  NSMutableArray* elements = [[NSMutableArray alloc] init];
  [elements
      addObjectsFromArray:[self uiElementsForAttribute:
                                    ax::mojom::IntListAttribute::kControlsIds]];
  [elements
      addObjectsFromArray:[self uiElementsForAttribute:
                                    ax::mojom::IntListAttribute::kFlowtoIds]];

  int targetId;
  if (delegate->GetIntAttribute(ax::mojom::IntAttribute::kInPageLinkTargetId,
                                &targetId)) {
    ui::AXPlatformNode* target = delegate->GetFromNodeID(targetId);
    if (target) {
      [elements addObject:target->GetNativeViewAccessible().Get()];
    }
  }

  [elements
      addObjectsFromArray:[self
                              uiElementsForAttribute:
                                  ax::mojom::IntListAttribute::kRadioGroupIds]];
  return elements;
}
// LINT.ThenChange(ui/accessibility/platform/browser_accessibility_cocoa.mm:accessibilityLinkedUIElements)

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

  if (ui::IsNameExposedInAXValueForRole(_node->GetRole())) {
    return @"";
  }

  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 (NSString* stringValue = ObjCCast<NSString>(value)) {
    data.value = base::SysNSStringToUTF8(stringValue);
  } else if (NSValue* valueValue = ObjCCast<NSValue>(value)) {
    // TODO(crbug.com/41115917): Is this case actually needed? The
    // NSObject accessibility implementation supported this, but can it actually
    // occur?
    NSRange range = valueValue.rangeValue;
    data.anchor_offset = range.location;
    data.focus_offset = NSMaxRange(range);
  }
  _node->GetDelegate()->AccessibilityPerformAction(data);
}

- (BOOL)isAccessibilitySelectorAllowed:(SEL)selector {
  TRACE_EVENT1(
      "accessibility", "AXPlatformNodeCocoa::isAccessibilitySelectorAllowed",
      "selector=", base::SysNSStringToUTF8(NSStringFromSelector(selector)));

  if (!_node) {
    return NO;
  }

  if (selector == @selector(setAccessibilityFocused:)) {
    return _node->IsFocusable();
  }

  if (selector == @selector(setAccessibilityValue:)) {
    switch (_node->GetRole()) {
      case ax::mojom::Role::kSlider:
        // When VoiceOver performs an increment/decrement action, it immediately
        // calls upon success of the action the selector setAccessibilityValue
        // on the slider that was just updated. The value passed to this
        // function is always equals to 5% of the slider's value range, so
        // actually setting that value to our slider would:
        //   1. render the increment/decrement action performed a moment before
        //      useless as it would override the modified value;
        //   2. make the slider value stuck in place, at 5% of its range.
        //
        // I haven't found much on the topic online, so the following is at best
        // a conjecture: I believe that VoiceOver "suggests" us to
        // increment/decrement the value by 5%. There might be a setting I'm not
        // aware of that allows the VO users to modify this value by a different
        // one, which would allow them to always increment/decrement sliders by
        // the same amount on all apps.
        //
        // However, in Chromium, we handle the increment and decrement actions
        // on the blink side and the step value is computed over there. That
        // way, the experience for changing the value of a slider by increments
        // is the same for all different inputs: whether it's the keyboard arrow
        // keys, an AT, etc.
        //
        // TL;DR: setAccessibilityValue, when called on sliders, is breaking our
        // increment and decrement AX actions, so don't allow it.
        return NO;
      case ax::mojom::Role::kTab:
        // Tabs use the radio button role on Mac, so they are selected by
        // calling setSelected on an individual tab, rather than by setting the
        // selected element on the tabstrip as a whole.
        return !_node->GetBoolAttribute(ax::mojom::BoolAttribute::kSelected);
      default:
        break;
    }
  }

  // Don't allow calling AX setters on disabled elements.
  // TODO(crbug.com/41301942): Once the underlying bug in
  // views::Textfield::SetSelectionRange() described in that bug is fixed,
  // remove the check here when the selector is setAccessibilitySelectedText*;
  // right now, this check serves to prevent accessibility clients from trying
  // to set the selection range, which won't work because of 692362.
  if (_node->GetDelegate() && _node->GetDelegate()->IsReadOnlyOrDisabled() &&
      IsAXSetter(selector)) {
    return NO;
  }

  NSString* selectorString = NSStringFromSelector(selector);
  if ([[self class] isMethodImplementedForNewAccessibilityAPI:selectorString] &&
      ![self supportsNewAccessibilityAPIMethod:selectorString]) {
    return NO;
  }

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

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

- (id)accessibilityParent {
  return [self AXParent];
}

// NSAccessibility: Assigning Roles.
- (BOOL)isAccessibilityRequired {
  TRACE_EVENT1("accessibility", "accessibilityRequired",
               "role=", ui::ToString([self internalRole]));

  if (![self instanceActive]) {
    return NO;
  }

  return _node->HasState(ax::mojom::State::kRequired);
}

- (NSAccessibilityRole)accessibilityRole {
  return [self AXRole];
}

- (NSAccessibilitySubrole)accessibilitySubrole {
  return [self AXSubrole];
}

- (NSString*)accessibilityRoleDescription {
  TRACE_EVENT1("accessibility", "accessibilityRoleDescription",
               "role=", ui::ToString([self internalRole]));
  if (![self instanceActive]) {
    return nil;
  }

  // Image annotations.
  if (_node->GetData().GetImageAnnotationStatus() ==
          ax::mojom::ImageAnnotationStatus::kEligibleForAnnotation ||
      _node->GetData().GetImageAnnotationStatus() ==
          ax::mojom::ImageAnnotationStatus::kSilentlyEligibleForAnnotation) {
    return base::SysUTF16ToNSString(
        _node->GetDelegate()->GetLocalizedRoleDescriptionForUnlabeledImage());
  }

  // ARIA role description.
  std::string roleDescription;
  if (_node->GetStringAttribute(ax::mojom::StringAttribute::kRoleDescription,
                                &roleDescription)) {
    return [base::SysUTF8ToNSString(_node->GetStringAttribute(
        ax::mojom::StringAttribute::kRoleDescription)) lowercaseString];
  }

  NSString* role = [self accessibilityRole];
  switch ([self internalRole]) {
    case ax::mojom::Role::kColorWell:            // Use platform's "color well"
    case ax::mojom::Role::kImage:                // Default: IDS_AX_ROLE_GRAPHIC
    case ax::mojom::Role::kInputTime:            // Use platform's "time field"
    case ax::mojom::Role::kMeter:        // Use platform's "level indicator"
    case ax::mojom::Role::kPopUpButton:  // Use platform's "popup button"
    case ax::mojom::Role::kTabList:      // Use platform's "tab group"
    case ax::mojom::Role::kTree:         // Use platform's "outline"
    case ax::mojom::Role::kTreeItem:     // Use platform's "outline row"
      break;
    case ax::mojom::Role::kHeader:  // Default: IDS_AX_ROLE_HEADER
      return l10n_util::GetNSString(IDS_AX_ROLE_BANNER);
    case ax::mojom::Role::kRootWebArea: {
      if ([role isEqualToString:NSAccessibilityWebAreaRole]) {
        return l10n_util::GetNSString(IDS_AX_ROLE_WEB_AREA);
      }
      // Preserve platform default of "group" in the case of the child
      // of a presentational <iframe> which has the internal role of
      // kRootWebArea.
      break;
    }
    default: {
      std::u16string result =
          _node->GetDelegate()->GetLocalizedStringForRoleDescription();
      if (!result.empty()) {
        return base::SysUTF16ToNSString(result);
      }
    }
  }

  return NSAccessibilityRoleDescription(role, [self accessibilitySubrole]);
}

// NSAccessibility: Configuring Table and Outline Views.
- (NSArray*)accessibilitySelectedRows {
  if (![self instanceActive]) {
    return nil;
  }

  NSArray* rows = [self accessibilityRows];
  // accessibilityRows returns an empty array unless instanceActive does,
  // not exist, so we do not need to check if rows is nil at this time.
  NSMutableArray* selectedRows = [NSMutableArray array];
  for (id row in rows) {
    if ([[row accessibilitySelected] boolValue]) {
      [selectedRows addObject:row];
    }
  }
  return selectedRows;
}

- (NSArray*)accessibilityColumnHeaderUIElements {
  if (![self instanceActive]) {
    return nil;
  }

  ui::AXPlatformNodeDelegate* delegate = _node->GetDelegate();

  NSMutableArray* ret = [NSMutableArray array];

  // If this is a table, return all column headers.
  ax::mojom::Role role = _node->GetRole();
  if (ui::IsTableLike(role)) {
    for (ui::AXNodeID id : delegate->GetColHeaderNodeIds()) {
      AXPlatformNodeCocoa* colheader = [self fromNodeID:id];
      if (colheader) {
        [ret addObject:colheader];
      }
    }
    return [ret count] ? ret : nil;
  }

  // Otherwise if this is a cell or a header cell, return the column headers for
  // it.
  if (!ui::IsCellOrTableHeader(role)) {
    return nil;
  }

  ui::AXPlatformNodeBase* table = _node->GetTable();
  if (!table) {
    return nil;
  }

  std::optional<int> column = delegate->GetTableCellColIndex();
  if (!column) {
    return nil;
  }

  ui::AXPlatformNodeDelegate* tableDelegate = table->GetDelegate();
  for (ui::AXNodeID id : tableDelegate->GetColHeaderNodeIds(*column)) {
    AXPlatformNodeCocoa* colheader = [self fromNodeID:id];
    if (colheader) {
      [ret addObject:colheader];
    }
  }
  return [ret count] ? ret : nil;
}

- (id)accessibilityHeader {
  // Keep logic consistent with `-[BrowserAccessibilityCocoa header]`
  if (![self instanceActive]) {
    return nil;
  }

  if (ui::IsTableLike(_node->GetRole())) {
    ui::AXPlatformNodeDelegate* delegate = _node->GetDelegate();
    // The table header container is a special node in the accessibility tree
    // only used on macOS. It has all of the table headers as its children, even
    // though those cells are also children of rows in the table. Internally
    // this is implemented using `AXTableInfo` and `indirect_child_ids` with the
    // result retrievable via `AXNode::GetExtraMacNodes()`.
    const std::vector<raw_ptr<ui::AXNode, VectorExperimental>>* nodes =
        delegate->node()->GetExtraMacNodes();
    if (nodes && !nodes->empty()) {
      ui::AXNode* lastChild = nodes->back();
      if (lastChild->GetRole() == ax::mojom::Role::kTableHeaderContainer) {
        // TODO(crbug.com/363275809): This works for `BrowserAccessibilityCocoa`
        // nodes but will otherwise fail with `-fromNodeID` returning nil. This
        // is due to the fact that `BrowserAccessibilityMac` ensures that the
        // internal "extra Mac nodes" are included in the platform accessibility
        // tree. See `BrowserAccessibilityMac::PlatformChildCount` and
        // `BrowserAccessibilityMac::PlatformGetChild` as examples.
        return [self fromNodeID:lastChild->id()];
      }
    }
    return nil;
  }

  int headerElementId = -1;
  if ([self internalRole] == ax::mojom::Role::kColumn) {
    _node->GetIntAttribute(ax::mojom::IntAttribute::kTableColumnHeaderId,
                           &headerElementId);
  } else if ([self internalRole] == ax::mojom::Role::kRow) {
    _node->GetIntAttribute(ax::mojom::IntAttribute::kTableRowHeaderId,
                           &headerElementId);
  }

  return headerElementId > 0 ? [self fromNodeID:headerElementId] : nil;
}

- (NSInteger)accessibilityColumnCount {
  if (![self instanceActive]) {
    return NSNotFound;
  }

  if (!ui::IsTableLike(_node->GetRole())) {
    return NSNotFound;
  }

  ui::AXPlatformNodeDelegate* delegate = _node->GetDelegate();
  std::optional<int> count = delegate->GetTableColCount();
  if (count.has_value()) {
    return *count;
  }

  return -1;
}

- (NSInteger)accessibilityRowCount {
  if (![self instanceActive]) {
    return NSNotFound;
  }

  if (!ui::IsTableLike(_node->GetRole())) {
    return NSNotFound;
  }

  ui::AXPlatformNodeDelegate* delegate = _node->GetDelegate();
  std::optional<int> count = delegate->GetTableRowCount();
  if (count.has_value()) {
    return *count;
  }

  return -1;
}

// LINT.IfChange(accessibilityRowHeaderUIElements)
- (NSArray*)accessibilityRowHeaderUIElements {
  if (![self instanceActive]) {
    return nil;
  }

  ax::mojom::Role role = [self internalRole];
  bool isCellOrTableHeader = ui::IsCellOrTableHeader(role);
  bool isTableLike = ui::IsTableLike(role);
  if (!isTableLike && !isCellOrTableHeader) {
    return nil;
  }

  ui::AXPlatformNodeDelegate* delegate = [self nodeDelegate];
  gfx::NativeViewAccessible table = delegate->GetTableAncestor();
  if (!table) {
    return nil;
  }

  ui::AXPlatformNode* tableNode =
      ui::AXPlatformNode::FromNativeViewAccessible(table);
  if (!tableNode) {
    return nil;
  }

  ui::AXPlatformNodeDelegate* tableDelegate = tableNode->GetDelegate();

  // A table with no row headers.
  if (isTableLike && !tableDelegate->GetTableRowCount().has_value()) {
    return nil;
  }

  NSMutableArray* rowHeaders = [[NSMutableArray alloc] init];

  if (isTableLike) {
    // Return the table's row headers.
    std::set<int32_t> headerIds;

    int numberOfRows = tableDelegate->GetTableRowCount().value();

    // Rows can have more than one row header cell. Also, we apparently need
    // to guard against duplicate row header ids. Storing in a set dedups.
    for (int i = 0; i < numberOfRows; i++) {
      std::vector<int32_t> rowHeaderIds = tableDelegate->GetRowHeaderNodeIds(i);
      for (int32_t rowHeaderId : rowHeaderIds) {
        headerIds.insert(rowHeaderId);
      }
    }

    for (int32_t headerId : headerIds) {
      ui::AXPlatformNode* cellNode = tableDelegate->GetFromNodeID(headerId);
      if (cellNode) {
        [rowHeaders addObject:cellNode->GetNativeViewAccessible().Get()];
      }
    }
  } else {
    // Otherwise this is a cell, return the row headers for this cell.
    for (int32_t nodeId : delegate->GetRowHeaderNodeIds()) {
      ui::AXPlatformNode* cellNode = delegate->GetFromNodeID(nodeId);
      if (cellNode) {
        [rowHeaders addObject:cellNode->GetNativeViewAccessible().Get()];
      }
    }
  }

  return [rowHeaders count] ? rowHeaders : nil;
}
// LINT.ThenChange(ui/accessibility/platform/browser_accessibility_cocoa.mm:accessibilityRowHeaderUIElements)

// LINT.IfChange(accessibilityColumns)
- (NSArray*)accessibilityColumns {
  if (![self instanceActive]) {
    return nil;
  }

  NSMutableArray* columns = [[NSMutableArray alloc] init];
  for (AXPlatformNodeCocoa* child in [self accessibilityChildren]) {
    if ([[child accessibilityRole] isEqualToString:NSAccessibilityColumnRole]) {
      [columns addObject:child];
    }
  }
  return columns;
}
// LINT.ThenChange(ui/accessibility/platform/browser_accessibility_cocoa.mm:accessibilityColumns)

- (NSArray*)accessibilityRows {
  if (![self instanceActive]) {
    return nil;
  }

  ui::AXPlatformNodeDelegate* delegate = [self nodeDelegate];
  if (!delegate) {
    return nil;
  }

  NSMutableArray* rows = [[NSMutableArray alloc] init];

  ax::mojom::Role role = [self internalRole];

  std::vector<int32_t> nodeIds;
  if (role == ax::mojom::Role::kTree) {
    [self getTreeItemDescendantNodeIds:&nodeIds];
  } else if (ui::IsTableLike(role)) {
    nodeIds = delegate->GetRowNodeIds();
  } else if (role == ax::mojom::Role::kColumn) {
    // Rows attribute for a column is the list of all the elements in that
    // column at each row.
    nodeIds = delegate->GetIntListAttribute(
        ax::mojom::IntListAttribute::kIndirectChildIds);
  }

  for (int32_t nodeId : nodeIds) {
    ui::AXPlatformNode* rowNode = delegate->GetFromNodeID(nodeId);
    if (rowNode) {
      [rows addObject:rowNode->GetNativeViewAccessible().Get()];
    }
  }

  return rows;
}

- (NSAccessibilitySortDirection)accessibilitySortDirection {
  // Keep logic consistent with `-[BrowserAccessibilityCocoa sortDirection]`
  if (![self instanceActive]) {
    return NSAccessibilitySortDirectionUnknown;
  }

  // If this object should not support `accessibilitySortDirection`, treat
  // the sort direction as unknown regardless of what's in the `AXNodeData`.
  if ([self internalRole] != ax::mojom::Role::kRowHeader &&
      [self internalRole] != ax::mojom::Role::kColumnHeader) {
    return NSAccessibilitySortDirectionUnknown;
  }

  // The Core-AAM states that `aria-sort=none` is "not mapped".
  int sortDirection;
  if (!_node->GetIntAttribute(ax::mojom::IntAttribute::kSortDirection,
                              &sortDirection) ||
      static_cast<ax::mojom::SortDirection>(sortDirection) ==
          ax::mojom::SortDirection::kUnsorted) {
    return NSAccessibilitySortDirectionUnknown;
  }

  switch (static_cast<ax::mojom::SortDirection>(sortDirection)) {
    case ax::mojom::SortDirection::kAscending:
      return NSAccessibilitySortDirectionAscending;
    case ax::mojom::SortDirection::kDescending:
      return NSAccessibilitySortDirectionDescending;
    case ax::mojom::SortDirection::kOther:
      return NSAccessibilitySortDirectionUnknown;
    default:
      NOTREACHED();
  }
}

- (id)accessibilityDisclosedByRow {
  if (![self instanceActive]) {
    return nil;
  }

  // The row that contains this row.
  // It should be the same as the first parent that is a treeitem.
  return nil;
}

- (id)accessibilityDisclosedRows {
  if (![self instanceActive]) {
    return nil;
  }

  // The rows that are considered inside this row.
  return nil;
}

- (NSInteger)accessibilityDisclosureLevel {
  if (![self instanceActive]) {
    return 0;
  }

  ax::mojom::Role role = [self internalRole];
  if (role == ax::mojom::Role::kRow || role == ax::mojom::Role::kTreeItem ||
      role == ax::mojom::Role::kHeading) {
    int level =
        _node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel);
    // Mac disclosureLevel is 0-based, but web levels are 1-based.
    if (level > 0) {
      level--;
    }
    return level;
  }
  return 0;
}

- (BOOL)isAccessibilityDisclosed {
  if (![self instanceActive]) {
    return NO;
  }

  if ([self internalRole] == ax::mojom::Role::kTreeItem) {
    return _node->HasState(ax::mojom::State::kExpanded);
  }
  return NO;
}

// NSAccessibility: Setting the Focus.
- (void)setAccessibilityFocused:(BOOL)isFocused {
  if (!_node)
    return;

  ui::AXActionData data;
  data.action =
      isFocused ? ax::mojom::Action::kFocus : ax::mojom::Action::kBlur;
  _node->GetDelegate()->AccessibilityPerformAction(data);
}

- (NSNumber*)treeItemRowIndex {
  // TODO(crbug.com/363275809): `-[BrowserAccessibilityCocoa treeItemRowIndex]`
  // and related logic such as `-[BrowswerAccessibilityCocoa findRowIndex]`
  // should be moved here unless doing so has some impact on view tree items.
  return nil;
}

- (NSInteger)accessibilityIndex {
  // Keep logic consistent with `-[BrowserAccessibilityCocoa index]`
  if (![self instanceActive]) {
    return NSNotFound;
  }

  if ([self internalRole] == ax::mojom::Role::kTreeItem) {
    return [[self treeItemRowIndex] integerValue];
  } else if ([self internalRole] == ax::mojom::Role::kColumn) {
    DCHECK(_node);
    std::optional<int> col_index =
        _node->GetDelegate()->node()->GetTableColColIndex();
    if (col_index.has_value()) {
      return *col_index;
    }
  } else if ([self internalRole] == ax::mojom::Role::kRow) {
    DCHECK(_node);
    std::optional<int> row_index =
        _node->GetDelegate()->node()->GetTableRowRowIndex();
    if (row_index.has_value()) {
      return *row_index;
    }
  }

  return NSNotFound;
}

// NSAccessibility: Configuring Text Elements.

// These are all "required" methods, although in practice the ones that are left
// NOTIMPLEMENTED() seem to not be called anywhere (and were NOTIMPLEMENTED in
// the old API as well).

// LINT.IfChange
- (NSInteger)accessibilityInsertionPointLineNumber {
  if (![self instanceActive]) {
    return NSNotFound;
  }

  // TODO(crbug.com/363275809): According to the comment in the old API code,
  // "multiline is not supported on views." If that is no longer the case, we
  // need an implementation here. Also the old API code in `AXPlatformNodeCocoa`
  // doesn't do any of the work done in by `BrowserAccessibilityCocoa`.
  return 0;
}
// LINT.ThenChange(AXInsertionPointLineNumber)

// LINT.IfChange
- (NSInteger)accessibilityNumberOfCharacters {
  if (![self instanceActive]) {
    return 0;
  }

  return [[self getAXValueAsString] length];
}
// LINT.ThenChange(AXNumberOfCharacters)

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

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

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

// LINT.IfChange
- (NSString*)accessibilitySelectedText {
  if (![self instanceActive]) {
    return nil;
  }

  NSRange selectedTextRange = [self accessibilitySelectedTextRange];
  return [[self getAXValueAsString] substringWithRange:selectedTextRange];
}
// LINT.ThenChange(AXSelectedText)

- (void)setAccessibilitySelectedText:(NSString*)text {
  if (!_node) {
    return;
  }

  ui::AXActionData data;
  data.action = ax::mojom::Action::kReplaceSelectedText;
  data.value = base::SysNSStringToUTF8(text);

  _node->GetDelegate()->AccessibilityPerformAction(data);
}

// LINT.IfChange
- (NSRange)accessibilitySelectedTextRange {
  if (![self instanceActive]) {
    return NSMakeRange(0, 0);
  }

  int start = 0, end = 0;
  if (_node->IsAtomicTextField() &&
      _node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart, &start) &&
      _node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd, &end)) {
    // NSRange cannot represent the direction the text was selected in.
    return NSMakeRange(static_cast<NSUInteger>(std::min(start, end)),
                       static_cast<NSUInteger>(abs(end - start)));
  }

  return NSMakeRange(0, 0);
}
// LINT.ThenChange(AXSelectedTextRange)

- (void)setAccessibilitySelectedTextRange:(NSRange)range {
  if (!_node) {
    return;
  }

  ui::AXActionData data;
  data.action = ax::mojom::Action::kSetSelection;
  data.anchor_offset = range.location;
  data.anchor_node_id = _node->GetData().id;
  data.focus_offset = NSMaxRange(range);
  data.focus_node_id = _node->GetData().id;
  _node->GetDelegate()->AccessibilityPerformAction(data);
}

- (NSArray*)accessibilitySelectedTextRanges {
  if (!_node)
    return nil;

  return @[ [self AXSelectedTextRange] ];
}

// LINT.IfChange
- (NSRange)accessibilityVisibleCharacterRange {
  if (![self instanceActive]) {
    return NSMakeRange(0, 0);
  }

  return NSMakeRange(0, [[self getAXValueAsString] length]);
}
// LINT.ThenChange(AXVisibleCharacterRange)

- (NSString*)accessibilityStringForRange:(NSRange)range {
  if (![self instanceActive]) {
    return nil;
  }
  return [[self getAXValueAsString] substringWithRange:range];
}

- (NSInteger)accessibilityLineForIndex:(NSInteger)index {
  // TODO: multiline is not supported on views.
  return 0;
}

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

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

- (id)AXLineForIndex:(id)parameter {
  NSNumber* lineNumber = ObjCCast<NSNumber>(parameter);
  if (!lineNumber) {
    return nil;
  }
  return @([self accessibilityLineForIndex:[lineNumber intValue]]);
}

- (NSRange)accessibilityRangeForIndex:(NSInteger)index {
  NOTIMPLEMENTED();
  return NSMakeRange(0, 0);
}

- (NSRange)accessibilityStyleRangeForIndex:(NSInteger)index {
  if (![self instanceActive]) {
    return NSMakeRange(0, 0);
  }

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

- (NSRange)accessibilityRangeForLine:(NSInteger)line {
  if (![self instanceActive]) {
    return NSMakeRange(0, 0);
  }
  return NSMakeRange(0, [[self getAXValueAsString] length]);
}

- (NSRange)accessibilityRangeForPosition:(NSPoint)point {
  // TODO(tapted): Hit-test [parameter pointValue] and return an NSRange.
  NOTIMPLEMENTED();
  return NSMakeRange(0, 0);
}

// NSAccessibility: setting content and values.
- (NSNumber*)accessibilitySelected {
  if (![self instanceActive])
    return nil;

  return @(_node->GetBoolAttribute(ax::mojom::BoolAttribute::kSelected));
}

- (NSURL*)accessibilityURL {
  TRACE_EVENT1("accessibility", "accessibilityURL",
               "role=", ui::ToString([self internalRole]));
  if (![self instanceActive])
    return nil;

  std::string url;
  if ([[self accessibilityRole] isEqualToString:NSAccessibilityWebAreaRole]) {
    url = _node->GetDelegate()->GetTreeData().url;
  } else {
    url = _node->GetStringAttribute(ax::mojom::StringAttribute::kUrl);
  }

  if (url.empty())
    return nil;

  return [NSURL URLWithString:(base::SysUTF8ToNSString(url))];
}

// LINT.IfChange(accessibilityTabs)
- (id)accessibilityTabs {
  if (![self instanceActive]) {
    return nil;
  }

  NSMutableArray* tabSubtree = [[NSMutableArray alloc] init];
  if ([self internalRole] == ax::mojom::Role::kTab) {
    [tabSubtree addObject:self];
  }

  for (AXPlatformNodeCocoa* child in [self accessibilityChildren]) {
    NSArray* tabChildren = [child accessibilityTabs];
    if ([tabChildren count] > 0) {
      [tabSubtree addObjectsFromArray:tabChildren];
    }
  }
  return tabSubtree;
}
// LINT.ThenChange(ui/accessibility/platform/browser_accessibility_cocoa.mm:accessibilityTabs)

- (id)accessibilitySplitters {
  // Chromium windows do not have NSSplitViews or anything similar.
  return nil;
}

- (id)accessibilityToolbarButton {
  // Chromium windows do not have a toolbar button.
  return nil;
}

- (id)accessibilityScrollBar:(ax::mojom::State)state {
  if (![self instanceActive]) {
    return nil;
  }

  // TODO(crbug.com/363275809): For this to work for `ScrollView`, `ScrollView`
  // should add `kControlsIds` on its horizontal and vertical scrollbars.
  std::vector<ui::AXPlatformNode*> targets =
      _node->GetDelegate()->GetSourceNodesForReverseRelations(
          ax::mojom::IntListAttribute::kControlsIds);
  for (auto target : targets) {
    if (auto* delegate = target->GetDelegate()) {
      if (delegate->GetRole() == ax::mojom::Role::kScrollBar &&
          delegate->HasState(state)) {
        return target->GetNativeViewAccessible().Get();
      }
    }
  }

  return nil;
}

- (id)accessibilityHorizontalScrollBar {
  return [self accessibilityScrollBar:ax::mojom::State::kHorizontal];
}

- (id)accessibilityVerticalScrollBar {
  return [self accessibilityScrollBar:ax::mojom::State::kVertical];
}

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

  return [self titleUIElement];
}

// LINT.IfChange(accessibilityCellForColumn)
- (id)accessibilityCellForColumn:(NSInteger)column row:(NSInteger)row {
  if (![self instanceActive] || ![self nodeDelegate]) {
    return nil;
  }

  if (!ui::IsTableLike([self internalRole])) {
    return nil;
  }

  std::optional<int32_t> cellId = [self nodeDelegate]->GetCellId(row, column);
  if (!cellId) {
    return nil;
  }

  ui::AXPlatformNode* cell = [self nodeDelegate]->GetFromNodeID(*cellId);
  if (!cell) {
    return nil;
  }

  return cell->GetNativeViewAccessible().Get();
}
// LINT.ThenChange(ui/accessibility/platform/browser_accessibility_cocoa.mm:accessibilityCellForColumn)

- (NSRange)accessibilityColumnIndexRange {
  if (![self instanceActive] || ![self nodeDelegate]) {
    return NSMakeRange(0, 0);
  }

  std::optional<int> column = [self nodeDelegate]->GetTableCellColIndex();
  std::optional<int> columnSpan = [self nodeDelegate]->GetTableCellColSpan();
  if (column && columnSpan) {
    return NSMakeRange(*column, *columnSpan);
  }
  return NSMakeRange(0, 0);
}

- (NSRange)accessibilityRowIndexRange {
  if (![self instanceActive] || ![self nodeDelegate]) {
    return NSMakeRange(0, 0);
  }

  std::optional<int> row = [self nodeDelegate]->GetTableCellRowIndex();
  std::optional<int> rowSpan = [self nodeDelegate]->GetTableCellRowSpan();
  if (row && rowSpan) {
    return NSMakeRange(*row, *rowSpan);
  }
  return NSMakeRange(0, 0);
}

// LINT.IfChange(accessibilityVisibleColumns)
- (NSArray*)accessibilityVisibleColumns {
  if (![self instanceActive]) {
    return nil;
  }

  NSMutableArray* columns = [[NSMutableArray alloc] init];
  for (AXPlatformNodeCocoa* child in [self accessibilityChildren]) {
    if ([[child accessibilityRole] isEqualToString:NSAccessibilityColumnRole]) {
      [columns addObject:child];
    }
  }
  return columns;
}
// LINT.ThenChange(ui/accessibility/platform/browser_accessibility_cocoa.mm:accessibilityVisibleColumns)

// LINT.IfChange(accessibilityVisibleCells)
- (NSArray*)accessibilityVisibleCells {
  if (![self instanceActive]) {
    return nil;
  }

  ui::AXPlatformNodeDelegate* table = [self nodeDelegate];
  if (!table) {
    return nil;
  }

  NSMutableArray* cells = [[NSMutableArray alloc] init];
  for (int32_t id : table->GetTableUniqueCellIds()) {
    ui::AXPlatformNode* cell = table->GetFromNodeID(id);
    if (cell) {
      [cells addObject:cell->GetNativeViewAccessible().Get()];
    }
  }
  return cells;
}
// LINT.ThenChange(ui/accessibility/platform/browser_accessibility_cocoa.mm:accessibilityVisibleCells)

// LINT.IfChange(accessibilityVisibleRows)
- (NSArray*)accessibilityVisibleRows {
  return [self accessibilityRows];
}
// LINT.ThenChange(ui/accessibility/platform/browser_accessibility_cocoa.mm:accessibilityVisibleRows)

//
// End of NSAccessibility protocol.
//

//
// AXCustomContentProvider
// https://developer.apple.com/documentation/accessibility/axcustomcontentprovider/3600104-accessibilitycustomcontent
//

- (NSArray*)accessibilityCustomContent {
  if (![self instanceActive]) {
    return nil;
  }

  // Only descriptions originating from ARIA are returned as custom content.
  // (Non-ARIA descriptions are returned as AXHelp.)
  if (![self descriptionIsFromAriaDescription]) {
    return nil;
  }

  // VoiceOver does not announce AXCustomContent for fieldsets, so we do not
  // expose it here.
  if ([[self getStringAttribute:ax::mojom::StringAttribute::kHtmlTag]
          isEqualToString:@"fieldset"]) {
    return nil;
  }

  NSString* description =
      [self getStringAttribute:ax::mojom::StringAttribute::kDescription];
  AXCustomContent* contentItem =
      [AXCustomContent customContentWithLabel:@"description" value:description];
  // A custom content importance of high causes it to be spoken
  // automatically, rather than "More content available".
  contentItem.importance = AXCustomContentImportanceHigh;
  return @[ contentItem ];
}

// MathML attributes.
// TODO(crbug.com/40673555): The MathML aam considers only in-flow children.
// TODO(crbug.com/40673555): When/if it is needed to expose this for other a11y
// APIs, then some of the logic below should probably be moved to the
// platform-independent classes.

- (id)AXMathFractionNumerator {
  if (![self instanceActive] ||
      _node->GetRole() != ax::mojom::Role::kMathMLFraction) {
    return nil;
  }
  NSArray* children = [self accessibilityChildren];
  if ([children count] >= 1)
    return children[0];
  return nil;
}

- (id)AXMathFractionDenominator {
  if (![self instanceActive] ||
      _node->GetRole() != ax::mojom::Role::kMathMLFraction) {
    return nil;
  }
  NSArray* children = [self accessibilityChildren];
  if ([children count] >= 2)
    return children[1];
  return nil;
}

- (id)AXMathRootRadicand {
  if (![self instanceActive] ||
      !(_node->GetRole() == ax::mojom::Role::kMathMLRoot ||
        _node->GetRole() == ax::mojom::Role::kMathMLSquareRoot)) {
    return nil;
  }
  NSArray* children = [self accessibilityChildren];
  if (_node->GetRole() == ax::mojom::Role::kMathMLRoot) {
    if ([children count] >= 1)
      return [NSArray arrayWithObjects:children[0], nil];
    return nil;
  }
  return children;
}

- (id)AXMathRootIndex {
  if (![self instanceActive] ||
      _node->GetRole() != ax::mojom::Role::kMathMLRoot) {
    return nil;
  }
  NSArray* children = [self accessibilityChildren];
  if ([children count] >= 2)
    return children[1];
  return nil;
}

- (id)AXMathBase {
  if (![self instanceActive] ||
      !(_node->GetRole() == ax::mojom::Role::kMathMLSub ||
        _node->GetRole() == ax::mojom::Role::kMathMLSup ||
        _node->GetRole() == ax::mojom::Role::kMathMLSubSup ||
        _node->GetRole() == ax::mojom::Role::kMathMLUnder ||
        _node->GetRole() == ax::mojom::Role::kMathMLOver ||
        _node->GetRole() == ax::mojom::Role::kMathMLUnderOver ||
        _node->GetRole() == ax::mojom::Role::kMathMLMultiscripts)) {
    return nil;
  }
  NSArray* children = [self accessibilityChildren];
  if ([children count] >= 1)
    return children[0];
  return nil;
}

- (id)AXMathUnder {
  if (![self instanceActive] ||
      !(_node->GetRole() == ax::mojom::Role::kMathMLUnder ||
        _node->GetRole() == ax::mojom::Role::kMathMLUnderOver)) {
    return nil;
  }
  NSArray* children = [self accessibilityChildren];
  if ([children count] >= 2)
    return children[1];
  return nil;
}

- (id)AXMathOver {
  if (![self instanceActive] ||
      !(_node->GetRole() == ax::mojom::Role::kMathMLOver ||
        _node->GetRole() == ax::mojom::Role::kMathMLUnderOver)) {
    return nil;
  }
  NSArray* children = [self accessibilityChildren];
  if (_node->GetRole() == ax::mojom::Role::kMathMLOver &&
      [children count] >= 2) {
    return children[1];
  }
  if (_node->GetRole() == ax::mojom::Role::kMathMLUnderOver &&
      [children count] >= 3) {
    return children[2];
  }
  return nil;
}

- (id)AXMathSubscript {
  if (![self instanceActive] ||
      !(_node->GetRole() == ax::mojom::Role::kMathMLSub ||
        _node->GetRole() == ax::mojom::Role::kMathMLSubSup)) {
    return nil;
  }
  NSArray* children = [self accessibilityChildren];
  if ([children count] >= 2)
    return children[1];
  return nil;
}

- (id)AXMathSuperscript {
  if (![self instanceActive] ||
      !(_node->GetRole() == ax::mojom::Role::kMathMLSup ||
        _node->GetRole() == ax::mojom::Role::kMathMLSubSup)) {
    return nil;
  }
  NSArray* children = [self accessibilityChildren];
  if (_node->GetRole() == ax::mojom::Role::kMathMLSup &&
      [children count] >= 2) {
    return children[1];
  }
  if (_node->GetRole() == ax::mojom::Role::kMathMLSubSup &&
      [children count] >= 3) {
    return children[2];
  }
  return nil;
}

namespace {

NSDictionary* CreateMathSubSupScriptsPair(AXPlatformNodeCocoa* subscript,
                                          AXPlatformNodeCocoa* superscript) {
  NSMutableDictionary* dictionary = [NSMutableDictionary dictionary];
  if (subscript) {
    dictionary[CFToNSPtrCast(kAXMathSubscriptAttribute)] = subscript;
  }
  if (superscript) {
    dictionary[CFToNSPtrCast(kAXMathSuperscriptAttribute)] = superscript;
  }
  return dictionary;
}

}  // namespace

- (NSArray*)AXMathPostscripts {
  if (![self instanceActive] ||
      _node->GetRole() != ax::mojom::Role::kMathMLMultiscripts)
    return nil;
  NSMutableArray* ret = [NSMutableArray array];
  bool foundBaseElement = false;
  AXPlatformNodeCocoa* subscript = nullptr;
  for (AXPlatformNodeCocoa* child in [self accessibilityChildren]) {
    if ([child internalRole] == ax::mojom::Role::kMathMLPrescriptDelimiter)
      break;
    if (!foundBaseElement) {
      foundBaseElement = true;
      continue;
    }
    if (!subscript) {
      subscript = child;
      continue;
    }
    AXPlatformNodeCocoa* superscript = child;
    [ret addObject:CreateMathSubSupScriptsPair(subscript, superscript)];
    subscript = nullptr;
  }
  return [ret count] ? ret : nil;
}

- (NSArray*)AXMathPrescripts {
  if (![self instanceActive] ||
      _node->GetRole() != ax::mojom::Role::kMathMLMultiscripts)
    return nil;
  NSMutableArray* ret = [NSMutableArray array];
  bool foundPrescriptDelimiter = false;
  AXPlatformNodeCocoa* subscript = nullptr;
  for (AXPlatformNodeCocoa* child in [self accessibilityChildren]) {
    if (!foundPrescriptDelimiter) {
      foundPrescriptDelimiter =
          ([child internalRole] == ax::mojom::Role::kMathMLPrescriptDelimiter);
      continue;
    }
    if (!subscript) {
      subscript = child;
      continue;
    }
    AXPlatformNodeCocoa* superscript = child;
    [ret addObject:CreateMathSubSupScriptsPair(subscript, superscript)];
    subscript = nullptr;
  }
  return [ret count] ? ret : nil;
}

@end
