// Copyright 2012 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/browser_accessibility_cocoa.h"

#include <ApplicationServices/ApplicationServices.h>
#include <Availability.h>
#import <Foundation/Foundation.h>
#include <execinfo.h>
#include <stddef.h>
#include <stdint.h>

#include <algorithm>
#include <iterator>
#include <map>
#include <memory>
#include <optional>
#include <utility>

#include "base/apple/bridging.h"
#include "base/apple/foundation_util.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/timer/elapsed_timer.h"
#include "base/trace_event/trace_event.h"
#include "third_party/abseil-cpp/absl/cleanup/cleanup.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/accessibility/ax_common.h"
#include "ui/accessibility/ax_enum_util.h"
#include "ui/accessibility/ax_range.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/accessibility/ax_selection.h"
#include "ui/accessibility/platform/ax_platform_node.h"
#import "ui/accessibility/platform/ax_platform_node_mac.h"
#include "ui/accessibility/platform/ax_platform_tree_manager_delegate.h"
#import "ui/accessibility/platform/ax_private_attributes_mac.h"
#import "ui/accessibility/platform/ax_private_webkit_constants_mac.h"
#include "ui/accessibility/platform/ax_utils_mac.h"
#include "ui/accessibility/platform/browser_accessibility_cocoa_test_helpers.h"
#include "ui/accessibility/platform/browser_accessibility_mac.h"
#include "ui/accessibility/platform/browser_accessibility_manager.h"
#include "ui/accessibility/platform/browser_accessibility_manager_mac.h"
#include "ui/accessibility/platform/one_shot_accessibility_tree_search.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/mac/coordinate_conversion.h"
#include "ui/strings/grit/ax_strings.h"

using AXPosition = ui::AXPlatformNodeDelegate::AXPosition;
using AXRange = ui::AXPlatformNodeDelegate::AXRange;
using StringAttribute = ax::mojom::StringAttribute;
using base::apple::CFToNSPtrCast;
using base::apple::ObjCCast;
using base::apple::ObjCCastStrict;
using ui::AccessibilityMatchPredicate;
using ui::AXActionHandlerRegistry;
using ui::AXNodeData;
using ui::AXPlatformTreeManagerDelegate;
using ui::AXPositionToAXTextMarker;
using ui::AXRangeToAXTextMarkerRange;
using ui::AXTextMarkerRangeToAXRange;
using ui::AXTextMarkerToAXPosition;
using ui::BrowserAccessibility;
using ui::BrowserAccessibilityManager;
using ui::BrowserAccessibilityManagerMac;
using ui::OneShotAccessibilityTreeSearch;

@interface AXPlatformNodeCocoa (PrivateMethods)
- (NSString*)getAXValueAsString;
@end

static_assert(
    std::is_trivially_copyable<BrowserAccessibility::SerializedPosition>::value,
    "BrowserAccessibility::SerializedPosition must be POD because it's used to "
    "back an AXTextMarker");

namespace {
// Test-only attribute name used by accessibility dump tests to read
// aria-actions custom-action names across the AXUIElementCopyAttributeValue
// marshaling boundary (NSAccessibilityCustomAction is not marshalable).
//
// The attribute is gated by a process-wide runtime flag. Production code
// never sets the flag; only the dump-test base class
// (DumpAccessibilityTestBase::SetUp) flips it on Mac. With the flag
// unset:
//   - the attribute is NOT advertised by -accessibilityAttributeNames, so
//     assistive technologies cannot discover it by enumeration, and
//   - the -customActionNamesForTesting selector short-circuits to nil, so
//     direct -accessibilityAttributeValue: queries by same-process callers
//     also receive nil.
//
// Avoids compile-time build-flag gating per the //PRESUBMIT.py banned-
// pattern check (gating the attribute plumbing on a shipping-build macro
// would leave it effectively untested per Chromium policy).
NSString* const kAXCustomActionNamesForTestingAttribute =
    @"AXCustomActionNamesForTesting";

// Set to true by ui::EnableAXCustomActionNamesForTestingProjection().
// Gates both:
//  - advertisement of the test attribute in
//    -internalAccessibilityAttributeNames, and
//  - the body of the -customActionNamesForTesting selector.
// Without the opt-in, direct -accessibilityAttributeValue: queries by
// same-process callers receive nil.
bool g_enable_ax_custom_action_names_for_testing_projection = false;

// A mapping from an accessibility attribute to its method name.
NSDictionary* gAttributeToMethodNameMap = nil;

// VoiceOver uses -1 to mean "no limit" for AXResultsLimit.
const int kAXResultsLimitNoLimit = -1;

AXRange CreateAXRange(const BrowserAccessibility& start_object,
                      int start_offset,
                      ax::mojom::TextAffinity start_affinity,
                      const BrowserAccessibility& end_object,
                      int end_offset,
                      ax::mojom::TextAffinity end_affinity) {
  AXPosition anchor =
      start_object.CreatePositionAt(start_offset, start_affinity);
  AXPosition focus = end_object.CreatePositionAt(end_offset, end_affinity);
  // |AXRange| takes ownership of its anchor and focus.
  return AXRange(std::move(anchor), std::move(focus));
}

AXRange GetSelectedRange(BrowserAccessibility& owner) {
  const BrowserAccessibilityManager* manager = owner.manager();
  if (!manager)
    return {};

  const ui::AXSelection unignored_selection =
      manager->ax_tree()->GetUnignoredSelection();
  int32_t anchor_id = unignored_selection.anchor_object_id;
  const BrowserAccessibility* anchor_object = manager->GetFromID(anchor_id);
  if (!anchor_object)
    return {};

  int32_t focus_id = unignored_selection.focus_object_id;
  const BrowserAccessibility* focus_object = manager->GetFromID(focus_id);
  if (!focus_object)
    return {};

  // |anchor_offset| and / or |focus_offset| refer to a character offset if
  // |anchor_object| / |focus_object| are text-only objects or atomic text
  // fields. Otherwise, they should be treated as child indices. An atomic text
  // field does not expose its internal implementation to assistive software,
  // appearing as a single leaf node in the accessibility tree. It includes
  // <input>, <textarea> and Views-based text fields.
  int anchor_offset = unignored_selection.anchor_offset;
  int focus_offset = unignored_selection.focus_offset;
  DCHECK_GE(anchor_offset, 0);
  DCHECK_GE(focus_offset, 0);

  ax::mojom::TextAffinity anchor_affinity = unignored_selection.anchor_affinity;
  ax::mojom::TextAffinity focus_affinity = unignored_selection.focus_affinity;

  return CreateAXRange(*anchor_object, anchor_offset, anchor_affinity,
                       *focus_object, focus_offset, focus_affinity);
}

NSString* GetTextForTextMarkerRange(id marker_range) {
  AXRange range = AXTextMarkerRangeToAXRange(marker_range);
  if (range.IsNull())
    return nil;
  return base::SysUTF16ToNSString(range.GetText());
}

// GetState checks the bitmask used in AXNodeData to check
// if the given state was set on the accessibility object.
bool GetState(BrowserAccessibility* accessibility, ax::mojom::State state) {
  return accessibility->HasState(state);
}

// Given a search key provided to AXUIElementCountForSearchPredicate or
// AXUIElementsForSearchPredicate, return a predicate that can be added
// to OneShotAccessibilityTreeSearch.
AccessibilityMatchPredicate PredicateForSearchKey(NSString* searchKey) {
  if ([searchKey isEqualToString:NSAccessibilityAnyTypeSearchKey]) {
    return [](BrowserAccessibility* start, BrowserAccessibility* current) {
      return true;
    };
  } else if ([searchKey
                 isEqualToString:NSAccessibilityBlockquoteSameLevelSearchKey]) {
    // TODO(dmazzoni): implement the "same level" part.
    return ui::AccessibilityBlockquotePredicate;
  } else if ([searchKey isEqualToString:NSAccessibilityBlockquoteSearchKey]) {
    return ui::AccessibilityBlockquotePredicate;
  } else if ([searchKey isEqualToString:NSAccessibilityBoldFontSearchKey]) {
    return ui::AccessibilityTextStyleBoldPredicate;
  } else if ([searchKey isEqualToString:NSAccessibilityButtonSearchKey]) {
    return ui::AccessibilityButtonPredicate;
  } else if ([searchKey isEqualToString:NSAccessibilityCheckBoxSearchKey]) {
    return ui::AccessibilityCheckboxPredicate;
  } else if ([searchKey isEqualToString:NSAccessibilityControlSearchKey]) {
    return ui::AccessibilityControlPredicate;
  } else if ([searchKey
                 isEqualToString:NSAccessibilityDifferentTypeSearchKey]) {
    return [](BrowserAccessibility* start, BrowserAccessibility* current) {
      return current->GetRole() != start->GetRole();
    };
  } else if ([searchKey isEqualToString:NSAccessibilityFontChangeSearchKey]) {
    // TODO(dmazzoni): implement this.
    return nullptr;
  } else if ([searchKey
                 isEqualToString:NSAccessibilityFontColorChangeSearchKey]) {
    // TODO(dmazzoni): implement this.
    return nullptr;
  } else if ([searchKey isEqualToString:NSAccessibilityFrameSearchKey]) {
    return ui::AccessibilityFramePredicate;
  } else if ([searchKey isEqualToString:NSAccessibilityGraphicSearchKey]) {
    return ui::AccessibilityGraphicPredicate;
  } else if ([searchKey
                 isEqualToString:NSAccessibilityHeadingLevel1SearchKey]) {
    return ui::AccessibilityH1Predicate;
  } else if ([searchKey
                 isEqualToString:NSAccessibilityHeadingLevel2SearchKey]) {
    return ui::AccessibilityH2Predicate;
  } else if ([searchKey
                 isEqualToString:NSAccessibilityHeadingLevel3SearchKey]) {
    return ui::AccessibilityH3Predicate;
  } else if ([searchKey
                 isEqualToString:NSAccessibilityHeadingLevel4SearchKey]) {
    return ui::AccessibilityH4Predicate;
  } else if ([searchKey
                 isEqualToString:NSAccessibilityHeadingLevel5SearchKey]) {
    return ui::AccessibilityH5Predicate;
  } else if ([searchKey
                 isEqualToString:NSAccessibilityHeadingLevel6SearchKey]) {
    return ui::AccessibilityH6Predicate;
  } else if ([searchKey
                 isEqualToString:NSAccessibilityHeadingSameLevelSearchKey]) {
    return ui::AccessibilityHeadingSameLevelPredicate;
  } else if ([searchKey isEqualToString:NSAccessibilityHeadingSearchKey]) {
    return ui::AccessibilityHeadingPredicate;
  } else if ([searchKey isEqualToString:@"AXHighlightedSearchKey"]) {
    // TODO(dmazzoni): implement this.
    return nullptr;
  } else if ([searchKey isEqualToString:NSAccessibilityItalicFontSearchKey]) {
    return ui::AccessibilityTextStyleItalicPredicate;
  } else if ([searchKey isEqualToString:NSAccessibilityLandmarkSearchKey]) {
    return ui::AccessibilityLandmarkPredicate;
  } else if ([searchKey isEqualToString:NSAccessibilityLinkSearchKey]) {
    return ui::AccessibilityLinkPredicate;
  } else if ([searchKey isEqualToString:NSAccessibilityListSearchKey]) {
    return ui::AccessibilityListPredicate;
  } else if ([searchKey isEqualToString:NSAccessibilityLiveRegionSearchKey]) {
    return ui::AccessibilityLiveRegionPredicate;
  } else if ([searchKey
                 isEqualToString:NSAccessibilityMisspelledWordSearchKey]) {
    // TODO(dmazzoni): implement this.
    return nullptr;
  } else if ([searchKey isEqualToString:NSAccessibilityOutlineSearchKey]) {
    return ui::AccessibilityTreePredicate;
  } else if ([searchKey isEqualToString:NSAccessibilityPlainTextSearchKey]) {
    // TODO(dmazzoni): implement this.
    return nullptr;
  } else if ([searchKey isEqualToString:NSAccessibilityRadioGroupSearchKey]) {
    return ui::AccessibilityRadioGroupPredicate;
  } else if ([searchKey isEqualToString:NSAccessibilitySameTypeSearchKey]) {
    return [](BrowserAccessibility* start, BrowserAccessibility* current) {
      return current->GetRole() == start->GetRole();
    };
  } else if ([searchKey isEqualToString:NSAccessibilityStaticTextSearchKey]) {
    return [](BrowserAccessibility* start, BrowserAccessibility* current) {
      return current->IsText();
    };
  } else if ([searchKey isEqualToString:NSAccessibilityStyleChangeSearchKey]) {
    // TODO(dmazzoni): implement this.
    return nullptr;
  } else if ([searchKey
                 isEqualToString:NSAccessibilityTableSameLevelSearchKey]) {
    // TODO(dmazzoni): implement the "same level" part.
    return ui::AccessibilityTablePredicate;
  } else if ([searchKey isEqualToString:NSAccessibilityTableSearchKey]) {
    return ui::AccessibilityTablePredicate;
  } else if ([searchKey isEqualToString:NSAccessibilityTextFieldSearchKey]) {
    return ui::AccessibilityTextfieldPredicate;
  } else if ([searchKey isEqualToString:NSAccessibilityUnderlineSearchKey]) {
    return ui::AccessibilityTextStyleUnderlinePredicate;
  } else if ([searchKey
                 isEqualToString:NSAccessibilityUnvisitedLinkSearchKey]) {
    return ui::AccessibilityUnvisitedLinkPredicate;
  } else if ([searchKey isEqualToString:NSAccessibilityVisitedLinkSearchKey]) {
    return ui::AccessibilityVisitedLinkPredicate;
  }

  return nullptr;
}

// Initialize a OneShotAccessibilityTreeSearch object given the parameters
// passed to AXUIElementCountForSearchPredicate or
// AXUIElementsForSearchPredicate. Return true on success.
bool InitializeAccessibilityTreeSearch(OneShotAccessibilityTreeSearch* search,
                                       id parameter) {
  NSDictionary* dictionary = ObjCCast<NSDictionary>(parameter);
  if (!dictionary) {
    return false;
  }

  BrowserAccessibilityCocoa* startElementParameter =
      ObjCCast<BrowserAccessibilityCocoa>(
          dictionary[NSAccessibilitySearchCurrentElementKey]);
  if (startElementParameter) {
    search->SetStartNode([startElementParameter owner]);
  }

  bool immediateDescendantsOnly = false;
  if (NSNumber* immediateDescendantsOnlyParameter =
          ObjCCast<NSNumber>(dictionary[@"AXImmediateDescendantsOnly"])) {
    immediateDescendantsOnly = immediateDescendantsOnlyParameter.boolValue;
  }

  bool onscreenOnly = false;
  // AXVisibleOnly actually means onscreen objects only -- nothing scrolled off.
  if (NSNumber* onscreenOnlyParameter =
          ObjCCast<NSNumber>(dictionary[@"AXVisibleOnly"])) {
    onscreenOnly = onscreenOnlyParameter.boolValue;
  }

  ui::OneShotAccessibilityTreeSearch::Direction direction =
      ui::OneShotAccessibilityTreeSearch::FORWARDS;
  if (NSString* directionParameter =
          ObjCCast<NSString>(dictionary[NSAccessibilitySearchDirectionKey])) {
    if ([directionParameter
            isEqualToString:NSAccessibilitySearchDirectionNext]) {
      direction = ui::OneShotAccessibilityTreeSearch::FORWARDS;
    } else if ([directionParameter
                   isEqualToString:NSAccessibilitySearchDirectionPrevious]) {
      direction = ui::OneShotAccessibilityTreeSearch::BACKWARDS;
    }
  }

  int resultsLimit = kAXResultsLimitNoLimit;
  if (NSNumber* resultsLimitParameter = ObjCCast<NSNumber>(
          dictionary[NSAccessibilitySearchResultsLimitKey])) {
    resultsLimit = resultsLimitParameter.intValue;
  }

  std::string searchText;
  if (NSString* searchTextParameter =
          ObjCCast<NSString>(dictionary[NSAccessibilitySearchTextKey])) {
    searchText = base::SysNSStringToUTF8(searchTextParameter);
  }

  search->SetDirection(direction);
  search->SetImmediateDescendantsOnly(immediateDescendantsOnly);
  search->SetOnscreenOnly(onscreenOnly);
  search->SetSearchText(searchText);

  // Mac uses resultsLimit == -1 for unlimited, that that's
  // the default for OneShotAccessibilityTreeSearch already.
  // Only set the results limit if it's nonnegative.
  if (resultsLimit >= 0) {
    search->SetResultLimit(resultsLimit);
  }

  id searchKey = [dictionary objectForKey:NSAccessibilitySearchIdentifiersKey];
  if (NSString* searchKeyString = ObjCCast<NSString>(searchKey)) {
    AccessibilityMatchPredicate predicate =
        PredicateForSearchKey(searchKeyString);
    if (predicate) {
      search->AddPredicate(predicate);
    }
  } else if (NSArray* searchKeyArray = ObjCCast<NSArray>(searchKey)) {
    for (id searchItem in searchKeyArray) {
      if (NSString* searchItemString = ObjCCast<NSString>(searchItem)) {
        AccessibilityMatchPredicate predicate =
            PredicateForSearchKey(searchItemString);
        if (predicate) {
          search->AddPredicate(predicate);
        }
      }
    }
  }

  return true;
}

// Tri-state value stored in BrowserAccessibilityCocoa's `emptyGroupCache`
// bitfield (2 bits). Keeps the predicate's per-node result memoized across
// repeated -subrole queries from VoiceOver's hit-test and linear-navigation
// passes; cleared by -childrenChanged (structural) and
// -invalidateEmptyGroupCacheUpwards (attribute) along the parent chain.
//
// DO NOT add new enumerators without updating the static_assert below and
// BrowserAccessibilityCocoaBitfields::emptyGroupCache width.
enum EmptyGroupCache : uint8_t {
  kEmptyGroupCacheUnknown = 0,
  kEmptyGroupCacheEmpty = 1,
  kEmptyGroupCacheNonEmpty = 2,
};
static_assert(kEmptyGroupCacheNonEmpty < (1u << 2),
              "EmptyGroupCache enumerators must fit in the 2-bit "
              "emptyGroupCache field in BrowserAccessibilityCocoaBitfields.");

// 1-byte bitfield bundle (1+2+5). Used as the @implementation ivar so layout
// is defined once; static_assert below locks the size.
struct BrowserAccessibilityCocoaBitfields {
  uint8_t gettingChildren : 1;
  uint8_t emptyGroupCache : 2;
  uint8_t reserved : 5;
};
static_assert(sizeof(BrowserAccessibilityCocoaBitfields) == 1,
              "BrowserAccessibilityCocoaBitfields must pack into one byte.");
}  // namespace

namespace ui {

AXTextEdit::AXTextEdit() = default;
AXTextEdit::AXTextEdit(std::u16string inserted_text,
                       std::u16string deleted_text,
                       id edit_text_marker)
    : inserted_text(inserted_text),
      deleted_text(deleted_text),
      edit_text_marker(edit_text_marker) {}
AXTextEdit::AXTextEdit(const AXTextEdit& other) = default;
AXTextEdit::~AXTextEdit() = default;

}  // namespace ui

// Per-node half of WebKit's `AXCoreObject::isEmptyGroup()` parity (the group
// role and subtree-walk halves live in -subrole / -isEmptyGroupSubtree):
// https://github.com/WebKit/WebKit/blob/98a71680bf36199e257bd8e195b4205c3fcd5fe9/Source/WebCore/accessibility/cocoa/AXCoreObjectCocoa.mm#L474
//
// WebKit evaluates emptiness on an already-pruned tree; Blink keeps the
// layout-only wrappers, so child-count accessors can't tell "has child nodes"
// from "has announceable content". We close that gap by having
// -isEmptyGroupSubtree walk every descendant, with this returning whether a
// single node is content-bearing (WebKit's "unignored child" /
// "renderWidgetChildren" notion). kChildTreeId covers all embedded-content
// hosts (iframe / plugin / PDF / OOPIF), matching WebKit's isRemoteFrame()
// early-out.
//
// This is the per-DESCENDANT test. The group whose subrole is being computed
// does NOT run it on itself (WebKit's isEmptyGroup() ignores the group's own
// name etc.); see -isEmptyGroupSubtree.
bool ui::HasNonEmptyGroupSemantics(const ui::AXNodeData& data) {
  // `aria-label=""` is intentionally NOT a signal: per accname-1.2 it produces
  // an empty accessible name, and WebKit consults no name signal at all here.
  if (!data.GetStringAttribute(ax::mojom::StringAttribute::kName).empty()) {
    return true;
  }
  if (!data.GetStringAttribute(ax::mojom::StringAttribute::kValue).empty()) {
    return true;
  }
  if (!data.GetStringAttribute(ax::mojom::StringAttribute::kDescription)
           .empty()) {
    return true;
  }
  // Embedded content (iframe, plugin, PDF) hangs off kChildTreeId. Such a host
  // is announceable even when its local subtree looks empty in this process.
  if (data.HasStringAttribute(ax::mojom::StringAttribute::kChildTreeId)) {
    return true;
  }
  // Range roles expose value via kValueForRange, not the string kValue
  // attribute checked above. This is the meaningful signal for non-focusable
  // range roles (meter, progress); focusable ones (slider, scrollbar,
  // spinbutton) are also caught by the kFocusable check below.
  if (data.IsRangeValueSupported()) {
    return true;
  }
  // IsClickable() already folds in kDefaultActionVerb and the role, so this one
  // call covers both the click-handler and role-driven clickable axes.
  if (data.IsClickable() || data.HasState(ax::mojom::State::kFocusable)) {
    return true;
  }
  return false;
}

namespace ui {
void EnableAXCustomActionNamesForTestingProjection() {
  g_enable_ax_custom_action_names_for_testing_projection = true;
}

bool IsAXCustomActionNamesForTestingProjectionEnabled() {
  return g_enable_ax_custom_action_names_for_testing_projection;
}
}  // namespace ui

@interface BrowserAccessibilityCocoa ()
- (NSArray<NSString*>*)customActionNamesForTesting;
@end

@interface BrowserAccessibilityCocoa (Private)
- (BOOL)resolveEmptyGroupSubtree;
- (BOOL)resolveChildrenEmptyGroupSubtrees:(NSArray*)children;
@end

@implementation BrowserAccessibilityCocoa {
  // Dangling pointer https://crbug.com/40928052.
  raw_ptr<ui::BrowserAccessibility, DanglingUntriaged> _owner;

  // The global unique ID of the `_owner`. Cached because this value might be
  // needed for comparisons/hashing after the owner value is reset.
  ui::AXPlatformNodeId _uniqueId;

  // An array of children of this object. Cached to avoid re-computing.
  NSMutableArray* __strong _children;

  // 1-byte bitfield bundle. Main thread only (AppKit / NSAccessibility).
  BrowserAccessibilityCocoaBitfields _bitfields;

  // Stores the previous value of an edit field.
  std::u16string _oldValue;
}

+ (void)initialize {
  gAttributeToMethodNameMap = @{
    NSAccessibilityColumnsAttribute : @"columns",
    NSAccessibilityColumnIndexRangeAttribute : @"columnIndexRange",
    NSAccessibilityContentsAttribute : @"contents",
    kAXCustomActionNamesForTestingAttribute : @"customActionNamesForTesting",
    NSAccessibilityDisclosingAttribute : @"disclosing",
    NSAccessibilityDisclosedByRowAttribute : @"disclosedByRow",
    NSAccessibilityDisclosureLevelAttribute : @"disclosureLevel",
    NSAccessibilityDisclosedRowsAttribute : @"disclosedRows",
    NSAccessibilityEnabledAttribute : @"enabled",
    CFToNSPtrCast(kAXEndTextMarkerAttribute) : @"endTextMarker",
    NSAccessibilityExpandedAttribute : @"expanded",
    NSAccessibilityHeaderAttribute : @"header",
    NSAccessibilityIndexAttribute : @"index",
    NSAccessibilityLanguageAttribute : @"language",
    NSAccessibilityLinkedUIElementsAttribute : @"linkedUIElements",
    NSAccessibilityMaxValueAttribute : @"maxValue",
    NSAccessibilityMinValueAttribute : @"minValue",
    NSAccessibilityOrientationAttribute : @"orientation",
    NSAccessibilityPositionAttribute : @"position",
    NSAccessibilityRoleAttribute : @"role",
    NSAccessibilityRowHeaderUIElementsAttribute : @"rowHeaders",
    NSAccessibilityRowIndexRangeAttribute : @"rowIndexRange",
    NSAccessibilityRowsAttribute : @"accessibilityRows",
    // TODO(aboxhall): expose
    // NSAccessibilityServesAsTitleForUIElementsAttribute
    CFToNSPtrCast(kAXStartTextMarkerAttribute) : @"startTextMarker",
    NSAccessibilitySelectedChildrenAttribute : @"selectedChildren",
    CFToNSPtrCast(kAXSelectedTextMarkerRangeAttribute) :
        @"selectedTextMarkerRange",
    NSAccessibilitySortDirectionAttribute : @"sortDirection",
    NSAccessibilitySubroleAttribute : @"subrole",
    NSAccessibilityTabsAttribute : @"tabs",
    NSAccessibilityTopLevelUIElementAttribute : @"window",
    NSAccessibilityValueAttribute : @"value",
    CFToNSPtrCast(kAXValueAutofillAvailableAttribute) :
        @"valueAutofillAvailable",
    // Not currently supported by Chrome -- information not stored:
    // NSAccessibilityValueAutofilledAttribute: @"valueAutofilled",
    // Not currently supported by Chrome -- mismatch of types supported:
    // NSAccessibilityValueAutofillTypeAttribute: @"valueAutofillType",
    NSAccessibilityValueDescriptionAttribute : @"valueDescription",
    NSAccessibilityVisibleCellsAttribute : @"visibleCells",
    NSAccessibilityVisibleChildrenAttribute : @"visibleChildren",
    NSAccessibilityVisibleColumnsAttribute : @"visibleColumns",
    NSAccessibilityVisibleRowsAttribute : @"visibleRows",
    NSAccessibilityWindowAttribute : @"window",
  };
}

- (instancetype)initWithObject:(BrowserAccessibility*)accessibility
              withPlatformNode:(ui::AXPlatformNodeMac*)platform_node {
  if ((self = [super initWithNode:platform_node])) {
    _owner = accessibility;
    _uniqueId = static_cast<ui::AXPlatformNodeDelegate*>(_owner)->GetUniqueId();
    _bitfields.gettingChildren = 0;
    _bitfields.emptyGroupCache = kEmptyGroupCacheUnknown;
  }
  return self;
}

- (void)detachAndNotifyDestroyed:(BOOL)shouldNotify {
  if (!_owner)
    return;

  _owner = nullptr;
  [super detachAndNotifyDestroyed:shouldNotify];
}

// Returns an array of BrowserAccessibilityCocoa objects, representing the
// accessibility children of this object.
- (NSArray*)accessibilityChildren {
  SCOPED_UMA_HISTOGRAM_TIMER_MICROS(
      "Accessibility.Performance.BrowserAccessibilityCocoa::"
      "childrenChanged");
  base::UmaHistogramBoolean("Accessibility.Performance."
                            "BrowserAccessibilityCocoa::needsToUpdateChildren",
                            !!_children);

  if (![self instanceActive])
    return nil;

  // Check to see if any of the Cocoa wrappers refer to an invalid backing
  // AXPlatformNode. If so, then we need to re-create the _children Cocoa
  // wrappers.
  BrowserAccessibility* browserAccessibility =
      static_cast<BrowserAccessibility*>([self nodeDelegate]);
  const std::vector<int32_t>& indirectChildIds = _owner->GetIntListAttribute(
      ax::mojom::IntListAttribute::kIndirectChildIds);

  if (_children) {
    size_t child_count = [_children count];
    if ((browserAccessibility->PlatformChildCount() +
         indirectChildIds.size()) != child_count) {
      // Number of children have changed.
      // TODO(crbug.com/425758499): investigate why this occurs; once ready,
      // CHECK above condition along with experiment.
      _children = nil;
      child_count = 0;
    }

    for (size_t child_index = 0; child_index < child_count; child_index++) {
      BrowserAccessibilityCocoa* child = _children[child_index];
      BrowserAccessibility* browserAccessibilityChild =
          static_cast<BrowserAccessibility*>([child nodeDelegate]);
      if (![child instanceActive] || !browserAccessibilityChild ||
          browserAccessibilityChild->PlatformGetParent() !=
              [self nodeDelegate]) {
        // Child unexpectedly refers to a deleted browser accessibility or a
        // reparented node.
        // TODO(crbug.com/425758499): investigate why this occurs; once ready,
        // CHECK above condition along with experiment.
        _children = nil;
        break;
      }
    }
  }

  if (!_children) {
    // `gettingChildren` lives in a bitfield; taking its address is ill-formed,
    // so use absl::Cleanup instead of base::AutoReset.
    _bitfields.gettingChildren = 1;
    absl::Cleanup resetGettingChildren = [self] {
      self->_bitfields.gettingChildren = 0;
    };
    // PlatformChildCount adds extra mac nodes if the node requires them.
    uint32_t childCount = _owner->PlatformChildCount();
    _children = [[NSMutableArray alloc] initWithCapacity:childCount];
    for (auto it = _owner->PlatformChildrenBegin();
         it != _owner->PlatformChildrenEnd(); ++it) {
      AXPlatformNodeCocoa* cocoa_child = ObjCCastStrict<AXPlatformNodeCocoa>(
          it->GetNativeViewAccessible().Get());
      if (![cocoa_child instanceActive]) {
        // TODO(crbug.com/425758499): change to CHECK once root cause addressed.
        DCHECK(false) << "Tried to add destroyed child, parent = "
                      << _owner->ToString();
        continue;
      }
      if (![cocoa_child nodeDelegate]) {
        // TODO(crbug.com/425758499): change to CHECK once root cause addressed.
        DCHECK(false) << "No delegate for child, parent = "
                      << _owner->ToString();
        continue;
      }
      [_children addObject:cocoa_child];
    }

    // Also, add indirect children (if any).
    for (ui::AXNodeID childId : indirectChildIds) {
      BrowserAccessibility* child = _owner->manager()->GetFromID(childId);
      if (!child) {
        // This only became necessary as a result of https://crbug.com/41440696.
        // It should be a DCHECK in the future.
        DCHECK(false) << "Tried to add null indirect child, parent = "
                      << _owner->ToString();
        continue;
      }
      AXPlatformNodeCocoa* cocoa_child = ObjCCastStrict<AXPlatformNodeCocoa>(
          child->GetNativeViewAccessible().Get());
      if (![cocoa_child instanceActive]) {
        // TODO(crbug.com/425758499): change to CHECK once root cause addressed.
        DCHECK(false) << "Tried to add destroyed indirect child, parent = "
                      << _owner->ToString();
        continue;
      }
      if (![cocoa_child nodeDelegate]) {
        // TODO(crbug.com/425758499): change to CHECK once root cause addressed.
        DCHECK(false) << "No delegate for indirect child, parent = "
                      << _owner->ToString();
        continue;
      }
      [_children addObject:cocoa_child];
    }
  }
  return NSAccessibilityUnignoredChildren(_children);
}

- (void)childrenChanged {
  // This function may be called in the middle of -accessibilityChildren if
  // this node adds extra mac nodes while its children are being requested. If
  if (![self instanceActive] || _bitfields.gettingChildren) {
    return;
  }
  _children = nil;
  // Structural change: the AXEmptyGroup result for this node depends on its
  // descendants, so drop the memoized verdict here and walk to the root.
  _bitfields.emptyGroupCache = kEmptyGroupCacheUnknown;
  BrowserAccessibility* parent = _owner->PlatformGetParent();
  if (parent) {
    BrowserAccessibilityCocoa* parentCocoa =
        ObjCCastStrict<BrowserAccessibilityCocoa>(
            parent->GetNativeViewAccessible().Get());
    [parentCocoa childrenChanged];
  }
}

- (void)invalidateEmptyGroupCacheUpwards {
  // Own-data signal changed on a descendant: drop cache here and walk to the
  // root. Siblings keep their caches (O(depth), not O(tree size)).
  if (![self instanceActive]) {
    return;
  }
  _bitfields.emptyGroupCache = kEmptyGroupCacheUnknown;
  BrowserAccessibility* parent = _owner->PlatformGetParent();
  if (parent) {
    BrowserAccessibilityCocoa* parentCocoa =
        ObjCCastStrict<BrowserAccessibilityCocoa>(
            parent->GetNativeViewAccessible().Get());
    [parentCocoa invalidateEmptyGroupCacheUpwards];
  }
}

// AXEmptyGroup root predicate. Largely WebKit `isEmptyGroup()` parity: the
// group does NOT count its own name/description (a label on empty content is a
// dead-end; this is why a named-but-childless group still flattens). It is
// empty unless it has an announceable descendant or carries its OWN importance:
//   - an embedded child tree (kChildTreeId == WebKit renderWidgetChildren),
//   - it is focusable (a tab stop; skipping it in linear nav while it stays a
//     tab stop is inconsistent), or
//   - it is a live region (alert / status / log), which must not be hidden from
//     linear navigation.
// Descendants use the full per-node predicate (-resolveEmptyGroupSubtree),
// which is how named / text leaves stay reachable.
- (BOOL)isEmptyGroupSubtree {
  base::ElapsedTimer timer;
  absl::Cleanup record_latency = [&] {
    base::UmaHistogramMicrosecondsTimes(
        "Accessibility.AXEmptyGroupSubrolePredicate.Latency", timer.Elapsed());
  };
  if (!_owner) {
    return YES;
  }
  // Own-node importance: never an empty wrapper regardless of descendants.
  // Checked before the cache fast-path because that cache is keyed on
  // -resolveEmptyGroupSubtree, which does not consult the live-region role.
  const ui::AXNodeData& data = _owner->GetData();
  if (data.HasState(ax::mojom::State::kFocusable) ||
      data.HasStringAttribute(ax::mojom::StringAttribute::kChildTreeId) ||
      ui::IsLiveRegion(data.role)) {
    return NO;
  }
  // Fast path: a cached descendant "empty" implies root-empty too (the root
  // additionally ignores the node's own name/description). "non-empty" is not
  // conclusive here -- it may be only the node's own name -- so fall through.
  if (_bitfields.emptyGroupCache == kEmptyGroupCacheEmpty) {
    return YES;
  }
  // Empty iff no child subtree carries announceable content.
  NSArray* children = [self accessibilityChildren];
  if (!children) {
    // nil only for an inactive wrapper; can't evaluate, so don't claim empty.
    return NO;
  }
  return [self resolveChildrenEmptyGroupSubtrees:children];
}

// Descendant verdict: returns whether this node's subtree carries no
// announceable content AND memoizes the result in _bitfields.emptyGroupCache
// (hence the verb name -- this mutates cache state, unlike the read-only root
// query -isEmptyGroupSubtree).
- (BOOL)resolveEmptyGroupSubtree {
  if (!_owner) {
    return YES;
  }
  if (_bitfields.emptyGroupCache == kEmptyGroupCacheEmpty) {
    return YES;
  }
  if (_bitfields.emptyGroupCache == kEmptyGroupCacheNonEmpty) {
    return NO;
  }

  // Unlike the root, a descendant DOES count its own announceable signals: that
  // is how a named / text leaf keeps itself (and its ancestors) reachable.
  if (ui::HasNonEmptyGroupSemantics(_owner->GetData())) {
    _bitfields.emptyGroupCache = kEmptyGroupCacheNonEmpty;
    return NO;
  }

  // Use the same child list as VoiceOver and dump tests
  // (-accessibilityChildren), not PlatformGetChild alone. Table header
  // containers and similar Mac-only wrappers expose cells via
  // indirect_child_ids with zero platform children.
  NSArray* children = [self accessibilityChildren];
  if (!children) {
    // -accessibilityChildren only returns nil for an inactive wrapper; a live
    // group with no children yields an empty array, not nil. We can't evaluate
    // the subtree, so report non-empty (avoid hiding content) and do NOT cache
    // the verdict, so a later active query re-evaluates.
    return NO;
  }

  BOOL empty = [self resolveChildrenEmptyGroupSubtrees:children];
  _bitfields.emptyGroupCache =
      empty ? kEmptyGroupCacheEmpty : kEmptyGroupCacheNonEmpty;
  return empty;
}

// Child-walk shared by -isEmptyGroupSubtree (root) and
// -resolveEmptyGroupSubtree (descendant). Resolves (and memoizes) each child's
// subtree and returns YES iff every child resolves to an empty group.
// `children` must be non-nil. A child that is not a BrowserAccessibilityCocoa
// can't be evaluated and is treated as non-empty -- never hide content behind
// AXEmptyGroup.
- (BOOL)resolveChildrenEmptyGroupSubtrees:(NSArray*)children {
  for (id childObj in children) {
    BrowserAccessibilityCocoa* childCocoa =
        ObjCCast<BrowserAccessibilityCocoa>(childObj);
    if (!childCocoa) {
      return NO;
    }
    if (![childCocoa resolveEmptyGroupSubtree]) {
      return NO;
    }
  }
  return YES;
}

- (NSUInteger)emptyGroupCacheValueForTesting {
  return _bitfields.emptyGroupCache;
}

- (NSValue*)columnIndexRange {
  // Note: keep in sync with accessibilityColumnIndexRange.
  if (![self instanceActive])
    return nil;

  std::optional<int> column = _owner->node()->GetTableCellColIndex();
  std::optional<int> colspan = _owner->node()->GetTableCellColSpan();
  if (column && colspan)
    return [NSValue valueWithRange:NSMakeRange(*column, *colspan)];
  return nil;
}

// LINT.IfChange(accessibilityColumns)
- (NSArray*)columns {
  if (![self instanceActive])
    return nil;
  NSMutableArray* ret = [[NSMutableArray alloc] init];
  for (AXPlatformNodeCocoa* child in [self accessibilityChildren]) {
    if ([[child accessibilityRole] isEqualToString:NSAccessibilityColumnRole])
      [ret addObject:child];
  }
  return ret;
}
// LINT.ThenChange(ui/accessibility/platform/ax_platform_node_cocoa.mm:accessibilityColumns)

- (BrowserAccessibility*)containingTable {
  BrowserAccessibility* table = _owner;
  while (table && !ui::IsTableLike(table->GetRole())) {
    table = table->PlatformGetParent();
  }
  return table;
}

- (NSNumber*)disclosing {
  return @([self isAccessibilityDisclosed]);
}

- (id)disclosedByRow {
  return [self accessibilityDisclosedByRow];
}

- (NSNumber*)disclosureLevel {
  return [NSNumber numberWithInteger:[self accessibilityDisclosureLevel]];
}

- (id)disclosedRows {
  return [self accessibilityDisclosedRows];
}

- (NSNumber*)enabled {
  if (![self instanceActive])
    return nil;
  return @(_owner->GetData().GetRestriction() !=
           ax::mojom::Restriction::kDisabled);
}

// Returns a text marker that points to the last character in the document that
// can be selected with VoiceOver.
- (id)endTextMarker {
  if (![self instanceActive])
    return nil;
  AXPosition position = _owner->CreateTextPositionAt(0);
  return AXPositionToAXTextMarker(position->CreatePositionAtEndOfContent());
}

- (NSNumber*)expanded {
  // Keep logic consistent with `-[AXPlatformNodeCocoa isAccessibilityExpanded]`
  if (![self instanceActive])
    return nil;
  return @(GetState(_owner, ax::mojom::State::kExpanded));
}

- (void)setAccessibilityFocused:(BOOL)flag {
  BrowserAccessibilityManager* manager = _owner->manager();
  if (flag) {
    manager->SetFocus(*_owner);
  }
}

- (id)header {
  // Keep logic consistent with `-[AXPlatformNodeCocoa accessibilityHeader]`
  if (![self instanceActive])
    return nil;
  int headerElementId = -1;
  if (ui::IsTableLike(_owner->GetRole())) {
    // The table header container is always the last child of the table,
    // if it exists. 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.
    uint32_t childCount = _owner->PlatformChildCount();
    if (childCount > 0) {
      BrowserAccessibility* tableHeader = _owner->PlatformGetLastChild();
      if (tableHeader->GetRole() == ax::mojom::Role::kTableHeaderContainer)
        return tableHeader->GetNativeViewAccessible().Get();
    }
  } else if ([self internalRole] == ax::mojom::Role::kColumn) {
    _owner->GetIntAttribute(ax::mojom::IntAttribute::kTableColumnHeaderId,
                            &headerElementId);
  } else if ([self internalRole] == ax::mojom::Role::kRow) {
    _owner->GetIntAttribute(ax::mojom::IntAttribute::kTableRowHeaderId,
                            &headerElementId);
  }

  if (headerElementId > 0) {
    BrowserAccessibility* headerObject =
        _owner->manager()->GetFromID(headerElementId);
    if (headerObject)
      return headerObject->GetNativeViewAccessible().Get();
  }
  return nil;
}

- (NSNumber*)index {
  // Keep logic consistent with `-[AXPlatformNodeCocoa accessibilityIndex]`
  if (![self instanceActive])
    return nil;

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

  return nil;
}

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

  DCHECK([self internalRole] == ax::mojom::Role::kTreeItem);
  DCHECK([[self role] isEqualToString:NSAccessibilityRowRole]);

  // First find an ancestor that establishes this tree or treegrid. We
  // will search in this ancestor to calculate our row index.
  BrowserAccessibility* container = [self owner]->PlatformGetParent();
  while (container && container->GetRole() != ax::mojom::Role::kTree &&
         container->GetRole() != ax::mojom::Role::kTreeGrid) {
    container = container->PlatformGetParent();
  }
  if (!container)
    return nil;

  const BrowserAccessibilityCocoa* cocoaContainer =
      ObjCCastStrict<BrowserAccessibilityCocoa>(
          container->GetNativeViewAccessible().Get());
  int currentIndex = 0;
  if ([cocoaContainer findRowIndex:self withCurrentIndex:&currentIndex]) {
    return @(currentIndex);
  }

  return nil;
}

- (bool)findRowIndex:(BrowserAccessibilityCocoa*)toFind
    withCurrentIndex:(int*)currentIndex {
  if (![self instanceActive])
    return false;

  DCHECK([[toFind accessibilityRole] isEqualToString:NSAccessibilityRowRole]);
  for (BrowserAccessibilityCocoa* childToCheck in
       [self accessibilityChildren]) {
    if ([toFind isEqual:childToCheck]) {
      return true;
    }

    if ([[childToCheck accessibilityRole]
            isEqualToString:NSAccessibilityRowRole]) {
      ++(*currentIndex);
    }

    if ([childToCheck findRowIndex:toFind withCurrentIndex:currentIndex]) {
      return true;
    }
  }

  return false;
}

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

  const AXRange range = GetSelectedRange(*_owner);

  // If the selection is not collapsed, then there is no visible caret.
  if (!range.IsCollapsed()) {
    return NSNotFound;
  }

  // "ax::mojom::MoveDirection" is only relevant on platforms that use object
  // replacement characters in the accessibility tree. Mac is not one of them.
  const AXPosition caretPosition = range.focus()->LowestCommonAncestorPosition(
      *_owner->CreateTextPositionAt(0), ax::mojom::MoveDirection::kForward);
  DCHECK(!caretPosition->IsNullPosition())
      << "Calling HasVisibleCaretOrSelection() should have ensured that there "
         "is a valid selection focus inside the current object.";
  const std::vector<int> lineStarts =
      _owner->GetIntListAttribute(ax::mojom::IntListAttribute::kLineStarts);

  // Find the text offset that starts the next line after the current caret
  // position, then subtract 1 to get the current line number.
  auto iterator =
      std::upper_bound(lineStarts.begin(), lineStarts.end(),
                       caretPosition->AsTextPosition()->text_offset());

  // If the caret is on a single line and the line is empty, then
  // the iterator will be equal to lineStarts.begin() because the lineStarts
  // vector will be empty. The line number should be 0 in this case.
  if (iterator == lineStarts.begin()) {
    return 0;
  }

  return std::distance(lineStarts.begin(), std::prev(iterator));
}
// LINT.ThenChange(AXInsertionPointLineNumber)

// LINT.IfChange
- (NSNumber*)AXInsertionPointLineNumber {
  if (![self instanceActive])
    return nil;
  if (!_owner->HasVisibleCaretOrSelection())
    return nil;

  const AXRange range = GetSelectedRange(*_owner);

  // If the selection is not collapsed, then there is no visible caret.
  if (!range.IsCollapsed())
    return nil;

  // "ax::mojom::MoveDirection" is only relevant on platforms that use object
  // replacement characters in the accessibility tree. Mac is not one of them.
  const AXPosition caretPosition = range.focus()->LowestCommonAncestorPosition(
      *_owner->CreateTextPositionAt(0), ax::mojom::MoveDirection::kForward);
  DCHECK(!caretPosition->IsNullPosition())
      << "Calling HasVisibleCaretOrSelection() should have ensured that there "
         "is a valid selection focus inside the current object.";
  const std::vector<int> lineStarts =
      _owner->GetIntListAttribute(ax::mojom::IntListAttribute::kLineStarts);

  // Find the text offset that starts the next line after the current caret
  // position, then subtract 1 to get the current line number.
  auto iterator =
      std::upper_bound(lineStarts.begin(), lineStarts.end(),
                       caretPosition->AsTextPosition()->text_offset());

  // If the caret is on a single line and the line is empty, then
  // the iterator will be equal to lineStarts.begin() because the lineStarts
  // vector will be empty. The line number should be 0 in this case.
  if (iterator == lineStarts.begin())
    return @(0);

  return @(std::distance(lineStarts.begin(), std::prev(iterator)));
}
// LINT.ThenChange(accessibilityInsertionPointLineNumber)

- (NSString*)language {
  if (![self instanceActive])
    return nil;
  ui::AXNode* node = _owner->node();
  DCHECK(node);
  return base::SysUTF8ToNSString(node->GetLanguage());
}

// LINT.IfChange(accessibilityLinkedUIElements)
- (NSArray*)linkedUIElements {
  NSMutableArray* elements = [[NSMutableArray alloc] init];
  [elements
      addObjectsFromArray:[self uiElementsForAttribute:
                                    ax::mojom::IntListAttribute::kControlsIds]];
  [elements
      addObjectsFromArray:[self uiElementsForAttribute:
                                    ax::mojom::IntListAttribute::kFlowtoIds]];

  int target_id;
  if (_owner->GetIntAttribute(ax::mojom::IntAttribute::kInPageLinkTargetId,
                              &target_id)) {
    BrowserAccessibility* target =
        _owner->manager()->GetFromID(static_cast<int32_t>(target_id));
    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)

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

  if (!_owner->HasFloatAttribute(ax::mojom::FloatAttribute::kValueForRange))
    return @0;  // Indeterminate value exposes AXMinValue/AXMaxValue of 0.

  float floatValue =
      _owner->GetFloatAttribute(ax::mojom::FloatAttribute::kMaxValueForRange);
  return @(floatValue);
}

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

  if (!_owner->HasFloatAttribute(ax::mojom::FloatAttribute::kValueForRange))
    return @0;  // Indeterminate value exposes AXMinValue/AXMaxValue of 0.

  float floatValue =
      _owner->GetFloatAttribute(ax::mojom::FloatAttribute::kMinValueForRange);
  return @(floatValue);
}

- (NSString*)orientation {
  if (![self instanceActive])
    return nil;
  if (GetState(_owner, ax::mojom::State::kVertical))
    return NSAccessibilityVerticalOrientationValue;
  else if (GetState(_owner, ax::mojom::State::kHorizontal))
    return NSAccessibilityHorizontalOrientationValue;

  return @"";
}

// LINT.IfChange
- (NSInteger)accessibilityNumberOfCharacters {
  // TODO(crbug.com/363275809): Why do we limit support to text fields here, but
  // not in `AXPlatformNodeCocoa`?
  if (![self instanceActive] || !_owner->IsTextField()) {
    return 0;
  }

  return static_cast<int>(_owner->GetValueForControl().size());
}
// LINT.ThenChange(AXNumberOfCharacters)

// LINT.IfChange
- (NSNumber*)AXNumberOfCharacters {
  // TODO(crbug.com/363275809): Why do we limit support to text fields here, but
  // not in `AXPlatformNodeCocoa`?
  if (![self instanceActive] || !_owner->IsTextField()) {
    return nil;
  }

  return @(static_cast<int>(_owner->GetValueForControl().size()));
}
// LINT.ThenChange(accessibilityNumberOfCharacters)

- (id)accessibilityParent {
  if (![self instanceActive]) {
    return nil;
  }
  if (_owner->PlatformGetParent()) {
    id unignored_parent = NSAccessibilityUnignoredAncestor(
        _owner->PlatformGetParent()->GetNativeViewAccessible().Get());
    DCHECK(unignored_parent);
    return unignored_parent;
  }

  if (!_owner->IsWebContent()) {
    // PlatformGetParent crosses the tree boundary; for views, when we reach the
    // root of the view tree, there's really no other ancestor to hook up to.
    return nil;
  }

  // A nil parent on web content means we're the root.
  // Hook back up to RenderWidgetHostViewCocoa to get the parent Chromium view.
  BrowserAccessibilityManagerMac* manager =
      _owner->manager()
          ->GetManagerForRootFrame()
          ->ToBrowserAccessibilityManagerMac();
  if (!manager) {
    // TODO(accessibility) Determine why this is happening.
    DCHECK(false);
    return nil;
  }
  SANITIZER_CHECK(manager->GetParentView());
  return manager->GetParentView();
}

- (NSValue*)position {
  if (![self instanceActive])
    return nil;
  NSPoint pointInScreen = [self accessibilityFrame].origin;
  return [NSValue valueWithPoint:pointInScreen];
}

- (ui::BrowserAccessibility*)owner {
  return _owner;
}

// Assumes that there is at most one insertion, deletion or replacement at once.
// TODO(nektar): Merge this method with
// |BrowserAccessibilityAndroid::CommonEndLengths|.
- (ui::AXTextEdit)computeTextEdit {
  if (!_owner->IsTextField())
    return ui::AXTextEdit();

  // Starting from macOS 10.11, if the user has edited some text we need to
  // dispatch the actual text that changed on the value changed notification.
  // We run this code on all macOS versions to get the highest test coverage.
  std::u16string oldValue = _oldValue;
  std::u16string newValue = _owner->CreateTextPositionAt(0)->GetText(
      ui::AXEmbeddedObjectBehavior::kSuppressCharacter);
  _oldValue = newValue;
  if (oldValue.empty() && newValue.empty())
    return ui::AXTextEdit();

  size_t i;
  size_t j;
  // Sometimes Blink doesn't use the same UTF16 characters to represent
  // whitespace.
  for (i = 0;
       i < oldValue.length() && i < newValue.length() &&
       (oldValue[i] == newValue[i] || (base::IsUnicodeWhitespace(oldValue[i]) &&
                                       base::IsUnicodeWhitespace(newValue[i])));
       ++i) {
  }
  for (j = 0;
       (i + j) < oldValue.length() && (i + j) < newValue.length() &&
       (oldValue[oldValue.length() - j - 1] ==
            newValue[newValue.length() - j - 1] ||
        (base::IsUnicodeWhitespace(oldValue[oldValue.length() - j - 1]) &&
         base::IsUnicodeWhitespace(newValue[newValue.length() - j - 1])));
       ++j) {
  }
  DCHECK_LE(i + j, oldValue.length());
  DCHECK_LE(i + j, newValue.length());

  std::u16string deletedText = oldValue.substr(i, oldValue.length() - i - j);
  std::u16string insertedText = newValue.substr(i, newValue.length() - i - j);

  // Heuristic for editable combobox. If more than 1 character is inserted or
  // deleted, and the caret is at the end of the field, assume the entire text
  // field changed.
  // TODO(nektar) Remove this once editing intents are implemented,
  // and the actual inserted and deleted text is passed over from Blink.
  if ([self internalRole] == ax::mojom::Role::kTextFieldWithComboBox &&
      (deletedText.length() > 1 || insertedText.length() > 1)) {
    int sel_start, sel_end;
    _owner->GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart, &sel_start);
    _owner->GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd, &sel_end);
    if (static_cast<size_t>(sel_start) == newValue.length() &&
        static_cast<size_t>(sel_end) == newValue.length()) {
      // Don't include oldValue as it would be announced -- very confusing.
      return ui::AXTextEdit(newValue, std::u16string(), nil);
    }
  }
  return ui::AXTextEdit(
      insertedText, deletedText,
      AXPositionToAXTextMarker(_owner->CreateTextPositionAt(i)));
}

// internal
- (NSRect)rectInScreen:(gfx::Rect)layout_rect {
  if (![self instanceActive])
    return NSZeroRect;

  auto rect = ScaleToRoundedRect(
      layout_rect, 1.f / _owner->manager()->device_scale_factor());

  // Get the delegate for the topmost BrowserAccessibilityManager, because
  // that's the only one that can convert points to their origin in the screen.
  ui::AXPlatformTreeManagerDelegate* delegate =
      _owner->manager()->GetDelegateFromRootManager();
  if (delegate) {
    return gfx::ScreenRectToNSRect(
        rect + delegate->AccessibilityGetViewBounds().OffsetFromOrigin());
  } else {
    return NSZeroRect;
  }
}

// Returns a string indicating the NSAccessibility role of this object.
- (NSString*)AXRole {
  return [self role];
}
- (NSString*)role {
  if (![self instanceActive]) {
    TRACE_EVENT0("accessibility", "BrowserAccessibilityCocoa::role nil");
    return nil;
  }

  NSString* cocoa_role = nil;
  ax::mojom::Role role = [self internalRole];
  if (role == ax::mojom::Role::kCanvas &&
      _owner->GetBoolAttribute(ax::mojom::BoolAttribute::kCanvasHasFallback)) {
    cocoa_role = NSAccessibilityGroupRole;
  } else if (_owner->IsTextField() &&
             _owner->HasState(ax::mojom::State::kMultiline) &&
             !_owner->GetData().IsSpinnerTextField()) {
    cocoa_role = NSAccessibilityTextAreaRole;
  } else if (ui::IsImage(_owner->GetRole()) && _owner->GetChildCount()) {
    // An image map is an image with children, and exposed on Mac as a group.
    cocoa_role = NSAccessibilityGroupRole;
  } else if (_owner->IsRootWebAreaForPresentationalIframe()) {
    cocoa_role = NSAccessibilityGroupRole;

  } else if (role == ax::mojom::Role::kMenu && ![self hasMenuItemDescendant]) {
    // A menu without menu item descendants should be exposed as a group rather
    // than a menu to avoid confusing assistive technologies. This ensures
    // VoiceControl can properly display number labels when the container
    // doesn't actually contain menu items.
    cocoa_role = NSAccessibilityGroupRole;
  } else {
    cocoa_role = [AXPlatformNodeCocoa nativeRoleFromAXRole:role];
  }

  TRACE_EVENT1("accessibility", "BrowserAccessibilityCocoa::role",
               "role=", base::SysNSStringToUTF8(cocoa_role));
  DCHECK(cocoa_role != NSAccessibilityUnknownRole);
  return cocoa_role;
}

// internal, matches WebKit's implementation of
// updateRoleAfterChildrenCreation(see
// https://github.com/WebKit/WebKit/blob/main/Source/WebCore/accessibility/AccessibilityRenderObject.cpp#L2655).
- (BOOL)hasMenuItemDescendant {
  if (![self instanceActive]) {
    return NO;
  }

  // Check direct children for menu items.
  for (id child in self.accessibilityChildren) {
    BrowserAccessibilityCocoa* childCocoa =
        ObjCCast<BrowserAccessibilityCocoa>(child);
    if (!childCocoa) {
      continue;
    }

    ax::mojom::Role childRole = [childCocoa internalRole];
    // Check if child is a menu item.
    if (ui::IsMenuItem(childRole)) {
      return YES;
    }

    // Per the ARIA spec, groups with menuitem children are allowed as
    // children of menus. https://w3c.github.io/aria/#menu.
    if (childRole != ax::mojom::Role::kGroup) {
      continue;
    }

    // Check grandchildren in groups for menu items.
    for (id grandchild in childCocoa.accessibilityChildren) {
      BrowserAccessibilityCocoa* grandchildCocoa =
          ObjCCast<BrowserAccessibilityCocoa>(grandchild);
      if (!grandchildCocoa) {
        continue;
      }

      if (ui::IsMenuItem([grandchildCocoa internalRole])) {
        return YES;
      }
    }
  }

  return NO;
}

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

  bool isCellOrTableHeader = ui::IsCellOrTableHeader(_owner->GetRole());
  bool isTableLike = ui::IsTableLike(_owner->GetRole());
  if (!isTableLike && !isCellOrTableHeader) {
    return nil;
  }

  BrowserAccessibility* table = [self containingTable];
  if (!table) {
    return nil;
  }

  // A table with no row headers.
  if (isTableLike && !table->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 = table->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 = table->GetRowHeaderNodeIds(i);
      for (int32_t rowHeaderId : rowHeaderIds) {
        headerIds.insert(rowHeaderId);
      }
    }

    for (int32_t headerId : headerIds) {
      BrowserAccessibility* cell = _owner->manager()->GetFromID(headerId);
      if (cell) {
        [rowHeaders addObject:cell->GetNativeViewAccessible().Get()];
      }
    }
  } else {
    // Otherwise this is a cell, return the row headers for this cell.
    for (int32_t nodeId : _owner->node()->GetTableCellRowHeaderNodeIds()) {
      BrowserAccessibility* cell = _owner->manager()->GetFromID(nodeId);
      if (cell) {
        [rowHeaders addObject:cell->GetNativeViewAccessible().Get()];
      }
    }
  }

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

- (NSValue*)rowIndexRange {
  // Note: keep in sync with accessibilityRowIndexRange.
  if (![self instanceActive])
    return nil;

  std::optional<int> row = _owner->node()->GetTableCellRowIndex();
  std::optional<int> rowspan = _owner->node()->GetTableCellRowSpan();
  if (row && rowspan)
    return [NSValue valueWithRange:NSMakeRange(*row, *rowspan)];
  return nil;
}

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

  NSMutableArray* ret = [[NSMutableArray alloc] init];
  BrowserAccessibility* focusedChild = _owner->manager()->GetFocus();

  // "IsDescendantOf" also returns true when the two objects are equivalent.
  if (focusedChild && focusedChild != _owner &&
      focusedChild->IsDescendantOf(_owner)) {
    // If this container is not multi-selectable, try to skip iterating over the
    // children because there could only be at most one selected child. The
    // selected child should also be equivalent to the focused child, because
    // selection is tethered to the focus.
    if (!GetState(_owner, ax::mojom::State::kMultiselectable) &&
        focusedChild->GetBoolAttribute(ax::mojom::BoolAttribute::kSelected)) {
      [ret addObject:focusedChild->GetNativeViewAccessible().Get()];
      return ret;
    }

    // If this container is multi-selectable, only append the focused child
    // if it is actually selected. This avoids falsely announcing unselected
    // focused items as selected.
    if (GetState(_owner, ax::mojom::State::kMultiselectable) &&
        focusedChild->GetBoolAttribute(ax::mojom::BoolAttribute::kSelected)) {
      [ret addObject:focusedChild->GetNativeViewAccessible().Get()];
    }
  }

  // If this container is multi-selectable, we need to return any additional
  // children (other than the focused child) with the "selected" state. If this
  // container is not multi-selectable, but none of its children have the focus,
  // we need to return all its children with the "selected" state.
  for (auto it = _owner->PlatformChildrenBegin();
       it != _owner->PlatformChildrenEnd(); ++it) {
    BrowserAccessibility* child = it.get();
    if (child->GetBoolAttribute(ax::mojom::BoolAttribute::kSelected) &&
        child != focusedChild) {
      [ret addObject:child->GetNativeViewAccessible().Get()];
    }
  }

  return ret;
}

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

  if (!_owner->HasVisibleCaretOrSelection()) {
    return nil;
  }

  const AXRange range = GetSelectedRange(*_owner);
  if (range.IsNull()) {
    return nil;
  }
  return base::SysUTF16ToNSString(range.GetText());
}
// LINT.ThenChange(AXSelectedText)

// LINT.IfChange
- (NSString*)AXSelectedText {
  if (![self instanceActive])
    return nil;
  if (!_owner->HasVisibleCaretOrSelection())
    return nil;

  const AXRange range = GetSelectedRange(*_owner);
  if (range.IsNull())
    return nil;
  return base::SysUTF16ToNSString(range.GetText());
}
// LINT.ThenChange(accessibilitySelectedText)

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

  if (!_owner->HasVisibleCaretOrSelection()) {
    return NSMakeRange(0, 0);
  }

  const AXRange range = GetSelectedRange(*_owner).AsForwardRange();
  if (range.IsNull()) {
    return NSMakeRange(0, 0);
  }

  // "ax::mojom::MoveDirection" is only relevant on platforms that use object
  // replacement characters in the accessibility tree. Mac is not one of them.
  const AXPosition startPosition = range.anchor()->LowestCommonAncestorPosition(
      *_owner->CreateTextPositionAt(0), ax::mojom::MoveDirection::kForward);
  DCHECK(!startPosition->IsNullPosition())
      << "Calling HasVisibleCaretOrSelection() should have ensured that there "
         "is a valid selection anchor inside the current object.";
  int selectionStart = startPosition->AsTextPosition()->text_offset();
  DCHECK_GE(selectionStart, 0);
  int selectionLength = range.GetText().length();
  return NSMakeRange(selectionStart, selectionLength);
}
// LINT.ThenChange(AXSelectedTextRange)

// Returns range of text under the current object that is selected.
//
// Example, caret at offset 5:
// NSRange:  “pos=5 len=0”
// LINT.IfChange
- (NSValue*)AXSelectedTextRange {
  if (![self instanceActive])
    return nil;
  if (!_owner->HasVisibleCaretOrSelection())
    return nil;

  const AXRange range = GetSelectedRange(*_owner).AsForwardRange();
  if (range.IsNull())
    return nil;

  // "ax::mojom::MoveDirection" is only relevant on platforms that use object
  // replacement characters in the accessibility tree. Mac is not one of them.
  const AXPosition startPosition = range.anchor()->LowestCommonAncestorPosition(
      *_owner->CreateTextPositionAt(0), ax::mojom::MoveDirection::kForward);
  DCHECK(!startPosition->IsNullPosition())
      << "Calling HasVisibleCaretOrSelection() should have ensured that there "
         "is a valid selection anchor inside the current object.";
  int selStart = startPosition->AsTextPosition()->text_offset();
  DCHECK_GE(selStart, 0);
  int selLength = range.GetText().length();
  return [NSValue valueWithRange:NSMakeRange(selStart, selLength)];
}
// LINT.ThenChange(accessibilitySelectedTextRange)

- (void)setAccessibilitySelectedTextRange:(NSRange)range {
  if (![self instanceActive])
    return;

  BrowserAccessibilityManager* manager = _owner->manager();
  manager->SetSelection(BrowserAccessibility::AXRange(
      _owner->CreateTextPositionAt(range.location)->AsDomSelectionPosition(),
      _owner->CreateTextPositionAt(NSMaxRange(range))
          ->AsDomSelectionPosition()));
}

- (id)selectedTextMarkerRange {
  if (![self instanceActive])
    return nil;
  AXRange ax_range = GetSelectedRange(*_owner);
  if (ax_range.IsNull())
    return nil;

  return AXRangeToAXTextMarkerRange(std::move(ax_range));
}

- (NSString*)sortDirection {
  // Keep logic consistent with
  // `-[AXPlatformNodeCocoa accessibilitySortDirection]`
  if (![self instanceActive])
    return nil;

  // If we know this object does not support `sortDirection`, don't return
  // anything.
  if (![[self internalAccessibilityAttributeNames]
          containsObject:NSAccessibilitySortDirectionAttribute]) {
    return nil;
  }

  int sortDirection;
  if (!_owner->GetIntAttribute(ax::mojom::IntAttribute::kSortDirection,
                               &sortDirection))
    return nil;

  switch (static_cast<ax::mojom::SortDirection>(sortDirection)) {
    case ax::mojom::SortDirection::kUnsorted:
      return nil;
    case ax::mojom::SortDirection::kAscending:
      return NSAccessibilityAscendingSortDirectionValue;
    case ax::mojom::SortDirection::kDescending:
      return NSAccessibilityDescendingSortDirectionValue;
    case ax::mojom::SortDirection::kOther:
      return NSAccessibilityUnknownSortDirectionValue;
    default:
      NOTREACHED();
  }
}

// Returns a text marker that points to the first character in the document that
// can be selected with VoiceOver.
- (id)startTextMarker {
  if (![self instanceActive])
    return nil;
  AXPosition position = _owner->CreateTextPositionAt(0);
  return AXPositionToAXTextMarker(position->CreatePositionAtStartOfContent());
}

- (NSString*)AXSubrole {
  return [self subrole];
}

// Returns a subrole based upon the role.
- (NSString*)subrole {
  if (![self instanceActive])
    return nil;

  if (_owner->IsAtomicTextField() &&
      GetState(_owner, ax::mojom::State::kProtected)) {
    return NSAccessibilitySecureTextFieldSubrole;
  }

  if ([self internalRole] == ax::mojom::Role::kForm &&
      !_owner->HasStringAttribute(ax::mojom::StringAttribute::kName)) {
    // Per HTML-AAM and Core-AAM, an unnamed <form> is not a landmark, so do not
    // expose the AXLandmarkForm subrole. Only named forms get the landmark
    // subrole below.
    return nil;
  }

  if ([self internalRole] == ax::mojom::Role::kDescriptionList)
    return NSAccessibilityDefinitionListSubrole;

  if ([self internalRole] == ax::mojom::Role::kDirectoryDeprecated ||
      [self internalRole] == ax::mojom::Role::kList) {
    return NSAccessibilityContentListSubrole;
  }

  if ([self internalRole] == ax::mojom::Role::kGroup &&
      _owner->GetStringAttribute(ax::mojom::StringAttribute::kHtmlTag) ==
          "fieldset") {
    return CFToNSPtrCast(kAXFieldsetSubrole);
  }

  // Math nodes are exposed as AXGroup with a native math subrole; do not
  // replace with AXEmptyGroup (unlike empty landmarks, which intentionally
  // override their landmark subrole).
  if (ui::IsMath([self internalRole])) {
    return [AXPlatformNodeCocoa nativeSubroleFromAXRole:[self internalRole]];
  }

  // For wrappers exposed as plain groups, expose AXEmptyGroup when the
  // subtree carries no information. VoiceOver uses this subrole to skip
  // style-only/layout-only containers during linear navigation, matching
  // WebKit's macOS behavior. The predicate is memoized per-Cocoa-wrapper
  // and invalidated by -childrenChanged / -invalidateEmptyGroupCacheUpwards,
  // so repeated -subrole queries against the same subtree share work.
  if ([[self role] isEqualToString:NSAccessibilityGroupRole] &&
      [self isEmptyGroupSubtree]) {
    return CFToNSPtrCast(kAXEmptyGroupSubrole);
  }

  return [AXPlatformNodeCocoa nativeSubroleFromAXRole:[self internalRole]];
}

// Returns all tabs in this subtree.
// LINT.IfChange(accessibilityTabs)
- (NSArray*)tabs {
  if (![self instanceActive]) {
    return nil;
  }

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

  if ([self internalRole] == ax::mojom::Role::kTab)
    [tabSubtree addObject:self];

  for (id child in [self accessibilityChildren]) {
    [tabSubtree addObjectsFromArray:[child tabs]];
  }

  return tabSubtree;
}
// LINT.ThenChange(ui/accessibility/platform/ax_platform_node_cocoa.mm:accessibilityTabs)

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

  DCHECK(_owner->node()->IsDataValid());

  if (ui::IsNameExposedInAXValueForRole([self internalRole])) {
    std::u16string name = _owner->GetNameAsString16();
    return base::SysUTF16ToNSString(name);
  }

  NSString* role = [self role];
  if (ui::IsHeading(_owner->GetRole())) {
    int level = 0;
    if (_owner->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel,
                                &level)) {
      return @(level);
    }
  } else if ([role isEqualToString:NSAccessibilityButtonRole]) {
    // AXValue does not make sense for pure buttons.
    return @"";
  } else if ([self isCheckable]) {
    int value;
    const auto checkedState = _owner->GetData().GetCheckedState();
    switch (checkedState) {
      case ax::mojom::CheckedState::kTrue:
        value = 1;
        break;
      case ax::mojom::CheckedState::kMixed:
        value = 2;
        break;
      default:
        value = _owner->GetBoolAttribute(ax::mojom::BoolAttribute::kSelected)
                    ? 1
                    : 0;
        break;
    }
    return @(value);
  } else if (_owner->GetData().IsRangeValueSupported()) {
    // Objects that support range values include progress bars, sliders, and
    // steppers. Only the native value or aria-valuenow should be exposed, not
    // the aria-valuetext. Aria-valuetext is exposed via
    // "accessibilityValueDescription".
    float floatValue;
    if (_owner->GetFloatAttribute(ax::mojom::FloatAttribute::kValueForRange,
                                  &floatValue)) {
      return @(floatValue);
    }
  } else if ([role isEqualToString:NSAccessibilityColorWellRole]) {
    unsigned int color = static_cast<unsigned int>(
        _owner->GetIntAttribute(ax::mojom::IntAttribute::kColorValue));
    unsigned int red = SkColorGetR(color);
    unsigned int green = SkColorGetG(color);
    unsigned int blue = SkColorGetB(color);
    // This string matches the one returned by a native Mac color well.
    return [NSString stringWithFormat:@"rgb %7.5f %7.5f %7.5f 1", red / 255.,
                                      green / 255., blue / 255.];
  }

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

- (NSNumber*)valueAutofillAvailable {
  if (![self instanceActive])
    return nil;
  return _owner->HasState(ax::mojom::State::kAutofillAvailable) ? @YES : @NO;
}

// Not currently supported, as Chrome does not store whether an autofill
// occurred. We could have autofill fire an event, however, and set an
// "is_autofilled" flag until the next edit. - (NSNumber*)valueAutofilled {
//  return @NO;
// }

// Not currently supported, as Chrome's autofill types aren't like Safari's.
// - (NSString*)valueAutofillType {
//  return @"none";
//}

- (NSString*)valueDescription {
  if (![self instanceActive] || !_owner->GetData().IsRangeValueSupported())
    return nil;

  // This method is only for exposing aria-valuetext to VoiceOver if present.
  // Blink places the value of aria-valuetext in
  // ax::mojom::StringAttribute::kAriaValueText for objects that support range
  // values, i.e., progress bars, sliders and steppers.
  return base::SysUTF8ToNSString(
      _owner->GetStringAttribute(ax::mojom::StringAttribute::kAriaValueText));
}

// LINT.IfChange
- (NSRange)accessibilityVisibleCharacterRange {
  if (![self instanceActive]) {
    return NSMakeRange(0, 0);
  }
  // TODO(crbug.com/363275809): Why do we limit support to text fields here, but
  // not in `AXPlatformNodeCocoa`?
  if (!_owner->IsTextField() || _owner->IsPasswordField()) {
    return NSMakeRange(0, 0);
  }

  return NSMakeRange(
      0, static_cast<NSUInteger>(_owner->GetValueForControl().size()));
}
// LINT.ThenChange(AXVisibleCharacterRange)

// LINT.IfChange
- (NSValue*)AXVisibleCharacterRange {
  if ([self instanceActive] && _owner->IsTextField() &&
      !_owner->IsPasswordField()) {
    return [NSValue
        valueWithRange:NSMakeRange(0,
                                   static_cast<NSUInteger>(
                                       _owner->GetValueForControl().size()))];
  }
  return nil;
}
// LINT.ThenChange(accessibilityVisibleCharacterRange)

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

  NSMutableArray* ret = [[NSMutableArray alloc] init];
  for (int32_t id : _owner->node()->GetTableUniqueCellIds()) {
    BrowserAccessibility* cell = _owner->manager()->GetFromID(id);
    if (cell)
      [ret addObject:cell->GetNativeViewAccessible().Get()];
  }
  return ret;
}
// LINT.ThenChange(ui/accessibility/platform/ax_platform_node_cocoa.mm:accessibilityVisibleCells)

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

// LINT.IfChange(accessibilityVisibleColumns)
- (NSArray*)visibleColumns {
  if (![self instanceActive])
    return nil;
  return [self columns];
}
// LINT.ThenChange(ui/accessibility/platform/ax_platform_node_cocoa.mm:accessibilityVisibleColumns)

// LINT.IfChange(accessibilityVisibleRows)
- (NSArray*)visibleRows {
  if (![self instanceActive])
    return nil;
  return [self accessibilityRows];
}
// LINT.ThenChange(ui/accessibility/platform/ax_platform_node_cocoa.mm:accessibilityVisibleRows)

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

  BrowserAccessibilityManagerMac* root_manager =
      _owner->manager()
          ->GetManagerForRootFrame()
          ->ToBrowserAccessibilityManagerMac();
  if (!root_manager) {
    // TODO(crbug.com/40234203) Find out why this happens -- there should always
    // be a root manager whenever an object is instanceActive. This used to be a
    // CHECK() but caused too many crashes, with unknown cause.
    return nil;
  }
  if (!root_manager->GetParentView()) {
    // TODO(crbug.com/40898856) Find out why this happens, there should always
    // be a parent view. This used to be a CHECK() but caused too many crashes.
    // Repro steps are available in the bug.
    return nil;
  }
  return root_manager->GetWindow();  // Can be null for inactive tabs.
}

- (NSString*)methodNameForAttribute:(NSString*)attribute {
  return [gAttributeToMethodNameMap objectForKey:attribute];
}

- (NSRect)frameForRange:(NSRange)range {
  if (!_owner->IsText() && !_owner->IsAtomicTextField())
    return CGRectNull;
  gfx::Rect rect = _owner->GetUnclippedRootFrameInnerTextRangeBoundsRect(
      range.location, NSMaxRange(range));
  return [self rectInScreen:rect];
}

// Returns the accessibility value for the given attribute.  If the value isn't
// supported this will return nil.
- (id)accessibilityAttributeValue:(NSString*)attribute {
  TRACE_EVENT2("accessibility",
               "BrowserAccessibilityCocoa::accessibilityAttributeValue",
               "role=", ui::ToString([self internalRole]),
               "attribute=", base::SysNSStringToUTF8(attribute));
  if (![self instanceActive])
    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([self methodNameForAttribute:attribute]);
  if (selector)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    return [self performSelector:selector];
#pragma clang diagnostic pop

  return [super accessibilityAttributeValue:attribute];
}

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

  if (ui::IsNameExposedInAXValueForRole([self internalRole])) {
    NSString* value = [self getAXValueAsString];
    if (NSMaxRange(range) > [value length]) {
      return nil;
    }
    return [value substringWithRange:range];
  }

  std::u16string textContent = _owner->GetTextContentUTF16();
  if (NSMaxRange(range) > textContent.length()) {
    return nil;
  }

  return base::SysUTF16ToNSString(
      textContent.substr(range.location, range.length));
}

- (NSInteger)accessibilityLineForIndex:(NSInteger)index {
  if (index < 0 ||
      index >= static_cast<int>(_owner->GetValueForControl().size())) {
    return NSNotFound;
  }

  const std::vector<int> lineStarts =
      _owner->GetIntListAttribute(ax::mojom::IntListAttribute::kLineStarts);
  auto iterator = std::upper_bound(lineStarts.begin(), lineStarts.end(), index);
  if (iterator == lineStarts.begin()) {
    return 0;
  }

  return std::distance(lineStarts.begin(), iterator) - 1;
}

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

  const std::vector<int> lineStarts =
      _owner->GetIntListAttribute(ax::mojom::IntListAttribute::kLineStarts);
  std::u16string value = _owner->GetValueForControl();
  int valueLength = static_cast<int>(value.size());

  int lineCount = static_cast<int>(lineStarts.size());
  if (lineIndex < 0 || lineIndex >= lineCount) {
    return NSMakeRange(0, 0);
  }

  int start = lineStarts[lineIndex];
  int end =
      (lineIndex < (lineCount - 1)) ? lineStarts[lineIndex + 1] : valueLength;
  return NSMakeRange(start, end - start);
}

// Returns the accessibility value for the given attribute and parameter. If the
// value isn't supported this will return nil.
//
// TODO(nektar): Implement programmatic text operations.
//
- (id)accessibilityAttributeValue:(NSString*)attribute
                     forParameter:(id)parameter {
  if (parameter && [parameter isKindOfClass:[NSNumber self]]) {
    TRACE_EVENT2(
        "accessibility",
        "BrowserAccessibilityCocoa::accessibilityAttributeValue:forParameter",
        "role=", ui::ToString([self internalRole]), "attribute=",
        base::SysNSStringToUTF8(attribute) +
            " parameter=" + base::SysNSStringToUTF8([parameter stringValue]));
  } else {
    TRACE_EVENT2(
        "accessibility",
        "BrowserAccessibilityCocoa::accessibilityAttributeValue:forParameter",
        "role=", ui::ToString([self internalRole]),
        "attribute=", base::SysNSStringToUTF8(attribute));
  }

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

  // LINT.IfChange(accessibilityCellForColumn)
  if ([attribute
          isEqualToString:
              NSAccessibilityCellForColumnAndRowParameterizedAttribute]) {
    if (!ui::IsTableLike([self internalRole]))
      return nil;
    if (![parameter isKindOfClass:[NSArray class]])
      return nil;
    if (2 != [parameter count])
      return nil;
    NSArray* array = parameter;
    int column = [[array objectAtIndex:0] intValue];
    int row = [[array objectAtIndex:1] intValue];

    ui::AXNode* cellNode = _owner->node()->GetTableCellFromCoords(row, column);
    if (!cellNode)
      return nil;

    BrowserAccessibility* cell = _owner->manager()->GetFromID(cellNode->id());
    if (cell) {
      return cell->GetNativeViewAccessible().Get();
    }
  }
  // LINT.ThenChange(ui/accessibility/platform/ax_platform_node_cocoa.mm:accessibilityCellForColumn)

  if ([attribute
          isEqualToString:
              CFToNSPtrCast(kAXUIElementForTextMarkerParameterizedAttribute)]) {
    AXPosition position = AXTextMarkerToAXPosition(parameter);
    if (!position->IsNullPosition()) {
      BrowserAccessibility* ui_element =
          _owner->manager()->GetFromAXNode(position->GetAnchor());
      if (ui_element) {
        return ui_element->GetNativeViewAccessible().Get();
      }
    }

    return nil;
  }

  if ([attribute
          isEqualToString:
              CFToNSPtrCast(
                  kAXTextMarkerRangeForUIElementParameterizedAttribute)]) {
    BrowserAccessibilityCocoa* parameterCocoa =
        ObjCCast<BrowserAccessibilityCocoa>(parameter);
    if (!parameterCocoa) {
      return nil;
    }

    BrowserAccessibility* parameterOwner = [parameterCocoa owner];
    if (!parameterOwner) {
      return nil;
    }

    AXPosition startPosition = parameterOwner->CreateTextPositionAt(0);
    AXPosition endPosition = startPosition->CreatePositionAtEndOfAnchor();
    AXRange range = AXRange(std::move(startPosition), std::move(endPosition));
    return AXRangeToAXTextMarkerRange(std::move(range));
  }

  if ([attribute isEqualToString:
                     CFToNSPtrCast(
                         kAXStringForTextMarkerRangeParameterizedAttribute)]) {
    return GetTextForTextMarkerRange(parameter);
  }

  if ([attribute
          isEqualToString:
              CFToNSPtrCast(
                  kAXNextTextMarkerForTextMarkerParameterizedAttribute)]) {
    AXPosition position = AXTextMarkerToAXPosition(parameter);
    if (position->IsNullPosition()) {
      return nil;
    }
    return AXPositionToAXTextMarker(
        position->CreateNextCharacterPosition(ui::AXMovementOptions(
            ui::AXBoundaryBehavior::kCrossBoundary,
            ui::AXBoundaryDetection::kDontCheckInitialPosition)));
  }

  if ([attribute
          isEqualToString:
              CFToNSPtrCast(
                  kAXPreviousTextMarkerForTextMarkerParameterizedAttribute)]) {
    AXPosition position = AXTextMarkerToAXPosition(parameter);
    if (position->IsNullPosition()) {
      return nil;
    }
    return AXPositionToAXTextMarker(
        position->CreatePreviousCharacterPosition(ui::AXMovementOptions(
            ui::AXBoundaryBehavior::kCrossBoundary,
            ui::AXBoundaryDetection::kDontCheckInitialPosition)));
  }

  if ([attribute
          isEqualToString:
              CFToNSPtrCast(
                  kAXLeftWordTextMarkerRangeForTextMarkerParameterizedAttribute)]) {
    AXPosition endPosition = AXTextMarkerToAXPosition(parameter);
    if (endPosition->IsNullPosition()) {
      return nil;
    }

    AXPosition startWordPosition =
        endPosition->CreatePreviousWordStartPosition(ui::AXMovementOptions(
            ui::AXBoundaryBehavior::kStopAtAnchorBoundary,
            ui::AXBoundaryDetection::kDontCheckInitialPosition));
    AXPosition endWordPosition =
        endPosition->CreatePreviousWordEndPosition(ui::AXMovementOptions(
            ui::AXBoundaryBehavior::kStopAtAnchorBoundary,
            ui::AXBoundaryDetection::kDontCheckInitialPosition));
    AXPosition startPosition = *startWordPosition <= *endWordPosition
                                   ? std::move(endWordPosition)
                                   : std::move(startWordPosition);
    AXRange range(std::move(startPosition), std::move(endPosition));
    return AXRangeToAXTextMarkerRange(std::move(range));
  }

  if ([attribute
          isEqualToString:
              CFToNSPtrCast(
                  kAXRightWordTextMarkerRangeForTextMarkerParameterizedAttribute)]) {
    AXPosition startPosition = AXTextMarkerToAXPosition(parameter);
    if (startPosition->IsNullPosition()) {
      return nil;
    }

    AXPosition endWordPosition =
        startPosition->CreateNextWordEndPosition(ui::AXMovementOptions(
            ui::AXBoundaryBehavior::kStopAtAnchorBoundary,
            ui::AXBoundaryDetection::kDontCheckInitialPosition));
    AXPosition startWordPosition =
        startPosition->CreateNextWordStartPosition(ui::AXMovementOptions(
            ui::AXBoundaryBehavior::kStopAtAnchorBoundary,
            ui::AXBoundaryDetection::kDontCheckInitialPosition));
    AXPosition endPosition = *startWordPosition <= *endWordPosition
                                 ? std::move(startWordPosition)
                                 : std::move(endWordPosition);
    AXRange range(std::move(startPosition), std::move(endPosition));
    return AXRangeToAXTextMarkerRange(std::move(range));
  }

  if ([attribute
          isEqualToString:
              CFToNSPtrCast(
                  kAXNextWordEndTextMarkerForTextMarkerParameterizedAttribute)]) {
    AXPosition position = AXTextMarkerToAXPosition(parameter);
    if (position->IsNullPosition()) {
      return nil;
    }
    return AXPositionToAXTextMarker(
        position->CreateNextWordEndPosition(ui::AXMovementOptions(
            ui::AXBoundaryBehavior::kCrossBoundary,
            ui::AXBoundaryDetection::kDontCheckInitialPosition)));
  }

  if ([attribute
          isEqualToString:
              CFToNSPtrCast(
                  kAXPreviousWordStartTextMarkerForTextMarkerParameterizedAttribute)]) {
    AXPosition position = AXTextMarkerToAXPosition(parameter);
    if (position->IsNullPosition()) {
      return nil;
    }
    return AXPositionToAXTextMarker(
        position->CreatePreviousWordStartPosition(ui::AXMovementOptions(
            ui::AXBoundaryBehavior::kCrossBoundary,
            ui::AXBoundaryDetection::kDontCheckInitialPosition)));
  }

  if ([attribute
          isEqualToString:CFToNSPtrCast(
                              kAXLineForTextMarkerParameterizedAttribute)]) {
    AXPosition position = AXTextMarkerToAXPosition(parameter);
    if (position->IsNullPosition()) {
      return nil;
    }

    int textOffset = position->AsTextPosition()->text_offset();
    const std::vector<int> lineStarts =
        _owner->GetIntListAttribute(ax::mojom::IntListAttribute::kLineStarts);
    const auto iterator =
        std::lower_bound(lineStarts.begin(), lineStarts.end(), textOffset);
    return @(std::distance(lineStarts.begin(), iterator));
  }

  if ([attribute
          isEqualToString:
              CFToNSPtrCast(kAXTextMarkerRangeForLineParameterizedAttribute)]) {
    int lineIndex = [(NSNumber*)parameter intValue];
    const std::vector<int> lineStarts =
        _owner->GetIntListAttribute(ax::mojom::IntListAttribute::kLineStarts);
    int lineCount = static_cast<int>(lineStarts.size());
    if (lineIndex < 0 || lineIndex >= lineCount) {
      return nil;
    }

    int lineStartOffset = lineStarts[lineIndex];
    AXPosition lineStartPosition = _owner->CreateTextPositionAt(
        lineStartOffset, ax::mojom::TextAffinity::kDownstream);
    if (lineStartPosition->IsNullPosition()) {
      return nil;
    }

    // Make sure that the line start position is really at the start of the
    // current line.
    lineStartPosition = lineStartPosition->CreatePreviousLineStartPosition(
        ui::AXMovementOptions(ui::AXBoundaryBehavior::kStopAtAnchorBoundary,
                              ui::AXBoundaryDetection::kCheckInitialPosition));
    AXPosition lineEndPosition =
        lineStartPosition->CreateNextLineEndPosition(ui::AXMovementOptions(
            ui::AXBoundaryBehavior::kStopAtAnchorBoundary,
            ui::AXBoundaryDetection::kDontCheckInitialPosition));
    AXRange range(std::move(lineStartPosition), std::move(lineEndPosition));
    return AXRangeToAXTextMarkerRange(std::move(range));
  }

  if ([attribute
          isEqualToString:
              CFToNSPtrCast(
                  kAXLeftLineTextMarkerRangeForTextMarkerParameterizedAttribute)]) {
    AXPosition endPosition = AXTextMarkerToAXPosition(parameter);
    if (endPosition->IsNullPosition()) {
      return nil;
    }

    AXPosition startLinePosition =
        endPosition->CreatePreviousLineStartPosition(ui::AXMovementOptions(
            ui::AXBoundaryBehavior::kStopAtLastAnchorBoundary,
            ui::AXBoundaryDetection::kDontCheckInitialPosition));
    AXPosition endLinePosition =
        endPosition->CreatePreviousLineEndPosition(ui::AXMovementOptions(
            ui::AXBoundaryBehavior::kStopAtLastAnchorBoundary,
            ui::AXBoundaryDetection::kDontCheckInitialPosition));
    AXPosition startPosition = *startLinePosition <= *endLinePosition
                                   ? std::move(endLinePosition)
                                   : std::move(startLinePosition);
    AXRange range(std::move(startPosition), std::move(endPosition));
    return AXRangeToAXTextMarkerRange(std::move(range));
  }

  if ([attribute
          isEqualToString:
              CFToNSPtrCast(
                  kAXRightLineTextMarkerRangeForTextMarkerParameterizedAttribute)]) {
    AXPosition startPosition = AXTextMarkerToAXPosition(parameter);
    if (startPosition->IsNullPosition()) {
      return nil;
    }

    AXPosition startLinePosition =
        startPosition->CreateNextLineStartPosition(ui::AXMovementOptions(
            ui::AXBoundaryBehavior::kStopAtLastAnchorBoundary,
            ui::AXBoundaryDetection::kDontCheckInitialPosition));
    AXPosition endLinePosition =
        startPosition->CreateNextLineEndPosition(ui::AXMovementOptions(
            ui::AXBoundaryBehavior::kStopAtLastAnchorBoundary,
            ui::AXBoundaryDetection::kDontCheckInitialPosition));
    AXPosition endPosition = *startLinePosition <= *endLinePosition
                                 ? std::move(startLinePosition)
                                 : std::move(endLinePosition);
    AXRange range(std::move(startPosition), std::move(endPosition));
    return AXRangeToAXTextMarkerRange(std::move(range));
  }

  if ([attribute
          isEqualToString:
              CFToNSPtrCast(
                  kAXNextLineEndTextMarkerForTextMarkerParameterizedAttribute)]) {
    AXPosition position = AXTextMarkerToAXPosition(parameter);
    if (position->IsNullPosition()) {
      return nil;
    }
    return AXPositionToAXTextMarker(
        position->CreateNextLineEndPosition(ui::AXMovementOptions(
            ui::AXBoundaryBehavior::kCrossBoundary,
            ui::AXBoundaryDetection::kDontCheckInitialPosition)));
  }

  if ([attribute
          isEqualToString:
              CFToNSPtrCast(
                  kAXPreviousLineStartTextMarkerForTextMarkerParameterizedAttribute)]) {
    AXPosition position = AXTextMarkerToAXPosition(parameter);
    if (position->IsNullPosition()) {
      return nil;
    }
    return AXPositionToAXTextMarker(
        position->CreatePreviousLineStartPosition(ui::AXMovementOptions(
            ui::AXBoundaryBehavior::kCrossBoundary,
            ui::AXBoundaryDetection::kDontCheckInitialPosition)));
  }

  if ([attribute
          isEqualToString:
              CFToNSPtrCast(
                  kAXSentenceTextMarkerRangeForTextMarkerParameterizedAttribute)]) {
    AXPosition position = AXTextMarkerToAXPosition(parameter);
    if (position->IsNullPosition()) {
      return nil;
    }

    AXRange range = position->ExpandToEnclosingTextBoundary(
        ax::mojom::TextBoundary::kSentenceStartOrEnd,
        ui::AXRangeExpandBehavior::kLeftFirst);
    return AXRangeToAXTextMarkerRange(std::move(range));
  }

  if ([attribute
          isEqualToString:
              CFToNSPtrCast(
                  kAXParagraphTextMarkerRangeForTextMarkerParameterizedAttribute)]) {
    AXPosition position = AXTextMarkerToAXPosition(parameter);
    if (position->IsNullPosition()) {
      return nil;
    }

    AXRange range = position->ExpandToEnclosingTextBoundary(
        ax::mojom::TextBoundary::kParagraphStartOrEnd,
        ui::AXRangeExpandBehavior::kLeftFirst);
    return AXRangeToAXTextMarkerRange(std::move(range));
  }

  if ([attribute
          isEqualToString:
              CFToNSPtrCast(
                  kAXNextParagraphEndTextMarkerForTextMarkerParameterizedAttribute)]) {
    AXPosition position = AXTextMarkerToAXPosition(parameter);
    if (position->IsNullPosition()) {
      return nil;
    }
    return AXPositionToAXTextMarker(
        position->CreateNextParagraphEndPosition(ui::AXMovementOptions(
            ui::AXBoundaryBehavior::kCrossBoundary,
            ui::AXBoundaryDetection::kDontCheckInitialPosition)));
  }

  if ([attribute
          isEqualToString:
              CFToNSPtrCast(
                  kAXPreviousParagraphStartTextMarkerForTextMarkerParameterizedAttribute)]) {
    AXPosition position = AXTextMarkerToAXPosition(parameter);
    if (position->IsNullPosition()) {
      return nil;
    }
    return AXPositionToAXTextMarker(
        position->CreatePreviousParagraphStartPosition(ui::AXMovementOptions(
            ui::AXBoundaryBehavior::kCrossBoundary,
            ui::AXBoundaryDetection::kDontCheckInitialPosition)));
  }

  if ([attribute
          isEqualToString:
              CFToNSPtrCast(
                  kAXStyleTextMarkerRangeForTextMarkerParameterizedAttribute)]) {
    AXPosition position = AXTextMarkerToAXPosition(parameter);
    if (position->IsNullPosition()) {
      return nil;
    }

    AXPosition startPosition = position->CreatePreviousFormatStartPosition(
        ui::AXMovementOptions(ui::AXBoundaryBehavior::kStopAtAnchorBoundary,
                              ui::AXBoundaryDetection::kCheckInitialPosition));
    AXPosition endPosition = position->CreateNextFormatEndPosition(
        ui::AXMovementOptions(ui::AXBoundaryBehavior::kStopAtAnchorBoundary,
                              ui::AXBoundaryDetection::kCheckInitialPosition));
    AXRange range(std::move(startPosition), std::move(endPosition));
    return AXRangeToAXTextMarkerRange(std::move(range));
  }

  if ([attribute isEqualToString:
                     CFToNSPtrCast(
                         kAXLengthForTextMarkerRangeParameterizedAttribute)]) {
    NSString* text = GetTextForTextMarkerRange(parameter);
    return @([text length]);
  }

  if ([attribute
          isEqualToString:CFToNSPtrCast(
                              kAXTextMarkerIsValidParameterizedAttribute)]) {
    return @(AXTextMarkerToAXPosition(parameter)->IsNullPosition());
  }

  if ([attribute
          isEqualToString:CFToNSPtrCast(
                              kAXIndexForTextMarkerParameterizedAttribute)]) {
    AXPosition position = AXTextMarkerToAXPosition(parameter);
    if (position->IsNullPosition()) {
      return nil;
    }
    return @(position->AsTextPosition()->text_offset());
  }

  if ([attribute
          isEqualToString:CFToNSPtrCast(
                              kAXTextMarkerForIndexParameterizedAttribute)]) {
    int index = [static_cast<NSNumber*>(parameter) intValue];
    if (index < 0) {
      return nil;
    }

    const BrowserAccessibility* root =
        _owner->manager()->GetBrowserAccessibilityRoot();
    if (!root) {
      return nil;
    }

    return AXPositionToAXTextMarker(root->CreateTextPositionAt(index));
  }

  if ([attribute isEqualToString:
                     NSAccessibilityBoundsForRangeParameterizedAttribute]) {
    NSRect rect = [self frameForRange:[(NSValue*)parameter rangeValue]];
    return CGRectIsNull(rect) ? nil : [NSValue valueWithRect:rect];
  }

  if ([attribute
          isEqualToString:
              NSAccessibilityUIElementCountForSearchPredicateParameterizedAttribute]) {
    OneShotAccessibilityTreeSearch search(_owner);
    if (InitializeAccessibilityTreeSearch(&search, parameter))
      return @(search.CountMatches());
    return nil;
  }

  if ([attribute
          isEqualToString:
              NSAccessibilityUIElementsForSearchPredicateParameterizedAttribute]) {
    OneShotAccessibilityTreeSearch search(_owner);
    if (InitializeAccessibilityTreeSearch(&search, parameter)) {
      size_t count = search.CountMatches();
      NSMutableArray* result = [NSMutableArray arrayWithCapacity:count];
      for (size_t i = 0; i < count; ++i) {
        BrowserAccessibility* match = search.GetMatchAtIndex(i);
        [result addObject:match->GetNativeViewAccessible().Get()];
      }
      return result;
    }
    return nil;
  }

  if ([attribute
          isEqualToString:
              CFToNSPtrCast(
                  kAXLineTextMarkerRangeForTextMarkerParameterizedAttribute)]) {
    AXPosition position = AXTextMarkerToAXPosition(parameter);
    if (position->IsNullPosition())
      return nil;

    // If the initial position is between lines, e.g. if it is on a soft line
    // break or on an ignored position that separates lines, we have to return
    // the previous line. This is what Safari does.
    //
    // Note that hard line breaks are on a line of their own.
    AXPosition startPosition = position->CreatePreviousLineStartPosition(
        ui::AXMovementOptions(ui::AXBoundaryBehavior::kStopAtLastAnchorBoundary,
                              ui::AXBoundaryDetection::kCheckInitialPosition));
    AXPosition endPosition =
        startPosition->CreateNextLineStartPosition(ui::AXMovementOptions(
            ui::AXBoundaryBehavior::kStopAtLastAnchorBoundary,
            ui::AXBoundaryDetection::kDontCheckInitialPosition));
    AXRange range(std::move(startPosition), std::move(endPosition));
    return AXRangeToAXTextMarkerRange(std::move(range));
  }

  if ([attribute isEqualToString:
                     CFToNSPtrCast(
                         kAXBoundsForTextMarkerRangeParameterizedAttribute)]) {
    BrowserAccessibility* startObject;
    BrowserAccessibility* endObject;
    int startOffset, endOffset;
    AXRange range = AXTextMarkerRangeToAXRange(parameter);
    if (range.IsNull())
      return nil;

    const AXPosition anchor = range.anchor()->AsTextPosition();
    const AXPosition focus = range.focus()->AsTextPosition();
    if (!anchor || !focus) {
      return nil;
    }

    startObject = _owner->manager()->GetFromAXNode(anchor->GetAnchor());
    endObject = _owner->manager()->GetFromAXNode(focus->GetAnchor());
    startOffset = anchor->text_offset();
    endOffset = focus->text_offset();
    DCHECK(startObject && endObject);
    DCHECK_GE(startOffset, 0);
    DCHECK_GE(endOffset, 0);
    if (!startObject || !endObject || startOffset < 0 || endOffset < 0)
      return nil;

    gfx::Rect rect =
        BrowserAccessibilityManager::GetRootFrameInnerTextRangeBoundsRect(
            *startObject, startOffset, *endObject, endOffset);
    NSRect nsrect = [self rectInScreen:rect];
    return [NSValue valueWithRect:nsrect];
  }

  if ([attribute
          isEqualToString:
              CFToNSPtrCast(
                  kAXTextMarkerRangeForUnorderedTextMarkersParameterizedAttribute)]) {
    if (![parameter isKindOfClass:[NSArray class]]) {
      return nil;
    }

    NSArray* textMarkerArray = parameter;
    if ([textMarkerArray count] != 2) {
      return nil;
    }

    AXPosition startPosition =
        AXTextMarkerToAXPosition([textMarkerArray objectAtIndex:0]);
    AXPosition endPosition =
        AXTextMarkerToAXPosition([textMarkerArray objectAtIndex:1]);
    if (*startPosition <= *endPosition) {
      return AXRangeToAXTextMarkerRange(
          AXRange(std::move(startPosition), std::move(endPosition)));
    } else {
      return AXRangeToAXTextMarkerRange(
          AXRange(std::move(endPosition), std::move(startPosition)));
    }
  }

  if ([attribute
          isEqualToString:
              NSAccessibilityTextMarkerDebugDescriptionParameterizedAttribute]) {
    AXPosition position = AXTextMarkerToAXPosition(parameter);
    return base::SysUTF8ToNSString(position->ToString());
  }

  if ([attribute
          isEqualToString:
              NSAccessibilityTextMarkerRangeDebugDescriptionParameterizedAttribute]) {
    AXRange range = AXTextMarkerRangeToAXRange(parameter);
    return base::SysUTF8ToNSString(range.ToString());
  }

  if ([attribute
          isEqualToString:
              NSAccessibilityTextMarkerNodeDebugDescriptionParameterizedAttribute]) {
    AXPosition position = AXTextMarkerToAXPosition(parameter);
    if (position->IsNullPosition())
      return @"nil";
    DCHECK(position->GetAnchor());
    return base::SysUTF8ToNSString(position->GetAnchor()->data().ToString());
  }

  if ([attribute
          isEqualToString:
              NSAccessibilityIndexForChildUIElementParameterizedAttribute]) {
    BrowserAccessibilityCocoa* parameterCocoa =
        ObjCCast<BrowserAccessibilityCocoa>(parameter);
    if (!parameterCocoa) {
      return nil;
    }

    BrowserAccessibility* parameterOwner = [parameterCocoa owner];
    if (!parameterOwner) {
      return nil;
    }

    if (parameterOwner->PlatformGetParent() != _owner) {
      return nil;
    }

    return @(parameterOwner->GetIndexInParent().value());
  }

  if (features::IsMacAccessibilityTextOperationEnabled()) {
    if ([attribute isEqualToString:
                       NSAccessibilityTextOperationParameterizedAttribute]) {
      return [self handleTextOperation:parameter];
    }
  }

  return [super accessibilityAttributeValue:attribute forParameter:parameter];
}

- (NSArray<NSString*>*)handleTextOperation:(id)parameter {
  NSDictionary* dict = ObjCCast<NSDictionary>(parameter);
  if (!dict) {
    return nil;
  }

  NSString* operationType = ObjCCast<NSString>(dict[@"AXTextOperationType"]);
  if (!operationType) {
    return nil;
  }

  NSArray* ranges = ObjCCast<NSArray>(dict[@"AXTextOperationMarkerRanges"]);
  if (!ranges.count) {
    return nil;
  }

  NSUInteger count = ranges.count;
  NSArray<NSString*>* replacementStrings;

  if ([operationType isEqualToString:@"TextOperationReplace"] ||
      [operationType isEqualToString:@"TextOperationReplacePreserveCase"]) {
    replacementStrings =
        ObjCCast<NSArray>(dict[@"AXTextOperationIndividualReplacementStrings"]);
    if (replacementStrings) {
      // The count of individual replacement strings must match the count of the
      // provided ranges for a valid text operation request.
      if (replacementStrings.count != count) {
        return nil;
      }

      for (id item in replacementStrings) {
        if (![item isKindOfClass:[NSString class]]) {
          return nil;
        }
      }
    } else {
      // When individual replacement strings aren't provided, we check for a
      // single replacement string that is applied to all of the ranges. This
      // matches WebKit's behavior at the time of writing.
      NSString* replacementString =
          ObjCCast<NSString>(dict[@"AXTextOperationReplacementString"]);
      if (!replacementString) {
        return nil;
      }

      NSMutableArray<NSString*>* strings = [[NSMutableArray alloc] init];
      for (NSUInteger i = 0; i < count; ++i) {
        [strings addObject:replacementString];
      }
      replacementStrings = strings;
    }
  }

  // Selects the text at the provided ranges. Unlike other operations it does
  // not perform a text replacement.
  if ([operationType isEqualToString:@"TextOperationSelect"]) {
    NSMutableArray<NSString*>* resultStrings =
        [[NSMutableArray alloc] initWithCapacity:count];
    for (NSUInteger i = 0; i < count; ++i) {
      NSString* text = GetTextForTextMarkerRange(ranges[i]);
      if (text) {
        // Based on WebKit's behavior at the time of writing; when multiple
        // ranges are provided, we return the associated text for each of the
        // ranges.
        [resultStrings addObject:text];
      }
    }

    // Only the last range is visually selected.
    AXRange range = AXTextMarkerRangeToAXRange(ranges.lastObject);
    if (range.IsNull()) {
      return nil;
    }

    _owner->manager()->SetSelection(range);
    return resultStrings;
  }

  // Replaces the text at the provided ranges, without modifying the provided
  // replacement text (unlike TextOperationReplace below which attempts to
  // match the case of the replaced string).
  if ([operationType isEqualToString:@"TextOperationReplacePreserveCase"]) {
    return [self applyTextOperationReplacementToRanges:ranges
                                    computeReplacement:^(NSUInteger i) {
      return replacementStrings[i];
    }];
  }

  // Replaces the text at the provided ranges, while modifying the replacement
  // text to match the case of the text being replaced.
  if ([operationType isEqualToString:@"TextOperationReplace"]) {
    return [self
        applyTextOperationReplacementToRanges:ranges
                           computeReplacement:^NSString*(NSUInteger i) {
      NSString* text =
         GetTextForTextMarkerRange(ranges[i]);
      if (!text) {
       return nil;
      }

      // Like in WebKit, we adjust the replacement string's case to match that
      // of the text being replaced, unless the replacement string is all caps
      // (assumed to be an acronym).
      //
      // This is useful for a find-and-replace style operation, where you have
      // multiple case-insensitive occurrences of a string and want to replace
      // them a new string.
      //
      // For example:
      //   Original text -> Replacement String == Actual text after replacement
      //   Hello         -> hey                == Hey           (capitalized)
      //   Hello         -> hey there          == Hey there     (capitalized)
      //   hello         -> Hey                == hey           (lowercased)
      //   hello         -> HEY                == HEY           (unmodified)
      //   hello         -> hey                == hey           (unmodified)
      NSString* replacement = replacementStrings[i];
      if (text.length && replacement.length > 2 &&
          ![replacement isEqualTo:replacement.localizedUppercaseString]) {
        NSString* firstChar = [text substringToIndex:1];
        if ([firstChar isEqualTo:firstChar.localizedUppercaseString]) {
          // Capitalize the first word when the first char of the existing
          // text is uppercase.
          // To correctly handle locale-aware capitalization, we should operate
          // on grapheme clusters rather than raw UTF-16 code units.
          // For simplicity, we capitalize the entire first word, which safely
          // includes the first grapheme cluster.
          NSRange firstSpace = [replacement
              rangeOfCharacterFromSet:NSCharacterSet
                                          .whitespaceAndNewlineCharacterSet];
          NSUInteger wordEndIndex = (firstSpace.location != NSNotFound)
                                        ? firstSpace.location
                                        : replacement.length;
          NSString* firstWord = [replacement substringToIndex:wordEndIndex];
          NSString* capitalizedFirstWord = firstWord.localizedCapitalizedString;
          NSString* rest = (wordEndIndex < replacement.length)
                               ? [replacement substringFromIndex:wordEndIndex]
                               : @"";
          replacement = [capitalizedFirstWord stringByAppendingString:rest];
        } else {
          // Lowercase the entire string when the first letter is lowercase.
          replacement = replacement.localizedLowercaseString;
        }
      }
      return replacement;
    }];
  }

  // Capitalizes the text at the provided ranges.
  if ([operationType isEqualToString:@"Capitalize"]) {
    return [self applyTextOperationReplacementToRanges:ranges
                                    computeReplacement:^(NSUInteger i) {
      return [GetTextForTextMarkerRange(ranges[i]) localizedCapitalizedString];
    }];
  }

  // Lowercases the text at the provided ranges.
  if ([operationType isEqualToString:@"Lowercase"]) {
    return [self applyTextOperationReplacementToRanges:ranges
                                    computeReplacement:^(NSUInteger i) {
      return [GetTextForTextMarkerRange(ranges[i]) localizedLowercaseString];
    }];
  }

  // Uppercases the text at the provided ranges.
  if ([operationType isEqualToString:@"Uppercase"]) {
    return [self applyTextOperationReplacementToRanges:ranges
                                    computeReplacement:^(NSUInteger i) {
      return [GetTextForTextMarkerRange(ranges[i]) localizedUppercaseString];
    }];
  }

  return nil;
}

- (NSArray<NSString*>*)applyTextOperationReplacementToRanges:(NSArray*)ranges
                                          computeReplacement:
                                              (NSString* (^)(NSUInteger))
                                                  computeReplacement {
  NSUInteger count = ranges.count;

  std::vector<AXRange> ranges_vector;
  std::vector<std::string> replacement_strings_vector;
  ranges_vector.reserve(count);
  replacement_strings_vector.reserve(count);
  NSMutableArray<NSString*>* resultStrings =
      [[NSMutableArray alloc] initWithCapacity:count];

  for (NSUInteger i = 0; i < count; ++i) {
    AXRange range = AXTextMarkerRangeToAXRange(ranges[i]);
    if (range.IsNull()) {
      continue;
    }

    NSString* transformedText = computeReplacement(i);
    if (!transformedText) {
      continue;
    }

    ranges_vector.push_back(std::move(range));
    [resultStrings addObject:transformedText];
    replacement_strings_vector.push_back(
        base::SysNSStringToUTF8(transformedText));
  }

  if (!ranges_vector.size() || !replacement_strings_vector.size()) {
    return nil;
  }

  _owner->manager()->ReplaceRanges(*_owner, ranges_vector,
                                   replacement_strings_vector);
  return resultStrings;
}

// Returns an array of parameterized attributes names that this object will
// respond to.
- (NSArray*)internalAccessibilityParameterizedAttributeNames {
  if (![self instanceActive])
    return nil;

  // General attributes.
  NSMutableArray* attributeNames = [@[
    NSAccessibilityAttributedStringForTextMarkerRangeWithOptionsParameterizedAttribute,
    NSAccessibilityBoundsForRangeParameterizedAttribute,
    CFToNSPtrCast(kAXBoundsForTextMarkerRangeParameterizedAttribute),
    NSAccessibilityEndTextMarkerForBoundsParameterizedAttribute,
    NSAccessibilityIndexForChildUIElementParameterizedAttribute,
    CFToNSPtrCast(
        kAXLeftLineTextMarkerRangeForTextMarkerParameterizedAttribute),
    CFToNSPtrCast(
        kAXLeftWordTextMarkerRangeForTextMarkerParameterizedAttribute),
    CFToNSPtrCast(kAXLengthForTextMarkerRangeParameterizedAttribute),
    CFToNSPtrCast(kAXLineForTextMarkerParameterizedAttribute),
    CFToNSPtrCast(kAXLineTextMarkerRangeForTextMarkerParameterizedAttribute),
    CFToNSPtrCast(kAXNextLineEndTextMarkerForTextMarkerParameterizedAttribute),
    CFToNSPtrCast(
        kAXNextParagraphEndTextMarkerForTextMarkerParameterizedAttribute),
    CFToNSPtrCast(
        kAXNextSentenceEndTextMarkerForTextMarkerParameterizedAttribute),
    CFToNSPtrCast(kAXNextTextMarkerForTextMarkerParameterizedAttribute),
    CFToNSPtrCast(kAXNextWordEndTextMarkerForTextMarkerParameterizedAttribute),
    CFToNSPtrCast(
        kAXParagraphTextMarkerRangeForTextMarkerParameterizedAttribute),
    CFToNSPtrCast(
        kAXPreviousLineStartTextMarkerForTextMarkerParameterizedAttribute),
    CFToNSPtrCast(
        kAXPreviousParagraphStartTextMarkerForTextMarkerParameterizedAttribute),
    CFToNSPtrCast(
        kAXPreviousSentenceStartTextMarkerForTextMarkerParameterizedAttribute),
    CFToNSPtrCast(kAXPreviousTextMarkerForTextMarkerParameterizedAttribute),
    CFToNSPtrCast(
        kAXPreviousWordStartTextMarkerForTextMarkerParameterizedAttribute),
    CFToNSPtrCast(
        kAXRightLineTextMarkerRangeForTextMarkerParameterizedAttribute),
    CFToNSPtrCast(
        kAXRightWordTextMarkerRangeForTextMarkerParameterizedAttribute),
    NSAccessibilitySelectTextWithCriteriaParameterizedAttribute,
    CFToNSPtrCast(
        kAXSentenceTextMarkerRangeForTextMarkerParameterizedAttribute),
    NSAccessibilityStartTextMarkerForBoundsParameterizedAttribute,
    NSAccessibilityStringForRangeParameterizedAttribute,
    CFToNSPtrCast(kAXStringForTextMarkerRangeParameterizedAttribute),
    CFToNSPtrCast(kAXStyleTextMarkerRangeForTextMarkerParameterizedAttribute),
    CFToNSPtrCast(kAXTextMarkerForPositionParameterizedAttribute),
    CFToNSPtrCast(kAXTextMarkerRangeForLineParameterizedAttribute),
    CFToNSPtrCast(kAXTextMarkerRangeForUIElementParameterizedAttribute),
    CFToNSPtrCast(
        kAXTextMarkerRangeForUnorderedTextMarkersParameterizedAttribute),
    NSAccessibilityUIElementCountForSearchPredicateParameterizedAttribute,
    CFToNSPtrCast(kAXUIElementForTextMarkerParameterizedAttribute),
    NSAccessibilityUIElementsForSearchPredicateParameterizedAttribute
  ] mutableCopy];

  if ([[self role] isEqualToString:NSAccessibilityTableRole] ||
      [[self role] isEqualToString:NSAccessibilityGridRole]) {
    [attributeNames
        addObject:NSAccessibilityCellForColumnAndRowParameterizedAttribute];
  }

  if (_owner->HasState(ax::mojom::State::kEditable)) {
    [attributeNames addObjectsFromArray:@[
      NSAccessibilityLineForIndexParameterizedAttribute,
      NSAccessibilityRangeForLineParameterizedAttribute,
      NSAccessibilityRangeForPositionParameterizedAttribute,
      NSAccessibilityRangeForIndexParameterizedAttribute,
      NSAccessibilityBoundsForRangeParameterizedAttribute,
      NSAccessibilityRTFForRangeParameterizedAttribute,
      NSAccessibilityStyleRangeForIndexParameterizedAttribute
    ]];
  }

  if ([self internalRole] == ax::mojom::Role::kStaticText)
    [attributeNames
        addObject:NSAccessibilityBoundsForRangeParameterizedAttribute];

  if (ui::IsPlatformDocument(_owner->GetRole())) {
    [attributeNames addObjectsFromArray:@[
      CFToNSPtrCast(kAXTextMarkerIsValidParameterizedAttribute),
      CFToNSPtrCast(kAXIndexForTextMarkerParameterizedAttribute),
      CFToNSPtrCast(kAXTextMarkerForIndexParameterizedAttribute)
    ]];
  }

  if (features::IsMacAccessibilityTextOperationEnabled()) {
    [attributeNames
        addObject:NSAccessibilityTextOperationParameterizedAttribute];
  }

  NSArray* superclassAttributeNames =
      [super internalAccessibilityParameterizedAttributeNames];
  [attributeNames addObjectsFromArray:superclassAttributeNames];
  return attributeNames;
}

// Returns an array of action names that this object will respond to.
- (NSMutableArray*)internalAccessibilityActionNames {
  if (![self instanceActive]) {
    return [NSMutableArray array];
  }

  NSMutableArray* actions = [NSMutableArray
      arrayWithObjects:NSAccessibilityShowMenuAction,
                       NSAccessibilityScrollToVisibleAction, nil];

  // VoiceOver expects the "press" action to be first.
  if (_owner->HasDefaultAction()) {
    [actions insertObject:NSAccessibilityPressAction atIndex:0];
  }

  if (ui::IsMenuRelated(_owner->GetRole()))
    [actions addObject:NSAccessibilityCancelAction];

  if ([self internalRole] == ax::mojom::Role::kSlider ||
      [self internalRole] == ax::mojom::Role::kSpinButton) {
    [actions addObjectsFromArray:@[
      NSAccessibilityIncrementAction, NSAccessibilityDecrementAction
    ]];
  }

  return actions;
}

// Returns the list of accessibility attributes that this object supports.
- (NSMutableArray*)internalAccessibilityAttributeNames {
  if (![self instanceActive])
    return nil;

  // General attributes.
  NSMutableArray* ret = [@[
    NSAccessibilityChildrenAttribute, NSAccessibilityEnabledAttribute,
    CFToNSPtrCast(kAXEndTextMarkerAttribute), NSAccessibilityFocusedAttribute,
    NSAccessibilityLinkedUIElementsAttribute, NSAccessibilityParentAttribute,
    NSAccessibilityPositionAttribute, NSAccessibilityRoleAttribute,
    NSAccessibilityRoleDescriptionAttribute,
    CFToNSPtrCast(kAXSelectedTextMarkerRangeAttribute),
    CFToNSPtrCast(kAXStartTextMarkerAttribute), NSAccessibilitySubroleAttribute,
    NSAccessibilityTopLevelUIElementAttribute, NSAccessibilityValueAttribute,
    NSAccessibilityWindowAttribute
  ] mutableCopy];

  // Specific role attributes.
  NSString* role = [self role];
  NSString* subrole = [self subrole];
  if ([role isEqualToString:NSAccessibilityTableRole] ||
      [role isEqualToString:NSAccessibilityGridRole]) {
    [ret addObjectsFromArray:@[
      NSAccessibilityColumnsAttribute,
      NSAccessibilityVisibleColumnsAttribute,
      NSAccessibilityRowsAttribute,
      NSAccessibilityVisibleRowsAttribute,
      NSAccessibilityVisibleCellsAttribute,
      NSAccessibilityHeaderAttribute,
      NSAccessibilityRowHeaderUIElementsAttribute,
    ]];
  } else if ([role isEqualToString:NSAccessibilityColumnRole]) {
    [ret addObjectsFromArray:@[
      NSAccessibilityIndexAttribute, NSAccessibilityHeaderAttribute,
      NSAccessibilityRowsAttribute, NSAccessibilityVisibleRowsAttribute
    ]];
  } else if ([role isEqualToString:NSAccessibilityCellRole]) {
    [ret addObjectsFromArray:@[
      NSAccessibilityColumnIndexRangeAttribute,
      NSAccessibilityRowIndexRangeAttribute,
    ]];
    if ([self internalRole] == ax::mojom::Role::kRowHeader ||
        [self internalRole] == ax::mojom::Role::kColumnHeader) {
      // The Core-AAM states that `aria-sort=none` is "not mapped".
      int sortDirection;
      if (_owner->GetIntAttribute(ax::mojom::IntAttribute::kSortDirection,
                                  &sortDirection) &&
          static_cast<ax::mojom::SortDirection>(sortDirection) !=
              ax::mojom::SortDirection::kUnsorted) {
        [ret addObject:CFToNSPtrCast(kAXSortDirectionAttribute)];
      }
    }
    if ([self internalRole] != ax::mojom::Role::kRowHeader)
      [ret addObject:NSAccessibilityRowHeaderUIElementsAttribute];
  } else if ([role isEqualToString:NSAccessibilityTabGroupRole]) {
    [ret addObject:NSAccessibilityTabsAttribute];
  } else if (_owner->GetData().IsRangeValueSupported()) {
    [ret addObjectsFromArray:@[
      NSAccessibilityMaxValueAttribute, NSAccessibilityMinValueAttribute,
      NSAccessibilityValueDescriptionAttribute
    ]];
  } else if ([role isEqualToString:NSAccessibilityRowRole]) {
    BrowserAccessibility* container = _owner->PlatformGetParent();
    if (container && container->GetRole() == ax::mojom::Role::kRowGroup)
      container = container->PlatformGetParent();
    if ([subrole isEqualToString:NSAccessibilityOutlineRowSubrole] ||
        (container && container->GetRole() == ax::mojom::Role::kTreeGrid)) {
      // clang-format off
      [ret addObjectsFromArray:@[
        NSAccessibilityIndexAttribute,
        NSAccessibilityDisclosedByRowAttribute,
        NSAccessibilityDisclosedRowsAttribute,
        NSAccessibilityDisclosingAttribute,
        NSAccessibilityDisclosureLevelAttribute
      ]];
      // clang-format on
    } else {
      [ret addObject:NSAccessibilityIndexAttribute];
    }
  } else if ([self internalRole] == ax::mojom::Role::kHeading) {
    // Heading level is exposed in both AXDisclosureLevel and AXValue.
    // Safari also exposes the level in both.
    [ret addObject:NSAccessibilityDisclosureLevelAttribute];
  } else if ([role isEqualToString:NSAccessibilityListRole]) {
    [ret addObjectsFromArray:@[
      NSAccessibilitySelectedChildrenAttribute,
      NSAccessibilityVisibleChildrenAttribute
    ]];
  } else if ([role isEqualToString:NSAccessibilityOutlineRole]) {
    [ret addObjectsFromArray:@[
      NSAccessibilityRowsAttribute, NSAccessibilityColumnsAttribute,
      NSAccessibilityOrientationAttribute
    ]];
  }

  if (_owner->IsTextField()) {
    [ret addObjectsFromArray:@[
      NSAccessibilityInsertionPointLineNumberAttribute,
      NSAccessibilityNumberOfCharactersAttribute,
      NSAccessibilityPlaceholderValueAttribute,
      NSAccessibilitySelectedTextAttribute,
      NSAccessibilitySelectedTextRangeAttribute,
      NSAccessibilityVisibleCharacterRangeAttribute,
      CFToNSPtrCast(kAXValueAutofillAvailableAttribute),
      // Not currently supported by Chrome:
      // NSAccessibilityValueAutofilledAttribute,
      // Not currently supported by Chrome:
      // NSAccessibilityValueAutofillTypeAttribute
    ]];
  }

  if (GetState(_owner, ax::mojom::State::kExpanded) ||
      GetState(_owner, ax::mojom::State::kCollapsed)) {
    [ret addObject:NSAccessibilityExpandedAttribute];
  }

  if (GetState(_owner, ax::mojom::State::kVertical) ||
      GetState(_owner, ax::mojom::State::kHorizontal)) {
    [ret addObject:NSAccessibilityOrientationAttribute];
  }

  // TODO(accessibility) What nodes should language be exposed on given new
  // auto detection features?
  //
  // Once lang attribute inheritance becomes stable most nodes will have a
  // language, so it may make more sense to always expose this attribute.
  //
  // For now we expose the language attribute if we have any language set.
  if (_owner->node() && !_owner->node()->GetLanguage().empty()) {
    [ret addObject:NSAccessibilityLanguageAttribute];
  }

  // TODO(aboxhall): expose NSAccessibilityServesAsTitleForUIElementsAttribute
  // for elements which are referred to by labelledby or are labels

  // Advertise the dump-test projection attribute only when test
  // infrastructure has opted in AND the node actually has custom actions,
  // so dump-test baselines stay focused on the nodes under test rather
  // than emitting an empty AXCustomActionNamesForTesting=[] entry on
  // every node in the tree.
  if (ui::IsAXCustomActionNamesForTestingProjectionEnabled() && _owner &&
      _owner->HasState(ax::mojom::State::kHasActions)) {
    [ret addObject:kAXCustomActionNamesForTestingAttribute];
  }

  [ret addObjectsFromArray:[super internalAccessibilityAttributeNames]];
  return ret;
}

// Returns the index of the child in this objects array of children.
- (NSUInteger)accessibilityGetIndexOf:(id)child {
  TRACE_EVENT1("accessibility",
               "BrowserAccessibilityCocoa::accessibilityGetIndexOf",
               "role=", ui::ToString([self internalRole]));
  if (![self instanceActive])
    return 0;

  NSUInteger index = 0;
  for (AXPlatformNodeCocoa* childToCheck in [self accessibilityChildren]) {
    if ([child isEqual:childToCheck])
      return index;
    ++index;
  }
  return NSNotFound;
}

// Returns whether or not the specified attribute can be set by the
// accessibility API via |accessibilitySetValue:forAttribute:|.
// This API is deprecated.
- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
  TRACE_EVENT2("accessibility",
               "BrowserAccessibilityCocoa::accessibilityIsAttributeSettable",
               "role=", ui::ToString([self internalRole]),
               "attribute=", base::SysNSStringToUTF8(attribute));
  if (![self instanceActive]) {
    return NO;
  }

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

  if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) {
    if ([self internalRole] == ax::mojom::Role::kDateTime) {
      return NO;
    }

    return _owner->IsFocusable();
  }

  if ([attribute isEqualToString:NSAccessibilityValueAttribute]) {
    return _owner->HasAction(ax::mojom::Action::kSetValue);
  }

  if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute] &&
      _owner->HasState(ax::mojom::State::kEditable)) {
    return YES;
  }

  if ([attribute
          isEqualToString:CFToNSPtrCast(kAXSelectedTextMarkerRangeAttribute)]) {
    return YES;
  }

  return NO;
}

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

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

- (NSRect)accessibilityFrame {
  if (![self instanceActive])
    return NSZeroRect;

  BrowserAccessibilityManager* manager = _owner->manager();

  // Clipping a table's cells results in cells with zero height or width. This
  // causes VoiceOver to treat these cells as if they don't exist at all,
  // making it impossible for the user to use VO to navigate to them. Instead,
  // make sure all rows and cells have an extent, even if not visible.
  ax::mojom::Role role = _owner->GetRole();
  bool isTableComponent = ui::IsTableColumn(role) || ui::IsTableRow(role) ||
                          ui::IsCellOrTableHeader(role);
  ui::AXClippingBehavior clipping_behavior =
      isTableComponent ? ui::AXClippingBehavior::kUnclipped
                       : ui::AXClippingBehavior::kClipped;
  auto rect = _owner->GetBoundsRect(ui::AXCoordinateSystem::kScreenDIPs,
                                    clipping_behavior);

  // TODO(vmpstr): GetBoundsRect() call above should account for this instead.
  auto result_rect =
      ScaleToRoundedRect(rect, 1.f / manager->device_scale_factor());

  return gfx::ScreenRectToNSRect(result_rect);
}

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

  return _owner->GetData().HasCheckedState() ||
         _owner->GetRole() == ax::mojom::Role::kTab;
}

// Performs the given accessibility action on the webkit accessibility object
// that backs this object.
// This API is deprecated.
- (void)accessibilityPerformAction:(NSString*)action {
  TRACE_EVENT2("accessibility",
               "BrowserAccessibilityCocoa::accessibilityPerformAction",
               "role=", ui::ToString([self internalRole]),
               "action=", base::SysNSStringToUTF8(action));
  if (![self instanceActive]) {
    return;
  }

  // TODO(dmazzoni): Support more actions.
  BrowserAccessibility* actionTarget = [self actionTarget];
  BrowserAccessibilityManager* manager = actionTarget->manager();
  if ([action isEqualToString:NSAccessibilityPressAction]) {
    // LINT.IfChange(NSAccessibilityPressAction)
    ui::AXNode* node = actionTarget->node();
    if (!node || !actionTarget->HasDefaultAction()) {
      return;
    }

    manager->DoDefaultAction(*actionTarget);
    if (actionTarget->GetData().GetRestriction() !=
            ax::mojom::Restriction::kNone ||
        ![self isCheckable]) {
      return;
    }

    // Hack: preemptively set the checked state to what it should become,
    // otherwise VoiceOver will very likely report the old, incorrect state to
    // the user as it requests the value too quickly.
    AXNodeData data(node->TakeData());  // Temporarily take data.
    if (data.role == ax::mojom::Role::kRadioButton) {
      data.SetCheckedState(ax::mojom::CheckedState::kTrue);
    } else if (data.role == ax::mojom::Role::kCheckBox ||
               data.role == ax::mojom::Role::kSwitch ||
               data.role == ax::mojom::Role::kToggleButton) {
      ax::mojom::CheckedState checkedState = data.GetCheckedState();
      ax::mojom::CheckedState newCheckedState =
          checkedState == ax::mojom::CheckedState::kFalse
              ? ax::mojom::CheckedState::kTrue
              : ax::mojom::CheckedState::kFalse;
      data.SetCheckedState(newCheckedState);
    }
    node->SetData(data);  // Set the data back in the node.
    // LINT.ThenChange(accessibilityPerformPress)
  } else if ([action isEqualToString:NSAccessibilityShowMenuAction]) {
    manager->ShowContextMenu(*actionTarget);
  } else if ([action isEqualToString:NSAccessibilityScrollToVisibleAction]) {
    ui::AXPlatformNodeBase* mac_obj =
        [ObjCCastStrict<BrowserAccessibilityCocoa>(
            actionTarget->GetNativeViewAccessible().Get()) node];
    mac_obj->ScrollToNode(ui::AXPlatformNodeMac::ScrollType::Anywhere);
  } else if ([action isEqualToString:NSAccessibilityIncrementAction]) {
    manager->Increment(*actionTarget);
  } else if ([action isEqualToString:NSAccessibilityDecrementAction]) {
    manager->Decrement(*actionTarget);
  }
}

// LINT.IfChange(accessibilityPerformPress)
- (BOOL)accessibilityPerformPress {
  if (![self instanceActive]) {
    return NO;
  }

  BrowserAccessibility* actionTarget = [self actionTarget];
  ui::AXNode* node = actionTarget->node();
  if (!node || !actionTarget->HasDefaultAction()) {
    return NO;
  }

  BrowserAccessibilityManager* manager = actionTarget->manager();
  manager->DoDefaultAction(*actionTarget);
  if (actionTarget->GetData().GetRestriction() !=
          ax::mojom::Restriction::kNone ||
      ![self isCheckable]) {
    return NO;
  }

  // Hack: preemptively set the checked state to what it should become,
  // otherwise VoiceOver will very likely report the old, incorrect state to
  // the user as it requests the value too quickly.
  AXNodeData data(node->TakeData());  // Temporarily take data.
  if (data.role == ax::mojom::Role::kRadioButton) {
    data.SetCheckedState(ax::mojom::CheckedState::kTrue);
  } else if (data.role == ax::mojom::Role::kCheckBox ||
             data.role == ax::mojom::Role::kSwitch ||
             data.role == ax::mojom::Role::kToggleButton) {
    ax::mojom::CheckedState checkedState = data.GetCheckedState();
    ax::mojom::CheckedState newCheckedState =
        checkedState == ax::mojom::CheckedState::kFalse
            ? ax::mojom::CheckedState::kTrue
            : ax::mojom::CheckedState::kFalse;
    data.SetCheckedState(newCheckedState);
  }
  node->SetData(data);  // Set the data back in the node.
  return YES;
}

- (BOOL)accessibilityPerformShowMenu {
  if (![self instanceActive]) {
    return NO;
  }
  BrowserAccessibility* actionTarget = [self actionTarget];
  BrowserAccessibilityManager* manager = actionTarget->manager();
  manager->ShowContextMenu(*actionTarget);
  return YES;
}

// Returns the description of the given action.
- (NSString*)accessibilityActionDescription:(NSString*)action {
  TRACE_EVENT2("accessibility",
               "BrowserAccessibilityCocoa::accessibilityActionDescription",
               "role=", ui::ToString([self internalRole]),
               "action=", base::SysNSStringToUTF8(action));
  if (![self instanceActive])
    return nil;

  return NSAccessibilityActionDescription(action);
}
// LINT.ThenChange(NSAccessibilityPressAction)

- (NSArray<NSAccessibilityCustomAction*>*)accessibilityCustomActions {
  if (![self instanceActive]) {
    return nil;
  }

  if (!_owner->HasState(ax::mojom::State::kHasActions)) {
    return nil;
  }

  const std::vector<int32_t>& action_ids =
      _owner->GetIntListAttribute(ax::mojom::IntListAttribute::kActionsIds);
  if (action_ids.empty()) {
    return nil;
  }

  NSMutableArray<NSAccessibilityCustomAction*>* custom_actions =
      [NSMutableArray arrayWithCapacity:action_ids.size()];
  BrowserAccessibilityManager* manager = _owner->manager();

  for (int32_t target_id : action_ids) {
    BrowserAccessibility* target = manager->GetFromID(target_id);
    if (!target) {
      continue;
    }

    std::u16string name = target->GetNameAsString16();
    if (name.empty()) {
      continue;
    }

    NSString* action_name = base::SysUTF16ToNSString(name);
    // Capture target_id by value; re-resolve the target in the handler to
    // handle tree mutations between action creation and invocation.
    int32_t captured_target_id = target_id;
    __weak BrowserAccessibilityCocoa* weak_self = self;
    NSAccessibilityCustomAction* action = [[NSAccessibilityCustomAction alloc]
        initWithName:action_name
             handler:^BOOL {
               BrowserAccessibilityCocoa* strong_self = weak_self;
               if (!strong_self || ![strong_self instanceActive]) {
                 return NO;
               }
               BrowserAccessibilityManager* mgr =
                   strong_self->_owner->manager();
               BrowserAccessibility* action_target =
                   mgr->GetFromID(captured_target_id);
               if (!action_target) {
                 return NO;
               }
               mgr->DoDefaultAction(*action_target);
               return YES;
             }];
    [custom_actions addObject:action];
  }

  return custom_actions.count > 0 ? custom_actions : nil;
}

// Projects the production -accessibilityCustomActions return value to a
// flat NSArray<NSString*> of action names. Exists so dump tests can read
// custom-action names through AXUIElementCopyAttributeValue, which cannot
// marshal opaque NSAccessibilityCustomAction objects across processes.
// See kAXCustomActionNamesForTestingAttribute for the surface-isolation
// contract.
//
// Returns nil (rather than an empty array) when:
//   - the test infrastructure has not called
//     ui::EnableAXCustomActionNamesForTestingProjection() (i.e. the
//     runtime opt-in is unset, which is the case in shipped Chrome and
//     in any browsertest that has not explicitly opted in), so direct
//     -accessibilityAttributeValue: queries by same-process callers
//     see nothing, OR
//   - the node has no custom actions.
// The dump-test formatter's AXOptionalNSObject filter treats nil as
// "not applicable" and omits it from baselines, in addition to the
// enumeration gate.
- (NSArray<NSString*>*)customActionNamesForTesting {
  if (!ui::IsAXCustomActionNamesForTestingProjectionEnabled()) {
    return nil;
  }
  NSArray<NSAccessibilityCustomAction*>* actions =
      [self accessibilityCustomActions];
  if (actions.count == 0) {
    return nil;
  }
  NSMutableArray<NSString*>* names =
      [NSMutableArray arrayWithCapacity:actions.count];
  for (NSAccessibilityCustomAction* action in actions) {
    CHECK(action.name);
    [names addObject:action.name];
  }
  return names;
}

// Sets an override value for a specific accessibility attribute.
// This class does not support this.
- (BOOL)accessibilitySetOverrideValue:(id)value
                         forAttribute:(NSString*)attribute {
  TRACE_EVENT2(
      "accessibility",
      "BrowserAccessibilityCocoa::accessibilitySetOverrideValue:forAttribute",
      "role=", ui::ToString([self internalRole]),
      "attribute=", base::SysNSStringToUTF8(attribute));
  if (![self instanceActive])
    return NO;
  return NO;
}

// Sets the value for an accessibility attribute via the accessibility API.
// This API is deprecated.
- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute {
  TRACE_EVENT2("accessibility",
               "BrowserAccessibilityCocoa::accessibilitySetValue:forAttribute",
               "role=", ui::ToString([self internalRole]),
               "attribute=", base::SysNSStringToUTF8(attribute));
  if (![self instanceActive])
    return;

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

  if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) {
    [self setAccessibilityFocused:[value boolValue]];
  }
  if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) {
    if (std::optional<NSRange> range = ui::NSValueGetRange(value)) {
      [self setAccessibilitySelectedTextRange:range.value()];
    }
  }
  if ([attribute
          isEqualToString:CFToNSPtrCast(kAXSelectedTextMarkerRangeAttribute)] &&
      // Condition also on when this node is editable. VoiceOver as of Mac 13
      // sets selections as users navigate on read only content. This has
      // adverse side effects on VoiceOver's a11y focus causing loops in
      // navigation.
      _owner->HasState(ax::mojom::State::kEditable)) {
    AXRange range = AXTextMarkerRangeToAXRange(value);
    if (range.IsNull()) {
      return;
    }
    BrowserAccessibilityManager* manager = _owner->manager();
    manager->SetSelection(AXRange(range.anchor()->AsDomSelectionPosition(),
                                  range.focus()->AsDomSelectionPosition()));
  }
}

- (id)accessibilityFocusedUIElement {
  TRACE_EVENT1("accessibility",
               "BrowserAccessibilityCocoa::accessibilityFocusedUIElement",
               "role=", ui::ToString([self internalRole]));
  if (![self instanceActive])
    return nil;

  ui::AXPlatformNodeDelegate* focus_node = _owner->manager()->GetFocus();
  return focus_node ? focus_node->GetNativeViewAccessible().Get() : nullptr;
}

// Returns the deepest accessibility child that should not be ignored.
// It is assumed that the hit test has been narrowed down to this object
// or one of its children, so this will never return nil unless this
// object is invalid.
- (id)accessibilityHitTest:(NSPoint)point {
  TRACE_EVENT2("accessibility",
               "BrowserAccessibilityCocoa::accessibilityHitTest",
               "role=", ui::ToString([self internalRole]),
               "point=", base::SysNSStringToUTF8(NSStringFromPoint(point)));
  if (![self instanceActive])
    return nil;

  // The point we receive is in frame coordinates.
  // Convert to screen coordinates and then to physical pixel coordinates.
  BrowserAccessibilityManager* manager = _owner->manager();
  gfx::Point screen_point_in_dips(point.x, point.y);

  auto offset_in_blink_space =
      manager->GetViewBoundsInScreenCoordinates().OffsetFromOrigin();

  // Blink space is physical, so we scale the
  // point first then add the offset. Otherwise, it's in DIPs so we add the
  // offset first and then scale.
  // TODO(vmpstr): GetViewBoundsInScreenCoordinates should return consistent
  // space.
  gfx::Point screen_point_in_physical_space;
  screen_point_in_physical_space =
      ScaleToRoundedPoint(screen_point_in_dips, manager->device_scale_factor());
  screen_point_in_physical_space += offset_in_blink_space;

  BrowserAccessibility* hit =
      manager->CachingAsyncHitTest(screen_point_in_physical_space);
  if (!hit)
    return nil;

  return NSAccessibilityUnignoredAncestor(hit->GetNativeViewAccessible().Get());
}

- (BOOL)isEqual:(id)object {
  if (BrowserAccessibilityCocoa* objectAccessibility =
          ObjCCast<BrowserAccessibilityCocoa>(object)) {
    return _uniqueId == objectAccessibility->_uniqueId;
  }

  return NO;
}

- (NSUInteger)hash {
  return _uniqueId;
}

- (BOOL)accessibilityNotifiesWhenDestroyed {
  TRACE_EVENT0("accessibility",
               "BrowserAccessibilityCocoa::accessibilityNotifiesWhenDestroyed");
  // Indicate that BrowserAccessibilityCocoa will post a notification when it's
  // destroyed (see -detach). This allows VoiceOver to do some internal things
  // more efficiently.
  return YES;
}

// Choose the appropriate accessibility object to receive an action depending
// on the characteristics of this accessibility node.
- (BrowserAccessibility*)actionTarget {
  // When an action is triggered on a container with selectable children and
  // one of those children is an active descendant or focused, retarget the
  // action to that child. See https://crbug.com/40711038.
  if (!ui::IsContainerWithSelectableChildren(_owner->GetRole()))
    return _owner;

  // Active descendant takes priority over focus, because the webpage author has
  // explicitly designated a different behavior for users of assistive software.
  BrowserAccessibility* activeDescendant =
      _owner->manager()->GetActiveDescendant(_owner);
  if (activeDescendant != _owner)
    return activeDescendant;

  BrowserAccessibility* focus = _owner->manager()->GetFocus();
  if (focus && focus->IsDescendantOf(_owner))
    return focus;

  return _owner;
}

@end
