chromium/ui/accessibility/platform/ax_platform_node_win.cc

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "ui/accessibility/platform/ax_platform_node_win.h"

#include <wrl/client.h>
#include <wrl/implements.h>

#include <map>
#include <set>
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>

#include "base/json/json_writer.h"
#include "base/lazy_instance.h"
#include "base/metrics/histogram_functions.h"
#include "base/numerics/safe_conversions.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_number_conversions_win.h"
#include "base/strings/string_util.h"
#include "base/strings/string_util_win.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/trace_event/typed_macros.h"
#include "base/values.h"
#include "base/win/enum_variant.h"
#include "base/win/scoped_bstr.h"
#include "base/win/scoped_safearray.h"
#include "base/win/scoped_variant.h"
#include "base/win/variant_vector.h"
#include "base/win/win_util.h"
#include "base/win/windows_version.h"
#include "skia/ext/skia_utils_win.h"
#include "third_party/iaccessible2/ia2_api_all.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_action_handler_registry.h"
#include "ui/accessibility/ax_active_popup.h"
#include "ui/accessibility/ax_constants.mojom.h"
#include "ui/accessibility/ax_enum_localization_util.h"
#include "ui/accessibility/ax_enum_util.h"
#include "ui/accessibility/ax_enums.mojom-shared.h"
#include "ui/accessibility/ax_mode_observer.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/accessibility/ax_node_position.h"
#include "ui/accessibility/ax_position.h"
#include "ui/accessibility/ax_range.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/accessibility/ax_selection.h"
#include "ui/accessibility/ax_tree_data.h"
#include "ui/accessibility/platform/ax_fragment_root_win.h"
#include "ui/accessibility/platform/ax_platform.h"
#include "ui/accessibility/platform/ax_platform_node_delegate.h"
#include "ui/accessibility/platform/ax_platform_node_delegate_utils_win.h"
#include "ui/accessibility/platform/ax_platform_node_textchildprovider_win.h"
#include "ui/accessibility/platform/ax_platform_node_textprovider_win.h"
#include "ui/accessibility/platform/ax_platform_relation_win.h"
#include "ui/accessibility/platform/child_iterator.h"
#include "ui/accessibility/platform/compute_attributes.h"
#include "ui/accessibility/platform/uia_registrar_win.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/win/atl_module.h"
#include "ui/display/win/screen_win.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/strings/grit/ax_strings.h"

//
// Macros to use at the top of any AXPlatformNodeWin function that implements
// a non-UIA COM interface. Because COM objects are reference counted and
// clients are completely untrusted, it's important to always first check that
// our object is still valid, and then check that all pointer arguments are not
// NULL.
//
#define COM_OBJECT_VALIDATE() \
  if (!GetDelegate())         \
    return E_FAIL;
#define COM_OBJECT_VALIDATE_1_ARG(arg) \
  if (!GetDelegate())                  \
    return E_FAIL;                     \
  if (!arg)                            \
    return E_INVALIDARG;               \
  *arg = {};
#define COM_OBJECT_VALIDATE_2_ARGS(arg1, arg2) \
  if (!GetDelegate())                          \
    return E_FAIL;                             \
  if (!arg1)                                   \
    return E_INVALIDARG;                       \
  *arg1 = {};                                  \
  if (!arg2)                                   \
    return E_INVALIDARG;                       \
  *arg2 = {};
#define COM_OBJECT_VALIDATE_3_ARGS(arg1, arg2, arg3) \
  if (!GetDelegate())                                \
    return E_FAIL;                                   \
  if (!arg1)                                         \
    return E_INVALIDARG;                             \
  *arg1 = {};                                        \
  if (!arg2)                                         \
    return E_INVALIDARG;                             \
  *arg2 = {};                                        \
  if (!arg3)                                         \
    return E_INVALIDARG;                             \
  *arg3 = {};
#define COM_OBJECT_VALIDATE_4_ARGS(arg1, arg2, arg3, arg4) \
  if (!GetDelegate())                                      \
    return E_FAIL;                                         \
  if (!arg1)                                               \
    return E_INVALIDARG;                                   \
  *arg1 = {};                                              \
  if (!arg2)                                               \
    return E_INVALIDARG;                                   \
  *arg2 = {};                                              \
  if (!arg3)                                               \
    return E_INVALIDARG;                                   \
  *arg3 = {};                                              \
  if (!arg4)                                               \
    return E_INVALIDARG;                                   \
  *arg4 = {};
#define COM_OBJECT_VALIDATE_5_ARGS(arg1, arg2, arg3, arg4, arg5) \
  if (!GetDelegate())                                            \
    return E_FAIL;                                               \
  if (!arg1)                                                     \
    return E_INVALIDARG;                                         \
  *arg1 = {};                                                    \
  if (!arg2)                                                     \
    return E_INVALIDARG;                                         \
  *arg2 = {};                                                    \
  if (!arg3)                                                     \
    return E_INVALIDARG;                                         \
  *arg3 = {};                                                    \
  if (!arg4)                                                     \
    return E_INVALIDARG;                                         \
  *arg4 = {};                                                    \
  if (!arg5)                                                     \
    return E_INVALIDARG;                                         \
  *arg5 = {};
#define COM_OBJECT_VALIDATE_VAR_ID_AND_GET_TARGET(var_id, target) \
  if (!GetDelegate())                                             \
    return E_FAIL;                                                \
  target = GetTargetFromChildID(var_id);                          \
  if (!target)                                                    \
    return E_INVALIDARG;                                          \
  if (!target->GetDelegate())                                     \
    return E_INVALIDARG;
#define COM_OBJECT_VALIDATE_VAR_ID_1_ARG_AND_GET_TARGET(var_id, arg, target) \
  if (!GetDelegate())                                                        \
    return E_FAIL;                                                           \
  if (!arg)                                                                  \
    return E_INVALIDARG;                                                     \
  *arg = {};                                                                 \
  target = GetTargetFromChildID(var_id);                                     \
  if (!target)                                                               \
    return E_INVALIDARG;                                                     \
  if (!target->GetDelegate())                                                \
    return E_INVALIDARG;
#define COM_OBJECT_VALIDATE_VAR_ID_2_ARGS_AND_GET_TARGET(var_id, arg1, arg2, \
                                                         target)             \
  if (!GetDelegate())                                                        \
    return E_FAIL;                                                           \
  if (!arg1)                                                                 \
    return E_INVALIDARG;                                                     \
  *arg1 = {};                                                                \
  if (!arg2)                                                                 \
    return E_INVALIDARG;                                                     \
  *arg2 = {};                                                                \
  target = GetTargetFromChildID(var_id);                                     \
  if (!target)                                                               \
    return E_INVALIDARG;                                                     \
  if (!target->GetDelegate())                                                \
    return E_INVALIDARG;
#define COM_OBJECT_VALIDATE_VAR_ID_3_ARGS_AND_GET_TARGET(var_id, arg1, arg2, \
                                                         arg3, target)       \
  if (!GetDelegate())                                                        \
    return E_FAIL;                                                           \
  if (!arg1)                                                                 \
    return E_INVALIDARG;                                                     \
  *arg1 = {};                                                                \
  if (!arg2)                                                                 \
    return E_INVALIDARG;                                                     \
  *arg2 = {};                                                                \
  if (!arg3)                                                                 \
    return E_INVALIDARG;                                                     \
  *arg3 = {};                                                                \
  target = GetTargetFromChildID(var_id);                                     \
  if (!target)                                                               \
    return E_INVALIDARG;                                                     \
  if (!target->GetDelegate())                                                \
    return E_INVALIDARG;
#define COM_OBJECT_VALIDATE_VAR_ID_4_ARGS_AND_GET_TARGET(var_id, arg1, arg2, \
                                                         arg3, arg4, target) \
  if (!GetDelegate())                                                        \
    return E_FAIL;                                                           \
  if (!arg1)                                                                 \
    return E_INVALIDARG;                                                     \
  *arg1 = {};                                                                \
  if (!arg2)                                                                 \
    return E_INVALIDARG;                                                     \
  *arg2 = {};                                                                \
  if (!arg3)                                                                 \
    return E_INVALIDARG;                                                     \
  *arg3 = {};                                                                \
  if (!arg4)                                                                 \
    return E_INVALIDARG;                                                     \
  *arg4 = {};                                                                \
  target = GetTargetFromChildID(var_id);                                     \
  if (!target)                                                               \
    return E_INVALIDARG;                                                     \
  if (!target->GetDelegate())                                                \
    return E_INVALIDARG;

namespace ui {

namespace {

typedef std::unordered_set<AXPlatformNodeWin*> AXPlatformNodeWinSet;
// Set of all AXPlatformNodeWin objects that were the target of an
// alert event.
base::LazyInstance<AXPlatformNodeWinSet>::Leaky g_alert_targets =
    LAZY_INSTANCE_INITIALIZER;

base::LazyInstance<
    base::ObserverList<WinAccessibilityAPIUsageObserver>::Unchecked>::Leaky
    g_win_accessibility_api_usage_observer_list = LAZY_INSTANCE_INITIALIZER;

// Sets the multiplier by which large changes to a RangeValueProvider are
// greater than small changes.
constexpr int kLargeChangeScaleFactor = 10;

// Sets the default small change amount for a RangeValueProvider when no
// step was set on the element. Note: This should be in-sync with the native
// default value, defined by the default constructor of:
//
//   third_party/blink/renderer/core/html/forms/step_range.cc
//
constexpr float kDefaultSmallChangeValue = 1.0f;

// The amount to scroll when UI Automation asks to scroll by a small increment.
// Value is in device independent pixels and is the same used by Blink when
// cursor keys are used to scroll a webpage.
constexpr float kSmallScrollIncrement = 40.0f;

// Helper function to GetPatternProviderFactoryMethod that, given a node,
// will return a pattern interface through result based on the provided type T.
template <typename T>
void PatternProvider(AXPlatformNodeWin* node, IUnknown** result) {
  node->AddRef();
  *result = static_cast<T*>(node);
}

}  // namespace

void AXPlatformNodeWin::AddAttributeToList(const char* name,
                                           const char* value,
                                           PlatformAttributeList* attributes) {
  std::string str_value = value;
  SanitizeStringAttribute(str_value, &str_value);
  attributes->push_back(base::UTF8ToWide(name) + L":" +
                        base::UTF8ToWide(str_value));
}

// This also sets kNativeAPIs and kWebContents to ensure we don't have an
// incorrect combination of AXModes.
const uint32_t kScreenReaderAccessibilityMode =
    AXMode::kNativeAPIs | AXMode::kWebContents | AXMode::kScreenReader;

//
// WinAccessibilityAPIUsageObserver
//

WinAccessibilityAPIUsageObserver::WinAccessibilityAPIUsageObserver() {}

WinAccessibilityAPIUsageObserver::~WinAccessibilityAPIUsageObserver() {}

// static
base::ObserverList<WinAccessibilityAPIUsageObserver>::Unchecked&
GetWinAccessibilityAPIUsageObserverList() {
  return g_win_accessibility_api_usage_observer_list.Get();
}

// Used to simplify calling StartFiringUIAEvents and EndFiringEvents
WinAccessibilityAPIUsageScopedUIAEventsNotifier::
    WinAccessibilityAPIUsageScopedUIAEventsNotifier() {
  for (WinAccessibilityAPIUsageObserver& observer :
       GetWinAccessibilityAPIUsageObserverList()) {
    observer.StartFiringUIAEvents();
  }
}
WinAccessibilityAPIUsageScopedUIAEventsNotifier::
    ~WinAccessibilityAPIUsageScopedUIAEventsNotifier() {
  for (WinAccessibilityAPIUsageObserver& observer :
       GetWinAccessibilityAPIUsageObserverList()) {
    observer.EndFiringUIAEvents();
  }
}

//
// AXPlatformNode::Create
//

// static
AXPlatformNode* AXPlatformNode::Create(AXPlatformNodeDelegate* delegate) {
  // Make sure ATL is initialized in this module.
  win::CreateATLModuleIfNeeded();

  CComObject<AXPlatformNodeWin>* instance = nullptr;
  HRESULT hr = CComObject<AXPlatformNodeWin>::CreateInstance(&instance);
  DCHECK(SUCCEEDED(hr));
  instance->Init(delegate);
  instance->AddRef();
  return instance;
}

// static
AXPlatformNode* AXPlatformNode::FromNativeViewAccessible(
    gfx::NativeViewAccessible accessible) {
  if (!accessible)
    return nullptr;
  Microsoft::WRL::ComPtr<AXPlatformNodeWin> ax_platform_node;
  accessible->QueryInterface(IID_PPV_ARGS(&ax_platform_node));
  return ax_platform_node.Get();
}

//
// AXPlatformNodeWin
//

AXPlatformNodeWin::AXPlatformNodeWin() {}

AXPlatformNodeWin::~AXPlatformNodeWin() {
  ClearOwnRelations();
}

void AXPlatformNodeWin::Init(AXPlatformNodeDelegate* delegate) {
  AXPlatformNodeBase::Init(delegate);
}

void AXPlatformNodeWin::ClearOwnRelations() {
  for (size_t i = 0; i < relations_.size(); ++i)
    relations_[i]->Invalidate();
  relations_.clear();
}

// Static
void AXPlatformNodeWin::SanitizeStringAttributeForUIAAriaProperty(
    const std::wstring& input,
    std::wstring* output) {
  DCHECK(output);
  // According to the UIA Spec, these characters need to be escaped with a
  // backslash in an AriaProperties string: backslash, equals and semicolon.
  // Note that backslash must be replaced first.
  base::ReplaceChars(input, L"\\", L"\\\\", output);
  base::ReplaceChars(*output, L"=", L"\\=", output);
  base::ReplaceChars(*output, L";", L"\\;", output);
}

void AXPlatformNodeWin::StringAttributeToUIAAriaProperty(
    std::vector<std::wstring>& properties,
    ax::mojom::StringAttribute attribute,
    const char* uia_aria_property) {
  std::string value;
  if (GetStringAttribute(attribute, &value)) {
    std::wstring wide_value = base::UTF8ToWide(value);
    SanitizeStringAttributeForUIAAriaProperty(wide_value, &wide_value);
    properties.push_back(base::ASCIIToWide(uia_aria_property) + L"=" +
                         wide_value);
  }
}

void AXPlatformNodeWin::BoolAttributeToUIAAriaProperty(
    std::vector<std::wstring>& properties,
    ax::mojom::BoolAttribute attribute,
    const char* uia_aria_property) {
  bool value;
  if (GetBoolAttribute(attribute, &value)) {
    properties.push_back(base::ASCIIToWide(uia_aria_property) + L"=" +
                         (value ? L"true" : L"false"));
  }
}

void AXPlatformNodeWin::IntAttributeToUIAAriaProperty(
    std::vector<std::wstring>& properties,
    ax::mojom::IntAttribute attribute,
    const char* uia_aria_property) {
  int value;
  if (GetIntAttribute(attribute, &value)) {
    properties.push_back(base::ASCIIToWide(uia_aria_property) + L"=" +
                         base::NumberToWString(value));
  }
}

void AXPlatformNodeWin::FloatAttributeToUIAAriaProperty(
    std::vector<std::wstring>& properties,
    ax::mojom::FloatAttribute attribute,
    const char* uia_aria_property) {
  float value;
  if (GetFloatAttribute(attribute, &value)) {
    properties.push_back(base::ASCIIToWide(uia_aria_property) + L"=" +
                         base::NumberToWString(value));
  }
}

void AXPlatformNodeWin::StateToUIAAriaProperty(
    std::vector<std::wstring>& properties,
    ax::mojom::State state,
    const char* uia_aria_property) {
  bool value = HasState(state);
  properties.push_back(base::ASCIIToWide(uia_aria_property) + L"=" +
                       (value ? L"true" : L"false"));
}

std::vector<AXPlatformNodeWin*>
AXPlatformNodeWin::CreatePlatformNodeVectorFromRelationIdVector(
    const std::vector<int32_t>& relation_id_list) {
  std::vector<AXPlatformNodeWin*> platform_node_list;

  for (int32_t id : relation_id_list) {
    AXPlatformNode* platform_node = GetDelegate()->GetFromNodeID(id);
    if (IsValidUiaRelationTarget(platform_node)) {
      platform_node_list.push_back(
          static_cast<AXPlatformNodeWin*>(platform_node));
    }
  }

  return platform_node_list;
}

SAFEARRAY* AXPlatformNodeWin::CreateUIAElementsSafeArray(
    const std::vector<AXPlatformNodeWin*>& platform_node_list) {
  if (platform_node_list.empty())
    return nullptr;

  SAFEARRAY* uia_array =
      SafeArrayCreateVector(VT_UNKNOWN, 0, platform_node_list.size());
  LONG i = 0;

  for (AXPlatformNodeWin* platform_node : platform_node_list) {
    // All incoming ids should already be validated to have a valid relation
    // targets so that this function does not need to re-check before allocating
    // the SAFEARRAY.
    DCHECK(IsValidUiaRelationTarget(platform_node));
    SafeArrayPutElement(uia_array, &i,
                        static_cast<IRawElementProviderSimple*>(platform_node));
    ++i;
  }

  return uia_array;
}

SAFEARRAY* AXPlatformNodeWin::CreateUIAControllerForArray() {
  std::vector<int32_t> relation_id_list =
      GetIntListAttribute(ax::mojom::IntListAttribute::kControlsIds);

  std::vector<AXPlatformNodeWin*> platform_node_list =
      CreatePlatformNodeVectorFromRelationIdVector(relation_id_list);

  if (GetActivePopupAxUniqueId() != std::nullopt) {
    AXPlatformNodeWin* view_popup_node_win = static_cast<AXPlatformNodeWin*>(
        GetFromUniqueId(GetActivePopupAxUniqueId().value()));

    if (IsValidUiaRelationTarget(view_popup_node_win))
      platform_node_list.push_back(view_popup_node_win);
  }

  // The aria-errormessage attribute (mapped to the kErrormessageIds) is
  // expected to be exposed through the ControllerFor property on UIA:
  // https://w3c.github.io/core-aam/#ariaErrorMessage
  if (HasIntListAttribute(ax::mojom::IntListAttribute::kErrormessageIds)) {
    std::vector<int32_t> error_message_node_ids =
        GetIntListAttribute(ax::mojom::IntListAttribute::kErrormessageIds);
    for (int32_t id : error_message_node_ids) {
      AXPlatformNodeWin* error_message_node_win =
          static_cast<AXPlatformNodeWin*>(GetDelegate()->GetFromNodeID(id));
      if (IsValidUiaRelationTarget(error_message_node_win)) {
        platform_node_list.push_back(error_message_node_win);
      }
    }
  }

  return CreateUIAElementsSafeArray(platform_node_list);
}

SAFEARRAY* AXPlatformNodeWin::CreateUIAElementsArrayForRelation(
    const ax::mojom::IntListAttribute& attribute) {
  return CreateUIAElementsSafeArray(
      CreatePlatformNodeVectorFromRelationIdVector(
          GetIntListAttribute(attribute)));
}

SAFEARRAY* AXPlatformNodeWin::CreateUIAElementsArrayForReverseRelation(
    const ax::mojom::IntListAttribute& attribute) {
  std::vector<AXPlatformNode*> reverse_relations =
      GetDelegate()->GetSourceNodesForReverseRelations(attribute);

  std::vector<int32_t> id_list;
  base::ranges::transform(
      reverse_relations, std::back_inserter(id_list),
      [](AXPlatformNode* platform_node) {
        return static_cast<AXPlatformNodeWin*>(platform_node)->GetData().id;
      });

  return CreateUIAElementsSafeArray(
      CreatePlatformNodeVectorFromRelationIdVector(id_list));
}

SAFEARRAY* AXPlatformNodeWin::CreateClickablePointArray() {
  SAFEARRAY* clickable_point_array = SafeArrayCreateVector(VT_R8, 0, 2);
  gfx::Point center = GetDelegate()
                          ->GetBoundsRect(AXCoordinateSystem::kScreenDIPs,
                                          AXClippingBehavior::kUnclipped)
                          .CenterPoint();

  double* double_array;
  SafeArrayAccessData(clickable_point_array,
                      reinterpret_cast<void**>(&double_array));
  double_array[0] = center.x();
  double_array[1] = center.y();
  SafeArrayUnaccessData(clickable_point_array);

  return clickable_point_array;
}

gfx::Vector2d AXPlatformNodeWin::CalculateUIAScrollPoint(
    const ScrollAmount horizontal_amount,
    const ScrollAmount vertical_amount) const {
  if (!GetDelegate() || !IsScrollable())
    return {};

  const gfx::Rect bounds = GetDelegate()->GetBoundsRect(
      AXCoordinateSystem::kScreenDIPs, AXClippingBehavior::kClipped);
  const int large_horizontal_change = bounds.width();
  const int large_vertical_change = bounds.height();

  const HWND hwnd = GetDelegate()->GetTargetForNativeAccessibilityEvent();
  DCHECK(hwnd);
  const float scale_factor =
      display::win::ScreenWin::GetScaleFactorForHWND(hwnd);
  const int small_change =
      base::ClampRound(kSmallScrollIncrement * scale_factor);

  const int x_min = GetIntAttribute(ax::mojom::IntAttribute::kScrollXMin);
  const int x_max = GetIntAttribute(ax::mojom::IntAttribute::kScrollXMax);
  const int y_min = GetIntAttribute(ax::mojom::IntAttribute::kScrollYMin);
  const int y_max = GetIntAttribute(ax::mojom::IntAttribute::kScrollYMax);

  int x = GetIntAttribute(ax::mojom::IntAttribute::kScrollX);
  int y = GetIntAttribute(ax::mojom::IntAttribute::kScrollY);

  switch (horizontal_amount) {
    case ScrollAmount_LargeDecrement:
      x -= large_horizontal_change;
      break;
    case ScrollAmount_LargeIncrement:
      x += large_horizontal_change;
      break;
    case ScrollAmount_NoAmount:
      break;
    case ScrollAmount_SmallDecrement:
      x -= small_change;
      break;
    case ScrollAmount_SmallIncrement:
      x += small_change;
      break;
  }
  x = std::min(x, x_max);
  x = std::max(x, x_min);

  switch (vertical_amount) {
    case ScrollAmount_LargeDecrement:
      y -= large_vertical_change;
      break;
    case ScrollAmount_LargeIncrement:
      y += large_vertical_change;
      break;
    case ScrollAmount_NoAmount:
      break;
    case ScrollAmount_SmallDecrement:
      y -= small_change;
      break;
    case ScrollAmount_SmallIncrement:
      y += small_change;
      break;
  }
  y = std::min(y, y_max);
  y = std::max(y, y_min);

  return {x, y};
}

//
// AXPlatformNodeBase implementation.
//

void AXPlatformNodeWin::Dispose() {
  Release();
}

void AXPlatformNodeWin::Destroy() {
  RemoveAlertTarget();

  // This will end up calling Dispose() which may result in deleting this object
  // if there are no more outstanding references.
  AXPlatformNodeBase::Destroy();
}

//
// AXPlatformNode implementation.
//

gfx::NativeViewAccessible AXPlatformNodeWin::GetNativeViewAccessible() {
  return this;
}

void AXPlatformNodeWin::NotifyAccessibilityEvent(ax::mojom::Event event_type) {
  TRACE_EVENT("accessibility", "NotifyAccessibilityEvent",
              perfetto::Flow::FromPointer(this), "event_type",
              base::NumberToString(static_cast<int32_t>(event_type)));
  AXPlatformNodeBase::NotifyAccessibilityEvent(event_type);
  // Menu items fire selection events but Windows screen readers work reliably
  // with focus events. Remap here.
  if (event_type == ax::mojom::Event::kSelection) {
    // A menu item could have something other than a role of
    // |ROLE_SYSTEM_MENUITEM|. Zoom modification controls for example have a
    // role of button.
    if (int role = MSAARole(); role == ROLE_SYSTEM_MENUITEM) {
      event_type = ax::mojom::Event::kFocus;
    } else if (role == ROLE_SYSTEM_LISTITEM) {
      if (const AXPlatformNodeBase* container = GetSelectionContainer()) {
        if (container->GetRole() == ax::mojom::Role::kListBox &&
            !container->HasState(ax::mojom::State::kMultiselectable) &&
            GetDelegate()->GetFocus() == GetNativeViewAccessible()) {
          event_type = ax::mojom::Event::kFocus;
        }
      }
    } else if (auto* parent = GetParentPlatformNodeWin(); parent) {
      if (int parent_role = parent->MSAARole();
          parent_role == ROLE_SYSTEM_MENUPOPUP ||
          parent_role == ROLE_SYSTEM_LIST) {
        event_type = ax::mojom::Event::kFocus;
      }
    }
  }

  // TODO(benjamin.beaudry): Uncomment DCHECK once https://crbug.com/331840469
  // is fixed.
  // DCHECK(event_type != ax::mojom::Event::kLiveRegionChanged ||
  //        GetDelegate()->IsWebContent() || IsUIAControl())
  //     << "For views, the LiveRegionChanged event should only be fired on
  //     nodes that are UIA controls.";

  if (event_type == ax::mojom::Event::kValueChanged ||
      event_type == ax::mojom::Event::kLiveRegionCreated ||
      event_type == ax::mojom::Event::kLiveRegionChanged) {
    // For the IAccessibleText interface to work on non-web content nodes, we
    // need to update the nodes' hypertext
    // when the value changes. Otherwise, for web and PDF content, this is
    // handled by "BrowserAccessibilityComWin".
    if (!GetDelegate()->IsWebContent())
      UpdateComputedHypertext();
  }

  if (std::optional<DWORD> native_event = MojoEventToMSAAEvent(event_type)) {
    HWND hwnd = GetDelegate()->GetTargetForNativeAccessibilityEvent();
    if (!hwnd)
      return;

    TRACE_EVENT("accessibility", "NotifyWinEvent", "native_event",
                base::StringPrintf("0x%04lX", native_event.value()));
    ::NotifyWinEvent((*native_event), hwnd, OBJID_CLIENT, -GetUniqueId());
  }

  if (std::optional<PROPERTYID> uia_property =
          MojoEventToUIAProperty(event_type)) {
    // For this event, we're not concerned with the old value.
    base::win::ScopedVariant old_value;
    ::VariantInit(old_value.Receive());
    base::win::ScopedVariant new_value;
    ::VariantInit(new_value.Receive());
    GetPropertyValueImpl((*uia_property), new_value.Receive());
    ::UiaRaiseAutomationPropertyChangedEvent(this, (*uia_property), old_value,
                                             new_value);
  }

  if (std::optional<EVENTID> uia_event = MojoEventToUIAEvent(event_type)) {
    ::UiaRaiseAutomationEvent(this, (*uia_event));
  }

  // Keep track of objects that are a target of an alert event.
  if (event_type == ax::mojom::Event::kAlert)
    AddAlertTarget();
}

bool AXPlatformNodeWin::HasActiveComposition() const {
  return active_composition_range_.end() > active_composition_range_.start();
}

gfx::Range AXPlatformNodeWin::GetActiveCompositionOffsets() const {
  return active_composition_range_;
}

void AXPlatformNodeWin::OnActiveComposition(
    const gfx::Range& range,
    const std::u16string& active_composition_text,
    bool is_composition_committed) {
  // Cache the composition range that will be used when
  // GetActiveComposition and GetConversionTarget is called in
  // AXPlatformNodeTextProviderWin
  active_composition_range_ = range;
  // Fire the UiaTextEditTextChangedEvent
  FireUiaTextEditTextChangedEvent(range,
                                  base::UTF16ToWide(active_composition_text),
                                  is_composition_committed);
}

void AXPlatformNodeWin::FireUiaTextEditTextChangedEvent(
    const gfx::Range& range,
    const std::wstring& active_composition_text,
    bool is_composition_committed) {
  if (!AXPlatform::GetInstance().IsUiaProviderEnabled()) {
    return;
  }

  // This API is only supported from Win8.1 onwards
  // Check if the function pointer is valid or not
  using UiaRaiseTextEditTextChangedEventFunction = HRESULT(WINAPI*)(
      IRawElementProviderSimple*, TextEditChangeType, SAFEARRAY*);
  UiaRaiseTextEditTextChangedEventFunction text_edit_text_changed_func =
      reinterpret_cast<UiaRaiseTextEditTextChangedEventFunction>(
          ::GetProcAddress(GetModuleHandle(L"uiautomationcore.dll"),
                           "UiaRaiseTextEditTextChangedEvent"));
  if (!text_edit_text_changed_func) {
    return;
  }

  TextEditChangeType text_edit_change_type =
      is_composition_committed ? TextEditChangeType_CompositionFinalized
                               : TextEditChangeType_Composition;
  // Composition has been finalized by TSF
  base::win::ScopedBstr composition_text(active_composition_text.c_str());
  base::win::ScopedSafearray changed_data(
      SafeArrayCreateVector(VT_BSTR /* element type */, 0 /* lower bound */,
                            1 /* number of elements */));
  if (!changed_data.Get()) {
    return;
  }

  LONG index = 0;
  HRESULT hr =
      SafeArrayPutElement(changed_data.Get(), &index, composition_text.Get());

  if (FAILED(hr)) {
    return;
  } else {
    // Fire the UiaRaiseTextEditTextChangedEvent
    text_edit_text_changed_func(this, text_edit_change_type,
                                changed_data.Release());
  }
}

bool AXPlatformNodeWin::IsValidUiaRelationTarget(
    AXPlatformNode* ax_platform_node) {
  if (!ax_platform_node)
    return false;
  if (!ax_platform_node->GetDelegate())
    return false;

  // This is needed for get_FragmentRoot.
  if (!ax_platform_node->GetDelegate()->GetTargetForNativeAccessibilityEvent())
    return false;

  return true;
}

AXPlatformNodeWin::UIARoleProperties AXPlatformNodeWin::GetUIARoleProperties() {
  // If this is a web area for a presentational iframe, give it a role of
  // something other than document so that the fact that it's a separate doc
  // is not exposed to AT.
  if (GetDelegate()->IsRootWebAreaForPresentationalIframe())
    return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId, L"group"};

  // See UIARoleProperties for descriptions of the properties.
  switch (GetRole()) {
    case ax::mojom::Role::kAlert:
      return {UIALocalizationStrategy::kDeferToAriaRole, UIA_TextControlTypeId,
              L"alert"};
    case ax::mojom::Role::kAlertDialog:
      // Our MSAA implementation suggests the use of
      // "alert", not "alertdialog" because some
      // Windows screen readers are not compatible with
      // |ax::mojom::Role::kAlertDialog| yet.
      return {UIALocalizationStrategy::kSupply, UIA_WindowControlTypeId,
              L"alert"};

    case ax::mojom::Role::kComment:
    case ax::mojom::Role::kSuggestion:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"group"};

    case ax::mojom::Role::kApplication:
      return {UIALocalizationStrategy::kDeferToAriaRole, UIA_PaneControlTypeId,
              L"application"};

    case ax::mojom::Role::kArticle:
      return {UIALocalizationStrategy::kDeferToAriaRole, UIA_GroupControlTypeId,
              L"article"};

    case ax::mojom::Role::kAudio:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"group"};

    case ax::mojom::Role::kBanner:
    case ax::mojom::Role::kHeader:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"banner"};

    case ax::mojom::Role::kBlockquote:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"group"};

    case ax::mojom::Role::kButton:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_ButtonControlTypeId, L"button"};

    case ax::mojom::Role::kCanvas:
      return {UIALocalizationStrategy::kSupply, UIA_ImageControlTypeId, L"img"};

    case ax::mojom::Role::kCaption:
      return {UIALocalizationStrategy::kSupply, UIA_TextControlTypeId,
              L"description"};

    case ax::mojom::Role::kCaret:
      return {UIALocalizationStrategy::kSupply, UIA_PaneControlTypeId,
              L"region"};

    case ax::mojom::Role::kCell:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_DataItemControlTypeId, L"gridcell"};

    case ax::mojom::Role::kCode:
      return {UIALocalizationStrategy::kSupply, UIA_TextControlTypeId, L"code"};

    case ax::mojom::Role::kCheckBox:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_CheckBoxControlTypeId, L"checkbox"};

    case ax::mojom::Role::kClient:
      return {UIALocalizationStrategy::kSupply, UIA_PaneControlTypeId,
              L"region"};

    case ax::mojom::Role::kColorWell:
      return {UIALocalizationStrategy::kSupply, UIA_ButtonControlTypeId,
              L"textbox"};

    case ax::mojom::Role::kColumn:
      return {UIALocalizationStrategy::kSupply, UIA_PaneControlTypeId,
              L"region"};

    case ax::mojom::Role::kColumnHeader:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_DataItemControlTypeId, L"columnheader"};

    case ax::mojom::Role::kComboBoxGrouping:
    case ax::mojom::Role::kComboBoxMenuButton:
      return {UIALocalizationStrategy::kSupply, UIA_ComboBoxControlTypeId,
              L"combobox"};

    case ax::mojom::Role::kComboBoxSelect:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_ComboBoxControlTypeId, L"combobox"};

    case ax::mojom::Role::kComplementary:
      return {UIALocalizationStrategy::kDeferToAriaRole, UIA_GroupControlTypeId,
              L"complementary"};

    case ax::mojom::Role::kContentDeletion:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"deletion"};

    case ax::mojom::Role::kContentInsertion:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"insertion"};

    case ax::mojom::Role::kContentInfo:
    case ax::mojom::Role::kFooter:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"contentinfo"};

    case ax::mojom::Role::kDate:
    case ax::mojom::Role::kDateTime:
      return {UIALocalizationStrategy::kSupply, UIA_EditControlTypeId,
              L"textbox"};

    case ax::mojom::Role::kDefinition:
      return {UIALocalizationStrategy::kDeferToAriaRole, UIA_GroupControlTypeId,
              L"definition"};

    case ax::mojom::Role::kDescriptionList:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_ListControlTypeId, L"list"};

    case ax::mojom::Role::kDesktop:
      return {UIALocalizationStrategy::kSupply, UIA_DocumentControlTypeId,
              L"document"};

    case ax::mojom::Role::kDetails:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"group"};

    case ax::mojom::Role::kDialog:
      return {UIALocalizationStrategy::kDeferToAriaRole,
              UIA_WindowControlTypeId, L"dialog"};

    case ax::mojom::Role::kDisclosureTriangle:
    case ax::mojom::Role::kDisclosureTriangleGrouped:
      return {UIALocalizationStrategy::kSupply, UIA_ButtonControlTypeId,
              L"button"};

    case ax::mojom::Role::kDocCover:
      return {UIALocalizationStrategy::kSupply, UIA_ImageControlTypeId, L"img"};

    case ax::mojom::Role::kDocBackLink:
    case ax::mojom::Role::kDocBiblioRef:
    case ax::mojom::Role::kDocGlossRef:
    case ax::mojom::Role::kDocNoteRef:
      return {UIALocalizationStrategy::kSupply, UIA_HyperlinkControlTypeId,
              L"link"};

    case ax::mojom::Role::kDocBiblioEntry:
    case ax::mojom::Role::kDocEndnote:
    case ax::mojom::Role::kDocFootnote:
      return {UIALocalizationStrategy::kSupply, UIA_ListItemControlTypeId,
              L"listitem"};

    case ax::mojom::Role::kDocPageBreak:
      return {UIALocalizationStrategy::kSupply, UIA_SeparatorControlTypeId,
              L"separator"};

    case ax::mojom::Role::kDocAbstract:
    case ax::mojom::Role::kDocAcknowledgments:
    case ax::mojom::Role::kDocAfterword:
    case ax::mojom::Role::kDocAppendix:
    case ax::mojom::Role::kDocBibliography:
    case ax::mojom::Role::kDocChapter:
    case ax::mojom::Role::kDocColophon:
    case ax::mojom::Role::kDocConclusion:
    case ax::mojom::Role::kDocCredit:
    case ax::mojom::Role::kDocCredits:
    case ax::mojom::Role::kDocDedication:
    case ax::mojom::Role::kDocEndnotes:
    case ax::mojom::Role::kDocEpigraph:
    case ax::mojom::Role::kDocEpilogue:
    case ax::mojom::Role::kDocErrata:
    case ax::mojom::Role::kDocExample:
    case ax::mojom::Role::kDocForeword:
    case ax::mojom::Role::kDocGlossary:
    case ax::mojom::Role::kDocIndex:
    case ax::mojom::Role::kDocIntroduction:
    case ax::mojom::Role::kDocNotice:
    case ax::mojom::Role::kDocPageFooter:
    case ax::mojom::Role::kDocPageHeader:
    case ax::mojom::Role::kDocPageList:
    case ax::mojom::Role::kDocPart:
    case ax::mojom::Role::kDocPreface:
    case ax::mojom::Role::kDocPrologue:
    case ax::mojom::Role::kDocPullquote:
    case ax::mojom::Role::kDocQna:
    case ax::mojom::Role::kDocSubtitle:
    case ax::mojom::Role::kDocTip:
    case ax::mojom::Role::kDocToc:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"group"};

    case ax::mojom::Role::kDocument:
      return {UIALocalizationStrategy::kSupply, UIA_DocumentControlTypeId,
              L"document"};
    case ax::mojom::Role::kPdfRoot:
    case ax::mojom::Role::kRootWebArea:
      return {UIALocalizationStrategy::kSupply, UIA_DocumentControlTypeId,
              L"document"};

    case ax::mojom::Role::kEmbeddedObject:
      if (GetDelegate()->GetChildCount()) {
        return {UIALocalizationStrategy::kSupply, UIA_PaneControlTypeId,
                L"group"};
      }
      return {UIALocalizationStrategy::kSupply, UIA_PaneControlTypeId,
              L"document"};

    case ax::mojom::Role::kEmphasis:
      return {UIALocalizationStrategy::kSupply, UIA_TextControlTypeId,
              L"emphasis"};

    case ax::mojom::Role::kFeed:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"feed"};

    case ax::mojom::Role::kFigcaption:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"description"};

    case ax::mojom::Role::kFigure:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"group"};

    case ax::mojom::Role::kForm:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"form"};

    case ax::mojom::Role::kGenericContainer:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"group"};

    case ax::mojom::Role::kGraphicsDocument:
      return {UIALocalizationStrategy::kSupply, UIA_DocumentControlTypeId,
              L"document"};

    case ax::mojom::Role::kGraphicsObject:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"group"};

    case ax::mojom::Role::kGraphicsSymbol:
      return {UIALocalizationStrategy::kSupply, UIA_ImageControlTypeId, L"img"};

    case ax::mojom::Role::kGrid:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_DataGridControlTypeId, L"grid"};

    case ax::mojom::Role::kGridCell:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_DataItemControlTypeId, L"gridcell"};

    case ax::mojom::Role::kGroup:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_GroupControlTypeId, L"group"};

    case ax::mojom::Role::kHeading:
      return {UIALocalizationStrategy::kDeferToAriaRole, UIA_TextControlTypeId,
              L"heading"};

    case ax::mojom::Role::kIframe:
      return {UIALocalizationStrategy::kSupply, UIA_DocumentControlTypeId,
              L"document"};

    case ax::mojom::Role::kIframePresentational:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"group"};

    case ax::mojom::Role::kImage:
      // We may want to expose additional details in the localized string such
      // as 'Unlabeled'
      if (IsImageWithMap()) {
        return {UIALocalizationStrategy::kSupply, UIA_DocumentControlTypeId,
                L"img"};
      }
      return {UIALocalizationStrategy::kSupply, UIA_ImageControlTypeId, L"img"};

    case ax::mojom::Role::kInputTime:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"group"};

    case ax::mojom::Role::kInlineTextBox:
      return {UIALocalizationStrategy::kSupply, UIA_DocumentControlTypeId,
              L"textbox"};

    case ax::mojom::Role::kLabelText:
          return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"description"};

    case ax::mojom::Role::kLegend:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"description"};

    case ax::mojom::Role::kLayoutTable:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_GroupControlTypeId, L"group"};

    case ax::mojom::Role::kLayoutTableCell:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_GroupControlTypeId, L"group"};

    case ax::mojom::Role::kLayoutTableRow:
      return {UIALocalizationStrategy::kDeferToAriaRole, UIA_GroupControlTypeId,
              L"group"};

    case ax::mojom::Role::kLink:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_HyperlinkControlTypeId, L"link"};

    case ax::mojom::Role::kList:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_ListControlTypeId, L"list"};

    case ax::mojom::Role::kListBox:
      return {UIALocalizationStrategy::kDeferToAriaRole, UIA_ListControlTypeId,
              L"listbox"};

    case ax::mojom::Role::kListBoxOption:
      return {UIALocalizationStrategy::kDeferToAriaRole,
              UIA_ListItemControlTypeId, L"option"};

    case ax::mojom::Role::kListGrid:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_DataGridControlTypeId, L"listview"};

    case ax::mojom::Role::kListItem:
      return {UIALocalizationStrategy::kSupply, UIA_ListItemControlTypeId,
              L"listitem"};

    case ax::mojom::Role::kListMarker:
      // Regular list markers only expose their alternative text, but do not
      // expose their descendants; and the descendants should be ignored. This
      // is because the alternative text depends on the counter style and can be
      // different from the actual (visual) marker text, and hence, inconsistent
      // with the descendants. We treat a list marker as non-text only if it
      // still has non-ignored descendants, which happens only when:
      // - The list marker itself is ignored but the descendants are not
      // - Or the list marker contains images
      if (!GetDelegate()->GetChildCount()) {
        return {UIALocalizationStrategy::kSupply, UIA_TextControlTypeId,
                L"description"};
      }
      return {UIALocalizationStrategy::kSupply, UIA_TextControlTypeId,
              L"group"};

    case ax::mojom::Role::kLog:
      return {UIALocalizationStrategy::kDeferToAriaRole, UIA_GroupControlTypeId,
              L"log"};

    case ax::mojom::Role::kMain:
      return {UIALocalizationStrategy::kDeferToAriaRole, UIA_GroupControlTypeId,
              L"main"};

    case ax::mojom::Role::kMark:
      return {UIALocalizationStrategy::kSupply, UIA_TextControlTypeId,
              L"description"};

    case ax::mojom::Role::kMarquee:
      return {UIALocalizationStrategy::kDeferToAriaRole, UIA_TextControlTypeId,
              L"marquee"};

    case ax::mojom::Role::kMath:
    case ax::mojom::Role::kMathMLMath:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"group"};

    // TODO(http://crbug.com/1260585): Refine this if/when a UIA API exists for
    // properly exposing MathML content.
    case ax::mojom::Role::kMathMLFraction:
    case ax::mojom::Role::kMathMLIdentifier:
    case ax::mojom::Role::kMathMLMultiscripts:
    case ax::mojom::Role::kMathMLNoneScript:
    case ax::mojom::Role::kMathMLNumber:
    case ax::mojom::Role::kMathMLOperator:
    case ax::mojom::Role::kMathMLOver:
    case ax::mojom::Role::kMathMLPrescriptDelimiter:
    case ax::mojom::Role::kMathMLRoot:
    case ax::mojom::Role::kMathMLRow:
    case ax::mojom::Role::kMathMLSquareRoot:
    case ax::mojom::Role::kMathMLStringLiteral:
    case ax::mojom::Role::kMathMLSub:
    case ax::mojom::Role::kMathMLSubSup:
    case ax::mojom::Role::kMathMLSup:
    case ax::mojom::Role::kMathMLTable:
    case ax::mojom::Role::kMathMLTableCell:
    case ax::mojom::Role::kMathMLTableRow:
    case ax::mojom::Role::kMathMLText:
    case ax::mojom::Role::kMathMLUnder:
    case ax::mojom::Role::kMathMLUnderOver:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"group"};

    case ax::mojom::Role::kMenu:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_MenuControlTypeId, L"menu"};

    case ax::mojom::Role::kMenuBar:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_MenuBarControlTypeId, L"menubar"};

    case ax::mojom::Role::kMenuItem:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_MenuItemControlTypeId, L"menuitem"};

    case ax::mojom::Role::kMenuItemCheckBox:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_CheckBoxControlTypeId, L"menuitemcheckbox"};

    case ax::mojom::Role::kMenuItemRadio:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_RadioButtonControlTypeId, L"menuitemradio"};

    case ax::mojom::Role::kMenuListPopup:
      return {UIALocalizationStrategy::kSupply, UIA_ListControlTypeId, L"list"};

    case ax::mojom::Role::kMenuListOption:
      return {UIALocalizationStrategy::kSupply, UIA_ListItemControlTypeId,
              L"listitem"};

    case ax::mojom::Role::kMeter:
      return {UIALocalizationStrategy::kSupply, UIA_ProgressBarControlTypeId,
              L"progressbar"};

    case ax::mojom::Role::kNavigation:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"navigation"};

    case ax::mojom::Role::kNote:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"note"};

    case ax::mojom::Role::kParagraph:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"group"};

    case ax::mojom::Role::kPdfActionableHighlight:
      return {UIALocalizationStrategy::kSupply, UIA_CustomControlTypeId,
              L"button"};

    case ax::mojom::Role::kPluginObject:
      // UIA_DocumentControlTypeId
      if (GetDelegate()->GetChildCount()) {
        return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
                L"group"};
      }
      return {UIALocalizationStrategy::kSupply, UIA_DocumentControlTypeId,
              L"document"};

    case ax::mojom::Role::kPopUpButton:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_ButtonControlTypeId, L"button"};

    case ax::mojom::Role::kProgressIndicator:
      return {UIALocalizationStrategy::kSupply, UIA_ProgressBarControlTypeId,
              L"progressbar"};

    case ax::mojom::Role::kRadioButton:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_RadioButtonControlTypeId, L"radio"};

    case ax::mojom::Role::kRadioGroup:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"radiogroup"};

    case ax::mojom::Role::kRegion:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"region"};

    case ax::mojom::Role::kRow:
      // Role changes depending on whether row is inside a treegrid
      // https://www.w3.org/TR/core-aam-1.1/#role-map-row
      if (IsInTreeGrid()) {
        return {UIALocalizationStrategy::kDeferToControlType,
                UIA_TreeItemControlTypeId, L"treeitem"};
      }
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_DataItemControlTypeId, L"row"};

    case ax::mojom::Role::kRowGroup:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"rowgroup"};

    case ax::mojom::Role::kRowHeader:
      return {UIALocalizationStrategy::kSupply, UIA_DataItemControlTypeId,
              L"rowheader"};

    case ax::mojom::Role::kRuby:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"group"};

    case ax::mojom::Role::kRubyAnnotation:
      // Generally exposed as description on <ruby> (Role::kRuby) element, not
      // as its own object in the tree.
      // However, it's possible to make a kRubyAnnotation element show up in the
      // AX tree, for example by adding tabindex="0" to the source <rp> or <rt>
      // element or making the source element the target of an aria-owns.
      // Therefore, browser side needs to gracefully handle it if it actually
      // shows up in the tree.
      return {UIALocalizationStrategy::kSupply, UIA_TextControlTypeId,
              L"description"};

    case ax::mojom::Role::kSection:
    case ax::mojom::Role::kSectionFooter:
    case ax::mojom::Role::kSectionHeader:
    case ax::mojom::Role::kSectionWithoutName:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"group"};

    case ax::mojom::Role::kScrollBar:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_ScrollBarControlTypeId, L"scrollbar"};

    case ax::mojom::Role::kScrollView:
      return {UIALocalizationStrategy::kSupply, UIA_PaneControlTypeId,
              L"region"};

    case ax::mojom::Role::kSearch:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"search"};

    case ax::mojom::Role::kSlider:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_SliderControlTypeId, L"slider"};

    case ax::mojom::Role::kSpinButton:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_SpinnerControlTypeId, L"spinbutton"};

    case ax::mojom::Role::kStrong:
      return {UIALocalizationStrategy::kSupply, UIA_TextControlTypeId,
              L"strong"};

    case ax::mojom::Role::kSwitch:
      return {UIALocalizationStrategy::kSupply, UIA_ButtonControlTypeId,
              L"switch"};

    case ax::mojom::Role::kStaticText:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_TextControlTypeId, L"description"};

    case ax::mojom::Role::kStatus:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_StatusBarControlTypeId, L"status"};

    case ax::mojom::Role::kSplitter:
      return {
          UIALocalizationStrategy::kDeferToControlType,
          IsFocusable() ? UIA_ThumbControlTypeId : UIA_SeparatorControlTypeId,
          L"separator"};

    case ax::mojom::Role::kSubscript:
    case ax::mojom::Role::kSuperscript:
      return {UIALocalizationStrategy::kSupply, UIA_TextControlTypeId,
              L"group"};

    case ax::mojom::Role::kSvgRoot:
      return {UIALocalizationStrategy::kSupply, UIA_ImageControlTypeId, L"img"};

    case ax::mojom::Role::kTab:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_TabItemControlTypeId, L"tab"};

    case ax::mojom::Role::kTable:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_TableControlTypeId, L"grid"};

    case ax::mojom::Role::kTableHeaderContainer:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"group"};

    case ax::mojom::Role::kTabList:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_TabControlTypeId, L"tablist"};

    case ax::mojom::Role::kTabPanel:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_PaneControlTypeId, L"tabpanel"};

    case ax::mojom::Role::kTerm:
      return {UIALocalizationStrategy::kSupply, UIA_ListItemControlTypeId,
              L"listitem"};

    case ax::mojom::Role::kTitleBar:
      return {UIALocalizationStrategy::kSupply, UIA_TitleBarControlTypeId,
              L"group"};

    case ax::mojom::Role::kToggleButton:
      return {UIALocalizationStrategy::kSupply, UIA_ButtonControlTypeId,
              L"button"};

    case ax::mojom::Role::kTextField:
      return {UIALocalizationStrategy::kDeferToControlType, UIA_EditControlTypeId,
              L"textbox"};

    case ax::mojom::Role::kSearchBox:
      return {UIALocalizationStrategy::kDeferToControlType, UIA_EditControlTypeId,
              L"searchbox"};

    case ax::mojom::Role::kTextFieldWithComboBox:
      return {UIALocalizationStrategy::kSupply, UIA_ComboBoxControlTypeId,
              L"combobox"};

    case ax::mojom::Role::kAbbr:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"description"};

    case ax::mojom::Role::kTime:
      return {UIALocalizationStrategy::kSupply, UIA_TextControlTypeId, L"time"};

    case ax::mojom::Role::kTimer:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_PaneControlTypeId, L"timer"};

    case ax::mojom::Role::kToolbar:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_ToolBarControlTypeId, L"toolbar"};

    case ax::mojom::Role::kTooltip:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_ToolTipControlTypeId, L"tooltip"};

    case ax::mojom::Role::kTree:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_TreeControlTypeId, L"tree"};

    case ax::mojom::Role::kTreeGrid:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_DataGridControlTypeId, L"treegrid"};

    case ax::mojom::Role::kTreeItem:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_TreeItemControlTypeId, L"treeitem"};

    case ax::mojom::Role::kLineBreak:
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_TextControlTypeId, L"description"};

    case ax::mojom::Role::kVideo:
      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
              L"group"};

    case ax::mojom::Role::kWebView:
      return {UIALocalizationStrategy::kSupply, UIA_DocumentControlTypeId,
              L"document"};

    case ax::mojom::Role::kImeCandidate:  // Internal role, not used on Windows.
      return {UIALocalizationStrategy::kDeferToControlType,
              UIA_PaneControlTypeId, L"group"};

    case ax::mojom::Role::kPane:
    case ax::mojom::Role::kWindow:
    case ax::mojom::Role::kKeyboard:
    case ax::mojom::Role::kNone:
    case ax::mojom::Role::kUnknown:
      return {UIALocalizationStrategy::kSupply, UIA_PaneControlTypeId,
              L"region"};

    case ax::mojom::Role::kDescriptionListTermDeprecated:
    case ax::mojom::Role::kDescriptionListDetailDeprecated:
    case ax::mojom::Role::kDirectoryDeprecated:
    case ax::mojom::Role::kPreDeprecated:
    case ax::mojom::Role::kPortalDeprecated:
      NOTREACHED();
  }
}

//
// IAccessible implementation.
//

IFACEMETHODIMP AXPlatformNodeWin::accHitTest(LONG screen_physical_pixel_x,
                                             LONG screen_physical_pixel_y,
                                             VARIANT* child) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("accHitTest");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_ACC_HIT_TEST);
  COM_OBJECT_VALIDATE_1_ARG(child);

  gfx::Point point(screen_physical_pixel_x, screen_physical_pixel_y);
  if (!GetDelegate()
           ->GetBoundsRect(AXCoordinateSystem::kScreenPhysicalPixels,
                           AXClippingBehavior::kClipped)
           .Contains(point)) {
    // Return S_FALSE and VT_EMPTY when outside the object's boundaries.
    child->vt = VT_EMPTY;
    return S_FALSE;
  }

  AXPlatformNode* current_result = this;
  while (true) {
    gfx::NativeViewAccessible hit_child =
        current_result->GetDelegate()->HitTestSync(screen_physical_pixel_x,
                                                   screen_physical_pixel_y);
    if (!hit_child) {
      child->vt = VT_EMPTY;
      return S_FALSE;
    }

    AXPlatformNode* hit_child_node =
        AXPlatformNode::FromNativeViewAccessible(hit_child);
    if (!hit_child_node)
      break;

    // If we get the same node, we're done.
    if (hit_child_node == current_result)
      break;

    // Prevent cycles / loops.
    //
    // This is a workaround for a bug where a hit test in web content might
    // return a node that's not a strict descendant. To catch that case
    // without disallowing other valid cases of hit testing, add the
    // following check:
    //
    // If the hit child comes from the same HWND, but it's not a descendant,
    // just ignore the result and stick with the current result. Note that
    // GetTargetForNativeAccessibilityEvent returns a node's owning HWND.
    //
    // Ideally this shouldn't happen - see http://crbug.com/1061323
    bool is_descendant = hit_child_node->IsDescendantOf(current_result);
    bool is_same_hwnd =
        hit_child_node->GetDelegate()->GetTargetForNativeAccessibilityEvent() ==
        current_result->GetDelegate()->GetTargetForNativeAccessibilityEvent();
    if (!is_descendant && is_same_hwnd)
      break;

    // Continue to check recursively. That's because HitTestSync may have
    // returned the best result within a particular accessibility tree,
    // but we might need to recurse further in a tree of a different type
    // (for example, from Views to Web).
    current_result = hit_child_node;
  }

  if (current_result == this) {
    // This object is the best match, so return CHILDID_SELF. It's tempting to
    // simplify the logic and use VT_DISPATCH everywhere, but the Windows
    // call AccessibleObjectFromPoint will keep calling accHitTest until some
    // object returns CHILDID_SELF.
    child->vt = VT_I4;
    child->lVal = CHILDID_SELF;
    return S_OK;
  }

  child->vt = VT_DISPATCH;
  child->pdispVal = static_cast<AXPlatformNodeWin*>(current_result);
  // Always increment ref when returning a reference to a COM object.
  child->pdispVal->AddRef();
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::accDoDefaultAction(VARIANT var_id) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("accDoDefaultAction");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_ACC_DO_DEFAULT_ACTION);
  AXPlatformNodeWin* target;
  COM_OBJECT_VALIDATE_VAR_ID_AND_GET_TARGET(var_id, target);

  AXActionData data;
  data.action = ax::mojom::Action::kDoDefault;

  if (target->GetDelegate()->AccessibilityPerformAction(data))
    return S_OK;
  return E_FAIL;
}

IFACEMETHODIMP AXPlatformNodeWin::accLocation(LONG* physical_pixel_left,
                                              LONG* physical_pixel_top,
                                              LONG* width,
                                              LONG* height,
                                              VARIANT var_id) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("accLocation");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_ACC_LOCATION);
  AXPlatformNodeWin* target;
  COM_OBJECT_VALIDATE_VAR_ID_4_ARGS_AND_GET_TARGET(
      var_id, physical_pixel_left, physical_pixel_top, width, height, target);
  NotifyObserverForMSAAUsage();

  gfx::Rect bounds = target->GetDelegate()->GetBoundsRect(
      AXCoordinateSystem::kScreenPhysicalPixels,
      AXClippingBehavior::kUnclipped);
  *physical_pixel_left = bounds.x();
  *physical_pixel_top = bounds.y();
  *width = bounds.width();
  *height = bounds.height();

  if (bounds.IsEmpty())
    return S_FALSE;

  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::accNavigate(LONG nav_dir,
                                              VARIANT start,
                                              VARIANT* end) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("accNavigate");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_ACC_NAVIGATE);
  AXPlatformNodeWin* target;
  COM_OBJECT_VALIDATE_VAR_ID_1_ARG_AND_GET_TARGET(start, end, target);

  end->vt = VT_EMPTY;
  if ((nav_dir == NAVDIR_FIRSTCHILD || nav_dir == NAVDIR_LASTCHILD) &&
      V_VT(&start) == VT_I4 && V_I4(&start) != CHILDID_SELF) {
    // MSAA states that navigating to first/last child can only be from self.
    return E_INVALIDARG;
  }

  IAccessible* result = nullptr;
  switch (nav_dir) {
    case NAVDIR_FIRSTCHILD:
      if (GetDelegate()->GetChildCount() > 0)
        result = GetDelegate()->GetFirstChild();
      break;

    case NAVDIR_LASTCHILD:
      if (GetDelegate()->GetChildCount() > 0)
        result = GetDelegate()->GetLastChild();
      break;

    case NAVDIR_NEXT: {
      AXPlatformNodeBase* next = target->GetNextSibling();
      if (next)
        result = next->GetNativeViewAccessible();
      break;
    }

    case NAVDIR_PREVIOUS: {
      AXPlatformNodeBase* previous = target->GetPreviousSibling();
      if (previous)
        result = previous->GetNativeViewAccessible();
      break;
    }

    case NAVDIR_DOWN: {
      // This direction is not implemented except in tables.
      if (!GetTableRow() || !GetTableRowSpan() || !GetTableColumn())
        return E_NOTIMPL;

      AXPlatformNodeBase* next = target->GetTableCell(
          *GetTableRow() + *GetTableRowSpan(), *GetTableColumn());
      if (!next)
        return S_OK;

      result = next->GetNativeViewAccessible();
      break;
    }

    case NAVDIR_UP: {
      // This direction is not implemented except in tables.
      if (!GetTableRow() || !GetTableColumn())
        return E_NOTIMPL;

      AXPlatformNodeBase* next =
          target->GetTableCell(*GetTableRow() - 1, *GetTableColumn());
      if (!next)
        return S_OK;

      result = next->GetNativeViewAccessible();
      break;
    }

    case NAVDIR_LEFT: {
      // This direction is not implemented except in tables.
      if (!GetTableRow() || !GetTableColumn())
        return E_NOTIMPL;

      AXPlatformNodeBase* next =
          target->GetTableCell(*GetTableRow(), *GetTableColumn() - 1);
      if (!next)
        return S_OK;

      result = next->GetNativeViewAccessible();
      break;
    }

    case NAVDIR_RIGHT: {
      // This direction is not implemented except in tables.

      if (!GetTableRow() || !GetTableColumn() || !GetTableColumnSpan())
        return E_NOTIMPL;

      AXPlatformNodeBase* next = target->GetTableCell(
          *GetTableRow(), *GetTableColumn() + *GetTableColumnSpan());
      if (!next)
        return S_OK;

      result = next->GetNativeViewAccessible();
      break;
    }
  }

  if (!result)
    return S_FALSE;

  end->vt = VT_DISPATCH;
  end->pdispVal = result;
  // Always increment ref when returning a reference to a COM object.
  end->pdispVal->AddRef();

  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_accChild(VARIANT var_child,
                                               IDispatch** disp_child) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_accChild");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_ACC_CHILD);

  *disp_child = nullptr;
  AXPlatformNodeWin* target;
  COM_OBJECT_VALIDATE_VAR_ID_AND_GET_TARGET(var_child, target);

  *disp_child = target;
  (*disp_child)->AddRef();
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_accChildCount(LONG* child_count) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_accChildCount");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_ACC_CHILD_COUNT);
  COM_OBJECT_VALIDATE_1_ARG(child_count);

  *child_count = GetDelegate()->GetChildCount();
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_accDefaultAction(VARIANT var_id,
                                                       BSTR* def_action) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_accDefaultAction");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_ACC_DEFAULT_ACTION);
  AXPlatformNodeWin* target;
  COM_OBJECT_VALIDATE_VAR_ID_1_ARG_AND_GET_TARGET(var_id, def_action, target);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  int action;
  if (!target->GetIntAttribute(ax::mojom::IntAttribute::kDefaultActionVerb,
                               &action)) {
    *def_action = nullptr;
    return S_FALSE;
  }

  std::wstring action_verb = base::UTF8ToWide(
      ToLocalizedString(static_cast<ax::mojom::DefaultActionVerb>(action)));
  if (action_verb.empty()) {
    *def_action = nullptr;
    return S_FALSE;
  }

  *def_action = SysAllocString(action_verb.c_str());
  DCHECK(def_action);
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_accDescription(VARIANT var_id,
                                                     BSTR* desc) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_accDescription");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_ACC_DESCRIPTION);
  AXPlatformNodeWin* target;
  COM_OBJECT_VALIDATE_VAR_ID_1_ARG_AND_GET_TARGET(var_id, desc, target);

  return target->GetStringAttributeAsBstr(
      ax::mojom::StringAttribute::kDescription, desc);
}

IFACEMETHODIMP AXPlatformNodeWin::get_accFocus(VARIANT* focus_child) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_accFocus");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_ACC_FOCUS);
  COM_OBJECT_VALIDATE_1_ARG(focus_child);

  gfx::NativeViewAccessible focus_accessible = GetDelegate()->GetFocus();
  if (focus_accessible == this) {
    focus_child->vt = VT_I4;
    focus_child->lVal = CHILDID_SELF;
  } else if (focus_accessible) {
    Microsoft::WRL::ComPtr<IDispatch> focus_idispatch;
    if (FAILED(
            focus_accessible->QueryInterface(IID_PPV_ARGS(&focus_idispatch)))) {
      focus_child->vt = VT_EMPTY;
      return E_FAIL;
    }

    focus_child->vt = VT_DISPATCH;
    focus_child->pdispVal = focus_idispatch.Detach();
  } else {
    focus_child->vt = VT_EMPTY;
  }

  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_accKeyboardShortcut(VARIANT var_id,
                                                          BSTR* acc_key) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_accKeyboardShortcut");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_ACC_KEYBOARD_SHORTCUT);
  AXPlatformNodeWin* target;
  COM_OBJECT_VALIDATE_VAR_ID_1_ARG_AND_GET_TARGET(var_id, acc_key, target);

  return target->GetStringAttributeAsBstr(
      ax::mojom::StringAttribute::kKeyShortcuts, acc_key);
}

IFACEMETHODIMP AXPlatformNodeWin::get_accName(VARIANT var_id, BSTR* name_bstr) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_accName");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_ACC_NAME);
  AXPlatformNodeWin* target;
  COM_OBJECT_VALIDATE_VAR_ID_1_ARG_AND_GET_TARGET(var_id, name_bstr, target);
  NotifyObserverForMSAAUsage();

  for (WinAccessibilityAPIUsageObserver& observer :
       GetWinAccessibilityAPIUsageObserverList()) {
    observer.OnAccNameCalled();
  }

  if (!IsNameExposed())
    return S_FALSE;

  bool has_name = target->HasStringAttribute(ax::mojom::StringAttribute::kName);
  std::wstring name = base::UTF8ToWide(target->GetName());

  if (name.empty() && !has_name)
    return S_FALSE;

  *name_bstr = SysAllocString(name.c_str());
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_accParent(IDispatch** disp_parent) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_accParent");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_ACC_PARENT);
  COM_OBJECT_VALIDATE_1_ARG(disp_parent);
  *disp_parent = GetParent();
  if (*disp_parent) {
    (*disp_parent)->AddRef();
    return S_OK;
  }

  return S_FALSE;
}

IFACEMETHODIMP AXPlatformNodeWin::get_accRole(VARIANT var_id, VARIANT* role) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_accRole");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_ACC_ROLE);
  AXPlatformNodeWin* target;
  COM_OBJECT_VALIDATE_VAR_ID_1_ARG_AND_GET_TARGET(var_id, role, target);
  NotifyObserverForMSAAUsage();

  role->vt = VT_I4;
  role->lVal = target->MSAARole();
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_accState(VARIANT var_id, VARIANT* state) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_accState");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_ACC_STATE);
  AXPlatformNodeWin* target;
  COM_OBJECT_VALIDATE_VAR_ID_1_ARG_AND_GET_TARGET(var_id, state, target);
  NotifyObserverForMSAAUsage();

  state->vt = VT_I4;
  state->lVal = target->MSAAState();
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_accHelp(VARIANT var_id, BSTR* help) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_accHelp");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_ACC_HELP);
  COM_OBJECT_VALIDATE_1_ARG(help);
  return S_FALSE;
}

IFACEMETHODIMP AXPlatformNodeWin::get_accValue(VARIANT var_id, BSTR* value) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_accValue");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_ACC_VALUE);
  AXPlatformNodeWin* target;
  COM_OBJECT_VALIDATE_VAR_ID_1_ARG_AND_GET_TARGET(var_id, value, target);

  // Special case for indeterminate progressbar.
  if (GetRole() == ax::mojom::Role::kProgressIndicator &&
      !HasStringAttribute(ax::mojom::StringAttribute::kValue) &&
      !HasFloatAttribute(ax::mojom::FloatAttribute::kValueForRange)) {
    // The MIXED state is also exposed for an indeterminate value.
    // However, without some value here, NVDA/JAWS 2022 will ignore the
    // progress indicator in the virtual buffer.
    *value = SysAllocString(L"0");
    return S_OK;
  }

  *value = GetValueAttributeAsBstr(target);
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::put_accValue(VARIANT var_id, BSTR new_value) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("put_accValue");
  AXPlatformNodeWin* target;
  COM_OBJECT_VALIDATE_VAR_ID_AND_GET_TARGET(var_id, target);
  if (!new_value)
    return E_INVALIDARG;

  AXActionData data;
  data.action = ax::mojom::Action::kSetValue;
  data.value = base::WideToUTF8(new_value);
  if (target->GetDelegate()->AccessibilityPerformAction(data))
    return S_OK;
  return E_FAIL;
}

IFACEMETHODIMP AXPlatformNodeWin::get_accSelection(VARIANT* selected) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_accSelection");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_ACC_SELECTION);
  COM_OBJECT_VALIDATE_1_ARG(selected);

  std::vector<Microsoft::WRL::ComPtr<IDispatch>> selected_nodes;
  for (size_t i = 0; i < GetDelegate()->GetChildCount(); ++i) {
    auto* node = static_cast<AXPlatformNodeWin*>(
        FromNativeViewAccessible(GetDelegate()->ChildAtIndex(i)));
    if (node && node->GetBoolAttribute(ax::mojom::BoolAttribute::kSelected)) {
      Microsoft::WRL::ComPtr<IDispatch> node_idispatch;
      if (SUCCEEDED(node->QueryInterface(IID_PPV_ARGS(&node_idispatch))))
        selected_nodes.push_back(node_idispatch);
    }
  }

  if (selected_nodes.empty()) {
    selected->vt = VT_EMPTY;
    return S_OK;
  }

  if (selected_nodes.size() == 1) {
    selected->vt = VT_DISPATCH;
    selected->pdispVal = selected_nodes[0].Detach();
    return S_OK;
  }

  // Multiple items are selected.
  LONG selected_count = static_cast<LONG>(selected_nodes.size());
  Microsoft::WRL::ComPtr<base::win::EnumVariant> enum_variant =
      Microsoft::WRL::Make<base::win::EnumVariant>(selected_count);
  for (LONG i = 0; i < selected_count; ++i) {
    enum_variant->ItemAt(i)->vt = VT_DISPATCH;
    enum_variant->ItemAt(i)->pdispVal = selected_nodes[i].Detach();
  }
  selected->vt = VT_UNKNOWN;
  return enum_variant.CopyTo(IID_PPV_ARGS(&V_UNKNOWN(selected)));
}

IFACEMETHODIMP AXPlatformNodeWin::accSelect(LONG flagsSelect, VARIANT var_id) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("accSelect");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_ACC_SELECT);
  AXPlatformNodeWin* target;
  COM_OBJECT_VALIDATE_VAR_ID_AND_GET_TARGET(var_id, target);

  if (flagsSelect & SELFLAG_TAKEFOCUS) {
    AXActionData action_data;
    action_data.action = ax::mojom::Action::kFocus;
    target->GetDelegate()->AccessibilityPerformAction(action_data);
    return S_OK;
  }

  return S_FALSE;
}

IFACEMETHODIMP AXPlatformNodeWin::get_accHelpTopic(BSTR* help_file,
                                                   VARIANT var_id,
                                                   LONG* topic_id) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_accHelpTopic");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_ACC_HELP_TOPIC);
  AXPlatformNodeWin* target;
  COM_OBJECT_VALIDATE_VAR_ID_2_ARGS_AND_GET_TARGET(var_id, help_file, topic_id,
                                                   target);

  if (help_file) {
    *help_file = nullptr;
  }
  if (topic_id) {
    *topic_id = static_cast<LONG>(-1);
  }
  return E_NOTIMPL;
}

IFACEMETHODIMP AXPlatformNodeWin::put_accName(VARIANT var_id, BSTR put_name) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("put_accName");
  // Deprecated by Microsoft and no good use cases; no need to implement.
  return E_NOTIMPL;
}

//
// IAccessible2 implementation.
//

IFACEMETHODIMP AXPlatformNodeWin::role(LONG* role) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("role");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_ROLE);
  COM_OBJECT_VALIDATE_1_ARG(role);

  *role = ComputeIA2Role();
  // If we didn't explicitly set the IAccessible2 role, make it the same
  // as the MSAA role.
  if (!*role)
    *role = MSAARole();
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_states(AccessibleStates* states) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_states");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_STATES);
  COM_OBJECT_VALIDATE_1_ARG(states);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  *states = ComputeIA2State();
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_uniqueID(LONG* id) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_uniqueID");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_UNIQUE_ID);
  COM_OBJECT_VALIDATE_1_ARG(id);
  // We want to negate the unique id for it to be consistent across different
  // Windows accessiblity APIs. The negative unique id convention originated
  // from ::NotifyWinEvent() takes an hwnd and a child id. A 0 child id means
  // self, and a positive child id means child #n. In order to fire an event for
  // an arbitrary descendant of the window, Firefox started the practice of
  // using a negative unique id. We follow the same negative unique id
  // convention here and when we fire events via
  // ::NotifyWinEvent().
  *id = -GetUniqueId();
  DCHECK(*id < 0);
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_windowHandle(HWND* window_handle) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_windowHandle");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_WINDOW_HANDLE);
  COM_OBJECT_VALIDATE_1_ARG(window_handle);
  *window_handle = GetDelegate()->GetTargetForNativeAccessibilityEvent();
  return *window_handle ? S_OK : S_FALSE;
}

IFACEMETHODIMP AXPlatformNodeWin::get_relationTargetsOfType(BSTR type_bstr,
                                                            LONG max_targets,
                                                            IUnknown*** targets,
                                                            LONG* n_targets) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_relationTargetsOfType");
  COM_OBJECT_VALIDATE_2_ARGS(targets, n_targets);
  if (!type_bstr)
    return E_INVALIDARG;

  *n_targets = 0;
  *targets = nullptr;

  // Special case for relations of type "alerts".
  std::wstring type(type_bstr);
  if (type == L"alerts") {
    // Collect all of the objects that have had an alert fired on them that
    // are a descendant of this object.
    std::vector<AXPlatformNodeWin*> alert_targets;
    for (auto iter = g_alert_targets.Get().begin();
         iter != g_alert_targets.Get().end(); ++iter) {
      AXPlatformNodeWin* target = *iter;
      if (IsDescendant(target))
        alert_targets.push_back(target);
    }

    LONG count = static_cast<LONG>(alert_targets.size());
    if (count == 0)
      return S_FALSE;

    // Don't return more targets than max_targets - but note that the caller
    // is allowed to specify max_targets=0 to mean no limit.
    if (max_targets > 0 && count > max_targets)
      count = max_targets;

    // Return the number of targets.
    *n_targets = count;

    // Allocate COM memory for the result array and populate it.
    *targets =
        static_cast<IUnknown**>(CoTaskMemAlloc(count * sizeof(IUnknown*)));
    for (LONG i = 0; i < count; ++i) {
      (*targets)[i] = static_cast<IAccessible*>(alert_targets[i]);
      (*targets)[i]->AddRef();
    }
    return S_OK;
  }

  std::wstring relation_type;
  std::vector<AXPlatformNode*> enumerated_targets;
  int found = AXPlatformRelationWin::EnumerateRelationships(
      this, 0, type, &relation_type, &enumerated_targets);
  if (found == 0)
    return S_FALSE;

  // Don't return more targets than max_targets - but note that the caller
  // is allowed to specify max_targets=0 to mean no limit.
  int count = static_cast<int>(enumerated_targets.size());
  if (max_targets > 0 && count > max_targets)
    count = max_targets;

  // Allocate COM memory for the result array and populate it.
  *targets = static_cast<IUnknown**>(CoTaskMemAlloc(count * sizeof(IUnknown*)));
  int index = 0;
  for (AXPlatformNode* target : enumerated_targets) {
    if (target) {
      AXPlatformNodeWin* win_target = static_cast<AXPlatformNodeWin*>(target);
      (*targets)[index] = static_cast<IAccessible*>(win_target);
      (*targets)[index]->AddRef();
      if (++index >= count) {
        break;
      }
    }
  }
  *n_targets = index;
  return index > 0 ? S_OK : S_FALSE;
}

IFACEMETHODIMP AXPlatformNodeWin::get_attributes(BSTR* attributes) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_attributes");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_IA2_GET_ATTRIBUTES);
  COM_OBJECT_VALIDATE_1_ARG(attributes);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);
  *attributes = nullptr;

  std::wstring attributes_str;
  std::vector<std::wstring> computed_attributes = ComputeIA2Attributes();
  for (const std::wstring& attribute : computed_attributes)
    attributes_str += attribute + L';';

  if (attributes_str.empty())
    return S_FALSE;

  *attributes = SysAllocString(attributes_str.c_str());
  DCHECK(*attributes);
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_indexInParent(LONG* index_in_parent) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_indexInParent");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_INDEX_IN_PARENT);
  COM_OBJECT_VALIDATE_1_ARG(index_in_parent);
  std::optional<int> index = GetIndexInParent();
  if (!index.has_value())
    return E_FAIL;

  *index_in_parent = index.value();
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_nRelations(LONG* n_relations) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_nRelations");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_N_RELATIONS);
  COM_OBJECT_VALIDATE_1_ARG(n_relations);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  int count = AXPlatformRelationWin::EnumerateRelationships(
      this, -1, std::wstring(), nullptr, nullptr);
  *n_relations = count;
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_relation(LONG relation_index,
                                               IAccessibleRelation** relation) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_relation");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_RELATION);
  COM_OBJECT_VALIDATE_1_ARG(relation);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  std::wstring relation_type;
  std::vector<AXPlatformNode*> targets;
  int found = AXPlatformRelationWin::EnumerateRelationships(
      this, relation_index, std::wstring(), &relation_type, &targets);
  if (found == 0)
    return E_INVALIDARG;

  CComObject<AXPlatformRelationWin>* relation_obj;
  HRESULT hr = CComObject<AXPlatformRelationWin>::CreateInstance(&relation_obj);
  DCHECK(SUCCEEDED(hr));
  relation_obj->AddRef();
  relation_obj->Initialize(relation_type);
  for (AXPlatformNode* target : targets) {
    if (target)
      relation_obj->AddTarget(static_cast<AXPlatformNodeWin*>(target));
  }

  // Maintain references to all relations returned by this object.
  // Every time this object changes state, invalidate them.
  relations_.push_back(relation_obj);
  *relation = relation_obj;
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_relations(LONG max_relations,
                                                IAccessibleRelation** relations,
                                                LONG* n_relations) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_relations");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_RELATIONS);
  COM_OBJECT_VALIDATE_2_ARGS(relations, n_relations);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  LONG count;
  HRESULT hr = get_nRelations(&count);
  if (!SUCCEEDED(hr))
    return hr;
  count = std::min(count, max_relations);
  *n_relations = count;
  for (LONG i = 0; i < count; i++) {
    hr = get_relation(i, &relations[i]);
    if (!SUCCEEDED(hr))
      return hr;
  }

  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_groupPosition(
    LONG* group_level,
    LONG* similar_items_in_group,
    LONG* position_in_group) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_groupPosition");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_GROUP_POSITION);
  COM_OBJECT_VALIDATE_3_ARGS(group_level, similar_items_in_group,
                             position_in_group);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  *group_level = GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel);
  *similar_items_in_group = GetSetSize().value_or(0);
  *position_in_group = GetPosInSet().value_or(0);

  if (!*group_level && !*similar_items_in_group && !*position_in_group)
    return S_FALSE;
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_localizedExtendedRole(
    BSTR* localized_extended_role) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_localizedExtendedRole");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_LOCALIZED_EXTENDED_ROLE);
  COM_OBJECT_VALIDATE_1_ARG(localized_extended_role);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  std::u16string role_description =
      GetRoleDescriptionFromImageAnnotationStatusOrFromAttribute();
  if (base::ContainsOnlyChars(role_description, base::kWhitespaceUTF16))
    return S_FALSE;

  *localized_extended_role = SysAllocString(base::as_wcstr(role_description));
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_attribute(BSTR name, VARIANT* attribute) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_attribute");
  COM_OBJECT_VALIDATE_1_ARG(attribute);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  std::wstring desired_attribute(name);

  // Each computed attribute from ComputeIA2Attributes is a string of
  // the form "key:value". Search for strings that start with the
  // attribute name plus a colon.
  std::wstring prefix = desired_attribute + L":";

  // Let's accept any case.
  const auto compare_case = base::CompareCase::INSENSITIVE_ASCII;

  const std::vector<std::wstring> computed_attributes = ComputeIA2Attributes();
  for (const std::wstring& computed_attribute : computed_attributes) {
    if (base::StartsWith(computed_attribute, prefix, compare_case)) {
      std::wstring value = computed_attribute.substr(prefix.size());
      attribute->vt = VT_BSTR;
      attribute->bstrVal = SysAllocString(value.c_str());
      return S_OK;
    }
  }

  return S_FALSE;
}

//
// IAccessible2 methods not implemented.
//

IFACEMETHODIMP AXPlatformNodeWin::get_extendedRole(BSTR* extended_role) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_extendedRole");
  return E_NOTIMPL;
}

IFACEMETHODIMP AXPlatformNodeWin::scrollTo(enum IA2ScrollType ia2_scroll_type) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("scrollTo");
  COM_OBJECT_VALIDATE();
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_IA2_SCROLL_TO);

  switch (ia2_scroll_type) {
    case IA2_SCROLL_TYPE_TOP_LEFT:
      ScrollToNode(ScrollType::TopLeft);
      break;
    case IA2_SCROLL_TYPE_BOTTOM_RIGHT:
      ScrollToNode(ScrollType::BottomRight);
      break;
    case IA2_SCROLL_TYPE_TOP_EDGE:
      ScrollToNode(ScrollType::TopEdge);
      break;
    case IA2_SCROLL_TYPE_BOTTOM_EDGE:
      ScrollToNode(ScrollType::BottomEdge);
      break;
    case IA2_SCROLL_TYPE_LEFT_EDGE:
      ScrollToNode(ScrollType::LeftEdge);
      break;
    case IA2_SCROLL_TYPE_RIGHT_EDGE:
      ScrollToNode(ScrollType::RightEdge);
      break;
    case IA2_SCROLL_TYPE_ANYWHERE:
      ScrollToNode(ScrollType::Anywhere);
      break;
  }
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::scrollToPoint(
    enum IA2CoordinateType coordinate_type,
    LONG x,
    LONG y) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("scrollToPoint");
  COM_OBJECT_VALIDATE();
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_SCROLL_TO_POINT);

  // Convert to screen-relative coordinates if necessary.
  gfx::Point scroll_to(x, y);
  if (coordinate_type == IA2_COORDTYPE_PARENT_RELATIVE) {
    if (GetParent()) {
      AXPlatformNodeBase* base = FromNativeViewAccessible(GetParent());
      scroll_to += base->GetDelegate()
                       ->GetBoundsRect(AXCoordinateSystem::kScreenDIPs,
                                       AXClippingBehavior::kUnclipped)
                       .OffsetFromOrigin();
    }
  } else if (coordinate_type != IA2_COORDTYPE_SCREEN_RELATIVE) {
    return E_INVALIDARG;
  }

  AXActionData action_data;
  action_data.target_node_id = GetData().id;
  action_data.action = ax::mojom::Action::kScrollToPoint;
  action_data.target_point = scroll_to;
  GetDelegate()->AccessibilityPerformAction(action_data);
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_nExtendedStates(LONG* n_extended_states) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_nExtendedStates");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_N_EXTENDED_STATES);
  return E_NOTIMPL;
}

IFACEMETHODIMP AXPlatformNodeWin::get_extendedStates(LONG max_extended_states,
                                                     BSTR** extended_states,
                                                     LONG* n_extended_states) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_extendedStates");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_EXTENDED_STATES);
  return E_NOTIMPL;
}

IFACEMETHODIMP AXPlatformNodeWin::get_localizedExtendedStates(
    LONG max_localized_extended_states,
    BSTR** localized_extended_states,
    LONG* n_localized_extended_states) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_localizedExtendedStates");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_LOCALIZED_EXTENDED_STATES);

  return E_NOTIMPL;
}

IFACEMETHODIMP AXPlatformNodeWin::get_locale(IA2Locale* locale) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_locale");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_LOCALE);
  return E_NOTIMPL;
}

IFACEMETHODIMP AXPlatformNodeWin::get_accessibleWithCaret(IUnknown** accessible,
                                                          LONG* caret_offset) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_accessibleWithCaret");
  return E_NOTIMPL;
}

//
// IAccessible2_3 implementation.
//

IFACEMETHODIMP AXPlatformNodeWin::get_selectionRanges(IA2Range** ranges,
                                                      LONG* nRanges) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_selectionRanges");
  COM_OBJECT_VALIDATE_2_ARGS(ranges, nRanges);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);
  AXSelection unignored_selection = GetDelegate()->GetUnignoredSelection();

  AXNodeID anchor_id = unignored_selection.anchor_object_id;
  auto* anchor_node =
      static_cast<AXPlatformNodeWin*>(GetDelegate()->GetFromNodeID(anchor_id));
  if (!anchor_node)
    return E_FAIL;

  AXNodeID focus_id = unignored_selection.focus_object_id;
  auto* focus_node =
      static_cast<AXPlatformNodeWin*>(GetDelegate()->GetFromNodeID(focus_id));
  if (!focus_node)
    return E_FAIL;

  if (!IsDescendant(anchor_node) || !IsDescendant(focus_node))
    return S_FALSE;  // No selection within this subtree.

  *ranges = reinterpret_cast<IA2Range*>(CoTaskMemAlloc(sizeof(IA2Range)));
  anchor_node->AddRef();
  ranges[0]->anchor = static_cast<IAccessible*>(anchor_node);
  ranges[0]->anchorOffset = unignored_selection.anchor_offset;
  focus_node->AddRef();
  ranges[0]->active = static_cast<IAccessible*>(focus_node);
  ranges[0]->activeOffset = unignored_selection.focus_offset;
  *nRanges = 1;
  return S_OK;
}

//
// IAccessible2_4 implementation.
//

IFACEMETHODIMP AXPlatformNodeWin::setSelectionRanges(LONG nRanges,
                                                     IA2Range* ranges) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("setSelectionRanges");
  COM_OBJECT_VALIDATE();
  // Blink supports only one selection range for now.
  if (nRanges != 1)
    return E_INVALIDARG;
  if (!ranges)
    return E_INVALIDARG;
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  if (!ranges->anchor)
    return E_INVALIDARG;
  if (!ranges->active)
    return E_INVALIDARG;

  Microsoft::WRL::ComPtr<IAccessible> anchor;
  if (FAILED(ranges->anchor->QueryInterface(IID_PPV_ARGS(&anchor))))
    return E_INVALIDARG;

  Microsoft::WRL::ComPtr<IAccessible> focus;
  if (FAILED(ranges->active->QueryInterface(IID_PPV_ARGS(&focus))))
    return E_INVALIDARG;

  const auto* anchor_node =
      static_cast<AXPlatformNodeWin*>(FromNativeViewAccessible(anchor.Get()));
  const auto* focus_node =
      static_cast<AXPlatformNodeWin*>(FromNativeViewAccessible(focus.Get()));
  if (!anchor_node || !focus_node)
    return E_INVALIDARG;

  // Blink only supports selections within a single tree.
  AXTreeID anchor_tree_id = anchor_node->GetDelegate()->GetTreeData().tree_id;
  AXTreeID focus_tree_id = focus_node->GetDelegate()->GetTreeData().tree_id;
  if (anchor_tree_id != focus_tree_id) {
    return E_INVALIDARG;
  }

  if (ranges->anchorOffset < 0 || ranges->activeOffset < 0)
    return E_INVALIDARG;

  if (anchor_node->IsLeaf()) {
    if (static_cast<size_t>(ranges->anchorOffset) >
        anchor_node->GetHypertext().length()) {
      return E_INVALIDARG;
    }
  } else {
    if (static_cast<size_t>(ranges->anchorOffset) >
        anchor_node->GetChildCount())
      return E_INVALIDARG;
  }

  if (focus_node->IsLeaf()) {
    if (static_cast<size_t>(ranges->activeOffset) >
        focus_node->GetHypertext().length())
      return E_INVALIDARG;
  } else {
    if (static_cast<size_t>(ranges->activeOffset) > focus_node->GetChildCount())
      return E_INVALIDARG;
  }

  AXActionData action_data;
  action_data.action = ax::mojom::Action::kSetSelection;
  action_data.anchor_node_id = anchor_node->GetData().id;
  action_data.target_tree_id = anchor_tree_id;
  action_data.anchor_offset = int32_t{ranges->anchorOffset};
  action_data.focus_node_id = focus_node->GetData().id;
  action_data.focus_offset = int32_t{ranges->activeOffset};
  if (GetDelegate()->AccessibilityPerformAction(action_data))
    return S_OK;
  return S_FALSE;
}

//
// IAccessibleEx implementation.
//

IFACEMETHODIMP AXPlatformNodeWin::GetObjectForChild(LONG child_id,
                                                    IAccessibleEx** result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("GetObjectForChild");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_OBJECT_FOR_CHILD);
  // No support for child IDs in this implementation.
  COM_OBJECT_VALIDATE_1_ARG(result);
  *result = nullptr;
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::GetIAccessiblePair(IAccessible** accessible,
                                                     LONG* child_id) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("GetIAccessiblePair");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_IACCESSIBLE_PAIR);
  COM_OBJECT_VALIDATE_2_ARGS(accessible, child_id);
  *accessible = static_cast<IAccessible*>(this);
  (*accessible)->AddRef();
  *child_id = CHILDID_SELF;
  return S_OK;
}

//
// IAnnotationProvider methods.
//

IFACEMETHODIMP AXPlatformNodeWin::get_AnnotationTypeId(int* type_id) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_AnnotationTypeId");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_ANNOTATION_GET_ANNOTATIONTYPEID);
  UIA_VALIDATE_CALL_1_ARG(type_id);

  *type_id = GetAnnotationTypeImpl();
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_AnnotationTypeName(BSTR* type_name) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_AnnotationTypeName");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_ANNOTATION_GET_ANNOTATIONTYPENAME);
  UIA_VALIDATE_CALL_1_ARG(type_name);

  // According to UIA spec, for well known AnnotationType, we do not need to
  // implement the type name, since UI Automation can provide a default name, so
  // we do not set |type_name|.
  // But for unknown type, we should provide a name, so we set |type_name| to
  // the localized role description.
  // https://docs.microsoft.com/en-us/windows/win32/winauto/uiauto-implementingannotation
  if (AnnotationType_Unknown == GetAnnotationTypeImpl())
    *type_name = SysAllocString(base::as_wcstr(GetRoleDescription()));

  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_Author(BSTR* author) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_Author");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_ANNOTATION_GET_AUTHOR);
  UIA_VALIDATE_CALL_1_ARG(author);
  // This method is optional, and currently does not have a mapping. So we
  // return S_OK with empty string.
  *author = SysAllocString(L"");
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_DateTime(BSTR* date_time) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_DateTime");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_ANNOTATION_GET_DATETIME);
  UIA_VALIDATE_CALL_1_ARG(date_time);
  // This method is optional, and currently does not have a mapping. So we
  // return S_OK with empty string.
  *date_time = SysAllocString(L"");
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_Target(
    IRawElementProviderSimple** target) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_Target");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_ANNOTATION_GET_TARGET);
  UIA_VALIDATE_CALL_1_ARG(target);
  std::vector<AXPlatformNode*> reverse_relations =
      GetDelegate()->GetSourceNodesForReverseRelations(
          ax::mojom::IntListAttribute::kDetailsIds);

  // If there is no reverse relation target, IAnnotationProvider
  // should not be exposed in the first place.
  DCHECK_GT(reverse_relations.size(), 0u);
  AXPlatformNodeWin* target_node;
  auto iter = reverse_relations.begin();
  target_node = static_cast<AXPlatformNodeWin*>(*iter);

  // Since this method is expected to return only one target, if the node has
  // multiple targets, we default to return the first one.
  // Since |reverse_relations| does not guarantee the order of the nodes. We
  // have to compare the nodes to find the first target node.
  ++iter;
  while (iter != reverse_relations.end()) {
    AXPlatformNodeWin* lhs = static_cast<AXPlatformNodeWin*>(*iter);
    if (lhs->CompareTo(*target_node) < 0)
      target_node = lhs;
    ++iter;
  }

  return target_node->QueryInterface(IID_PPV_ARGS(target));
}

int AXPlatformNodeWin::GetAnnotationTypeImpl() const {
  switch (GetRole()) {
    case ax::mojom::Role::kComment:
      return AnnotationType_Comment;
    case ax::mojom::Role::kDocEndnote:
      return AnnotationType_Endnote;
    case ax::mojom::Role::kDocFootnote:
      return AnnotationType_Footnote;
    case ax::mojom::Role::kMark:
      return AnnotationType_Highlighted;
    case ax::mojom::Role::kGroup:
    case ax::mojom::Role::kRegion: {
      if (DescendantHasComment(this))
        return AnnotationType_Comment;

      [[fallthrough]];
    }
    default:
      return AnnotationType_Unknown;
  }
}

//
// IExpandCollapseProvider implementation.
//

IFACEMETHODIMP AXPlatformNodeWin::Collapse() {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("Collapse");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_EXPANDCOLLAPSE_COLLAPSE);
  UIA_VALIDATE_CALL();
  if (GetData().GetRestriction() == ax::mojom::Restriction::kDisabled)
    return UIA_E_ELEMENTNOTAVAILABLE;

  if (HasState(ax::mojom::State::kCollapsed))
    return UIA_E_INVALIDOPERATION;

  AXActionData action_data;
  action_data.action = ax::mojom::Action::kCollapse;
  if (GetDelegate()->AccessibilityPerformAction(action_data))
    return S_OK;
  return E_FAIL;
}

IFACEMETHODIMP AXPlatformNodeWin::Expand() {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("Expand");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_EXPANDCOLLAPSE_EXPAND);
  UIA_VALIDATE_CALL();
  if (GetData().GetRestriction() == ax::mojom::Restriction::kDisabled)
    return UIA_E_ELEMENTNOTAVAILABLE;

  if (HasState(ax::mojom::State::kExpanded))
    return UIA_E_INVALIDOPERATION;

  AXActionData action_data;
  action_data.action = ax::mojom::Action::kExpand;
  if (GetDelegate()->AccessibilityPerformAction(action_data))
    return S_OK;
  return E_FAIL;
}

ExpandCollapseState AXPlatformNodeWin::ComputeExpandCollapseState() const {
  if (HasState(ax::mojom::State::kExpanded)) {
    return ExpandCollapseState_Expanded;
  } else if (HasState(ax::mojom::State::kCollapsed)) {
    return ExpandCollapseState_Collapsed;
  } else if (GetData().IsMenuButton()) {
    // Since a menu button implies there is a popup and it is either expanded or
    // collapsed, it should not support ExpandCollapseState_LeafNode.
    // According to the UIA spec, ExpandCollapseState_LeafNode indicates that
    // the element neither expands nor collapses.
    if (GetData().IsButtonPressed()) {
      return ExpandCollapseState_Expanded;
    } else {
      return ExpandCollapseState_Collapsed;
    }
  } else {
    return ExpandCollapseState_LeafNode;
  }
}

IFACEMETHODIMP AXPlatformNodeWin::get_ExpandCollapseState(
    ExpandCollapseState* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_ExpandCollapseState");
  WIN_ACCESSIBILITY_API_HISTOGRAM(
      UMA_API_EXPANDCOLLAPSE_GET_EXPANDCOLLAPSESTATE);
  UIA_VALIDATE_CALL_1_ARG(result);

  *result = ComputeExpandCollapseState();

  return S_OK;
}

//
// IGridItemProvider implementation.
//

IFACEMETHODIMP AXPlatformNodeWin::get_Column(int* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_Column");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GRIDITEM_GET_COLUMN);
  UIA_VALIDATE_CALL_1_ARG(result);

  std::optional<int> column = GetDelegate()->GetTableCellAriaColIndex();

  // |aria-colindex| starts at 1, where as IGridItemProvider::get_Column's index
  // starts at 0, so we need to subtract by 1 if |aria-colindex| attribute is
  // present.
  //  https://www.w3.org/TR/core-aam-1.2/#aria-colindex
  if (column)
    *column = *column - 1;
  else
    column = GetTableColumn();

  if (!column)
    return E_FAIL;
  *result = *column;
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_ColumnSpan(int* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_ColumnSpan");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GRIDITEM_GET_COLUMNSPAN);
  UIA_VALIDATE_CALL_1_ARG(result);
  std::optional<int> column_span = GetTableColumnSpan();
  if (!column_span)
    return E_FAIL;
  *result = *column_span;
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_ContainingGrid(
    IRawElementProviderSimple** result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_ContainingGrid");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GRIDITEM_GET_CONTAININGGRID);
  UIA_VALIDATE_CALL_1_ARG(result);

  AXPlatformNodeBase* table = GetTable();
  if (!table)
    return E_FAIL;

  auto* node_win = static_cast<AXPlatformNodeWin*>(table);
  node_win->AddRef();
  *result = static_cast<IRawElementProviderSimple*>(node_win);
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_Row(int* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_Row");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GRIDITEM_GET_ROW);
  UIA_VALIDATE_CALL_1_ARG(result);

  std::optional<int> row = GetDelegate()->GetTableCellAriaRowIndex();

  // |aria-rowindex| starts at 1, where as IGridItemProvider::get_Row's index
  // starts at 0, so we need to subtract by 1 if |aria-rowindex| attribute is
  // present.
  //  https://www.w3.org/TR/core-aam-1.2/#aria-rowindex
  if (row)
    *row = *row - 1;
  else
    row = GetTableRow();

  if (!row)
    return E_FAIL;
  *result = *row;
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_RowSpan(int* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_RowSpan");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GRIDITEM_GET_ROWSPAN);
  UIA_VALIDATE_CALL_1_ARG(result);
  std::optional<int> row_span = GetTableRowSpan();
  if (!row_span)
    return E_FAIL;
  *result = *row_span;
  return S_OK;
}

//
// IGridProvider implementation.
//

IFACEMETHODIMP AXPlatformNodeWin::GetItem(int row,
                                          int column,
                                          IRawElementProviderSimple** result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("GetItem");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GRID_GETITEM);
  UIA_VALIDATE_CALL_1_ARG(result);

  // While UIA is 0-based, aria is 1-based, so correct here.
  AXPlatformNodeBase* cell = GetAriaTableCell(row + 1, column + 1);
  if (!cell) {
    cell = GetTableCell(row, column);
  }

  if (!cell) {
    return E_INVALIDARG;
  }

  auto* node_win = static_cast<AXPlatformNodeWin*>(cell);
  node_win->AddRef();
  *result = static_cast<IRawElementProviderSimple*>(node_win);
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_RowCount(int* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_RowCount");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GRID_GET_ROWCOUNT);
  UIA_VALIDATE_CALL_1_ARG(result);

  std::optional<int> row_count = GetTableAriaRowCount();
  if (!row_count)
    row_count = GetTableRowCount();

  if (!row_count || *row_count == ax::mojom::kUnknownAriaColumnOrRowCount)
    return E_UNEXPECTED;
  *result = *row_count;
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_ColumnCount(int* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_ColumnCount");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GRID_GET_COLUMNCOUNT);
  UIA_VALIDATE_CALL_1_ARG(result);

  std::optional<int> column_count = GetTableAriaColumnCount();
  if (!column_count)
    column_count = GetTableColumnCount();

  if (!column_count ||
      *column_count == ax::mojom::kUnknownAriaColumnOrRowCount) {
    return E_UNEXPECTED;
  }
  *result = *column_count;
  return S_OK;
}

//
// IInvokeProvider implementation.
//

IFACEMETHODIMP AXPlatformNodeWin::Invoke() {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("Invoke");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_INVOKE_INVOKE);
  UIA_VALIDATE_CALL();

  if (GetData().GetRestriction() == ax::mojom::Restriction::kDisabled)
    return UIA_E_ELEMENTNOTENABLED;

  AXActionData action_data;
  action_data.action = ax::mojom::Action::kDoDefault;
  GetDelegate()->AccessibilityPerformAction(action_data);

  return S_OK;
}

//
// IScrollItemProvider implementation.
//

IFACEMETHODIMP AXPlatformNodeWin::ScrollIntoView() {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("ScrollIntoView");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_SCROLLITEM_SCROLLINTOVIEW);
  UIA_VALIDATE_CALL();
  gfx::Rect r = gfx::ToEnclosingRect(GetData().relative_bounds.bounds);
  r -= r.OffsetFromOrigin();

  AXActionData action_data;
  action_data.action = ax::mojom::Action::kScrollToMakeVisible;
  action_data.target_node_id = GetData().id;
  action_data.target_rect = r;
  action_data.horizontal_scroll_alignment =
      ax::mojom::ScrollAlignment::kScrollAlignmentCenter;
  action_data.vertical_scroll_alignment =
      ax::mojom::ScrollAlignment::kScrollAlignmentCenter;
  action_data.scroll_behavior =
      ax::mojom::ScrollBehavior::kDoNotScrollIfVisible;
  if (GetDelegate()->AccessibilityPerformAction(action_data))
    return S_OK;
  return E_FAIL;
}

//
// IScrollProvider implementation.
//

IFACEMETHODIMP AXPlatformNodeWin::Scroll(ScrollAmount horizontal_amount,
                                         ScrollAmount vertical_amount) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("Scroll");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_SCROLL_SCROLL);
  UIA_VALIDATE_CALL();
  if (!IsScrollable())
    return E_FAIL;

  AXActionData action_data;
  action_data.target_node_id = GetData().id;
  action_data.action = ax::mojom::Action::kSetScrollOffset;
  action_data.target_point = gfx::PointAtOffsetFromOrigin(
      CalculateUIAScrollPoint(horizontal_amount, vertical_amount));
  if (GetDelegate()->AccessibilityPerformAction(action_data))
    return S_OK;
  return E_FAIL;
}

IFACEMETHODIMP AXPlatformNodeWin::SetScrollPercent(double horizontal_percent,
                                                   double vertical_percent) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("SetScrollPercent");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_SCROLL_SETSCROLLPERCENT);
  UIA_VALIDATE_CALL();
  if (!IsScrollable())
    return E_FAIL;

  const double x_min = GetIntAttribute(ax::mojom::IntAttribute::kScrollXMin);
  const double x_max = GetIntAttribute(ax::mojom::IntAttribute::kScrollXMax);
  const double y_min = GetIntAttribute(ax::mojom::IntAttribute::kScrollYMin);
  const double y_max = GetIntAttribute(ax::mojom::IntAttribute::kScrollYMax);
  const int x =
      base::ClampRound(horizontal_percent / 100.0 * (x_max - x_min) + x_min);
  const int y =
      base::ClampRound(vertical_percent / 100.0 * (y_max - y_min) + y_min);
  const gfx::Point scroll_to(x, y);

  AXActionData action_data;
  action_data.target_node_id = GetData().id;
  action_data.action = ax::mojom::Action::kSetScrollOffset;
  action_data.target_point = scroll_to;
  if (GetDelegate()->AccessibilityPerformAction(action_data))
    return S_OK;
  return E_FAIL;
}

IFACEMETHODIMP AXPlatformNodeWin::get_HorizontallyScrollable(BOOL* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_HorizontallyScrollable");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_SCROLL_GET_HORIZONTALLYSCROLLABLE);
  UIA_VALIDATE_CALL_1_ARG(result);
  *result = IsHorizontallyScrollable();
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_HorizontalScrollPercent(double* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_HorizontalScrollPercent");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_SCROLL_GET_HORIZONTALSCROLLPERCENT);
  UIA_VALIDATE_CALL_1_ARG(result);
  *result = GetHorizontalScrollPercent();
  return S_OK;
}

// Horizontal size of the viewable region as a percentage of the total content
// area.
IFACEMETHODIMP AXPlatformNodeWin::get_HorizontalViewSize(double* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_HorizontalViewSize");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_SCROLL_GET_HORIZONTALVIEWSIZE);
  UIA_VALIDATE_CALL_1_ARG(result);
  if (!IsHorizontallyScrollable()) {
    *result = 100.;
    return S_OK;
  }

  gfx::RectF clipped_bounds(GetDelegate()->GetBoundsRect(
      AXCoordinateSystem::kScreenDIPs, AXClippingBehavior::kClipped));
  float x_min = GetIntAttribute(ax::mojom::IntAttribute::kScrollXMin);
  float x_max = GetIntAttribute(ax::mojom::IntAttribute::kScrollXMax);
  float total_width = clipped_bounds.width() + x_max - x_min;
  DCHECK_LE(clipped_bounds.width(), total_width);
  *result = 100.0 * clipped_bounds.width() / total_width;
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_VerticallyScrollable(BOOL* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_VerticallyScrollable");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_SCROLL_GET_VERTICALLYSCROLLABLE);
  UIA_VALIDATE_CALL_1_ARG(result);
  *result = IsVerticallyScrollable();
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_VerticalScrollPercent(double* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_VerticalScrollPercent");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_SCROLL_GET_VERTICALSCROLLPERCENT);
  UIA_VALIDATE_CALL_1_ARG(result);
  *result = GetVerticalScrollPercent();
  return S_OK;
}

// Vertical size of the viewable region as a percentage of the total content
// area.
IFACEMETHODIMP AXPlatformNodeWin::get_VerticalViewSize(double* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_VerticalViewSize");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_SCROLL_GET_VERTICALVIEWSIZE);
  UIA_VALIDATE_CALL_1_ARG(result);
  if (!IsVerticallyScrollable()) {
    *result = 100.0;
    return S_OK;
  }

  gfx::RectF clipped_bounds(GetDelegate()->GetBoundsRect(
      AXCoordinateSystem::kScreenDIPs, AXClippingBehavior::kClipped));
  float y_min = GetIntAttribute(ax::mojom::IntAttribute::kScrollYMin);
  float y_max = GetIntAttribute(ax::mojom::IntAttribute::kScrollYMax);
  float total_height = clipped_bounds.height() + y_max - y_min;
  DCHECK_LE(clipped_bounds.height(), total_height);
  *result = 100.0 * clipped_bounds.height() / total_height;
  return S_OK;
}

//
// ISelectionItemProvider implementation.
//

HRESULT AXPlatformNodeWin::ISelectionItemProviderSetSelected(
    bool selected) const {
  UIA_VALIDATE_CALL();
  if (GetData().GetRestriction() == ax::mojom::Restriction::kDisabled)
    return UIA_E_ELEMENTNOTENABLED;

  // The platform implements selection follows focus for single-selection
  // container elements. Focus action can change a node's accessibility selected
  // state, but does not cause the actual control to be selected.
  // https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_selection_follows_focus
  // https://www.w3.org/TR/core-aam-1.2/#mapping_events_selection
  //
  // We don't want to perform |Action::kDoDefault| for an ax node that has
  // |kSelected=true| and |kSelectedFromFocus=false|, because perform
  // |Action::kDoDefault| may cause the control to be unselected. However, if an
  // ax node is selected due to focus, i.e. |kSelectedFromFocus=true|, we need
  // to perform |Action::kDoDefault| on the ax node, since focus action only
  // changes an ax node's accessibility selected state to |kSelected=true| and
  // no |Action::kDoDefault| was performed on that node yet. So we need to
  // perform |Action::kDoDefault| on the ax node to cause its associated control
  // to be selected.
  if (selected == ISelectionItemProviderIsSelected() &&
      !GetBoolAttribute(ax::mojom::BoolAttribute::kSelectedFromFocus))
    return S_OK;

  AXActionData data;
  data.action = ax::mojom::Action::kDoDefault;
  if (GetDelegate()->AccessibilityPerformAction(data))
    return S_OK;
  return UIA_E_INVALIDOPERATION;
}

bool AXPlatformNodeWin::ISelectionItemProviderIsSelected() const {
  // https://www.w3.org/TR/core-aam-1.1/#mapping_state-property_table
  // SelectionItem.IsSelected is set according to the True or False value of
  // aria-checked for 'radio' and 'menuitemradio' roles.
  if (GetRole() == ax::mojom::Role::kRadioButton ||
      GetRole() == ax::mojom::Role::kMenuItemRadio)
    return GetData().GetCheckedState() == ax::mojom::CheckedState::kTrue;

  // https://www.w3.org/TR/wai-aria-1.1/#aria-selected
  // SelectionItem.IsSelected is set according to the True or False value of
  // aria-selected.
  return GetBoolAttribute(ax::mojom::BoolAttribute::kSelected);
}

ToggleState AXPlatformNodeWin::GetToggleStateImpl() {
  const auto checked_state = GetData().GetCheckedState();
  if (checked_state == ax::mojom::CheckedState::kTrue) {
    return ToggleState_On;
  } else if (checked_state == ax::mojom::CheckedState::kMixed) {
    return ToggleState_Indeterminate;
  } else {
    return ToggleState_Off;
  }
}

bool AXPlatformNodeWin::IsNodeInaccessibleForUIA() const {
  // Ignored nodes and those that are descendants of a leaf node shouldn't be
  // exposed to UIA. For example, an atomic text field can have text children
  // in our internal AX tree but isn't supposed to have any in UIA's AX tree.
  // We also don't expose inline text boxes to UIA.
  return GetData().IsIgnored() ||
         GetRole() == ax::mojom::Role::kInlineTextBox || IsChildOfLeaf();
}

IFACEMETHODIMP AXPlatformNodeWin::AddToSelection() {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("AddToSelection");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_SELECTIONITEM_ADDTOSELECTION);
  return ISelectionItemProviderSetSelected(true);
}

IFACEMETHODIMP AXPlatformNodeWin::RemoveFromSelection() {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("RemoveFromSelection");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_SELECTIONITEM_REMOVEFROMSELECTION);
  return ISelectionItemProviderSetSelected(false);
}

IFACEMETHODIMP AXPlatformNodeWin::Select() {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("Select");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_SELECTIONITEM_SELECT);
  return ISelectionItemProviderSetSelected(true);
}

IFACEMETHODIMP AXPlatformNodeWin::get_IsSelected(BOOL* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_IsSelected");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_SELECTIONITEM_GET_ISSELECTED);
  UIA_VALIDATE_CALL_1_ARG(result);
  *result = ISelectionItemProviderIsSelected();
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_SelectionContainer(
    IRawElementProviderSimple** result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_SelectionContainer");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_SELECTIONITEM_GET_SELECTIONCONTAINER);
  UIA_VALIDATE_CALL_1_ARG(result);

  auto* node_win = static_cast<AXPlatformNodeWin*>(GetSelectionContainer());
  if (!node_win)
    return E_FAIL;

  node_win->AddRef();
  *result = static_cast<IRawElementProviderSimple*>(node_win);
  return S_OK;
}

//
// ISelectionProvider implementation.
//

IFACEMETHODIMP AXPlatformNodeWin::GetSelection(SAFEARRAY** result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("GetSelection");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_SELECTION_GETSELECTION);
  UIA_VALIDATE_CALL_1_ARG(result);

  std::vector<AXPlatformNodeBase*> selected_children;
  int max_items = GetMaxSelectableItems();
  if (max_items)
    GetSelectedItems(max_items, &selected_children);

  LONG selected_children_count = selected_children.size();
  *result = SafeArrayCreateVector(VT_UNKNOWN, 0, selected_children_count);
  if (!*result)
    return E_OUTOFMEMORY;

  for (LONG i = 0; i < selected_children_count; ++i) {
    AXPlatformNodeWin* children =
        static_cast<AXPlatformNodeWin*>(selected_children[i]);
    HRESULT hr = SafeArrayPutElement(
        *result, &i, static_cast<IRawElementProviderSimple*>(children));
    if (FAILED(hr)) {
      SafeArrayDestroy(*result);
      *result = nullptr;
      return hr;
    }
  }
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_CanSelectMultiple(BOOL* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_CanSelectMultiple");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_SELECTION_GET_CANSELECTMULTIPLE);
  UIA_VALIDATE_CALL_1_ARG(result);
  *result = HasState(ax::mojom::State::kMultiselectable);
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_IsSelectionRequired(BOOL* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_IsSelectionRequired");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_SELECTION_GET_ISSELECTIONREQUIRED);
  UIA_VALIDATE_CALL_1_ARG(result);
  *result = HasState(ax::mojom::State::kRequired);
  return S_OK;
}

//
// ITableItemProvider methods.
//

IFACEMETHODIMP AXPlatformNodeWin::GetColumnHeaderItems(SAFEARRAY** result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("GetColumnHeaderItems");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TABLEITEM_GETCOLUMNHEADERITEMS);
  UIA_VALIDATE_CALL_1_ARG(result);

  std::optional<int> column = GetTableColumn();
  if (!column)
    return E_FAIL;

  *result =
      CreateUIAElementsSafeArray(CreatePlatformNodeVectorFromRelationIdVector(
          GetDelegate()->GetColHeaderNodeIds(*column)));
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::GetRowHeaderItems(SAFEARRAY** result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("GetRowHeaderItems");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TABLEITEM_GETROWHEADERITEMS);
  UIA_VALIDATE_CALL_1_ARG(result);

  std::optional<int> row = GetTableRow();
  if (!row)
    return E_FAIL;

  *result =
      CreateUIAElementsSafeArray(CreatePlatformNodeVectorFromRelationIdVector(
          GetDelegate()->GetRowHeaderNodeIds(*row)));
  return S_OK;
}

//
// ITableProvider methods.
//

IFACEMETHODIMP AXPlatformNodeWin::GetColumnHeaders(SAFEARRAY** result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("GetColumnHeaders");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TABLE_GETCOLUMNHEADERS);
  UIA_VALIDATE_CALL_1_ARG(result);

  *result =
      CreateUIAElementsSafeArray(CreatePlatformNodeVectorFromRelationIdVector(
          GetDelegate()->GetColHeaderNodeIds()));
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::GetRowHeaders(SAFEARRAY** result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("GetRowHeaders");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TABLE_GETROWHEADERS);
  UIA_VALIDATE_CALL_1_ARG(result);

  *result =
      CreateUIAElementsSafeArray(CreatePlatformNodeVectorFromRelationIdVector(
          GetDelegate()->GetRowHeaderNodeIds()));
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_RowOrColumnMajor(
    RowOrColumnMajor* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_RowOrColumnMajor");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TABLE_GET_ROWORCOLUMNMAJOR);
  UIA_VALIDATE_CALL_1_ARG(result);

  // Tables and ARIA grids are always in row major order
  // see AXPlatformNodeBase::GetTableCell
  *result = RowOrColumnMajor_RowMajor;
  return S_OK;
}

//
// IToggleProvider implementation.
//

IFACEMETHODIMP AXPlatformNodeWin::Toggle() {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("Toggle");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TOGGLE_TOGGLE);
  UIA_VALIDATE_CALL();
  AXActionData action_data;
  action_data.action = ax::mojom::Action::kDoDefault;

  if (GetDelegate()->AccessibilityPerformAction(action_data))
    return S_OK;
  return E_FAIL;
}

IFACEMETHODIMP AXPlatformNodeWin::get_ToggleState(ToggleState* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_ToggleState");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TOGGLE_GET_TOGGLESTATE);
  UIA_VALIDATE_CALL_1_ARG(result);
  *result = GetToggleStateImpl();
  return S_OK;
}

//
// IValueProvider implementation.
//

IFACEMETHODIMP AXPlatformNodeWin::SetValue(LPCWSTR value) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("SetValue");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_VALUE_SETVALUE);
  UIA_VALIDATE_CALL();
  if (!value)
    return E_INVALIDARG;

  if (GetDelegate()->IsReadOnlyOrDisabled())
    return UIA_E_ELEMENTNOTENABLED;

  AXActionData data;
  data.action = ax::mojom::Action::kSetValue;
  data.value = base::WideToUTF8(value);
  if (GetDelegate()->AccessibilityPerformAction(data))
    return S_OK;
  return E_FAIL;
}

IFACEMETHODIMP AXPlatformNodeWin::get_IsReadOnly(BOOL* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_IsReadOnly");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_VALUE_GET_ISREADONLY);
  UIA_VALIDATE_CALL_1_ARG(result);
  *result = GetDelegate()->IsReadOnlyOrDisabled();
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_Value(BSTR* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_Value");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_VALUE_GET_VALUE);
  UIA_VALIDATE_CALL_1_ARG(result);
  *result = GetValueAttributeAsBstr(this);
  return S_OK;
}

//
// IWindowProvider implementation.
//

IFACEMETHODIMP AXPlatformNodeWin::SetVisualState(
    WindowVisualState window_visual_state) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("SetVisualState");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_WINDOW_SETVISUALSTATE);
  UIA_VALIDATE_CALL();
  return UIA_E_NOTSUPPORTED;
}

IFACEMETHODIMP AXPlatformNodeWin::Close() {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("Close");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_WINDOW_CLOSE);
  UIA_VALIDATE_CALL();
  return UIA_E_NOTSUPPORTED;
}

IFACEMETHODIMP AXPlatformNodeWin::WaitForInputIdle(int milliseconds,
                                                   BOOL* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("WaitForInputIdle");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_WINDOW_WAITFORINPUTIDLE);
  UIA_VALIDATE_CALL_1_ARG(result);
  return UIA_E_NOTSUPPORTED;
}

IFACEMETHODIMP AXPlatformNodeWin::get_CanMaximize(BOOL* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_CanMaximize");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_WINDOW_GET_CANMAXIMIZE);
  UIA_VALIDATE_CALL_1_ARG(result);
  return UIA_E_NOTSUPPORTED;
}

IFACEMETHODIMP AXPlatformNodeWin::get_CanMinimize(BOOL* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_CanMinimize");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_WINDOW_GET_CANMINIMIZE);
  UIA_VALIDATE_CALL_1_ARG(result);
  return UIA_E_NOTSUPPORTED;
}

IFACEMETHODIMP AXPlatformNodeWin::get_IsModal(BOOL* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_IsModal");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_WINDOW_GET_ISMODAL);
  UIA_VALIDATE_CALL_1_ARG(result);

  *result = GetBoolAttribute(ax::mojom::BoolAttribute::kModal);

  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_WindowVisualState(
    WindowVisualState* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_WindowVisualState");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_WINDOW_GET_WINDOWVISUALSTATE);
  UIA_VALIDATE_CALL_1_ARG(result);
  return UIA_E_NOTSUPPORTED;
}

IFACEMETHODIMP AXPlatformNodeWin::get_WindowInteractionState(
    WindowInteractionState* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_WindowInteractionState");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_WINDOW_GET_WINDOWINTERACTIONSTATE);
  UIA_VALIDATE_CALL_1_ARG(result);
  return UIA_E_NOTSUPPORTED;
}

IFACEMETHODIMP AXPlatformNodeWin::get_IsTopmost(BOOL* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_IsTopmost");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_WINDOW_GET_ISTOPMOST);
  UIA_VALIDATE_CALL_1_ARG(result);
  return UIA_E_NOTSUPPORTED;
}

//
// IRangeValueProvider implementation.
//

IFACEMETHODIMP AXPlatformNodeWin::SetValue(double value) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("SetValue");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_RANGEVALUE_SETVALUE);
  UIA_VALIDATE_CALL();
  AXActionData data;
  data.action = ax::mojom::Action::kSetValue;
  data.value = base::NumberToString(value);
  if (GetDelegate()->AccessibilityPerformAction(data))
    return S_OK;
  return E_FAIL;
}

IFACEMETHODIMP AXPlatformNodeWin::get_LargeChange(double* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_LargeChange");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_RANGEVALUE_GET_LARGECHANGE);
  UIA_VALIDATE_CALL_1_ARG(result);
  float attribute;
  if (GetFloatAttribute(ax::mojom::FloatAttribute::kStepValueForRange,
                        &attribute)) {
    *result = attribute * kLargeChangeScaleFactor;
    return S_OK;
  } else {
    // For native sliders and spin buttons, when no explicit step value was
    // set, use the default value instead.
    std::string html_input_type =
        GetStringAttribute(ax::mojom::StringAttribute::kInputType);
    if (html_input_type == "range" || html_input_type == "number") {
      *result = kDefaultSmallChangeValue * kLargeChangeScaleFactor;
      return S_OK;
    }
    return E_FAIL;
  }
  return E_FAIL;
}

IFACEMETHODIMP AXPlatformNodeWin::get_Maximum(double* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_Maximum");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_RANGEVALUE_GET_MAXIMUM);
  UIA_VALIDATE_CALL_1_ARG(result);
  float attribute;
  if (GetFloatAttribute(ax::mojom::FloatAttribute::kMaxValueForRange,
                        &attribute)) {
    *result = attribute;
    return S_OK;
  }
  return E_FAIL;
}

IFACEMETHODIMP AXPlatformNodeWin::get_Minimum(double* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_Minimum");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_RANGEVALUE_GET_MINIMUM);
  UIA_VALIDATE_CALL_1_ARG(result);
  float attribute;
  if (GetFloatAttribute(ax::mojom::FloatAttribute::kMinValueForRange,
                        &attribute)) {
    *result = attribute;
    return S_OK;
  }
  return E_FAIL;
}

IFACEMETHODIMP AXPlatformNodeWin::get_SmallChange(double* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_SmallChange");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_RANGEVALUE_GET_SMALLCHANGE);
  UIA_VALIDATE_CALL_1_ARG(result);
  float attribute;
  if (GetFloatAttribute(ax::mojom::FloatAttribute::kStepValueForRange,
                        &attribute)) {
    *result = attribute;
    return S_OK;
  } else {
    // For native sliders and spin buttons, when no explicit step value was
    // set, use the default value instead.
    std::string html_input_type =
        GetStringAttribute(ax::mojom::StringAttribute::kInputType);
    if (html_input_type == "range" || html_input_type == "number") {
      *result = kDefaultSmallChangeValue;
      return S_OK;
    }
    return E_FAIL;
  }
  return E_FAIL;
}

IFACEMETHODIMP AXPlatformNodeWin::get_Value(double* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_Value");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_RANGEVALUE_GET_VALUE);
  UIA_VALIDATE_CALL_1_ARG(result);
  float attribute;
  if (GetFloatAttribute(ax::mojom::FloatAttribute::kValueForRange,
                        &attribute)) {
    *result = attribute;
    return S_OK;
  }
  return E_FAIL;
}

// IAccessibleEx methods not implemented.
IFACEMETHODIMP
AXPlatformNodeWin::ConvertReturnedElement(IRawElementProviderSimple* element,
                                          IAccessibleEx** acc) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("ConvertReturnedElement");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_CONVERT_RETURNED_ELEMENT);
  return E_NOTIMPL;
}

//
// IAccessibleTable methods.
//

IFACEMETHODIMP AXPlatformNodeWin::get_accessibleAt(LONG row,
                                                   LONG column,
                                                   IUnknown** accessible) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_accessibleAt");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_ACCESSIBLE_AT);
  COM_OBJECT_VALIDATE_1_ARG(accessible);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  AXPlatformNodeBase* cell = GetTableCell(int{row}, int{column});
  if (!cell)
    return E_INVALIDARG;

  auto* node_win = static_cast<AXPlatformNodeWin*>(cell);
  return node_win->QueryInterface(IID_PPV_ARGS(accessible));
}

IFACEMETHODIMP AXPlatformNodeWin::get_caption(IUnknown** accessible) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_caption");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_CAPTION);
  COM_OBJECT_VALIDATE_1_ARG(accessible);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  AXPlatformNodeBase* caption = GetTableCaption();
  if (!caption)
    return S_FALSE;

  auto* node_win = static_cast<AXPlatformNodeWin*>(caption);
  return node_win->QueryInterface(IID_PPV_ARGS(accessible));
}

IFACEMETHODIMP AXPlatformNodeWin::get_childIndex(LONG row,
                                                 LONG column,
                                                 LONG* cell_index) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_childIndex");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_CHILD_INDEX);
  COM_OBJECT_VALIDATE_1_ARG(cell_index);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  AXPlatformNodeBase* cell = GetTableCell(int{row}, int{column});
  if (!cell)
    return E_INVALIDARG;

  std::optional<int> index = cell->GetTableCellIndex();
  if (!index)
    return E_FAIL;

  *cell_index = LONG{*index};
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_columnDescription(LONG column,
                                                        BSTR* description) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_columnDescription");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_COLUMN_DESCRIPTION);
  COM_OBJECT_VALIDATE_1_ARG(description);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  std::optional<int> columns = GetTableColumnCount();
  if (!columns)
    return E_FAIL;

  if (column < 0 || column >= *columns)
    return E_INVALIDARG;

  std::vector<int32_t> column_header_ids =
      GetDelegate()->GetColHeaderNodeIds(int{column});
  for (int32_t node_id : column_header_ids) {
    AXPlatformNodeWin* cell =
        static_cast<AXPlatformNodeWin*>(GetDelegate()->GetFromNodeID(node_id));
    if (!cell)
      continue;

    std::wstring cell_name = base::UTF8ToWide(cell->GetName());
    if (!cell_name.empty()) {
      *description = SysAllocString(cell_name.c_str());
      return S_OK;
    }

    cell_name = base::UTF8ToWide(
        cell->GetStringAttribute(ax::mojom::StringAttribute::kDescription));
    if (!cell_name.empty()) {
      *description = SysAllocString(cell_name.c_str());
      return S_OK;
    }
  }

  return S_FALSE;
}

IFACEMETHODIMP AXPlatformNodeWin::get_columnExtentAt(LONG row,
                                                     LONG column,
                                                     LONG* n_columns_spanned) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_columnExtentAt");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_COLUMN_EXTENT_AT);
  COM_OBJECT_VALIDATE_1_ARG(n_columns_spanned);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  AXPlatformNodeBase* cell = GetTableCell(int{row}, int{column});
  if (!cell)
    return E_INVALIDARG;

  std::optional<int> column_span = cell->GetTableColumnSpan();
  if (!column_span)
    return E_FAIL;
  *n_columns_spanned = LONG{*column_span};
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_columnHeader(
    IAccessibleTable** accessible_table,
    LONG* starting_row_index) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_columnHeader");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_COLUMN_HEADER);
  COM_OBJECT_VALIDATE_2_ARGS(accessible_table, starting_row_index);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  // Currently unimplemented.
  return E_NOTIMPL;
}

IFACEMETHODIMP AXPlatformNodeWin::get_columnIndex(LONG cell_index,
                                                  LONG* column_index) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_columnIndex");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_COLUMN_INDEX);
  COM_OBJECT_VALIDATE_1_ARG(column_index);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  AXPlatformNodeBase* cell = GetTableCell(cell_index);
  if (!cell)
    return E_INVALIDARG;

  std::optional<int> cell_column = cell->GetTableColumn();
  if (!cell_column)
    return E_FAIL;
  *column_index = LONG{*cell_column};
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_nColumns(LONG* column_count) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_nColumns");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_N_COLUMNS);
  COM_OBJECT_VALIDATE_1_ARG(column_count);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);
  std::optional<int> columns = GetTableColumnCount();
  if (!columns)
    return E_FAIL;
  *column_count = LONG{*columns};
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_nRows(LONG* row_count) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_nRows");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_N_ROWS);
  COM_OBJECT_VALIDATE_1_ARG(row_count);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);
  std::optional<int> rows = GetTableRowCount();
  if (!rows)
    return E_FAIL;
  *row_count = LONG{*rows};
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_nSelectedChildren(LONG* cell_count) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_nSelectedChildren");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_N_SELECTED_CHILDREN);
  COM_OBJECT_VALIDATE_1_ARG(cell_count);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  std::optional<int> columns = GetTableColumnCount();
  std::optional<int> rows = GetTableRowCount();
  if (!columns || !rows)
    return E_FAIL;

  LONG result = 0;
  for (int r = 0; r < *rows; ++r) {
    for (int c = 0; c < *columns; ++c) {
      AXPlatformNodeBase* cell = GetTableCell(r, c);
      if (cell && cell->GetBoolAttribute(ax::mojom::BoolAttribute::kSelected))
        ++result;
    }
  }
  *cell_count = result;
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_nSelectedColumns(LONG* column_count) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_nSelectedColumns");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_N_SELECTED_COLUMNS);
  COM_OBJECT_VALIDATE_1_ARG(column_count);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  std::optional<int> columns = GetTableColumnCount();
  std::optional<int> rows = GetTableRowCount();
  if (!columns || !rows)
    return E_FAIL;

  // If every cell in a column is selected, then that column is selected.
  LONG result = 0;
  for (int c = 0; c < *columns; ++c) {
    bool selected = true;
    for (int r = 0; r < *rows && selected == true; ++r) {
      const AXPlatformNodeBase* cell = GetTableCell(r, c);
      if (!cell ||
          !(cell->GetBoolAttribute(ax::mojom::BoolAttribute::kSelected)))
        selected = false;
    }
    if (selected)
      ++result;
  }

  *column_count = result;
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_nSelectedRows(LONG* row_count) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_nSelectedRows");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_N_SELECTED_ROWS);
  COM_OBJECT_VALIDATE_1_ARG(row_count);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  std::optional<int> columns = GetTableColumnCount();
  std::optional<int> rows = GetTableRowCount();
  if (!columns || !rows)
    return E_FAIL;

  // If every cell in a row is selected, then that row is selected.
  LONG result = 0;
  for (int r = 0; r < *rows; ++r) {
    bool selected = true;
    for (int c = 0; c < *columns && selected == true; ++c) {
      const AXPlatformNodeBase* cell = GetTableCell(r, c);
      if (!cell ||
          !(cell->GetBoolAttribute(ax::mojom::BoolAttribute::kSelected)))
        selected = false;
    }
    if (selected)
      ++result;
  }

  *row_count = result;
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_rowDescription(LONG row,
                                                     BSTR* description) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_rowDescription");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_ROW_DESCRIPTION);
  COM_OBJECT_VALIDATE_1_ARG(description);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  std::optional<int> rows = GetTableRowCount();
  if (!rows)
    return E_FAIL;

  if (row < 0 || row >= *rows)
    return E_INVALIDARG;

  std::vector<int32_t> row_header_ids =
      GetDelegate()->GetRowHeaderNodeIds(int{row});
  for (int32_t node_id : row_header_ids) {
    AXPlatformNodeWin* cell =
        static_cast<AXPlatformNodeWin*>(GetDelegate()->GetFromNodeID(node_id));
    if (!cell)
      continue;

    std::wstring cell_name = base::UTF8ToWide(cell->GetName());
    if (!cell_name.empty()) {
      *description = SysAllocString(cell_name.c_str());
      return S_OK;
    }

    cell_name = base::UTF8ToWide(
        cell->GetStringAttribute(ax::mojom::StringAttribute::kDescription));
    if (!cell_name.empty()) {
      *description = SysAllocString(cell_name.c_str());
      return S_OK;
    }
  }

  return S_FALSE;
}

IFACEMETHODIMP AXPlatformNodeWin::get_rowExtentAt(LONG row,
                                                  LONG column,
                                                  LONG* n_rows_spanned) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_rowExtentAt");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_ROW_EXTENT_AT);
  COM_OBJECT_VALIDATE_1_ARG(n_rows_spanned);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  const AXPlatformNodeBase* cell = GetTableCell(int{row}, int{column});
  if (!cell)
    return E_INVALIDARG;

  std::optional<int> cell_row_span = cell->GetTableRowSpan();
  if (!cell_row_span)
    return E_FAIL;
  *n_rows_spanned = LONG{*cell_row_span};
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_rowHeader(
    IAccessibleTable** accessible_table,
    LONG* starting_column_index) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_rowHeader");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_ROW_HEADER);
  COM_OBJECT_VALIDATE_2_ARGS(accessible_table, starting_column_index);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  // Currently unimplemented.
  return E_NOTIMPL;
}

IFACEMETHODIMP AXPlatformNodeWin::get_rowIndex(LONG cell_index,
                                               LONG* row_index) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_rowIndex");
  COM_OBJECT_VALIDATE_1_ARG(row_index);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  const AXPlatformNodeBase* cell = GetTableCell(cell_index);
  if (!cell)
    return E_INVALIDARG;

  std::optional<int> cell_row = cell->GetTableRow();
  if (!cell_row)
    return E_FAIL;
  *row_index = LONG{*cell_row};
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_selectedChildren(LONG max_children,
                                                       LONG** children,
                                                       LONG* n_children) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_selectedChildren");
  COM_OBJECT_VALIDATE_2_ARGS(children, n_children);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  if (max_children <= 0)
    return E_INVALIDARG;

  std::optional<int> columns = GetTableColumnCount();
  std::optional<int> rows = GetTableRowCount();
  if (!columns || !rows)
    return E_FAIL;

  std::vector<LONG> results;
  for (int r = 0; r < *rows; ++r) {
    for (int c = 0; c < *columns; ++c) {
      const AXPlatformNodeBase* cell = GetTableCell(r, c);
      if (cell && cell->GetBoolAttribute(ax::mojom::BoolAttribute::kSelected)) {
        std::optional<int> cell_index = cell->GetTableCellIndex();
        if (!cell_index)
          return E_FAIL;

        results.push_back(*cell_index);
      }
    }
  }

  return AllocateComArrayFromVector(results, max_children, children,
                                    n_children);
}

IFACEMETHODIMP AXPlatformNodeWin::get_selectedColumns(LONG max_columns,
                                                      LONG** columns,
                                                      LONG* n_columns) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_selectedColumns");
  COM_OBJECT_VALIDATE_2_ARGS(columns, n_columns);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  if (max_columns <= 0)
    return E_INVALIDARG;

  std::optional<int> column_count = GetTableColumnCount();
  std::optional<int> row_count = GetTableRowCount();
  if (!column_count || !row_count)
    return E_FAIL;

  std::vector<LONG> results;
  for (int c = 0; c < *column_count; ++c) {
    bool selected = true;
    for (int r = 0; r < *row_count && selected == true; ++r) {
      const AXPlatformNodeBase* cell = GetTableCell(r, c);
      if (!cell ||
          !(cell->GetBoolAttribute(ax::mojom::BoolAttribute::kSelected)))
        selected = false;
    }
    if (selected)
      results.push_back(c);
  }

  return AllocateComArrayFromVector(results, max_columns, columns, n_columns);
}

IFACEMETHODIMP AXPlatformNodeWin::get_selectedRows(LONG max_rows,
                                                   LONG** rows,
                                                   LONG* n_rows) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_selectedRows");
  COM_OBJECT_VALIDATE_2_ARGS(rows, n_rows);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  if (max_rows <= 0)
    return E_INVALIDARG;

  std::optional<int> column_count = GetTableColumnCount();
  std::optional<int> row_count = GetTableRowCount();
  if (!column_count || !row_count)
    return E_FAIL;

  std::vector<LONG> results;
  for (int r = 0; r < *row_count; ++r) {
    bool selected = true;
    for (int c = 0; c < *column_count && selected == true; ++c) {
      const AXPlatformNodeBase* cell = GetTableCell(r, c);
      if (!cell ||
          !(cell->GetBoolAttribute(ax::mojom::BoolAttribute::kSelected)))
        selected = false;
    }
    if (selected)
      results.push_back(r);
  }

  return AllocateComArrayFromVector(results, max_rows, rows, n_rows);
}

IFACEMETHODIMP AXPlatformNodeWin::get_summary(IUnknown** accessible) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_summary");
  COM_OBJECT_VALIDATE_1_ARG(accessible);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  // Current unimplemented.
  return E_NOTIMPL;
}

IFACEMETHODIMP AXPlatformNodeWin::get_isColumnSelected(LONG column,
                                                       boolean* is_selected) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_isColumnSelected");
  COM_OBJECT_VALIDATE_1_ARG(is_selected);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  std::optional<int> columns = GetTableColumnCount();
  std::optional<int> rows = GetTableRowCount();
  if (!columns || !rows)
    return E_FAIL;

  if (column < 0 || column >= *columns)
    return E_INVALIDARG;

  for (int r = 0; r < *rows; ++r) {
    const AXPlatformNodeBase* cell = GetTableCell(r, column);
    if (!cell || !(cell->GetBoolAttribute(ax::mojom::BoolAttribute::kSelected)))
      return S_OK;
  }

  *is_selected = true;
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_isRowSelected(LONG row,
                                                    boolean* is_selected) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_isRowSelected");
  COM_OBJECT_VALIDATE_1_ARG(is_selected);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  std::optional<int> columns = GetTableColumnCount();
  std::optional<int> rows = GetTableRowCount();
  if (!columns || !rows)
    return E_FAIL;

  if (row < 0 || row >= *rows)
    return E_INVALIDARG;

  for (int c = 0; c < *columns; ++c) {
    const AXPlatformNodeBase* cell = GetTableCell(row, c);
    if (!cell || !(cell->GetBoolAttribute(ax::mojom::BoolAttribute::kSelected)))
      return S_OK;
  }

  *is_selected = true;
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_isSelected(LONG row,
                                                 LONG column,
                                                 boolean* is_selected) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_isSelected");
  COM_OBJECT_VALIDATE_1_ARG(is_selected);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  std::optional<int> columns = GetTableColumnCount();
  std::optional<int> rows = GetTableRowCount();
  if (!columns || !rows)
    return E_FAIL;

  const AXPlatformNodeBase* cell = GetTableCell(int{row}, int{column});
  if (!cell)
    return E_INVALIDARG;

  if (cell->GetBoolAttribute(ax::mojom::BoolAttribute::kSelected))
    *is_selected = true;

  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_rowColumnExtentsAtIndex(
    LONG index,
    LONG* row,
    LONG* column,
    LONG* row_extents,
    LONG* column_extents,
    boolean* is_selected) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_rowColumnExtentsAtIndex");
  COM_OBJECT_VALIDATE_5_ARGS(row, column, row_extents, column_extents,
                             is_selected);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  const AXPlatformNodeBase* cell = GetTableCell(index);
  if (!cell)
    return E_INVALIDARG;

  std::optional<int> row_index = cell->GetTableRow();
  std::optional<int> column_index = cell->GetTableColumn();
  std::optional<int> row_span = cell->GetTableRowSpan();
  std::optional<int> column_span = cell->GetTableColumnSpan();
  if (!row_index || !column_index || !row_span || !column_span)
    return E_FAIL;

  *row = LONG{*row_index};
  *column = LONG{*column_index};
  *row_extents = LONG{*row_span};
  *column_extents = LONG{*column_span};
  if (cell->GetBoolAttribute(ax::mojom::BoolAttribute::kSelected))
    *is_selected = true;
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::selectRow(LONG row) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("selectRow");
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  std::optional<int> rows = GetTableRowCount();
  if (!rows)
    return E_FAIL;

  if (row < 0 || row >= *rows)
    return E_INVALIDARG;

  return E_NOTIMPL;
}

IFACEMETHODIMP AXPlatformNodeWin::selectColumn(LONG column) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("selectColumn");
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  std::optional<int> columns = GetTableColumnCount();
  if (!columns)
    return E_FAIL;

  if (column < 0 || column >= *columns)
    return E_INVALIDARG;

  return E_NOTIMPL;
}

IFACEMETHODIMP AXPlatformNodeWin::unselectRow(LONG row) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("unselectRow");
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  std::optional<int> rows = GetTableRowCount();
  if (!rows)
    return E_FAIL;

  if (row < 0 || row >= *rows)
    return E_INVALIDARG;

  return E_NOTIMPL;
}

IFACEMETHODIMP AXPlatformNodeWin::unselectColumn(LONG column) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("unselectColumn");
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  std::optional<int> columns = GetTableColumnCount();
  if (!columns)
    return E_FAIL;

  if (column < 0 || column >= *columns)
    return E_INVALIDARG;

  // Currently unimplemented.
  return E_NOTIMPL;
}

IFACEMETHODIMP AXPlatformNodeWin::get_modelChange(
    IA2TableModelChange* model_change) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_modelChange");
  COM_OBJECT_VALIDATE_1_ARG(model_change);

  // Currently unimplemented.
  return E_NOTIMPL;
}

//
// IAccessibleTable2 methods.
//

IFACEMETHODIMP AXPlatformNodeWin::get_cellAt(LONG row,
                                             LONG column,
                                             IUnknown** cell) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_cellAt");
  COM_OBJECT_VALIDATE_1_ARG(cell);
  NotifyAddAXModeFlagsForIA2(AXMode::kScreenReader);

  AXPlatformNodeBase* table_cell = GetTableCell(int{row}, int{column});
  if (!table_cell)
    return E_INVALIDARG;

  auto* node_win = static_cast<AXPlatformNodeWin*>(table_cell);
  return node_win->QueryInterface(IID_PPV_ARGS(cell));
}

IFACEMETHODIMP AXPlatformNodeWin::get_nSelectedCells(LONG* cell_count) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_nSelectedCells");
  // Note that this method does not need to set any ax mode since it
  // calls into get_nSelectedChildren() which does.
  return get_nSelectedChildren(cell_count);
}

IFACEMETHODIMP AXPlatformNodeWin::get_selectedCells(IUnknown*** cells,
                                                    LONG* n_selected_cells) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_selectedCells");
  COM_OBJECT_VALIDATE_2_ARGS(cells, n_selected_cells);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  std::optional<int> columns = GetTableColumnCount();
  std::optional<int> rows = GetTableRowCount();
  if (!columns || !rows)
    return E_FAIL;

  std::vector<AXPlatformNodeBase*> selected;
  for (int r = 0; r < *rows; ++r) {
    for (int c = 0; c < *columns; ++c) {
      AXPlatformNodeBase* cell = GetTableCell(r, c);
      if (cell && cell->GetBoolAttribute(ax::mojom::BoolAttribute::kSelected))
        selected.push_back(cell);
    }
  }

  *n_selected_cells = static_cast<LONG>(selected.size());
  *cells = static_cast<IUnknown**>(
      CoTaskMemAlloc(selected.size() * sizeof(IUnknown*)));

  for (size_t i = 0; i < selected.size(); ++i) {
    auto* node_win = static_cast<AXPlatformNodeWin*>(selected[i]);
    node_win->QueryInterface(IID_PPV_ARGS(&(*cells)[i]));
  }
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_selectedColumns(LONG** columns,
                                                      LONG* n_columns) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_selectedColumns");
  return get_selectedColumns(INT_MAX, columns, n_columns);
}

IFACEMETHODIMP AXPlatformNodeWin::get_selectedRows(LONG** rows, LONG* n_rows) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_selectedRows");
  return get_selectedRows(INT_MAX, rows, n_rows);
}

//
// IAccessibleTableCell methods.
//

IFACEMETHODIMP AXPlatformNodeWin::get_columnExtent(LONG* n_columns_spanned) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_columnExtent");
  COM_OBJECT_VALIDATE_1_ARG(n_columns_spanned);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  std::optional<int> column_span = GetTableColumnSpan();
  if (!column_span)
    return E_FAIL;
  *n_columns_spanned = LONG{*column_span};
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_columnHeaderCells(
    IUnknown*** cell_accessibles,
    LONG* n_column_header_cells) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_columnHeaderCells");
  COM_OBJECT_VALIDATE_2_ARGS(cell_accessibles, n_column_header_cells);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  std::optional<int> column = GetTableColumn();
  if (!column)
    return E_FAIL;

  std::vector<int32_t> column_header_ids =
      GetDelegate()->GetColHeaderNodeIds(*column);
  *cell_accessibles = static_cast<IUnknown**>(
      CoTaskMemAlloc(column_header_ids.size() * sizeof(IUnknown*)));
  int index = 0;
  for (int32_t node_id : column_header_ids) {
    AXPlatformNodeWin* node_win =
        static_cast<AXPlatformNodeWin*>(GetDelegate()->GetFromNodeID(node_id));
    if (node_win) {
      node_win->QueryInterface(IID_PPV_ARGS(&(*cell_accessibles)[index]));
      ++index;
    }
  }

  *n_column_header_cells = static_cast<LONG>(column_header_ids.size());
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_columnIndex(LONG* column_index) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_columnIndex");
  COM_OBJECT_VALIDATE_1_ARG(column_index);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  std::optional<int> column = GetTableColumn();
  if (!column)
    return E_FAIL;
  *column_index = LONG{*column};
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_rowExtent(LONG* n_rows_spanned) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_rowExtent");
  COM_OBJECT_VALIDATE_1_ARG(n_rows_spanned);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  std::optional<int> row_span = GetTableRowSpan();
  if (!row_span)
    return E_FAIL;
  *n_rows_spanned = LONG{*row_span};
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_rowHeaderCells(
    IUnknown*** cell_accessibles,
    LONG* n_row_header_cells) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_rowHeaderCells");
  COM_OBJECT_VALIDATE_2_ARGS(cell_accessibles, n_row_header_cells);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  std::optional<int> row = GetTableRow();
  if (!row)
    return E_FAIL;

  std::vector<int32_t> row_header_ids =
      GetDelegate()->GetRowHeaderNodeIds(*row);
  *cell_accessibles = static_cast<IUnknown**>(
      CoTaskMemAlloc(row_header_ids.size() * sizeof(IUnknown*)));
  int index = 0;
  for (int32_t node_id : row_header_ids) {
    AXPlatformNodeWin* node_win =
        static_cast<AXPlatformNodeWin*>(GetDelegate()->GetFromNodeID(node_id));
    if (node_win) {
      node_win->QueryInterface(IID_PPV_ARGS(&(*cell_accessibles)[index]));
      ++index;
    }
  }

  *n_row_header_cells = static_cast<LONG>(row_header_ids.size());
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_rowIndex(LONG* row_index) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_rowIndex");
  COM_OBJECT_VALIDATE_1_ARG(row_index);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  std::optional<int> row = GetTableRow();
  if (!row)
    return E_FAIL;
  *row_index = LONG{*row};
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_isSelected(boolean* is_selected) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_isSelected");
  COM_OBJECT_VALIDATE_1_ARG(is_selected);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  if (GetBoolAttribute(ax::mojom::BoolAttribute::kSelected))
    *is_selected = true;
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_rowColumnExtents(LONG* row_index,
                                                       LONG* column_index,
                                                       LONG* row_extents,
                                                       LONG* column_extents,
                                                       boolean* is_selected) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_rowColumnExtents");
  COM_OBJECT_VALIDATE_5_ARGS(row_index, column_index, row_extents,
                             column_extents, is_selected);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  std::optional<int> row = GetTableRow();
  std::optional<int> column = GetTableColumn();
  std::optional<int> row_span = GetTableRowSpan();
  std::optional<int> column_span = GetTableColumnSpan();
  if (!row || !column || !row_span || !column_span)
    return E_FAIL;

  *row_index = LONG{*row};
  *column_index = LONG{*column};
  *row_extents = LONG{*row_span};
  *column_extents = LONG{*column_span};
  if (GetBoolAttribute(ax::mojom::BoolAttribute::kSelected))
    *is_selected = true;
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_table(IUnknown** table) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_table");
  COM_OBJECT_VALIDATE_1_ARG(table);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  AXPlatformNodeBase* table_node = GetTable();
  if (!table_node)
    return E_FAIL;

  auto* node_win = static_cast<AXPlatformNodeWin*>(table_node);
  return node_win->QueryInterface(IID_PPV_ARGS(table));
}

//
// IAccessibleText
//

IFACEMETHODIMP AXPlatformNodeWin::get_nCharacters(LONG* n_characters) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_nCharacters");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_N_CHARACTERS);
  COM_OBJECT_VALIDATE_1_ARG(n_characters);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode |
                             AXMode::kInlineTextBoxes);

  std::u16string text = GetHypertext();
  *n_characters = static_cast<LONG>(text.size());

  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_caretOffset(LONG* offset) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_caretOffset");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_CARET_OFFSET);
  COM_OBJECT_VALIDATE_1_ARG(offset);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);
  *offset = 0;

  if (!HasVisibleCaretOrSelection())
    return S_FALSE;

  int selection_start, selection_end;
  GetSelectionOffsets(&selection_start, &selection_end);
  // The caret is always at the end of the selection.
  *offset = selection_end;
  if (*offset < 0)
    return S_FALSE;

  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_nSelections(LONG* n_selections) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_nSelections");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_N_SELECTIONS);
  COM_OBJECT_VALIDATE_1_ARG(n_selections);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  *n_selections = 0;
  int selection_start, selection_end;
  GetSelectionOffsets(&selection_start, &selection_end);
  if (selection_start >= 0 && selection_end >= 0 &&
      selection_start != selection_end) {
    *n_selections = 1;
  }
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_selection(LONG selection_index,
                                                LONG* start_offset,
                                                LONG* end_offset) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_selection");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_SELECTION);
  COM_OBJECT_VALIDATE_2_ARGS(start_offset, end_offset);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  if (!start_offset || !end_offset || selection_index != 0)
    return E_INVALIDARG;

  *start_offset = 0;
  *end_offset = 0;
  int selection_start, selection_end;
  GetSelectionOffsets(&selection_start, &selection_end);
  if (selection_start >= 0 && selection_end >= 0 &&
      selection_start != selection_end) {
    // We should ignore the direction of the selection when exposing start and
    // end offsets. According to the IA2 Spec the end offset is always
    // increased by one past the end of the selection. This wouldn't make
    // sense if end < start.
    if (selection_end < selection_start)
      std::swap(selection_start, selection_end);

    *start_offset = selection_start;
    *end_offset = selection_end;
    return S_OK;
  }

  return E_INVALIDARG;
}

IFACEMETHODIMP AXPlatformNodeWin::get_text(LONG start_offset,
                                           LONG end_offset,
                                           BSTR* text) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_text");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_TEXT);
  COM_OBJECT_VALIDATE_1_ARG(text);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);
  HandleSpecialTextOffset(&start_offset);
  HandleSpecialTextOffset(&end_offset);

  // The spec allows the arguments to be reversed.
  if (start_offset > end_offset)
    std::swap(start_offset, end_offset);

  const std::u16string str = GetHypertext();
  LONG str_len = static_cast<LONG>(str.length());
  if (start_offset < 0 || start_offset > str_len)
    return E_INVALIDARG;
  if (end_offset < 0 || end_offset > str_len)
    return E_INVALIDARG;

  std::u16string substr = str.substr(start_offset, end_offset - start_offset);
  if (substr.empty())
    return S_FALSE;

  *text = SysAllocString(base::as_wcstr(substr));
  DCHECK(*text);
  return S_OK;
}

HRESULT AXPlatformNodeWin::IAccessibleTextGetTextForOffsetType(
    TextOffsetType text_offset_type,
    LONG offset,
    enum IA2TextBoundaryType boundary_type,
    LONG* start_offset,
    LONG* end_offset,
    BSTR* text) {
  COM_OBJECT_VALIDATE_3_ARGS(start_offset, end_offset, text);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode |
                             AXMode::kInlineTextBoxes);

  HandleSpecialTextOffset(&offset);
  if (offset < 0)
    return E_INVALIDARG;

  const std::u16string& text_str = GetHypertext();
  LONG text_len = text_str.length();

  // https://accessibility.linuxfoundation.org/a11yspecs/ia2/docs/html/interface_i_accessible_text.html
  // All methods that operate on particular characters use character indices
  // (e.g. IAccessibleText::textAtOffset) from 0 to length-1.
  if (offset >= text_len) {
    // We aren't strictly following the spec here by allowing offset to be equal
    // to the text length for IA2_TEXT_BOUNDARY_LINE in case screen readers
    // expect this behavior which has existed since Feb. 2015,
    // commit: 6baff46f520e31ff92669890207be5708064d16e.
    const bool offset_for_line_text_len =
        offset == text_len && boundary_type == IA2_TEXT_BOUNDARY_LINE;
    if (!offset_for_line_text_len)
      return E_INVALIDARG;
  }

  LONG start, end;
  switch (text_offset_type) {
    case TextOffsetType::kAtOffset: {
      end = FindBoundary(boundary_type, offset,
                         ax::mojom::MoveDirection::kForward);
      // Early return if the range will be degenerate containing no text.
      if (end <= 0)
        return S_FALSE;
      start = FindBoundary(boundary_type, offset,
                           ax::mojom::MoveDirection::kBackward);
      break;
    }
    case TextOffsetType::kBeforeOffset: {
      // Find the start of the boundary at |offset| and assign to |end|,
      // then find the start of the preceding boundary and assign to |start|.
      end = FindBoundary(boundary_type, offset,
                         ax::mojom::MoveDirection::kBackward);
      // Early return if the range will be degenerate containing no text,
      // or the range is after |offset|. Because the character at |offset| must
      // be excluded, |end| and |offset| may be equal.
      if (end <= 0 || end > offset)
        return S_FALSE;
      start = FindBoundary(boundary_type, end - 1,
                           ax::mojom::MoveDirection::kBackward);
      break;
    }
    case TextOffsetType::kAfterOffset: {
      // Find the end of the boundary at |offset| and assign to |start|,
      // then find the end of the following boundary and assign to |end|.
      start = FindBoundary(boundary_type, offset,
                           ax::mojom::MoveDirection::kForward);
      // Early return if the range will be degenerate containing no text,
      // or the range is before or includes|offset|. Because the character at
      // |offset| must be excluded, |start| and |offset| cannot be equal.
      if (start >= text_len || start <= offset)
        return S_FALSE;
      end = FindBoundary(boundary_type, start,
                         ax::mojom::MoveDirection::kForward);
      break;
    }
  }

  DCHECK_LE(start, end);
  if (start >= end)
    return S_FALSE;

  *start_offset = start;
  *end_offset = end;
  return get_text(start, end, text);
}

IFACEMETHODIMP AXPlatformNodeWin::get_textAtOffset(
    LONG offset,
    enum IA2TextBoundaryType boundary_type,
    LONG* start_offset,
    LONG* end_offset,
    BSTR* text) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_textAtOffset");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_TEXT_AT_OFFSET);
  return IAccessibleTextGetTextForOffsetType(TextOffsetType::kAtOffset, offset,
                                             boundary_type, start_offset,
                                             end_offset, text);
}

IFACEMETHODIMP AXPlatformNodeWin::get_textBeforeOffset(
    LONG offset,
    enum IA2TextBoundaryType boundary_type,
    LONG* start_offset,
    LONG* end_offset,
    BSTR* text) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_textBeforeOffset");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_TEXT_BEFORE_OFFSET);
  return IAccessibleTextGetTextForOffsetType(TextOffsetType::kBeforeOffset,
                                             offset, boundary_type,
                                             start_offset, end_offset, text);
}

IFACEMETHODIMP AXPlatformNodeWin::get_textAfterOffset(
    LONG offset,
    enum IA2TextBoundaryType boundary_type,
    LONG* start_offset,
    LONG* end_offset,
    BSTR* text) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_textAfterOffset");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_TEXT_AFTER_OFFSET);
  return IAccessibleTextGetTextForOffsetType(TextOffsetType::kAfterOffset,
                                             offset, boundary_type,
                                             start_offset, end_offset, text);
}

IFACEMETHODIMP AXPlatformNodeWin::get_offsetAtPoint(
    LONG x,
    LONG y,
    enum IA2CoordinateType coord_type,
    LONG* offset) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_offsetAtPoint");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_OFFSET_AT_POINT);
  COM_OBJECT_VALIDATE_1_ARG(offset);

  *offset = -1;

  if (coord_type == IA2CoordinateType::IA2_COORDTYPE_PARENT_RELATIVE) {
    // We don't support when the IA2 coordinate type is parent relative, but
    // we have to return something rather than E_NOTIMPL or screen readers
    // will complain.
    // See http://crbug.com/1010726
    NOTIMPLEMENTED_LOG_ONCE();
    return S_FALSE;
  }

  // We currently only handle IA2 screen relative coord type.
  DCHECK_EQ(coord_type, IA2_COORDTYPE_SCREEN_RELATIVE);

  const AXPlatformNodeWin* hit_child = static_cast<AXPlatformNodeWin*>(
      FromNativeViewAccessible(GetDelegate()->HitTestSync(x, y)));

  if (!hit_child || !hit_child->IsText()) {
    return S_FALSE;
  }

  for (int i = 0, text_length = hit_child->GetTextContentUTF16().length();
       i < text_length; ++i) {
    gfx::Rect char_bounds =
        hit_child->GetDelegate()->GetInnerTextRangeBoundsRect(
            i, i + 1, AXCoordinateSystem::kScreenDIPs,
            AXClippingBehavior::kUnclipped);
    if (char_bounds.Contains(x, y)) {
      *offset = i;
      break;
    }
  }

  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::addSelection(LONG start_offset,
                                               LONG end_offset) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("addSelection");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_ADD_SELECTION);
  COM_OBJECT_VALIDATE();
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  // We only support one selection.
  return setSelection(0, start_offset, end_offset);
}

IFACEMETHODIMP AXPlatformNodeWin::removeSelection(LONG selection_index) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("removeSelection");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_REMOVE_SELECTION);
  COM_OBJECT_VALIDATE();
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  if (selection_index != 0)
    return E_INVALIDARG;
  // Simply collapse the selection to the position of the caret if a caret is
  // visible, otherwise set the selection to 0.
  return setCaretOffset(GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd));
}

IFACEMETHODIMP AXPlatformNodeWin::setCaretOffset(LONG offset) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("setCaretOffset");
  return setSelection(0, offset, offset);
}

IFACEMETHODIMP AXPlatformNodeWin::setSelection(LONG selection_index,
                                               LONG start_offset,
                                               LONG end_offset) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("setSelection");
  if (selection_index != 0)
    return E_INVALIDARG;

  HandleSpecialTextOffset(&start_offset);
  HandleSpecialTextOffset(&end_offset);
  if (start_offset < 0 ||
      start_offset > static_cast<LONG>(GetHypertext().length())) {
    return E_INVALIDARG;
  }
  if (end_offset < 0 ||
      end_offset > static_cast<LONG>(GetHypertext().length())) {
    return E_INVALIDARG;
  }

  if (SetHypertextSelection(int{start_offset}, int{end_offset})) {
    return S_OK;
  }
  return E_FAIL;
}

//
// IAccessibleTextSelectionContainer methods.
//

IFACEMETHODIMP
AXPlatformNodeWin::get_selections(IA2TextSelection** selections,
                                  LONG* nSelections) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_selections");
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  COM_OBJECT_VALIDATE_2_ARGS(selections, nSelections);

  AXSelection unignored_selection = GetDelegate()->GetUnignoredSelection();

  AXNodeID anchor_id = unignored_selection.anchor_object_id;
  AXPlatformNodeWin* anchor_node =
      static_cast<AXPlatformNodeWin*>(GetDelegate()->GetFromNodeID(anchor_id));
  if (!anchor_node)
    return E_FAIL;

  // If the selection endpoint is inside this object and therefore, at least
  // from this side, we do not need to crop the selection. Simply convert
  // selection anchor/focus offset to a hypertext offset within anchor/focus
  // object. Otherwise per the IA2 Spec, we need to crop the selection to be
  // within this object. `AXPlatformNodeBase::GetHypertextOffsetFromEndpoint`
  // can correctly handle endpoint offsets that are outside this object by
  // returning either 0 or hypertext length; see the related comment in
  // the method's declaration.

  int anchor_offset = unignored_selection.anchor_offset;
  if (anchor_node->IsDescendant(this)) {
    anchor_offset =
        anchor_node->GetHypertextOffsetFromEndpoint(anchor_node, anchor_offset);
  } else {
    anchor_offset = GetHypertextOffsetFromEndpoint(anchor_node, anchor_offset);
    anchor_node = this;
  }
  DCHECK_GE(anchor_offset, 0)
      << "This value is unexpected here, since we have already determined in "
         "this method that anchor_object is in the accessibility tree.";

  AXNodeID focus_id = unignored_selection.focus_object_id;
  AXPlatformNodeWin* focus_node =
      static_cast<AXPlatformNodeWin*>(GetDelegate()->GetFromNodeID(focus_id));
  if (!focus_node)
    return E_FAIL;

  int focus_offset = unignored_selection.focus_offset;
  if (focus_node->IsDescendant(this)) {
    focus_offset =
        focus_node->GetHypertextOffsetFromEndpoint(focus_node, focus_offset);
  } else {
    focus_offset = GetHypertextOffsetFromEndpoint(focus_node, focus_offset);
    focus_node = this;
  }
  DCHECK_GE(focus_offset, 0)
      << "This value is unexpected here, since we have already determined in "
         "this method that focus_object is in the accessibility tree.";

  if (anchor_node == focus_node && anchor_offset == focus_offset)
    return S_FALSE;  // No selection within this subtree.

  Microsoft::WRL::ComPtr<IAccessibleText> anchor_text_node;
  if (FAILED(anchor_node->QueryInterface(IID_PPV_ARGS(&anchor_text_node))))
    return E_FAIL;

  Microsoft::WRL::ComPtr<IAccessibleText> focus_text_node;
  if (FAILED(focus_node->QueryInterface(IID_PPV_ARGS(&focus_text_node))))
    return E_FAIL;

  *selections = reinterpret_cast<IA2TextSelection*>(
      CoTaskMemAlloc(sizeof(IA2TextSelection)));

  if (unignored_selection.is_backward) {
    selections[0]->startObj = focus_text_node.Detach();
    selections[0]->startOffset = focus_offset;
    selections[0]->endObj = anchor_text_node.Detach();
    selections[0]->endOffset = anchor_offset;
    selections[0]->startIsActive = true;
  } else {
    selections[0]->startObj = anchor_text_node.Detach();
    selections[0]->startOffset = anchor_offset;
    selections[0]->endObj = focus_text_node.Detach();
    selections[0]->endOffset = focus_offset;
    selections[0]->startIsActive = false;
  }

  *nSelections = 1;
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::setSelections(LONG nSelections,
                                                IA2TextSelection* selections) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("setSelections");
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  COM_OBJECT_VALIDATE();

  // Chromium does not currently support more than one selection.
  if (nSelections != 1 || !selections)
    return E_INVALIDARG;

  if (!selections->startObj || !selections->endObj)
    return E_INVALIDARG;

  Microsoft::WRL::ComPtr<IAccessible> start_obj;
  if (FAILED(selections->startObj->QueryInterface(IID_PPV_ARGS(&start_obj))))
    return E_INVALIDARG;

  Microsoft::WRL::ComPtr<IAccessible> end_obj;
  if (FAILED(selections->endObj->QueryInterface(IID_PPV_ARGS(&end_obj))))
    return E_INVALIDARG;

  const auto* start_node = static_cast<AXPlatformNodeWin*>(
      FromNativeViewAccessible(start_obj.Get()));
  const auto* end_node =
      static_cast<AXPlatformNodeWin*>(FromNativeViewAccessible(end_obj.Get()));
  if (!start_node || !end_node)
    return E_INVALIDARG;

  AXPosition start_position =
      start_node->HypertextOffsetToEndpoint(selections->startOffset)
          ->AsDomSelectionPosition();
  AXPosition end_position =
      end_node->HypertextOffsetToEndpoint(selections->endOffset)
          ->AsDomSelectionPosition();
  if (start_position->IsNullPosition() || end_position->IsNullPosition())
    return E_INVALIDARG;

  AXActionData action_data;
  action_data.action = ax::mojom::Action::kSetSelection;
  action_data.target_tree_id = start_position->tree_id();
  int start_offset = start_position->IsTextPosition()
                         ? start_position->text_offset()
                         : start_position->child_index();
  int end_offset = end_position->IsTextPosition() ? end_position->text_offset()
                                                  : end_position->child_index();
  if (selections->startIsActive) {
    action_data.focus_node_id = start_position->anchor_id();
    action_data.focus_offset = start_offset;
    action_data.anchor_node_id = end_position->anchor_id();
    action_data.anchor_offset = end_offset;
  } else {
    action_data.anchor_node_id = start_position->anchor_id();
    action_data.anchor_offset = start_offset;
    action_data.focus_node_id = end_position->anchor_id();
    action_data.focus_offset = end_offset;
  }

  if (GetDelegate()->AccessibilityPerformAction(action_data))
    return S_OK;

  return S_FALSE;
}

//
// IAccessibleHypertext methods not implemented.
//

IFACEMETHODIMP AXPlatformNodeWin::get_nHyperlinks(LONG* hyperlink_count) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_nHyperlinks");
  return E_NOTIMPL;
}

IFACEMETHODIMP AXPlatformNodeWin::get_hyperlink(
    LONG index,
    IAccessibleHyperlink** hyperlink) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_hyperlink");
  return E_NOTIMPL;
}

IFACEMETHODIMP AXPlatformNodeWin::get_hyperlinkIndex(LONG char_index,
                                                     LONG* hyperlink_index) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_hyperlinkIndex");
  return E_NOTIMPL;
}

//
// IAccessibleText methods not implemented.
//

IFACEMETHODIMP AXPlatformNodeWin::get_newText(IA2TextSegment* new_text) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_newText");
  return E_NOTIMPL;
}
IFACEMETHODIMP AXPlatformNodeWin::get_oldText(IA2TextSegment* old_text) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_oldText");
  return E_NOTIMPL;
}
IFACEMETHODIMP AXPlatformNodeWin::get_characterExtents(
    LONG offset,
    enum IA2CoordinateType coord_type,
    LONG* x,
    LONG* y,
    LONG* width,
    LONG* height) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_characterExtents");
  return E_NOTIMPL;
}
IFACEMETHODIMP AXPlatformNodeWin::scrollSubstringTo(
    LONG start_index,
    LONG end_index,
    enum IA2ScrollType scroll_type) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("scrollSubstringTo");
  return E_NOTIMPL;
}
IFACEMETHODIMP AXPlatformNodeWin::scrollSubstringToPoint(
    LONG start_index,
    LONG end_index,
    enum IA2CoordinateType coordinate_type,
    LONG x,
    LONG y) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("scrollSubstringToPoint");
  return E_NOTIMPL;
}
IFACEMETHODIMP AXPlatformNodeWin::get_attributes(LONG offset,
                                                 LONG* start_offset,
                                                 LONG* end_offset,
                                                 BSTR* text_attributes) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_attributes");
  return E_NOTIMPL;
}

//
// IAccessibleValue methods.
//

IFACEMETHODIMP AXPlatformNodeWin::get_currentValue(VARIANT* value) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_currentValue");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_CURRENT_VALUE);
  COM_OBJECT_VALIDATE_1_ARG(value);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  float float_val;
  if (GetFloatAttribute(ax::mojom::FloatAttribute::kValueForRange,
                        &float_val)) {
    value->vt = VT_R8;
    value->dblVal = float_val;
    return S_OK;
  }

  value->vt = VT_EMPTY;
  return S_FALSE;
}

IFACEMETHODIMP AXPlatformNodeWin::get_minimumValue(VARIANT* value) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_minimumValue");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_MINIMUM_VALUE);
  COM_OBJECT_VALIDATE_1_ARG(value);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  float float_val;
  if (GetFloatAttribute(ax::mojom::FloatAttribute::kMinValueForRange,
                        &float_val)) {
    value->vt = VT_R8;
    value->dblVal = float_val;
    return S_OK;
  }

  value->vt = VT_EMPTY;
  return S_FALSE;
}

IFACEMETHODIMP AXPlatformNodeWin::get_maximumValue(VARIANT* value) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_maximumValue");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_MAXIMUM_VALUE);
  COM_OBJECT_VALIDATE_1_ARG(value);
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  float float_val;
  if (GetFloatAttribute(ax::mojom::FloatAttribute::kMaxValueForRange,
                        &float_val)) {
    value->vt = VT_R8;
    value->dblVal = float_val;
    return S_OK;
  }

  value->vt = VT_EMPTY;
  return S_FALSE;
}

IFACEMETHODIMP AXPlatformNodeWin::setCurrentValue(VARIANT new_value) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("setCurrentValue");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_SET_CURRENT_VALUE);
  COM_OBJECT_VALIDATE();
  NotifyAddAXModeFlagsForIA2(kScreenReaderAccessibilityMode);

  double double_value = 0.0;
  if (V_VT(&new_value) == VT_R8)
    double_value = V_R8(&new_value);
  else if (V_VT(&new_value) == VT_R4)
    double_value = V_R4(&new_value);
  else if (V_VT(&new_value) == VT_I4)
    double_value = V_I4(&new_value);
  else
    return E_INVALIDARG;

  AXActionData data;
  data.action = ax::mojom::Action::kSetValue;
  data.value = base::NumberToString(double_value);
  if (GetDelegate()->AccessibilityPerformAction(data))
    return S_OK;
  return E_FAIL;
}

//
// IRawElementProviderFragment implementation.
//

IFACEMETHODIMP AXPlatformNodeWin::Navigate(
    NavigateDirection direction,
    IRawElementProviderFragment** element_provider) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("Navigate");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_NAVIGATE);
  WIN_ACCESSIBILITY_API_PERF_HISTOGRAM(UMA_API_NAVIGATE);
  UIA_VALIDATE_CALL_1_ARG(element_provider);

  *element_provider = nullptr;

  //
  // Navigation to a fragment root node:
  //
  // In order for the platform-neutral accessibility tree to support IA2 and UIA
  // simultaneously, we handle navigation to and from fragment roots in UIA
  // specific code. Consider the following platform-neutral tree:
  //
  //         N1
  //   _____/ \_____
  //  /             \
  // N2---N3---N4---N5
  //     / \       / \
  //   N6---N7   N8---N9
  //
  // N3 and N5 are nodes for which we need a fragment root. This will correspond
  // to the following tree in UIA:
  //
  //         U1
  //   _____/ \_____
  //  /             \
  // U2---R3---U4---R5
  //      |         |
  //      U3        U5
  //     / \       / \
  //   U6---U7   U8---U9
  //
  // Ux is the platform node for Nx.
  // R3 and R5 are the fragment root nodes for U3 and U5 respectively.
  //
  // Navigation has the following behaviors:
  //
  // 1. Parent navigation: If source node Ux is the child of a fragment root,
  //    return Rx. Otherwise, consult the platform-neutral tree.
  // 2. First/last child navigation: If target node Ux is the child of a
  //    fragment root and the source node isn't Rx, return Rx. Otherwise, return
  //    Ux.
  // 3. Next/previous sibling navigation:
  //    a. If source node Ux is the child of a fragment root, return nullptr.
  //    b. If target node Ux is the child of a fragment root, return Rx.
  //       Otherwise, return Ux.
  //
  // Note that the condition in 3b is a special case of the condition in 2. In
  // 3b, the source node is never Rx. So in the code, we collapse them to a
  // common implementation.
  //
  // Navigation from an Rx node is set up by delegate APIs on AXFragmentRootWin.
  //
  gfx::NativeViewAccessible neighbor = nullptr;
  switch (direction) {
    case NavigateDirection_Parent: {
      // 1. If source node Ux is the child of a fragment root, return Rx.
      // Otherwise, consult the platform-neutral tree.
      AXFragmentRootWin* fragment_root =
          AXFragmentRootWin::GetFragmentRootParentOf(GetNativeViewAccessible());
      if (fragment_root) [[unlikely]] {
        neighbor = fragment_root->GetNativeViewAccessible();
      } else {
        neighbor = GetParent();
      }
    } break;

    case NavigateDirection_FirstChild:
      if (GetChildCount() > 0)
        neighbor = GetFirstChild()->GetNativeViewAccessible();
      break;

    case NavigateDirection_LastChild:
      if (GetChildCount() > 0)
        neighbor = GetLastChild()->GetNativeViewAccessible();
      break;

    case NavigateDirection_NextSibling:
      // 3a. If source node Ux is the child of a fragment root, return nullptr.
      if (AXFragmentRootWin::GetFragmentRootParentOf(
              GetNativeViewAccessible()) == nullptr) {
        AXPlatformNodeBase* neighbor_node = GetNextSibling();
        if (neighbor_node)
          neighbor = neighbor_node->GetNativeViewAccessible();
      }
      break;

    case NavigateDirection_PreviousSibling:
      // 3a. If source node Ux is the child of a fragment root, return nullptr.
      if (AXFragmentRootWin::GetFragmentRootParentOf(
              GetNativeViewAccessible()) == nullptr) {
        AXPlatformNodeBase* neighbor_node = GetPreviousSibling();
        if (neighbor_node)
          neighbor = neighbor_node->GetNativeViewAccessible();
      }
      break;

    default:
      NOTREACHED_IN_MIGRATION();
      break;
  }

  if (neighbor) {
    if (direction != NavigateDirection_Parent) {
      // 2 / 3b. If target node Ux is the child of a fragment root and the
      // source node isn't Rx, return Rx.
      AXFragmentRootWin* fragment_root =
          AXFragmentRootWin::GetFragmentRootParentOf(neighbor);
      if (fragment_root && fragment_root != GetDelegate()) [[unlikely]] {
        neighbor = fragment_root->GetNativeViewAccessible();
      }
    }
    neighbor->QueryInterface(IID_PPV_ARGS(element_provider));
  }

  return S_OK;
}

void AXPlatformNodeWin::GetRuntimeIdArray(
    AXPlatformNodeWin::RuntimeIdArray& runtime_id) {
  runtime_id[0] = UiaAppendRuntimeId;

  // The combination of tree/frame id and Blink (DOM) id is unique and gives
  // nodes stable ids across layouts/tree movement. If there's a valid tree
  // id, use that, otherwise fall back to the globally unique id.
  int dom_id = GetData().GetDOMNodeId();
  if (dom_id) {
    AXTreeID tree_id = GetDelegate()->GetTreeData().tree_id;
    if (tree_id != AXTreeIDUnknown()) {
      AXActionHandlerRegistry::FrameID frame_id =
          AXActionHandlerRegistry::GetInstance()->GetFrameID(tree_id);
      runtime_id[1] = frame_id.first;
      runtime_id[2] = frame_id.second;
      runtime_id[3] = dom_id;
      return;
    }
  }

  runtime_id[1] = 0;
  runtime_id[2] = 0;
  runtime_id[3] = GetUniqueId();
}

IFACEMETHODIMP AXPlatformNodeWin::GetRuntimeId(SAFEARRAY** runtime_id) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("GetRuntimeId");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_RUNTIME_ID);
  UIA_VALIDATE_CALL_1_ARG(runtime_id);

  RuntimeIdArray id_array;
  GetRuntimeIdArray(id_array);
  *runtime_id = ::SafeArrayCreateVector(VT_I4, 0, id_array.size());

  int* array_data = nullptr;
  ::SafeArrayAccessData(*runtime_id, reinterpret_cast<void**>(&array_data));

  size_t runtime_id_byte_count = id_array.size() * sizeof(int);
  memcpy_s(array_data, runtime_id_byte_count, id_array.data(),
           runtime_id_byte_count);

  ::SafeArrayUnaccessData(*runtime_id);

  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_BoundingRectangle(
    UiaRect* screen_physical_pixel_bounds) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_BoundingRectangle");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_BOUNDINGRECTANGLE);
  WIN_ACCESSIBILITY_API_PERF_HISTOGRAM(UMA_API_GET_BOUNDINGRECTANGLE);

  UIA_VALIDATE_CALL_1_ARG(screen_physical_pixel_bounds);

  gfx::Rect bounds =
      delegate_->GetBoundsRect(AXCoordinateSystem::kScreenPhysicalPixels,
                               AXClippingBehavior::kUnclipped);
  screen_physical_pixel_bounds->left = bounds.x();
  screen_physical_pixel_bounds->top = bounds.y();
  screen_physical_pixel_bounds->width = bounds.width();
  screen_physical_pixel_bounds->height = bounds.height();
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::GetEmbeddedFragmentRoots(
    SAFEARRAY** embedded_fragment_roots) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("GetEmbeddedFragmentRoots");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GETEMBEDDEDFRAGMENTROOTS);
  UIA_VALIDATE_CALL_1_ARG(embedded_fragment_roots);

  *embedded_fragment_roots = nullptr;
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::SetFocus() {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("SetFocus");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_SETFOCUS);
  UIA_VALIDATE_CALL();

  AXActionData action_data;
  action_data.action = ax::mojom::Action::kFocus;
  delegate_->AccessibilityPerformAction(action_data);
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_FragmentRoot(
    IRawElementProviderFragmentRoot** fragment_root) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_FragmentRoot");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_FRAGMENTROOT);
  UIA_VALIDATE_CALL_1_ARG(fragment_root);

  gfx::AcceleratedWidget widget =
      delegate_->GetTargetForNativeAccessibilityEvent();
  if (widget) {
    AXFragmentRootWin* root =
        AXFragmentRootWin::GetForAcceleratedWidget(widget);
    if (root != nullptr) {
      root->GetNativeViewAccessible()->QueryInterface(
          IID_PPV_ARGS(fragment_root));
      DCHECK(*fragment_root);
      return S_OK;
    }
  }

  *fragment_root = nullptr;
  return UIA_E_ELEMENTNOTAVAILABLE;
}

//
// IRawElementProviderSimple implementation.
//

IFACEMETHODIMP AXPlatformNodeWin::GetPatternProvider(PATTERNID pattern_id,
                                                     IUnknown** result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("GetPatternProvider");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_PATTERN_PROVIDER);
  WIN_ACCESSIBILITY_API_PERF_HISTOGRAM(UMA_API_GET_PATTERN_PROVIDER);
  NotifyAPIObserverForPatternRequest(pattern_id);
  return GetPatternProviderImpl(pattern_id, result);
}

HRESULT AXPlatformNodeWin::GetPatternProviderImpl(PATTERNID pattern_id,
                                                  IUnknown** result) {
  UIA_VALIDATE_CALL_1_ARG(result);

  *result = nullptr;

  PatternProviderFactoryMethod factory_method =
      GetPatternProviderFactoryMethod(pattern_id);
  if (factory_method)
    (*factory_method)(this, result);

  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::GetPropertyValue(PROPERTYID property_id,
                                                   VARIANT* result) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("GetPropertyValue");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_PROPERTY_VALUE);
  WIN_ACCESSIBILITY_API_PERF_HISTOGRAM(UMA_API_GET_PROPERTY_VALUE);

  constexpr LONG kFirstKnownUiaPropertyId = UIA_RuntimeIdPropertyId;
  constexpr LONG kLastKnownUiaPropertyId = UIA_IsDialogPropertyId;
  if (property_id >= kFirstKnownUiaPropertyId &&
      property_id <= kLastKnownUiaPropertyId) {
    base::UmaHistogramSparse("Accessibility.WinAPIs.GetPropertyValue",
                             property_id);
  } else {
    // Collapse all unknown property IDs into a single bucket.
    base::UmaHistogramSparse("Accessibility.WinAPIs.GetPropertyValue", 0);
  }

  NotifyAPIObserverForPropertyRequest(property_id);

  return GetPropertyValueImpl(property_id, result);
}

HRESULT AXPlatformNodeWin::GetPropertyValueImpl(PROPERTYID property_id,
                                                VARIANT* result) {
  UIA_VALIDATE_CALL_1_ARG(result);

  result->vt = VT_EMPTY;
  int int_attribute;
  // Default UIA Property Ids.
  switch (property_id) {
    case UIA_AnnotationObjectsPropertyId: {
      result->vt = VT_ARRAY | VT_UNKNOWN;
      result->parray = CreateUIAElementsArrayForRelation(
          ax::mojom::IntListAttribute::kDetailsIds);
      break;
    }
    case UIA_AriaPropertiesPropertyId:
      result->vt = VT_BSTR;
      result->bstrVal = SysAllocString(ComputeUIAProperties().c_str());
      break;

    case UIA_AriaRolePropertyId:
      result->vt = VT_BSTR;
      result->bstrVal = SysAllocString(GetUIARoleProperties().aria_role);
      break;

    case UIA_AutomationIdPropertyId: {
      // The kRootWebArea is the only element in a web page that cannot have
      // an author provided id. In this case, we return a constant string
      // that needs to be the same for all locales.
      std::u16string automation_id = GetRole() == ax::mojom::Role::kRootWebArea
                                         ? u"RootWebArea"
                                         : GetDelegate()->GetAuthorUniqueId();
      V_VT(result) = VT_BSTR;
      V_BSTR(result) = SysAllocString(base::as_wcstr(automation_id));
      break;
    }
    case UIA_ClassNamePropertyId:
      result->vt = VT_BSTR;
      GetStringAttributeAsBstr(ax::mojom::StringAttribute::kClassName,
                               &result->bstrVal);
      break;

    case UIA_ClickablePointPropertyId:
      if (!GetDelegate()->IsOffscreen()) {
        result->vt = VT_ARRAY | VT_R8;
        result->parray = CreateClickablePointArray();
      }
      break;

    case UIA_ControllerForPropertyId:
      result->vt = VT_ARRAY | VT_UNKNOWN;
      result->parray = CreateUIAControllerForArray();
      break;

    case UIA_ControlTypePropertyId:
      result->vt = VT_I4;
      result->lVal = GetUIARoleProperties().control_type;
      break;

    case UIA_CulturePropertyId: {
      std::optional<LCID> lcid = GetCultureAttributeAsLCID();
      if (!lcid)
        return E_FAIL;
      result->vt = VT_I4;
      result->lVal = lcid.value();
      break;
    }

    case UIA_DescribedByPropertyId:
      result->vt = VT_ARRAY | VT_UNKNOWN;
      result->parray = CreateUIAElementsArrayForRelation(
          ax::mojom::IntListAttribute::kDetailsIds);
      break;

    case UIA_FlowsFromPropertyId:
      V_VT(result) = VT_ARRAY | VT_UNKNOWN;
      V_ARRAY(result) = CreateUIAElementsArrayForReverseRelation(
          ax::mojom::IntListAttribute::kFlowtoIds);
      break;

    case UIA_FlowsToPropertyId:
      result->vt = VT_ARRAY | VT_UNKNOWN;
      result->parray = CreateUIAElementsArrayForRelation(
          ax::mojom::IntListAttribute::kFlowtoIds);
      break;

    case UIA_FrameworkIdPropertyId:
      V_VT(result) = VT_BSTR;
      V_BSTR(result) = SysAllocString(FRAMEWORK_ID);
      break;

    case UIA_HasKeyboardFocusPropertyId:
      result->vt = VT_BOOL;
      result->boolVal = (delegate_->GetFocus() == GetNativeViewAccessible())
                            ? VARIANT_TRUE
                            : VARIANT_FALSE;
      break;

    case UIA_FullDescriptionPropertyId:
      result->vt = VT_BSTR;
      GetStringAttributeAsBstr(ax::mojom::StringAttribute::kDescription,
                               &result->bstrVal);
      break;

    case UIA_HelpTextPropertyId:
      if (HasStringAttribute(ax::mojom::StringAttribute::kPlaceholder)) {
        V_VT(result) = VT_BSTR;
        GetStringAttributeAsBstr(ax::mojom::StringAttribute::kPlaceholder,
                                 &V_BSTR(result));
      } else if (GetNameFrom() == ax::mojom::NameFrom::kPlaceholder ||
                 GetNameFrom() == ax::mojom::NameFrom::kTitle) {
        V_VT(result) = VT_BSTR;
        GetNameAsBstr(&V_BSTR(result));
      } else if (HasStringAttribute(ax::mojom::StringAttribute::kTooltip)) {
        V_VT(result) = VT_BSTR;
        GetStringAttributeAsBstr(ax::mojom::StringAttribute::kTooltip,
                                 &V_BSTR(result));
      }
      break;

    case UIA_IsContentElementPropertyId:
    case UIA_IsControlElementPropertyId:
      result->vt = VT_BOOL;
      result->boolVal = IsUIAControl() ? VARIANT_TRUE : VARIANT_FALSE;
      break;

    case UIA_IsDataValidForFormPropertyId:
      if (GetIntAttribute(ax::mojom::IntAttribute::kInvalidState,
                          &int_attribute)) {
        result->vt = VT_BOOL;
        result->boolVal =
            (static_cast<int>(ax::mojom::InvalidState::kFalse) == int_attribute)
                ? VARIANT_TRUE
                : VARIANT_FALSE;
      }
      break;

    case UIA_IsDialogPropertyId:
      result->vt = VT_BOOL;
      result->boolVal = IsDialog(GetRole()) ? VARIANT_TRUE : VARIANT_FALSE;
      break;

    case UIA_IsKeyboardFocusablePropertyId:
      result->vt = VT_BOOL;
      result->boolVal = IsFocusable() ? VARIANT_TRUE : VARIANT_FALSE;
      break;

    case UIA_IsOffscreenPropertyId:
      result->vt = VT_BOOL;
      result->boolVal =
          GetDelegate()->IsOffscreen() ? VARIANT_TRUE : VARIANT_FALSE;
      break;

    case UIA_IsRequiredForFormPropertyId:
      result->vt = VT_BOOL;
      if (HasState(ax::mojom::State::kRequired)) {
        result->boolVal = VARIANT_TRUE;
      } else {
        result->boolVal = VARIANT_FALSE;
      }
      break;

    case UIA_ItemStatusPropertyId: {
      // https://www.w3.org/TR/core-aam-1.1/#mapping_state-property_table
      // aria-sort='ascending|descending|other' is mapped for the
      // HeaderItem Control Type.
      int32_t sort_direction;
      if (IsTableHeader(GetRole()) &&
          GetIntAttribute(ax::mojom::IntAttribute::kSortDirection,
                          &sort_direction)) {
        switch (static_cast<ax::mojom::SortDirection>(sort_direction)) {
          case ax::mojom::SortDirection::kNone:
          case ax::mojom::SortDirection::kUnsorted:
            break;
          case ax::mojom::SortDirection::kAscending:
            V_VT(result) = VT_BSTR;
            V_BSTR(result) = SysAllocString(L"ascending");
            break;
          case ax::mojom::SortDirection::kDescending:
            V_VT(result) = VT_BSTR;
            V_BSTR(result) = SysAllocString(L"descending");
            break;
          case ax::mojom::SortDirection::kOther:
            V_VT(result) = VT_BSTR;
            V_BSTR(result) = SysAllocString(L"other");
            break;
        }
      }
      break;
    }

    case UIA_LabeledByPropertyId:
      if (AXPlatformNodeWin* node = ComputeUIALabeledBy()) {
        result->vt = VT_UNKNOWN;
        result->punkVal = node->GetNativeViewAccessible();
        result->punkVal->AddRef();
      }
      break;

    case UIA_LocalizedControlTypePropertyId: {
      // Always favor the explicitly set aria-roledescription value if there's
      // one.
      std::u16string role_description;
      if (GetString16Attribute(ax::mojom::StringAttribute::kRoleDescription,
                               &role_description)) {
        result->vt = VT_BSTR;
        result->bstrVal = SysAllocString(base::as_wcstr(role_description));
        break;
      }

      // UIA core handles Localized Control type for some built-in types and
      // also has a mapping for ARIA roles. To get these defaults, we need to
      // have returned VT_EMPTY.
      // With this in mind, we should return VT_EMPTY if the internal role is
      // accurately described by its UIA Control Type or aria role. Conversely,
      // if the internal role cannot be accurately described by its UIA Control
      // Type or aria role, we should instead provide our own localized
      // description.
      if (GetUIARoleProperties().localization_strategy ==
          UIALocalizationStrategy::kSupply) {
        // According to the HTML-AAM, UIA expects <output> to have a
        // Localized Control Type of "output" whereas the Core-AAM states
        // the Localized Control Type of the ARIA status role should be
        // "status".
        const std::string& html_tag =
            GetStringAttribute(ax::mojom::StringAttribute::kHtmlTag);
        std::u16string localized_control_type =
            html_tag == "output" ? l10n_util::GetStringUTF16(IDS_AX_ROLE_OUTPUT)
                                 : GetRoleDescription();

        if (!localized_control_type.empty()) {
          result->vt = VT_BSTR;
          result->bstrVal =
              SysAllocString(base::as_wcstr(localized_control_type));
        }
      }  // If a role description has not been provided, leave as VT_EMPTY.
      break;
    }
    case UIA_NamePropertyId:
      if (IsNameExposed()) {
        result->vt = VT_BSTR;

        // We need to handle listitems name property differently because UIA
        // expects a name for listitems, whereas other APIs do not.
        if (GetRole() == ax::mojom::Role::kListItem &&
            !HasStringAttribute(ax::mojom::StringAttribute::kName)) {
          ComputeListItemNameAsBstr(&result->bstrVal);
        } else {
          GetNameAsBstr(&result->bstrVal);
        }
      }
      break;

    case UIA_OrientationPropertyId:
      if (SupportsOrientation(GetRole())) {
        if (HasState(ax::mojom::State::kHorizontal) &&
            HasState(ax::mojom::State::kVertical)) {
          NOTREACHED_IN_MIGRATION()
              << "An accessibility object cannot have a horizontal "
                 "and a vertical orientation at the same time.";
        }
        if (HasState(ax::mojom::State::kHorizontal)) {
          result->vt = VT_I4;
          result->intVal = OrientationType_Horizontal;
        }
        if (HasState(ax::mojom::State::kVertical)) {
          result->vt = VT_I4;
          result->intVal = OrientationType_Vertical;
        }
      } else {
        result->vt = VT_I4;
        result->intVal = OrientationType_None;
      }
      break;

    case UIA_IsEnabledPropertyId:
      V_VT(result) = VT_BOOL;
      switch (GetData().GetRestriction()) {
        case ax::mojom::Restriction::kDisabled:
          V_BOOL(result) = VARIANT_FALSE;
          break;

        case ax::mojom::Restriction::kNone:
        case ax::mojom::Restriction::kReadOnly:
          V_BOOL(result) = VARIANT_TRUE;
          break;
      }
      break;

    case UIA_IsPasswordPropertyId:
      result->vt = VT_BOOL;
      result->boolVal =
          HasState(ax::mojom::State::kProtected) ? VARIANT_TRUE : VARIANT_FALSE;
      break;

    case UIA_AcceleratorKeyPropertyId:
      if (HasStringAttribute(ax::mojom::StringAttribute::kKeyShortcuts)) {
        result->vt = VT_BSTR;
        GetStringAttributeAsBstr(ax::mojom::StringAttribute::kKeyShortcuts,
                                 &result->bstrVal);
      }
      break;

    case UIA_AccessKeyPropertyId:
      if (HasStringAttribute(ax::mojom::StringAttribute::kAccessKey)) {
        result->vt = VT_BSTR;
        GetStringAttributeAsBstr(ax::mojom::StringAttribute::kAccessKey,
                                 &result->bstrVal);
      }
      break;

    case UIA_IsPeripheralPropertyId:
      result->vt = VT_BOOL;
      result->boolVal = VARIANT_FALSE;
      break;

    case UIA_LevelPropertyId:
      if (GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel,
                          &int_attribute)) {
        result->vt = VT_I4;
        result->intVal = int_attribute;
      }
      break;

    case UIA_LiveSettingPropertyId: {
      result->vt = VT_I4;
      result->intVal = LiveSetting::Off;

      std::string string_attribute;
      if (GetStringAttribute(ax::mojom::StringAttribute::kLiveStatus,
                             &string_attribute)) {
        if (string_attribute == "polite")
          result->intVal = LiveSetting::Polite;
        else if (string_attribute == "assertive")
          result->intVal = LiveSetting::Assertive;
      }
      break;
    }

    case UIA_OptimizeForVisualContentPropertyId:
      result->vt = VT_BOOL;
      result->boolVal = VARIANT_FALSE;
      break;

    case UIA_PositionInSetPropertyId: {
      std::optional<int> pos_in_set = GetPosInSet();
      if (pos_in_set) {
        result->vt = VT_I4;
        result->intVal = *pos_in_set;
      }
    } break;

    case UIA_ScrollHorizontalScrollPercentPropertyId: {
      V_VT(result) = VT_R8;
      V_R8(result) = GetHorizontalScrollPercent();
      break;
    }

    case UIA_ScrollVerticalScrollPercentPropertyId: {
      V_VT(result) = VT_R8;
      V_R8(result) = GetVerticalScrollPercent();
      break;
    }

    case UIA_SelectionItemIsSelectedPropertyId: {
      result->vt = VT_BOOL;
      result->boolVal =
          GetDelegate()->IsUIANodeSelected() ? VARIANT_TRUE : VARIANT_FALSE;
      break;
    }

    case UIA_SizeOfSetPropertyId: {
      std::optional<int> set_size = GetSetSize();
      if (set_size) {
        result->vt = VT_I4;
        result->intVal = *set_size;
      }
      break;
    }

    case UIA_ToggleToggleStatePropertyId: {
      result->vt = VT_I4;
      result->intVal = GetToggleStateImpl();
      break;
    }

    case UIA_LandmarkTypePropertyId: {
      std::optional<LONG> landmark_type = ComputeUIALandmarkType();
      if (landmark_type) {
        result->vt = VT_I4;
        result->intVal = landmark_type.value();
      }
      break;
    }

    case UIA_LocalizedLandmarkTypePropertyId: {
      std::u16string localized_landmark_type =
          GetDelegate()->GetLocalizedStringForLandmarkType();
      if (!localized_landmark_type.empty()) {
        result->vt = VT_BSTR;
        result->bstrVal =
            SysAllocString(base::as_wcstr(localized_landmark_type));
      }
      break;
    }

    case UIA_ExpandCollapseExpandCollapseStatePropertyId:
      result->vt = VT_I4;
      result->intVal = static_cast<int>(ComputeExpandCollapseState());
      break;

    // Not currently implemented.
    case UIA_AnnotationTypesPropertyId:
    case UIA_CenterPointPropertyId:
    case UIA_FillColorPropertyId:
    case UIA_FillTypePropertyId:
    case UIA_HeadingLevelPropertyId:
    case UIA_ItemTypePropertyId:
    case UIA_OutlineColorPropertyId:
    case UIA_OutlineThicknessPropertyId:
    case UIA_RotationPropertyId:
    case UIA_SizePropertyId:
    case UIA_VisualEffectsPropertyId:
      break;

    // Provided by UIA Core; we should not implement.
    case UIA_BoundingRectanglePropertyId:
    case UIA_NativeWindowHandlePropertyId:
    case UIA_ProcessIdPropertyId:
    case UIA_ProviderDescriptionPropertyId:
    case UIA_RuntimeIdPropertyId:
      break;
    default:
      // We can't simply add these custom properties ids to the switch case
      // because they are not constant expressions.
      //
      // Custom UIA Property Ids.
      if (property_id ==
          UiaRegistrarWin::GetInstance().GetUniqueIdPropertyId()) {
        // We want to negate the unique id for it to be consistent across
        // different Windows accessiblity APIs. The negative unique id
        // convention originated from ::NotifyWinEvent() takes an hwnd and a
        // child id. A 0 child id means self, and a positive child id means
        // child #n. In order to fire an event for an arbitrary descendant of
        // the window, Firefox started the practice of using a negative unique
        // id. We follow the same negative unique id convention here and when we
        // fire events via ::NotifyWinEvent().
        result->vt = VT_BSTR;
        result->bstrVal =
            SysAllocString(base::NumberToWString(-GetUniqueId()).c_str());
      } else if (features::IsAccessibilityAriaVirtualContentEnabled() &&
                 property_id == UiaRegistrarWin::GetInstance()
                                    .GetVirtualContentPropertyId()) {
        if (HasStringAttribute(ax::mojom::StringAttribute::kVirtualContent)) {
          V_VT(result) = VT_BSTR;
          GetStringAttributeAsBstr(ax::mojom::StringAttribute::kVirtualContent,
                                   &V_BSTR(result));
        }
      }
      break;
  }

  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_ProviderOptions(ProviderOptions* ret) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_ProviderOptions");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_PROVIDER_OPTIONS);
  UIA_VALIDATE_CALL_1_ARG(ret);

  *ret = ProviderOptions_ServerSideProvider | ProviderOptions_UseComThreading |
         ProviderOptions_RefuseNonClientSupport |
         ProviderOptions_HasNativeIAccessible;
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_HostRawElementProvider(
    IRawElementProviderSimple** provider) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_HostRawElementProvider");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_HOST_RAW_ELEMENT_PROVIDER);
  UIA_VALIDATE_CALL_1_ARG(provider);

  *provider = nullptr;
  return S_OK;
}

//
// IRawElementProviderSimple2 implementation.
//

IFACEMETHODIMP AXPlatformNodeWin::ShowContextMenu() {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("ShowContextMenu");
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_SHOWCONTEXTMENU);
  UIA_VALIDATE_CALL();

  AXActionData action_data;
  action_data.action = ax::mojom::Action::kShowContextMenu;
  delegate_->AccessibilityPerformAction(action_data);
  return S_OK;
}

//
// IChromeAccessible implementation.
//

void SendBulkFetchResponse(
    Microsoft::WRL::ComPtr<IChromeAccessibleDelegate> delegate,
    LONG request_id,
    std::string json_result) {
  std::wstring json_result_wide = base::UTF8ToWide(json_result);
  delegate->put_bulkFetchResult(request_id,
                                SysAllocString(json_result_wide.c_str()));
}

IFACEMETHODIMP AXPlatformNodeWin::get_bulkFetch(
    BSTR input_json,
    LONG request_id,
    IChromeAccessibleDelegate* delegate) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_bulkFetch");
  COM_OBJECT_VALIDATE();
  if (!delegate)
    return E_INVALIDARG;

  // TODO(crbug.com/40692720): if parsing |input_json|, use
  // DataDecoder because the json is untrusted. For now, this is just
  // a stub that calls PostTask so that it's async, but it doesn't
  // actually parse the input.

  base::Value::Dict result;
  result.Set("role", base::Value(ui::ToString(GetRole())));

  gfx::Rect bounds = GetDelegate()->GetBoundsRect(
      AXCoordinateSystem::kScreenDIPs, AXClippingBehavior::kUnclipped);
  result.Set("x", base::Value(bounds.x()));
  result.Set("y", base::Value(bounds.y()));
  result.Set("width", base::Value(bounds.width()));
  result.Set("height", base::Value(bounds.height()));
  std::string json_result;
  base::JSONWriter::Write(result, &json_result);
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(
          &SendBulkFetchResponse,
          Microsoft::WRL::ComPtr<IChromeAccessibleDelegate>(delegate),
          request_id, json_result));
  return S_OK;
}

IFACEMETHODIMP AXPlatformNodeWin::get_hitTest(
    LONG screen_physical_pixel_x,
    LONG screen_physical_pixel_y,
    LONG request_id,
    IChromeAccessibleDelegate* delegate) {
  WIN_ACCESSIBILITY_API_TRACE_EVENT("get_hitTest");
  COM_OBJECT_VALIDATE();

  if (!delegate)
    return E_INVALIDARG;

  // TODO(crbug.com/40692720): Plumb through an actual async hit test.
  AXPlatformNodeWin* hit_child = static_cast<AXPlatformNodeWin*>(
      FromNativeViewAccessible(GetDelegate()->HitTestSync(
          screen_physical_pixel_x, screen_physical_pixel_y)));

  delegate->put_hitTestResult(request_id, static_cast<IAccessible*>(hit_child));
  return S_OK;
}

//
// IServiceProvider implementation.
//

IFACEMETHODIMP AXPlatformNodeWin::QueryService(REFGUID guidService,
                                               REFIID riid,
                                               void** object) {
  TRACE_EVENT("accessibility", "QueryService",
              perfetto::Flow::FromPointer(this), "guidService",
              base::WideToASCII(base::win::WStringFromGUID(guidService)),
              "riid", base::WideToASCII(base::win::WStringFromGUID(riid)));
  COM_OBJECT_VALIDATE_1_ARG(object);

  if (!GetDelegate()) {
    *object = nullptr;
    return E_FAIL;
  }

  if (riid == IID_IAccessible2) {
    for (WinAccessibilityAPIUsageObserver& observer :
         GetWinAccessibilityAPIUsageObserverList()) {
      if (!features::IsAccessibilityRestrictiveIA2AXModesEnabled()) {
        observer.OnAdvancedIAccessible2Used();
        continue;
      }

      if (GetDelegate()->IsWebContent()) {
        observer.OnAdvancedIAccessible2Used();
      } else {
        observer.OnBasicIAccessible2Used();
      }
    }
  }

  if (guidService == IID_IAccessible || guidService == IID_IAccessible2 ||
      guidService == IID_IAccessible2_2 ||
      guidService == IID_IAccessibleTable ||
      guidService == IID_IAccessibleTable2 ||
      guidService == IID_IAccessibleTableCell ||
      guidService == IID_IAccessibleText ||
      guidService == IID_IAccessibleTextSelectionContainer ||
      guidService == IID_IAccessibleValue) {
    return QueryInterface(riid, object);
  }

  if (guidService == IID_IChromeAccessible) {
    if (features::IsIChromeAccessibleEnabled()) {
      return QueryInterface(riid, object);
    }
  }

  // TODO(suproteem): Include IAccessibleEx in the list, potentially checking
  // for version.

  *object = nullptr;
  return E_FAIL;
}

//
// Methods used by the ATL COM map.
//

// static
STDMETHODIMP AXPlatformNodeWin::InternalQueryInterface(
    void* this_ptr,
    const _ATL_INTMAP_ENTRY* entries,
    REFIID riid,
    void** object) {
  if (!object)
    return E_INVALIDARG;
  *object = nullptr;
  AXPlatformNodeWin* accessible =
      reinterpret_cast<AXPlatformNodeWin*>(this_ptr);
  DCHECK(accessible);

  if (riid == IID_IAccessibleTable || riid == IID_IAccessibleTable2) {
    if (!IsTableLike(accessible->GetRole()))
      return E_NOINTERFACE;
  } else if (riid == IID_IAccessibleTableCell) {
    if (!IsCellOrTableHeader(accessible->GetRole()))
      return E_NOINTERFACE;
  } else if (riid == IID_IAccessibleText || riid == IID_IAccessibleHypertext) {
    if (IsImageOrVideo(accessible->GetRole())) {
      return E_NOINTERFACE;
    }
  } else if (riid == IID_IAccessibleValue) {
    if (!accessible->GetData().IsRangeValueSupported()) {
      return E_NOINTERFACE;
    }
  } else if (riid == IID_IChromeAccessible) {
    if (!features::IsIChromeAccessibleEnabled()) {
      return E_NOINTERFACE;
    }
  }

  return CComObjectRootBase::InternalQueryInterface(this_ptr, entries, riid,
                                                    object);
}

HRESULT AXPlatformNodeWin::GetTextAttributeValue(
    TEXTATTRIBUTEID attribute_id,
    const std::optional<int>& start_offset,
    const std::optional<int>& end_offset,
    base::win::VariantVector* result) {
  DCHECK(!start_offset || start_offset.value() >= 0);
  DCHECK(!end_offset || end_offset.value() >= 0);
  switch (attribute_id) {
    case UIA_AnnotationObjectsAttributeId:
      GetAnnotationObjectsAttribute(result);
      break;
    case UIA_AnnotationTypesAttributeId:
      return GetAnnotationTypesAttribute(start_offset, end_offset, result);
    case UIA_BackgroundColorAttributeId:
      result->Insert<VT_I4>(
          GetIntAttributeAsCOLORREF(ax::mojom::IntAttribute::kBackgroundColor));
      break;
    case UIA_BulletStyleAttributeId:
      result->Insert<VT_I4>(ComputeUIABulletStyle());
      break;
    case UIA_CultureAttributeId: {
      std::optional<LCID> lcid = GetCultureAttributeAsLCID();
      if (!lcid)
        return E_FAIL;
      result->Insert<VT_I4>(lcid.value());
      break;
    }
    case UIA_FontNameAttributeId:
      result->Insert<VT_BSTR>(GetFontNameAttributeAsBSTR());
      break;
    case UIA_FontSizeAttributeId: {
      std::optional<float> font_size_in_points = GetFontSizeInPoints();
      if (font_size_in_points) {
        result->Insert<VT_R8>(*font_size_in_points);
      }
      break;
    }
    case UIA_FontWeightAttributeId:
      result->Insert<VT_I4>(
          GetFloatAttribute(ax::mojom::FloatAttribute::kFontWeight));
      break;
    case UIA_ForegroundColorAttributeId:
      result->Insert<VT_I4>(
          GetIntAttributeAsCOLORREF(ax::mojom::IntAttribute::kColor));
      break;
    case UIA_IsHiddenAttributeId:
      result->Insert<VT_BOOL>(IsInvisibleOrIgnored());
      break;
    case UIA_IsItalicAttributeId:
      result->Insert<VT_BOOL>(HasTextStyle(ax::mojom::TextStyle::kItalic));
      break;
    case UIA_IsReadOnlyAttributeId: {
      // If inside a text field, the text field's readonly state rules.
      const AXPlatformNodeWin* text_field = static_cast<AXPlatformNodeWin*>(
          FromNativeViewAccessible(GetDelegate()->GetTextFieldAncestor()));
      if (text_field) {
        result->Insert<VT_BOOL>(
            text_field->GetDelegate()->IsReadOnlyOrDisabled());
      } else {
        result->Insert<VT_BOOL>(GetDelegate()->IsReadOnlyOrDisabled());
      }
      break;
    }
    case UIA_IsSubscriptAttributeId:
      result->Insert<VT_BOOL>(GetData().GetTextPosition() ==
                              ax::mojom::TextPosition::kSubscript);
      break;
    case UIA_IsSuperscriptAttributeId:
      result->Insert<VT_BOOL>(GetData().GetTextPosition() ==
                              ax::mojom::TextPosition::kSuperscript);
      break;
    case UIA_OverlineStyleAttributeId:
      result->Insert<VT_I4>(GetUIATextDecorationStyle(
          ax::mojom::IntAttribute::kTextOverlineStyle));
      break;
    case UIA_StrikethroughStyleAttributeId:
      result->Insert<VT_I4>(GetUIATextDecorationStyle(
          ax::mojom::IntAttribute::kTextStrikethroughStyle));
      break;
    case UIA_StyleNameAttributeId:
      result->Insert<VT_BSTR>(GetStyleNameAttributeAsBSTR());
      break;
    case UIA_StyleIdAttributeId:
      result->Insert<VT_I4>(ComputeUIAStyleId());
      break;
    case UIA_HorizontalTextAlignmentAttributeId: {
      std::optional<HorizontalTextAlignment> horizontal_text_alignment =
          AXTextAlignToUIAHorizontalTextAlignment(GetData().GetTextAlign());
      if (horizontal_text_alignment)
        result->Insert<VT_I4>(*horizontal_text_alignment);
      break;
    }
    case UIA_UnderlineStyleAttributeId:
      result->Insert<VT_I4>(GetUIATextDecorationStyle(
          ax::mojom::IntAttribute::kTextUnderlineStyle));
      break;
    case UIA_TextFlowDirectionsAttributeId:
      result->Insert<VT_I4>(
          TextDirectionToFlowDirections(GetData().GetTextDirection()));
      break;
    default: {
      Microsoft::WRL::ComPtr<IUnknown> not_supported_value;
      HRESULT hr = ::UiaGetReservedNotSupportedValue(&not_supported_value);
      if (SUCCEEDED(hr))
        result->Insert<VT_UNKNOWN>(not_supported_value.Get());
      return hr;
    }
  }

  return S_OK;
}

void AXPlatformNodeWin::GetAnnotationObjectsAttribute(
    base::win::VariantVector* result) {
  // Most times AnnotationObject attribute is set on the container (immediate
  // parent) of the text node, but it can be on any ancestor of the text node.
  // TODO(vicfei): Need to find an efficient algorithm to walk up current node's
  // ancestors to find the attribute. https://crbug.com/1201327
  AXPlatformNodeWin* parent_platform_node = GetParentPlatformNodeWin();

  if (!parent_platform_node || !IsText())
    return;

  for (AXPlatformNodeWin* platform_node :
       CreatePlatformNodeVectorFromRelationIdVector(
           parent_platform_node->GetIntListAttribute(
               ax::mojom::IntListAttribute::kDetailsIds))) {
    Microsoft::WRL::ComPtr<IUnknown> annotation_object;
    if (SUCCEEDED(
            platform_node->QueryInterface(IID_PPV_ARGS(&annotation_object)))) {
      result->Insert<VT_UNKNOWN>(annotation_object.Get());
    }
  }
}

HRESULT AXPlatformNodeWin::GetAnnotationTypesAttribute(
    const std::optional<int>& start_offset,
    const std::optional<int>& end_offset,
    base::win::VariantVector* result) {
  base::win::VariantVector variant_vector;
  MarkerTypeRangeResult grammar_result = MarkerTypeRangeResult::kNone;
  MarkerTypeRangeResult spelling_result = MarkerTypeRangeResult::kNone;
  MarkerTypeRangeResult highlight_result = MarkerTypeRangeResult::kNone;
  MarkerTypeRangeResult highlight_spelling_result =
      MarkerTypeRangeResult::kNone;
  MarkerTypeRangeResult highlight_grammar_result = MarkerTypeRangeResult::kNone;

  if (IsText() || IsAtomicTextField()) {
    grammar_result = GetMarkerTypeFromRange(start_offset, end_offset,
                                            ax::mojom::MarkerType::kGrammar);
    spelling_result = GetMarkerTypeFromRange(start_offset, end_offset,
                                             ax::mojom::MarkerType::kSpelling);
    highlight_result = GetMarkerTypeFromRange(
        start_offset, end_offset, ax::mojom::MarkerType::kHighlight,
        ax::mojom::HighlightType::kHighlight);
    highlight_spelling_result = GetMarkerTypeFromRange(
        start_offset, end_offset, ax::mojom::MarkerType::kHighlight,
        ax::mojom::HighlightType::kSpellingError);
    highlight_grammar_result = GetMarkerTypeFromRange(
        start_offset, end_offset, ax::mojom::MarkerType::kHighlight,
        ax::mojom::HighlightType::kGrammarError);
  }

  if (grammar_result == MarkerTypeRangeResult::kMixed ||
      spelling_result == MarkerTypeRangeResult::kMixed ||
      highlight_result == MarkerTypeRangeResult::kMixed ||
      highlight_spelling_result == MarkerTypeRangeResult::kMixed ||
      highlight_grammar_result == MarkerTypeRangeResult::kMixed) {
    Microsoft::WRL::ComPtr<IUnknown> mixed_attribute_value;
    HRESULT hr = ::UiaGetReservedMixedAttributeValue(&mixed_attribute_value);
    if (SUCCEEDED(hr))
      result->Insert<VT_UNKNOWN>(mixed_attribute_value.Get());
    return hr;
  }

  if (spelling_result == MarkerTypeRangeResult::kMatch ||
      highlight_spelling_result == MarkerTypeRangeResult::kMatch)
    result->Insert<VT_I4>(AnnotationType_SpellingError);
  if (grammar_result == MarkerTypeRangeResult::kMatch ||
      highlight_grammar_result == MarkerTypeRangeResult::kMatch)
    result->Insert<VT_I4>(AnnotationType_GrammarError);
  if (highlight_result == MarkerTypeRangeResult::kMatch)
    result->Insert<VT_I4>(AnnotationType_Highlighted);

  return S_OK;
}

std::optional<LCID> AXPlatformNodeWin::GetCultureAttributeAsLCID() const {
  const std::u16string language =
      base::UTF8ToUTF16(GetDelegate()->GetLanguage());
  const LCID lcid =
      LocaleNameToLCID(base::as_wcstr(language), LOCALE_ALLOW_NEUTRAL_NAMES);
  if (!lcid)
    return std::nullopt;

  return lcid;
}

COLORREF AXPlatformNodeWin::GetIntAttributeAsCOLORREF(
    ax::mojom::IntAttribute attribute) const {
  SkColor color;
  auto maybe_value = ComputeAttribute(delegate_, attribute);
  if (maybe_value.has_value())
    color = maybe_value.value();
  else
    color = GetIntAttribute(attribute);
  return skia::SkColorToCOLORREF(color);
}

BulletStyle AXPlatformNodeWin::ComputeUIABulletStyle() const {
  // UIA expects the list style of a non-list-item to be none however the
  // default list style cascaded is disc not none. Therefore we must ensure that
  // this node is contained within a list-item to distinguish non-list-items and
  // disc styled list items.
  const AXPlatformNodeBase* current_node = this;
  while (current_node &&
         current_node->GetRole() != ax::mojom::Role::kListItem) {
    current_node = FromNativeViewAccessible(current_node->GetParent());
  }

  const ax::mojom::ListStyle list_style =
      current_node ? current_node->GetData().GetListStyle()
                   : ax::mojom::ListStyle::kNone;

  switch (list_style) {
    case ax::mojom::ListStyle::kNone:
      return BulletStyle::BulletStyle_None;
    case ax::mojom::ListStyle::kCircle:
      return BulletStyle::BulletStyle_HollowRoundBullet;
    case ax::mojom::ListStyle::kDisc:
      return BulletStyle::BulletStyle_FilledRoundBullet;
    case ax::mojom::ListStyle::kImage:
      return BulletStyle::BulletStyle_Other;
    case ax::mojom::ListStyle::kNumeric:
    case ax::mojom::ListStyle::kOther:
      return BulletStyle::BulletStyle_None;
    case ax::mojom::ListStyle::kSquare:
      return BulletStyle::BulletStyle_FilledSquareBullet;
  }
}

LONG AXPlatformNodeWin::ComputeUIAStyleId() const {
  const AXPlatformNodeBase* current_node = this;
  do {
    switch (current_node->GetRole()) {
      case ax::mojom::Role::kHeading:
        return AXHierarchicalLevelToUIAStyleId(current_node->GetIntAttribute(
            ax::mojom::IntAttribute::kHierarchicalLevel));
      case ax::mojom::Role::kListItem:
        return AXListStyleToUIAStyleId(current_node->GetData().GetListStyle());
      case ax::mojom::Role::kMark:
        return StyleId_Custom;
      case ax::mojom::Role::kBlockquote:
        return StyleId_Quote;
      default:
        break;
    }
    current_node = FromNativeViewAccessible(current_node->GetParent());
  } while (current_node);

  return StyleId_Normal;
}

// static
std::optional<HorizontalTextAlignment>
AXPlatformNodeWin::AXTextAlignToUIAHorizontalTextAlignment(
    ax::mojom::TextAlign text_align) {
  switch (text_align) {
    case ax::mojom::TextAlign::kNone:
      return std::nullopt;
    case ax::mojom::TextAlign::kLeft:
      return HorizontalTextAlignment_Left;
    case ax::mojom::TextAlign::kRight:
      return HorizontalTextAlignment_Right;
    case ax::mojom::TextAlign::kCenter:
      return HorizontalTextAlignment_Centered;
    case ax::mojom::TextAlign::kJustify:
      return HorizontalTextAlignment_Justified;
  }
}

// static
LONG AXPlatformNodeWin::AXHierarchicalLevelToUIAStyleId(
    int32_t hierarchical_level) {
  switch (hierarchical_level) {
    case 0:
      return StyleId_Normal;
    case 1:
      return StyleId_Heading1;
    case 2:
      return StyleId_Heading2;
    case 3:
      return StyleId_Heading3;
    case 4:
      return StyleId_Heading4;
    case 5:
      return StyleId_Heading5;
    case 6:
      return StyleId_Heading6;
    case 7:
      return StyleId_Heading7;
    case 8:
      return StyleId_Heading8;
    case 9:
      return StyleId_Heading9;
    default:
      return StyleId_Custom;
  }
}

// static
LONG AXPlatformNodeWin::AXListStyleToUIAStyleId(
    ax::mojom::ListStyle list_style) {
  switch (list_style) {
    case ax::mojom::ListStyle::kNone:
      return StyleId_Normal;
    case ax::mojom::ListStyle::kCircle:
    case ax::mojom::ListStyle::kDisc:
    case ax::mojom::ListStyle::kImage:
    case ax::mojom::ListStyle::kSquare:
      return StyleId_BulletedList;
    case ax::mojom::ListStyle::kNumeric:
    case ax::mojom::ListStyle::kOther:
      return StyleId_NumberedList;
  }
}

// static
FlowDirections AXPlatformNodeWin::TextDirectionToFlowDirections(
    ax::mojom::WritingDirection text_direction) {
  switch (text_direction) {
    case ax::mojom::WritingDirection::kNone:
      return FlowDirections::FlowDirections_Default;
    case ax::mojom::WritingDirection::kLtr:
      return FlowDirections::FlowDirections_Default;
    case ax::mojom::WritingDirection::kRtl:
      return FlowDirections::FlowDirections_RightToLeft;
    case ax::mojom::WritingDirection::kTtb:
      return FlowDirections::FlowDirections_Vertical;
    case ax::mojom::WritingDirection::kBtt:
      return FlowDirections::FlowDirections_BottomToTop;
  }
}

// static
void AXPlatformNodeWin::AggregateRangesForMarkerType(
    AXPlatformNodeBase* node,
    ax::mojom::MarkerType marker_type,
    int offset_ranges_amount,
    std::vector<std::pair<int, int>>* ranges,
    const std::optional<ax::mojom::HighlightType>& highlight_type) {
  DCHECK(node->IsText());
  const std::vector<int32_t>& marker_types =
      node->GetIntListAttribute(ax::mojom::IntListAttribute::kMarkerTypes);
  const std::vector<int32_t>& highlight_types =
      node->GetIntListAttribute(ax::mojom::IntListAttribute::kHighlightTypes);
  const std::vector<int>& marker_starts =
      node->GetIntListAttribute(ax::mojom::IntListAttribute::kMarkerStarts);
  const std::vector<int>& marker_ends =
      node->GetIntListAttribute(ax::mojom::IntListAttribute::kMarkerEnds);

  CHECK_EQ(marker_types.size(), marker_starts.size());
  CHECK_EQ(marker_types.size(), marker_ends.size());

  for (size_t i = 0; i < marker_types.size(); ++i) {
    if (static_cast<ax::mojom::MarkerType>(marker_types[i]) != marker_type)
      continue;

    if (marker_type == ax::mojom::MarkerType::kHighlight) {
      CHECK_EQ(highlight_types.size(), marker_types.size());
      if (highlight_type !=
          static_cast<ax::mojom::HighlightType>(highlight_types[i])) {
        continue;
      }
    }

    const int marker_start = marker_starts[i] + offset_ranges_amount;
    const int marker_end = marker_ends[i] + offset_ranges_amount;
    ranges->emplace_back(std::make_pair(marker_start, marker_end));
  }
}

AXPlatformNodeWin::MarkerTypeRangeResult
AXPlatformNodeWin::GetMarkerTypeFromRange(
    const std::optional<int>& start_offset,
    const std::optional<int>& end_offset,
    ax::mojom::MarkerType marker_type,
    const std::optional<ax::mojom::HighlightType>& highlight_type) {
  DCHECK(IsText() || IsAtomicTextField());
  std::vector<std::pair<int, int>> relevant_ranges;

  if (IsText()) {
    AggregateRangesForMarkerType(this, marker_type, /*offset_ranges_amount=*/0,
                                 &relevant_ranges, highlight_type);
  } else if (IsAtomicTextField()) {
    int offset_ranges_amount = 0;
    for (AXPlatformNodeBase* static_text = GetFirstTextOnlyDescendant();
         static_text; static_text = static_text->GetNextSibling()) {
      const int child_offset_ranges_amount = offset_ranges_amount;
      if (start_offset || end_offset) {
        // Break if the current node is after the desired |end_offset|.
        if (end_offset && child_offset_ranges_amount > end_offset.value())
          break;

        // Skip over nodes preceding the desired |start_offset|.
        offset_ranges_amount += static_text->GetHypertext().length();
        if (start_offset && offset_ranges_amount < start_offset.value())
          continue;
      }

      AggregateRangesForMarkerType(static_text, marker_type,
                                   child_offset_ranges_amount, &relevant_ranges,
                                   highlight_type);
    }
  }

  // Sort the ranges by their start offset.
  const auto sort_ranges_by_start_offset = [](const std::pair<int, int>& a,
                                              const std::pair<int, int>& b) {
    return a.first < b.first;
  };
  std::sort(relevant_ranges.begin(), relevant_ranges.end(),
            sort_ranges_by_start_offset);

  // Validate that the desired range has a contiguous MarkerType.
  std::optional<std::pair<int, int>> contiguous_range;
  for (const std::pair<int, int>& range : relevant_ranges) {
    if (end_offset && range.first > end_offset.value())
      break;
    if (start_offset && range.second < start_offset.value())
      continue;

    if (!contiguous_range) {
      contiguous_range = range;
      continue;
    }

    // If there is a gap, then the range must be mixed.
    if ((range.first - contiguous_range->second) > 1)
      return MarkerTypeRangeResult::kMixed;

    // Expand the range if possible.
    contiguous_range->second = std::max(contiguous_range->second, range.second);
  }

  // The desired range does not overlap with |marker_type|.
  if (!contiguous_range)
    return MarkerTypeRangeResult::kNone;

  // If there is a partial overlap, then the desired range must be mixed.
  // 1. The |start_offset| is not specified, treat it as offset 0.
  if (!start_offset && contiguous_range->first > 0)
    return MarkerTypeRangeResult::kMixed;
  // 2. The |end_offset| is not specified, treat it as max text offset.
  if (!end_offset &&
      static_cast<size_t>(contiguous_range->second) < GetHypertext().length())
    return MarkerTypeRangeResult::kMixed;
  // 3. The |start_offset| is specified, but is before the first matching range.
  if (start_offset && start_offset.value() < contiguous_range->first)
    return MarkerTypeRangeResult::kMixed;
  // 4. The |end_offset| is specified, but is after the last matching range.
  if (end_offset && end_offset.value() > contiguous_range->second)
    return MarkerTypeRangeResult::kMixed;

  // The desired range is a complete match for |marker_type|.
  return MarkerTypeRangeResult::kMatch;
}

// IRawElementProviderSimple support methods.

bool AXPlatformNodeWin::IsPatternProviderSupported(PATTERNID pattern_id) {
  return GetPatternProviderFactoryMethod(pattern_id);
}

//
// Private member functions.
//
int AXPlatformNodeWin::MSAARole() {
  // If this is a web area for a presentational iframe, give it a role of
  // something other than DOCUMENT so that the fact that it's a separate doc
  // is not exposed to AT.
  if (GetDelegate()->IsRootWebAreaForPresentationalIframe())
    return ROLE_SYSTEM_GROUPING;

  switch (GetRole()) {
    case ax::mojom::Role::kAlert:
      return ROLE_SYSTEM_ALERT;

    case ax::mojom::Role::kAlertDialog:
      return ROLE_SYSTEM_DIALOG;

    case ax::mojom::Role::kComment:
    case ax::mojom::Role::kSuggestion:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kApplication:
      return ROLE_SYSTEM_APPLICATION;

    case ax::mojom::Role::kArticle:
      return ROLE_SYSTEM_DOCUMENT;

    case ax::mojom::Role::kAudio:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kBanner:
    case ax::mojom::Role::kHeader:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kBlockquote:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kButton:
      return ROLE_SYSTEM_PUSHBUTTON;

    case ax::mojom::Role::kCanvas:
      return ROLE_SYSTEM_GRAPHIC;

    case ax::mojom::Role::kCaption:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kCaret:
      return ROLE_SYSTEM_CARET;

    case ax::mojom::Role::kCell:
      return ROLE_SYSTEM_CELL;

    case ax::mojom::Role::kCheckBox:
      return ROLE_SYSTEM_CHECKBUTTON;

    case ax::mojom::Role::kClient:
      return ROLE_SYSTEM_PANE;

    case ax::mojom::Role::kColorWell:
      return ROLE_SYSTEM_TEXT;

    case ax::mojom::Role::kColumn:
      return ROLE_SYSTEM_COLUMN;

    case ax::mojom::Role::kColumnHeader:
      return ROLE_SYSTEM_COLUMNHEADER;

    case ax::mojom::Role::kComboBoxGrouping:
    case ax::mojom::Role::kComboBoxMenuButton:
    case ax::mojom::Role::kComboBoxSelect:
      return ROLE_SYSTEM_COMBOBOX;

    case ax::mojom::Role::kComplementary:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kContentDeletion:
    case ax::mojom::Role::kContentInsertion:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kContentInfo:
    case ax::mojom::Role::kFooter:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kDate:
    case ax::mojom::Role::kDateTime:
      return ROLE_SYSTEM_DROPLIST;

    case ax::mojom::Role::kDefinition:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kDescriptionList:
      return ROLE_SYSTEM_LIST;

    case ax::mojom::Role::kDesktop:
      return ROLE_SYSTEM_PANE;

    case ax::mojom::Role::kDetails:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kDialog:
      return ROLE_SYSTEM_DIALOG;

    case ax::mojom::Role::kDisclosureTriangle:
    case ax::mojom::Role::kDisclosureTriangleGrouped:
      return ROLE_SYSTEM_PUSHBUTTON;

    case ax::mojom::Role::kDocCover:
      return ROLE_SYSTEM_GRAPHIC;

    case ax::mojom::Role::kDocBackLink:
    case ax::mojom::Role::kDocBiblioRef:
    case ax::mojom::Role::kDocGlossRef:
    case ax::mojom::Role::kDocNoteRef:
      return ROLE_SYSTEM_LINK;

    case ax::mojom::Role::kDocBiblioEntry:
    case ax::mojom::Role::kDocEndnote:
    case ax::mojom::Role::kDocFootnote:
      return ROLE_SYSTEM_LISTITEM;

    case ax::mojom::Role::kDocPageBreak:
      return ROLE_SYSTEM_SEPARATOR;

    case ax::mojom::Role::kDocAbstract:
    case ax::mojom::Role::kDocAcknowledgments:
    case ax::mojom::Role::kDocAfterword:
    case ax::mojom::Role::kDocAppendix:
    case ax::mojom::Role::kDocBibliography:
    case ax::mojom::Role::kDocChapter:
    case ax::mojom::Role::kDocColophon:
    case ax::mojom::Role::kDocConclusion:
    case ax::mojom::Role::kDocCredit:
    case ax::mojom::Role::kDocCredits:
    case ax::mojom::Role::kDocDedication:
    case ax::mojom::Role::kDocEndnotes:
    case ax::mojom::Role::kDocEpigraph:
    case ax::mojom::Role::kDocEpilogue:
    case ax::mojom::Role::kDocErrata:
    case ax::mojom::Role::kDocExample:
    case ax::mojom::Role::kDocForeword:
    case ax::mojom::Role::kDocGlossary:
    case ax::mojom::Role::kDocIndex:
    case ax::mojom::Role::kDocIntroduction:
    case ax::mojom::Role::kDocNotice:
    case ax::mojom::Role::kDocPageFooter:
    case ax::mojom::Role::kDocPageHeader:
    case ax::mojom::Role::kDocPageList:
    case ax::mojom::Role::kDocPart:
    case ax::mojom::Role::kDocPreface:
    case ax::mojom::Role::kDocPrologue:
    case ax::mojom::Role::kDocPullquote:
    case ax::mojom::Role::kDocQna:
    case ax::mojom::Role::kDocSubtitle:
    case ax::mojom::Role::kDocTip:
    case ax::mojom::Role::kDocToc:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kDocument:
    case ax::mojom::Role::kPdfRoot:
    case ax::mojom::Role::kRootWebArea:
      return ROLE_SYSTEM_DOCUMENT;

    case ax::mojom::Role::kEmbeddedObject:
      // Even though the HTML-AAM has ROLE_SYSTEM_CLIENT for <embed>, we are
      // forced to use ROLE_SYSTEM_GROUPING when the <embed> has children in the
      // accessibility tree.
      // https://www.w3.org/TR/html-aam-1.0/#html-element-role-mappings
      //
      // Screen readers Jaws and NVDA do not "see" any of the <embed>'s contents
      // if they are represented as its children in the accessibility tree. For
      // example, one of the places that would be negatively impacted is the
      // reading of PDFs.
      if (GetDelegate()->GetChildCount()) {
        return ROLE_SYSTEM_GROUPING;
      } else {
        return ROLE_SYSTEM_CLIENT;
      }

    case ax::mojom::Role::kFigcaption:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kFigure:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kFeed:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kForm:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kGenericContainer:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kGraphicsDocument:
      return ROLE_SYSTEM_DOCUMENT;

    case ax::mojom::Role::kGraphicsObject:
      return ROLE_SYSTEM_PANE;

    case ax::mojom::Role::kGraphicsSymbol:
      return ROLE_SYSTEM_GRAPHIC;

    case ax::mojom::Role::kGrid:
      return ROLE_SYSTEM_TABLE;

    case ax::mojom::Role::kGridCell:
      return ROLE_SYSTEM_CELL;

    case ax::mojom::Role::kGroup:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kHeading:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kIframe:
      return ROLE_SYSTEM_DOCUMENT;

    case ax::mojom::Role::kIframePresentational:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kImage:
      return ROLE_SYSTEM_GRAPHIC;

    case ax::mojom::Role::kInputTime:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kInlineTextBox:
      return ROLE_SYSTEM_STATICTEXT;

    case ax::mojom::Role::kLabelText:
    case ax::mojom::Role::kLegend:
      return ROLE_SYSTEM_TEXT;

    case ax::mojom::Role::kLayoutTable:
      return ROLE_SYSTEM_TABLE;

    case ax::mojom::Role::kLayoutTableCell:
      return ROLE_SYSTEM_CELL;

    case ax::mojom::Role::kLayoutTableRow:
      return ROLE_SYSTEM_ROW;

    case ax::mojom::Role::kLink:
      return ROLE_SYSTEM_LINK;

    case ax::mojom::Role::kList:
      return ROLE_SYSTEM_LIST;

    case ax::mojom::Role::kListBox:
      return ROLE_SYSTEM_LIST;

    case ax::mojom::Role::kListBoxOption:
      return ROLE_SYSTEM_LISTITEM;

    case ax::mojom::Role::kListGrid:
      return ROLE_SYSTEM_LIST;

    case ax::mojom::Role::kListItem:
      return ROLE_SYSTEM_LISTITEM;

    case ax::mojom::Role::kListMarker:
      // If a name is exposed, it's legacy layout, and this will be a leaf.
      // Otherwise, it's LayoutNG, and the text will be exposed in children.
      // In this case use an MSAA role of group, but IA2_ROLE_REDUNDANT_OBJECT
      // in order to avoid having the object be announced in JAWS/NVDA.
      return IsNameExposed() ? ROLE_SYSTEM_STATICTEXT : ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kLog:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kMain:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kMark:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kMarquee:
      return ROLE_SYSTEM_ANIMATION;

    case ax::mojom::Role::kMath:
    case ax::mojom::Role::kMathMLMath:
      return ROLE_SYSTEM_EQUATION;

    // TODO(http://crbug.com/1260584): Refine this if/when a MSAA API exists for
    // properly exposing MathML content.
    case ax::mojom::Role::kMathMLFraction:
    case ax::mojom::Role::kMathMLIdentifier:
    case ax::mojom::Role::kMathMLMultiscripts:
    case ax::mojom::Role::kMathMLNoneScript:
    case ax::mojom::Role::kMathMLNumber:
    case ax::mojom::Role::kMathMLOperator:
    case ax::mojom::Role::kMathMLOver:
    case ax::mojom::Role::kMathMLPrescriptDelimiter:
    case ax::mojom::Role::kMathMLRoot:
    case ax::mojom::Role::kMathMLRow:
    case ax::mojom::Role::kMathMLSquareRoot:
    case ax::mojom::Role::kMathMLStringLiteral:
    case ax::mojom::Role::kMathMLSub:
    case ax::mojom::Role::kMathMLSubSup:
    case ax::mojom::Role::kMathMLSup:
    case ax::mojom::Role::kMathMLTable:
    case ax::mojom::Role::kMathMLTableCell:
    case ax::mojom::Role::kMathMLTableRow:
    case ax::mojom::Role::kMathMLText:
    case ax::mojom::Role::kMathMLUnder:
    case ax::mojom::Role::kMathMLUnderOver:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kMenu:
      return ROLE_SYSTEM_MENUPOPUP;

    case ax::mojom::Role::kMenuBar:
      return ROLE_SYSTEM_MENUBAR;

    case ax::mojom::Role::kMenuItem:
    case ax::mojom::Role::kMenuItemCheckBox:
    case ax::mojom::Role::kMenuItemRadio:
      return ROLE_SYSTEM_MENUITEM;

    case ax::mojom::Role::kMenuListPopup:
      return ROLE_SYSTEM_LIST;

    case ax::mojom::Role::kMenuListOption:
      return ROLE_SYSTEM_LISTITEM;

    case ax::mojom::Role::kMeter:
      return ROLE_SYSTEM_PROGRESSBAR;

    case ax::mojom::Role::kNavigation:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kNote:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kParagraph:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kPdfActionableHighlight:
      return ROLE_SYSTEM_PUSHBUTTON;

    case ax::mojom::Role::kPluginObject:
      // See also case ax::mojom::Role::kEmbeddedObject.
      if (GetDelegate()->GetChildCount()) {
        return ROLE_SYSTEM_GROUPING;
      } else {
        return ROLE_SYSTEM_CLIENT;
      }

    case ax::mojom::Role::kPopUpButton:
      return ROLE_SYSTEM_BUTTONMENU;

    case ax::mojom::Role::kProgressIndicator:
      return ROLE_SYSTEM_PROGRESSBAR;

    case ax::mojom::Role::kRadioButton:
      return ROLE_SYSTEM_RADIOBUTTON;

    case ax::mojom::Role::kRadioGroup:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kRegion:
      return ROLE_SYSTEM_PANE;

    case ax::mojom::Role::kRow: {
      // Role changes depending on whether row is inside a treegrid
      // https://www.w3.org/TR/core-aam-1.1/#role-map-row
      return IsInTreeGrid() ? ROLE_SYSTEM_OUTLINEITEM : ROLE_SYSTEM_ROW;
    }

    case ax::mojom::Role::kRowGroup:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kRowHeader:
      return ROLE_SYSTEM_ROWHEADER;

    case ax::mojom::Role::kRuby:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kRubyAnnotation:
      // Generally exposed as description on <ruby> (Role::kRuby) element, not
      // as its own object in the tree.
      // However, it's possible to make a kRubyAnnotation element show up in the
      // AX tree, for example by adding tabindex="0" to the source <rp> or <rt>
      // element or making the source element the target of an aria-owns.
      // Therefore, browser side needs to gracefully handle it if it actually
      // shows up in the tree.
      return ROLE_SYSTEM_STATICTEXT;

    case ax::mojom::Role::kSection:
    case ax::mojom::Role::kSectionFooter:
    case ax::mojom::Role::kSectionHeader:
    case ax::mojom::Role::kSectionWithoutName:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kScrollBar:
      return ROLE_SYSTEM_SCROLLBAR;

    case ax::mojom::Role::kScrollView:
      return ROLE_SYSTEM_PANE;

    case ax::mojom::Role::kSearch:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kSlider:
      return ROLE_SYSTEM_SLIDER;

    case ax::mojom::Role::kSpinButton:
      return ROLE_SYSTEM_SPINBUTTON;

    case ax::mojom::Role::kSwitch:
      return ROLE_SYSTEM_CHECKBUTTON;

    case ax::mojom::Role::kStaticText:
      return ROLE_SYSTEM_STATICTEXT;

    case ax::mojom::Role::kStatus:
      return ROLE_SYSTEM_STATUSBAR;

    case ax::mojom::Role::kSplitter:
      return ROLE_SYSTEM_SEPARATOR;

    case ax::mojom::Role::kSubscript:
    case ax::mojom::Role::kSuperscript:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kSvgRoot:
      return ROLE_SYSTEM_GRAPHIC;

    case ax::mojom::Role::kTab:
      return ROLE_SYSTEM_PAGETAB;

    case ax::mojom::Role::kTable:
      return ROLE_SYSTEM_TABLE;

    case ax::mojom::Role::kTableHeaderContainer:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kTabList:
      return ROLE_SYSTEM_PAGETABLIST;

    case ax::mojom::Role::kTabPanel:
      return ROLE_SYSTEM_PROPERTYPAGE;

    case ax::mojom::Role::kTerm:
      return ROLE_SYSTEM_LISTITEM;

    case ax::mojom::Role::kTitleBar:
      return ROLE_SYSTEM_TITLEBAR;

    case ax::mojom::Role::kToggleButton:
      return ROLE_SYSTEM_PUSHBUTTON;

    case ax::mojom::Role::kTextField:
    case ax::mojom::Role::kSearchBox:
      return ROLE_SYSTEM_TEXT;

    case ax::mojom::Role::kTextFieldWithComboBox:
      return ROLE_SYSTEM_COMBOBOX;

    case ax::mojom::Role::kAbbr:
    case ax::mojom::Role::kCode:
    case ax::mojom::Role::kEmphasis:
    case ax::mojom::Role::kStrong:
      return ROLE_SYSTEM_TEXT;

    case ax::mojom::Role::kTime:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kTimer:
      return ROLE_SYSTEM_CLOCK;

    case ax::mojom::Role::kToolbar:
      return ROLE_SYSTEM_TOOLBAR;

    case ax::mojom::Role::kTooltip:
      return ROLE_SYSTEM_TOOLTIP;

    case ax::mojom::Role::kTree:
      return ROLE_SYSTEM_OUTLINE;

    case ax::mojom::Role::kTreeGrid:
      return ROLE_SYSTEM_OUTLINE;

    case ax::mojom::Role::kTreeItem:
      return ROLE_SYSTEM_OUTLINEITEM;

    case ax::mojom::Role::kLineBreak:
      return ROLE_SYSTEM_WHITESPACE;

    case ax::mojom::Role::kVideo:
      return ROLE_SYSTEM_GROUPING;

    case ax::mojom::Role::kWebView:
      return ROLE_SYSTEM_CLIENT;

    case ax::mojom::Role::kPane:
    case ax::mojom::Role::kWindow:
      // Do not return ROLE_SYSTEM_WINDOW as that is a special MSAA system
      // role used to indicate a real native window object. It is
      // automatically created by oleacc.dll as a parent of the root of our
      // hierarchy, matching the HWND.
      return ROLE_SYSTEM_PANE;

    case ax::mojom::Role::kImeCandidate:
    case ax::mojom::Role::kKeyboard:
    case ax::mojom::Role::kNone:
    case ax::mojom::Role::kUnknown:
      return ROLE_SYSTEM_PANE;
    case ax::mojom::Role::kDescriptionListTermDeprecated:
    case ax::mojom::Role::kDescriptionListDetailDeprecated:
    case ax::mojom::Role::kDirectoryDeprecated:
    case ax::mojom::Role::kPreDeprecated:
    case ax::mojom::Role::kPortalDeprecated:
      NOTREACHED();
  }
}

AXPlatformNodeWin* AXPlatformNodeWin::GetParentPlatformNodeWin() const {
  return static_cast<AXPlatformNodeWin*>(
      AXPlatformNode::FromNativeViewAccessible(GetParent()));
}

int32_t AXPlatformNodeWin::ComputeIA2State() {
  int32_t ia2_state = IA2_STATE_OPAQUE;

  if (IsPlatformCheckable())
    ia2_state |= IA2_STATE_CHECKABLE;

  if (HasIntAttribute(ax::mojom::IntAttribute::kInvalidState) &&
      GetIntAttribute(ax::mojom::IntAttribute::kInvalidState) !=
          static_cast<int32_t>(ax::mojom::InvalidState::kFalse))
    ia2_state |= IA2_STATE_INVALID_ENTRY;
  if (HasState(ax::mojom::State::kRequired))
    ia2_state |= IA2_STATE_REQUIRED;
  if (HasState(ax::mojom::State::kVertical))
    ia2_state |= IA2_STATE_VERTICAL;
  if (HasState(ax::mojom::State::kHorizontal))
    ia2_state |= IA2_STATE_HORIZONTAL;

  if (HasState(ax::mojom::State::kEditable))
    ia2_state |= IA2_STATE_EDITABLE;

  if (IsTextField()) {
    if (HasState(ax::mojom::State::kMultiline)) {
      ia2_state |= IA2_STATE_MULTI_LINE;
    } else {
      ia2_state |= IA2_STATE_SINGLE_LINE;
    }
    if (!IsInvisibleOrIgnored())
      ia2_state |= IA2_STATE_SELECTABLE_TEXT;
  }

  if (!GetStringAttribute(ax::mojom::StringAttribute::kAutoComplete).empty() ||
      HasState(ax::mojom::State::kAutofillAvailable)) {
    ia2_state |= IA2_STATE_SUPPORTS_AUTOCOMPLETION;
  }

  if (GetBoolAttribute(ax::mojom::BoolAttribute::kModal))
    ia2_state |= IA2_STATE_MODAL;

  // Clear editable state on some widgets.
  switch (GetRole()) {
    case ax::mojom::Role::kTreeItem:
    case ax::mojom::Role::kListBoxOption:
      // Clear editable state if text selection changes should not be spoken.
      // Subwidgets in a contenteditable such as tree items will clear
      // IA2_STATE_EDITABLE when used with aria-activedescendant.
      // HACK: look to remove in 2022 or later, once Google Slides no longer
      // needs this to avoid NVDA double speaking in slides thumb view.
      // Normally, NVDA will speak both a text selection and activedescendant
      // changes in an editor, but clearing IA2_STATE_EDITABLE prevents that.
      // This helps with user interfaces like Google slides that have a tree or
      // listbox widget inside an editor, which they currently do in order to
      // enable paste operations. Eventually this need should go away once IE11
      // support is no longer needed and Slides instead relies on paste events.
      if (!IsFocusable() ||
          GetBoolAttribute(ax::mojom::BoolAttribute::kNonAtomicTextFieldRoot))
        break;  // Not used with activedescendant, so preserve editable state.
      [[fallthrough]];  // Will clear editable state.
    case ax::mojom::Role::kMenuListPopup:
    case ax::mojom::Role::kMenuListOption:
      ia2_state &= ~(IA2_STATE_EDITABLE);
      break;
    default:
      break;
  }

  return ia2_state;
}

// ComputeIA2Role() only returns a role if the MSAA role doesn't suffice,
// otherwise this method returns 0. See AXPlatformNodeWin::role().
int32_t AXPlatformNodeWin::ComputeIA2Role() {
  // If this is a web area for a presentational iframe, give it a role of
  // something other than DOCUMENT so that the fact that it's a separate doc
  // is not exposed to AT.
  if (GetDelegate()->IsRootWebAreaForPresentationalIframe()) {
    return ROLE_SYSTEM_GROUPING;
  }

  int32_t ia2_role = 0;

  switch (GetRole()) {
    case ax::mojom::Role::kComment:
      return IA2_ROLE_COMMENT;
    case ax::mojom::Role::kSuggestion:
      return IA2_ROLE_SUGGESTION;
    case ax::mojom::Role::kBanner:
    case ax::mojom::Role::kHeader:
      // CORE-AAM recommends LANDMARK instead of HEADER.
      ia2_role = IA2_ROLE_LANDMARK;
      break;
    case ax::mojom::Role::kBlockquote:
      ia2_role = IA2_ROLE_BLOCK_QUOTE;
      break;
    case ax::mojom::Role::kCanvas:
      if (GetBoolAttribute(ax::mojom::BoolAttribute::kCanvasHasFallback)) {
        ia2_role = IA2_ROLE_CANVAS;
      }
      break;
    case ax::mojom::Role::kCaption:
      ia2_role = IA2_ROLE_CAPTION;
      break;
    case ax::mojom::Role::kColorWell:
      ia2_role = IA2_ROLE_COLOR_CHOOSER;
      break;
    case ax::mojom::Role::kComplementary:
      // CORE-AAM recommends LANDMARK instead of COMPLEMENTARY_CONTENT.
      ia2_role = IA2_ROLE_LANDMARK;
      break;
    case ax::mojom::Role::kContentDeletion:
      ia2_role = IA2_ROLE_CONTENT_DELETION;
      break;
    case ax::mojom::Role::kContentInsertion:
      ia2_role = IA2_ROLE_CONTENT_INSERTION;
      break;
    case ax::mojom::Role::kContentInfo:
    case ax::mojom::Role::kFooter:
      // CORE-AAM recommends LANDMARK instead of FOOTER.
      ia2_role = IA2_ROLE_LANDMARK;
      break;
    case ax::mojom::Role::kDate:
    case ax::mojom::Role::kDateTime:
      ia2_role = IA2_ROLE_DATE_EDITOR;
      break;
    case ax::mojom::Role::kDocPageFooter:
      ia2_role = IA2_ROLE_FOOTER;
      break;
    case ax::mojom::Role::kDocPageHeader:
      ia2_role = IA2_ROLE_HEADER;
      break;
    case ax::mojom::Role::kDocAcknowledgments:
    case ax::mojom::Role::kDocAfterword:
    case ax::mojom::Role::kDocAppendix:
    case ax::mojom::Role::kDocBibliography:
    case ax::mojom::Role::kDocChapter:
    case ax::mojom::Role::kDocConclusion:
    case ax::mojom::Role::kDocCredits:
    case ax::mojom::Role::kDocEndnotes:
    case ax::mojom::Role::kDocEpilogue:
    case ax::mojom::Role::kDocErrata:
    case ax::mojom::Role::kDocForeword:
    case ax::mojom::Role::kDocGlossary:
    case ax::mojom::Role::kDocIndex:
    case ax::mojom::Role::kDocIntroduction:
    case ax::mojom::Role::kDocPageList:
    case ax::mojom::Role::kDocPart:
    case ax::mojom::Role::kDocPreface:
    case ax::mojom::Role::kDocPrologue:
    case ax::mojom::Role::kDocToc:
      ia2_role = IA2_ROLE_LANDMARK;
      break;
    case ax::mojom::Role::kDocAbstract:
    case ax::mojom::Role::kDocColophon:
    case ax::mojom::Role::kDocCredit:
    case ax::mojom::Role::kDocDedication:
    case ax::mojom::Role::kDocEpigraph:
    case ax::mojom::Role::kDocExample:
    case ax::mojom::Role::kDocPullquote:
    case ax::mojom::Role::kDocQna:
      ia2_role = IA2_ROLE_SECTION;
      break;
    case ax::mojom::Role::kDocSubtitle:
      ia2_role = IA2_ROLE_HEADING;
      break;
    case ax::mojom::Role::kDocTip:
    case ax::mojom::Role::kDocNotice:
      ia2_role = IA2_ROLE_NOTE;
      break;
    case ax::mojom::Role::kDocFootnote:
      ia2_role = IA2_ROLE_FOOTNOTE;
      break;
    case ax::mojom::Role::kEmbeddedObject:
      // Even though the HTML-AAM has IA2_ROLE_EMBEDDED_OBJECT for <embed>, we
      // are forced to use IA2_ROLE_SECTION when the <embed> has children in the
      // accessibility tree.
      // https://www.w3.org/TR/html-aam-1.0/#html-element-role-mappings
      //
      // Screen readers Jaws and NVDA do not "see" any of the <embed>'s contents
      // if they are represented as its children in the accessibility tree. For
      // example, one of the places that would be negatively impacted is the
      // reading of PDFs.
      if (GetDelegate()->GetChildCount()) {
        ia2_role = IA2_ROLE_SECTION;
      } else {
        ia2_role = IA2_ROLE_EMBEDDED_OBJECT;
      }
      break;
    case ax::mojom::Role::kFigcaption:
      ia2_role = IA2_ROLE_CAPTION;
      break;
    case ax::mojom::Role::kForm:
      ia2_role = IA2_ROLE_FORM;
      break;
    case ax::mojom::Role::kGenericContainer:
      ia2_role = IA2_ROLE_SECTION;
      break;
    case ax::mojom::Role::kHeading:
      ia2_role = IA2_ROLE_HEADING;
      break;
    case ax::mojom::Role::kIframe:
      ia2_role = IA2_ROLE_INTERNAL_FRAME;
      break;
    case ax::mojom::Role::kImage:
      if (IsImageWithMap())
        ia2_role = IA2_ROLE_IMAGE_MAP;
      break;
    case ax::mojom::Role::kLabelText:
    case ax::mojom::Role::kLegend:
      ia2_role = IA2_ROLE_LABEL;
      break;
    case ax::mojom::Role::kListMarker:
      if (!IsNameExposed()) {
        // This role causes JAWS and NVDA to ignore the object.
        // Otherwise, they speak "group" before each bullet or item number.
        ia2_role = IA2_ROLE_REDUNDANT_OBJECT;
      }
      break;
    case ax::mojom::Role::kMain:
      ia2_role = IA2_ROLE_LANDMARK;
      break;
    case ax::mojom::Role::kMark:
      ia2_role = IA2_ROLE_MARK;
      break;
    case ax::mojom::Role::kMenuItemCheckBox:
      ia2_role = IA2_ROLE_CHECK_MENU_ITEM;
      break;
    case ax::mojom::Role::kMenuItemRadio:
      ia2_role = IA2_ROLE_RADIO_MENU_ITEM;
      break;
    case ax::mojom::Role::kMeter:
      // TODO(accessibiity) Uncomment IA2_ROLE_LEVEL_BAR once screen readers
      // adopt it. Currently, a <meter> ends up being spoken as a progress
      // bar, which is confusing. IA2_ROLE_LEVEL_BAR is the correct mapping
      // according to CORE-AAM. ia2_role = IA2_ROLE_LEVEL_BAR;
      break;
    case ax::mojom::Role::kNavigation:
      ia2_role = IA2_ROLE_LANDMARK;
      break;
    case ax::mojom::Role::kNote:
      ia2_role = IA2_ROLE_NOTE;
      break;
    case ax::mojom::Role::kParagraph:
      ia2_role = IA2_ROLE_PARAGRAPH;
      break;
    case ax::mojom::Role::kPluginObject:
      // See also case ax::mojom::Role::kEmbeddedObject.
      if (GetDelegate()->GetChildCount()) {
        ia2_role = IA2_ROLE_SECTION;
      } else {
        ia2_role = IA2_ROLE_EMBEDDED_OBJECT;
      }
      break;
    case ax::mojom::Role::kRegion:
      ia2_role = IA2_ROLE_LANDMARK;
      break;
    case ax::mojom::Role::kRuby:
      ia2_role = IA2_ROLE_SECTION;
      break;
    case ax::mojom::Role::kSearch:
      ia2_role = IA2_ROLE_LANDMARK;
      break;
    case ax::mojom::Role::kSection:
    case ax::mojom::Role::kSectionWithoutName:
      ia2_role = IA2_ROLE_SECTION;
      break;
    case ax::mojom::Role::kSwitch:
      ia2_role = IA2_ROLE_TOGGLE_BUTTON;
      break;
    case ax::mojom::Role::kTableHeaderContainer:
      ia2_role = IA2_ROLE_SECTION;
      break;
    case ax::mojom::Role::kToggleButton:
      ia2_role = IA2_ROLE_TOGGLE_BUTTON;
      break;
    case ax::mojom::Role::kAbbr:
    case ax::mojom::Role::kCode:
    case ax::mojom::Role::kEmphasis:
    case ax::mojom::Role::kStrong:
    case ax::mojom::Role::kSubscript:
    case ax::mojom::Role::kSuperscript:
    case ax::mojom::Role::kTerm:
    case ax::mojom::Role::kTime:
      ia2_role = IA2_ROLE_TEXT_FRAME;
      break;
    case ax::mojom::Role::kDescriptionListDetailDeprecated:
    case ax::mojom::Role::kPreDeprecated:
      NOTREACHED();
    default:
      break;
  }
  return ia2_role;
}

std::vector<std::wstring> AXPlatformNodeWin::ComputeIA2Attributes() {
  std::vector<std::wstring> attribute_list;
  ComputeAttributes(&attribute_list);
  return attribute_list;
}

std::wstring AXPlatformNodeWin::ComputeUIAProperties() {
  std::vector<std::wstring> properties;
  BoolAttributeToUIAAriaProperty(
      properties, ax::mojom::BoolAttribute::kLiveAtomic, "atomic");
  BoolAttributeToUIAAriaProperty(properties, ax::mojom::BoolAttribute::kBusy,
                                 "busy");

  switch (GetData().GetCheckedState()) {
    case ax::mojom::CheckedState::kNone:
      break;
    case ax::mojom::CheckedState::kFalse:
      if (GetRole() == ax::mojom::Role::kToggleButton) {
        properties.emplace_back(L"pressed=false");
      } else if (GetRole() == ax::mojom::Role::kSwitch) {
        // ARIA switches are exposed to Windows accessibility as toggle
        // buttons. For maximum compatibility with ATs, we expose both the
        // pressed and checked states.
        properties.emplace_back(L"pressed=false");
        properties.emplace_back(L"checked=false");
      } else {
        properties.emplace_back(L"checked=false");
      }
      break;
    case ax::mojom::CheckedState::kTrue:
      if (GetRole() == ax::mojom::Role::kToggleButton) {
        properties.emplace_back(L"pressed=true");
      } else if (GetRole() == ax::mojom::Role::kSwitch) {
        // ARIA switches are exposed to Windows accessibility as toggle
        // buttons. For maximum compatibility with ATs, we expose both the
        // pressed and checked states.
        properties.emplace_back(L"pressed=true");
        properties.emplace_back(L"checked=true");
      } else {
        properties.emplace_back(L"checked=true");
      }
      break;
    case ax::mojom::CheckedState::kMixed:
      if (GetRole() == ax::mojom::Role::kToggleButton) {
        properties.emplace_back(L"pressed=mixed");
      } else if (GetRole() == ax::mojom::Role::kSwitch) {
        // This is disallowed both by the ARIA standard and by Blink.
        NOTREACHED_IN_MIGRATION();
      } else {
        properties.emplace_back(L"checked=mixed");
      }
      break;
  }

  const auto restriction = static_cast<ax::mojom::Restriction>(
      GetIntAttribute(ax::mojom::IntAttribute::kRestriction));
  if (restriction == ax::mojom::Restriction::kDisabled) {
    properties.push_back(L"disabled=true");
  } else {
    // The readonly property is complex on Windows. We set "readonly=true"
    // on *some* document structure roles such as paragraph, heading or list
    // even if the node data isn't marked as read only, as long as the
    // node is not editable.
    if (GetDelegate()->IsReadOnlyOrDisabled())
      properties.push_back(L"readonly=true");
  }

  // aria-dropeffect is deprecated in WAI-ARIA 1.1.
  if (HasIntAttribute(ax::mojom::IntAttribute::kDropeffectDeprecated)) {
    NOTREACHED_IN_MIGRATION();
  }
  StateToUIAAriaProperty(properties, ax::mojom::State::kExpanded, "expanded");

  switch (static_cast<ax::mojom::HasPopup>(
      GetIntAttribute(ax::mojom::IntAttribute::kHasPopup))) {
    case ax::mojom::HasPopup::kFalse:
      break;
    case ax::mojom::HasPopup::kTrue:
      properties.push_back(L"haspopup=true");
      break;
    case ax::mojom::HasPopup::kMenu:
      properties.push_back(L"haspopup=menu");
      break;
    case ax::mojom::HasPopup::kListbox:
      properties.push_back(L"haspopup=listbox");
      break;
    case ax::mojom::HasPopup::kTree:
      properties.push_back(L"haspopup=tree");
      break;
    case ax::mojom::HasPopup::kGrid:
      properties.push_back(L"haspopup=grid");
      break;
    case ax::mojom::HasPopup::kDialog:
      properties.push_back(L"haspopup=dialog");
      break;
  }

  if (IsInvisibleOrIgnored())
    properties.push_back(L"hidden=true");

  if (HasIntAttribute(ax::mojom::IntAttribute::kInvalidState) &&
      GetIntAttribute(ax::mojom::IntAttribute::kInvalidState) !=
          static_cast<int32_t>(ax::mojom::InvalidState::kFalse)) {
    properties.push_back(L"invalid=true");
  }

  IntAttributeToUIAAriaProperty(
      properties, ax::mojom::IntAttribute::kHierarchicalLevel, "level");
  StringAttributeToUIAAriaProperty(
      properties, ax::mojom::StringAttribute::kLiveStatus, "live");
  StateToUIAAriaProperty(properties, ax::mojom::State::kMultiline, "multiline");
  StateToUIAAriaProperty(properties, ax::mojom::State::kMultiselectable,
                         "multiselectable");
  IntAttributeToUIAAriaProperty(properties, ax::mojom::IntAttribute::kPosInSet,
                                "posinset");
  StringAttributeToUIAAriaProperty(
      properties, ax::mojom::StringAttribute::kLiveRelevant, "relevant");
  StateToUIAAriaProperty(properties, ax::mojom::State::kRequired, "required");
  BoolAttributeToUIAAriaProperty(
      properties, ax::mojom::BoolAttribute::kSelected, "selected");
  IntAttributeToUIAAriaProperty(properties, ax::mojom::IntAttribute::kSetSize,
                                "setsize");

  int32_t sort_direction;
  if (IsTableHeader(GetRole()) &&
      GetIntAttribute(ax::mojom::IntAttribute::kSortDirection,
                      &sort_direction)) {
    switch (static_cast<ax::mojom::SortDirection>(sort_direction)) {
      case ax::mojom::SortDirection::kNone:
        break;
      case ax::mojom::SortDirection::kUnsorted:
        properties.push_back(L"sort=none");
        break;
      case ax::mojom::SortDirection::kAscending:
        properties.push_back(L"sort=ascending");
        break;
      case ax::mojom::SortDirection::kDescending:
        properties.push_back(L"sort=descending");
        break;
      case ax::mojom::SortDirection::kOther:
        properties.push_back(L"sort=other");
        break;
    }
  }

  if (GetData().IsRangeValueSupported()) {
    FloatAttributeToUIAAriaProperty(
        properties, ax::mojom::FloatAttribute::kMaxValueForRange, "valuemax");
    FloatAttributeToUIAAriaProperty(
        properties, ax::mojom::FloatAttribute::kMinValueForRange, "valuemin");
    StringAttributeToUIAAriaProperty(
        properties, ax::mojom::StringAttribute::kValue, "valuetext");

    std::wstring value_now = base::UTF16ToWide(GetValueForControl());
    SanitizeStringAttributeForUIAAriaProperty(value_now, &value_now);
    if (!value_now.empty())
      properties.push_back(L"valuenow=" + value_now);
  }

  // Expose the aria-current attribute as 'current=<value>' if <value> is not
  // 'none'.
  int32_t aria_current_attribute;
  if (GetIntAttribute(ax::mojom::IntAttribute::kAriaCurrentState,
                      &aria_current_attribute)) {
    ax::mojom::AriaCurrentState aria_current_state =
        static_cast<ax::mojom::AriaCurrentState>(aria_current_attribute);
    if (aria_current_state != ax::mojom::AriaCurrentState::kNone &&
        aria_current_state != ax::mojom::AriaCurrentState::kFalse) {
      std::string value = ui::ToString(aria_current_state);
      std::wstring wide_value = base::UTF8ToWide(value);
      SanitizeStringAttributeForUIAAriaProperty(wide_value, &wide_value);
      properties.push_back(L"current=" + wide_value);
    }
  }

  std::wstring result = base::JoinString(properties, L";");
  return result;
}

AXPlatformNodeWin* AXPlatformNodeWin::ComputeUIALabeledBy() {
  // Not all control types expect a value for this property.
  if (!CanHaveUIALabeledBy())
    return nullptr;

  // This property only accepts static text elements to be returned. Find the
  // first static text used to label this node.
  for (int32_t id :
       GetIntListAttribute(ax::mojom::IntListAttribute::kLabelledbyIds)) {
    auto* node_win =
        static_cast<AXPlatformNodeWin*>(GetDelegate()->GetFromNodeID(id));
    if (!node_win)
      continue;

    // If this node is a static text, then simply return the node itself.
    if (IsValidUiaRelationTarget(node_win) &&
        node_win->GetRole() == ax::mojom::Role::kStaticText) {
      return node_win;
    }

    // Otherwise, find the first static text node in its descendants.
    for (auto iter = node_win->GetDelegate()->ChildrenBegin();
         *iter != *node_win->GetDelegate()->ChildrenEnd(); ++(*iter)) {
      AXPlatformNodeWin* child = static_cast<AXPlatformNodeWin*>(
          AXPlatformNode::FromNativeViewAccessible(
              iter->GetNativeViewAccessible()));
      if (IsValidUiaRelationTarget(child) &&
          child->GetRole() == ax::mojom::Role::kStaticText) {
        return child;
      }
    }
  }

  return nullptr;
}

bool AXPlatformNodeWin::CanHaveUIALabeledBy() {
  // Not all control types expect a value for this property. See
  // https://docs.microsoft.com/en-us/windows/win32/winauto/uiauto-supportinguiautocontroltypes
  // for a complete list of control types. Each one of them has specific
  // expectations regarding the UIA_LabeledByPropertyId.
  switch (GetUIARoleProperties().control_type) {
    case UIA_ButtonControlTypeId:
    case UIA_CheckBoxControlTypeId:
    case UIA_DataItemControlTypeId:
    case UIA_MenuControlTypeId:
    case UIA_MenuBarControlTypeId:
    case UIA_RadioButtonControlTypeId:
    case UIA_ScrollBarControlTypeId:
    case UIA_SeparatorControlTypeId:
    case UIA_StatusBarControlTypeId:
    case UIA_TabItemControlTypeId:
    case UIA_TextControlTypeId:
    case UIA_ToolBarControlTypeId:
    case UIA_ToolTipControlTypeId:
    case UIA_TreeItemControlTypeId:
      return false;
    default:
      return true;
  }
}

bool AXPlatformNodeWin::IsNameExposed() const {
  switch (GetRole()) {
    case ax::mojom::Role::kListMarker:
      return !GetDelegate()->GetChildCount();
    default:
      return true;
  }
}

bool AXPlatformNodeWin::IsUIAControl() const {
  // UIA provides multiple "views": raw, content and control. We only want to
  // populate the content and control views with items that make sense to
  // traverse over.

  if (GetDelegate()->IsWebContent()) {
    // Invisible or ignored elements should not show up in control view at all.
    if (IsInvisibleOrIgnored())
      return false;

    if (IsText()) {
      // A text leaf can be a UIAControl, but text inside of a heading, link,
      // button, etc. where the role allows the name to be generated from the
      // content is not. We want to avoid reading out a button, moving to the
      // next item, and then reading out the button's text child, causing the
      // text to be effectively repeated.
      auto* ancestor = FromNativeViewAccessible(GetDelegate()->GetParent());
      while (ancestor) {
        if (IsUIACellOrTableHeader(ancestor->GetRole()))
          return false;
        switch (ancestor->GetRole()) {
          // There are elements inside the `kColorWell` element that we want
          // exposed as UIA Control even if they are inside other elements that
          // are not exposed as UIA Controls. Like for example the text live
          // regions of the RGB channels inside the `kColorWell`. Without this
          // case, if we have a `kColorWell` inside a table cell, the RGB
          // channels text does not get announced by Narrator since we would
          // break and return false on the condition above this one when going
          // up the ancestor nodes.
          // TODO(accessibility): This is a special case mitigation for
          // `kColorWell`, there is a broader bug https://crbug.com/1414227 with
          // live region elements inside these elements that are not exposed as
          // UIA Controls that will require more work and investigation.
          case ax::mojom::Role::kColorWell:
            return true;
          case ax::mojom::Role::kListItem:
            // We only want to hide in the case that the list item is able
            // to have its name generated from its children.
            // See |ComputeListItemNameAsBstr|. This is only possible when the
            // element is a direct child of the list item, otherwise the child
            // should be exposed as a UIA Control.
            return ancestor !=
                   FromNativeViewAccessible(GetDelegate()->GetParent());
          case ax::mojom::Role::kButton:
          case ax::mojom::Role::kCheckBox:
          case ax::mojom::Role::kHeading:
          case ax::mojom::Role::kLineBreak:
          case ax::mojom::Role::kLink:
          case ax::mojom::Role::kListBoxOption:
          case ax::mojom::Role::kMenuItem:
          case ax::mojom::Role::kMenuItemCheckBox:
          case ax::mojom::Role::kMenuItemRadio:
          case ax::mojom::Role::kMenuListOption:
          case ax::mojom::Role::kPdfActionableHighlight:
          case ax::mojom::Role::kRadioButton:
          case ax::mojom::Role::kRow:
          case ax::mojom::Role::kRowGroup:
          case ax::mojom::Role::kStaticText:
          case ax::mojom::Role::kSwitch:
          case ax::mojom::Role::kTab:
          case ax::mojom::Role::kTooltip:
          case ax::mojom::Role::kTreeItem:
            return false;
          default:
            break;
        }
        ancestor = FromNativeViewAccessible(ancestor->GetParent());
      }
    }  // end of text only case.

    // https://docs.microsoft.com/en-us/windows/win32/winauto/uiauto-treeoverview#control-view
    // The control view also includes noninteractive UI items that contribute
    // to the logical structure of the UI.
    if (IsControl(GetRole()) || ComputeUIALandmarkType() ||
        IsUIATableLike(GetRole()) || IsList(GetRole())) {
      return true;
    }
    if (IsImage(GetRole())) {
      // If the author provides an explicitly empty alt text attribute then
      // the image is decorational and should not be considered as a control.
      if (GetRole() == ax::mojom::Role::kImage &&
          GetNameFrom() == ax::mojom::NameFrom::kAttributeExplicitlyEmpty) {
        return false;
      }
      return true;
    }
    switch (GetRole()) {
      case ax::mojom::Role::kArticle:
      case ax::mojom::Role::kBlockquote:
      case ax::mojom::Role::kCell:
      case ax::mojom::Role::kColumn:
      case ax::mojom::Role::kDetails:
      case ax::mojom::Role::kFigure:
      case ax::mojom::Role::kFooter:
      case ax::mojom::Role::kGridCell:
      case ax::mojom::Role::kHeader:
      case ax::mojom::Role::kListBoxOption:
      case ax::mojom::Role::kListItem:
      // Treat the root of a MathML tree as content/control so that it is seen
      // by UIA clients. The remainder of the tree remains as text for now until
      // UIA mappings for MathML are defined (https://crbug.com/1260585).
      case ax::mojom::Role::kMathMLMath:
      case ax::mojom::Role::kMeter:
      case ax::mojom::Role::kProgressIndicator:
      case ax::mojom::Role::kRow:
      case ax::mojom::Role::kSection:
      case ax::mojom::Role::kSectionFooter:
      case ax::mojom::Role::kSectionHeader:
      case ax::mojom::Role::kSplitter:
      case ax::mojom::Role::kStatus:
      case ax::mojom::Role::kTime:
        return true;
      default:
        break;
    }
    // Classify generic containers that are not clickable or focusable and have
    // no name, description, landmark type, and is not the root of editable
    // content as not controls.
    // Doing so helps Narrator find all the content of live regions.
    if (!GetBoolAttribute(ax::mojom::BoolAttribute::kHasAriaAttribute) &&
        !GetBoolAttribute(ax::mojom::BoolAttribute::kNonAtomicTextFieldRoot) &&
        GetName().empty() &&
        GetStringAttribute(ax::mojom::StringAttribute::kDescription).empty() &&
        !HasState(ax::mojom::State::kFocusable) && !GetData().IsClickable()) {
      return false;
    }

    return true;
  }  // end of web-content only case.

  // TODO(accessibility): This condition is very wide - it returns true for most
  // elements, except the ones that are explicitly invisible/ignored and not
  // focusable, and the ones that are expected to fire live region events. We
  // might want to revisit this implementation to match the specs:
  // https://learn.microsoft.com/en-us/windows/win32/winauto/uiauto-treeoverview#control-view.
  //
  // Also, should we really have a different implementation for Views than for
  // web content?
  return !(IsInvisibleOrIgnored() && !IsFocusable()) ||
         GetRole() == ax::mojom::Role::kStatus;
}

std::optional<LONG> AXPlatformNodeWin::ComputeUIALandmarkType() const {
  switch (GetRole()) {
    case ax::mojom::Role::kBanner:
    case ax::mojom::Role::kComplementary:
    case ax::mojom::Role::kContentInfo:
    case ax::mojom::Role::kFooter:
    case ax::mojom::Role::kHeader:
      return UIA_CustomLandmarkTypeId;

    case ax::mojom::Role::kForm:
      // https://www.w3.org/TR/html-aam-1.0/#html-element-role-mappings
      // https://w3c.github.io/core-aam/#mapping_role_table
      // While the HTML-AAM spec states that <form> without an accessible name
      // should have no corresponding role, removing the role breaks both
      // aria-setsize and aria-posinset.
      // The only other difference for UIA is that it should not be a landmark.
      // If the author provided an accessible name, or the role was explicit,
      // then allow the form landmark.
      if (HasStringAttribute(ax::mojom::StringAttribute::kName) ||
          HasStringAttribute(ax::mojom::StringAttribute::kRole)) {
        return UIA_FormLandmarkTypeId;
      }
      return {};

    case ax::mojom::Role::kMain:
      return UIA_MainLandmarkTypeId;

    case ax::mojom::Role::kNavigation:
      return UIA_NavigationLandmarkTypeId;

    case ax::mojom::Role::kSearch:
      return UIA_SearchLandmarkTypeId;

    case ax::mojom::Role::kRegion:
      return UIA_CustomLandmarkTypeId;

    default:
      return {};
  }
}

bool AXPlatformNodeWin::IsInaccessibleForUIA() const {
  if (IsNodeInaccessibleForUIA())
    return true;

  AXPlatformNodeWin* parent = GetParentPlatformNodeWin();
  while (parent) {
    if (parent->IsNodeInaccessibleForUIA() ||
        parent->ShouldHideChildrenForUIA()) {
      return true;
    }
    parent = parent->GetParentPlatformNodeWin();
  }
  return false;
}

bool AXPlatformNodeWin::ShouldHideChildrenForUIA() const {
  if (IsAtomicTextField())
    return true;

  auto role = GetRole();
  switch (role) {
    // Even though a node with  role kButton has presentational children, it
    // should only hide its children from UIA when it has a single text node
    // (to avoid having its name announced twice). This is because buttons can
    // have complex structures and they shouldn't hide their subtree.
    case ax::mojom::Role::kButton:
    // TODO(bebeaudr): We might be able to remove ax::mojom::Role::kLink once
    // http://crbug.com/1054514 is fixed. Links should not have to hide their
    // children.
    case ax::mojom::Role::kLink:
      // Links with a single text-only child should hide their subtree.
      if (GetChildCount() == 1) {
        AXPlatformNodeBase* only_child = GetFirstChild();
        return only_child && only_child->IsText();
      }
      return false;
    // TODO(virens): |kPdfActionableHighlight| needs to follow a fix similar to
    // links. At present Pdf highlights have text nodes as children. But, we may
    // enable pdf highlights to have complex children like links based on user
    // feedback.
    case ax::mojom::Role::kPdfActionableHighlight:
      return true;
    default:
      // UIA expects nodes that have "Children Presentational: True" to hide
      // their children.
      return HasPresentationalChildren(role);
  }
}

ULONG AXPlatformNodeWin::InternalAddRef() {
  // Instances of AXPlatformNodeWin hold a reference to themselves (acquired in
  // `Create`; released in `Dispose`). When the refcount rises from 1 to 2
  // before the node has been disposed, infer that the instance is being used
  // for some COM-ish purpose; for example, being handed to an accessibility
  // tool via a WM_GETOBJECT message handler.
  const auto ref_count = SequenceAffineComObjectRoot::InternalAddRef();
  if (delegate_ && ref_count == 2) {
    OnReferenced();
  }
  return ref_count;
}

ULONG AXPlatformNodeWin::InternalRelease() {
  // As above, infer that the instance is no longer being used for some COM-ish
  // purpose when the refcount drops back down to 1 (if it has yet to be
  // disposed) or 0 (if it has been).
  const auto ref_count = SequenceAffineComObjectRoot::InternalRelease();
  if (ref_count == (delegate_ ? 1 : 0)) {
    OnDereferenced();
  }
  return ref_count;
}

void AXPlatformNodeWin::OnReferenced() {
  TRACE_EVENT("accessibility", "OnReferenced",
              perfetto::Flow::FromPointer(this), "UniqueId",
              base::NumberToString(GetUniqueId()));
}

void AXPlatformNodeWin::OnDereferenced() {
  TRACE_EVENT("accessibility", "OnDereferenced",
              perfetto::TerminatingFlow::FromPointer(this));
}

bool AXPlatformNodeWin::IsPlatformCheckable() const {
  if (GetRole() == ax::mojom::Role::kToggleButton)
    return false;

  return AXPlatformNodeBase::IsPlatformCheckable();
}

int AXPlatformNodeWin::MSAAState() const {
  int msaa_state = 0;

  // Map the ax::mojom::State to MSAA state. Note that some of the states are
  // not currently handled.

  // TODO(accessibility): https://crbug.com/1292018
  // Exposing the busy state on the root web area means the NVDA user will end
  // up without a virtualBuffer until the page fully loads. So if we have
  // content, don't expose the busy state.
  if (GetBoolAttribute(ax::mojom::BoolAttribute::kBusy)) {
    if (!IsPlatformDocument() || !GetChildCount())
      msaa_state |= STATE_SYSTEM_BUSY;
  }

  if (HasState(ax::mojom::State::kCollapsed))
    msaa_state |= STATE_SYSTEM_COLLAPSED;

  if (HasState(ax::mojom::State::kDefault))
    msaa_state |= STATE_SYSTEM_DEFAULT;

  // TODO(dougt) unhandled ux::ax::mojom::State::kEditable

  if (HasState(ax::mojom::State::kExpanded))
    msaa_state |= STATE_SYSTEM_EXPANDED;

  if (IsFocusable())
    msaa_state |= STATE_SYSTEM_FOCUSABLE;

  // Built-in autofill and autocomplete wil also set has popup.
  if (HasIntAttribute(ax::mojom::IntAttribute::kHasPopup))
    msaa_state |= STATE_SYSTEM_HASPOPUP;

  // TODO(dougt) unhandled ux::ax::mojom::State::kHorizontal

  if (HasState(ax::mojom::State::kHovered)) {
    // Expose whether or not the mouse is over an element, but suppress
    // this for tests because it can make the test results flaky depending
    // on the position of the mouse.
    if (GetDelegate()->ShouldIgnoreHoveredStateForTesting())
      msaa_state |= STATE_SYSTEM_HOTTRACKED;
  }

  // If the node is ignored, we want these elements to be invisible so that
  // they are hidden from the screen reader.
  if (IsInvisibleOrIgnored())
    msaa_state |= STATE_SYSTEM_INVISIBLE;

  if (HasState(ax::mojom::State::kLinked))
    msaa_state |= STATE_SYSTEM_LINKED;

  // TODO(dougt) unhandled ux::ax::mojom::State::kMultiline

  if (HasState(ax::mojom::State::kMultiselectable)) {
    msaa_state |= STATE_SYSTEM_EXTSELECTABLE;
    msaa_state |= STATE_SYSTEM_MULTISELECTABLE;
  }

  if (GetDelegate()->IsOffscreen())
    msaa_state |= STATE_SYSTEM_OFFSCREEN;

  if (HasState(ax::mojom::State::kProtected))
    msaa_state |= STATE_SYSTEM_PROTECTED;

  // TODO(dougt) unhandled ux::ax::mojom::State::kRequired
  // TODO(dougt) unhandled ux::ax::mojom::State::kRichlyEditable

  if (GetData().IsSelectable())
    msaa_state |= STATE_SYSTEM_SELECTABLE;

  if (GetBoolAttribute(ax::mojom::BoolAttribute::kSelected))
    msaa_state |= STATE_SYSTEM_SELECTED;

  // TODO(dougt) unhandled VERTICAL

  if (HasState(ax::mojom::State::kVisited))
    msaa_state |= STATE_SYSTEM_TRAVERSED;

  //
  // Checked state
  //

  switch (GetData().GetCheckedState()) {
    case ax::mojom::CheckedState::kNone:
    case ax::mojom::CheckedState::kFalse:
      break;
    case ax::mojom::CheckedState::kTrue:
      if (GetRole() == ax::mojom::Role::kToggleButton) {
        msaa_state |= STATE_SYSTEM_PRESSED;
      } else if (GetRole() == ax::mojom::Role::kSwitch) {
        // ARIA switches are exposed to Windows accessibility as toggle
        // buttons. For maximum compatibility with ATs, we expose both the
        // pressed and checked states.
        msaa_state |= STATE_SYSTEM_PRESSED | STATE_SYSTEM_CHECKED;
      } else {
        msaa_state |= STATE_SYSTEM_CHECKED;
      }
      break;
    case ax::mojom::CheckedState::kMixed:
      msaa_state |= STATE_SYSTEM_MIXED;
      break;
  }

  const auto restriction = static_cast<ax::mojom::Restriction>(
      GetIntAttribute(ax::mojom::IntAttribute::kRestriction));
  switch (restriction) {
    case ax::mojom::Restriction::kDisabled:
      msaa_state |= STATE_SYSTEM_UNAVAILABLE;
      break;
    case ax::mojom::Restriction::kReadOnly:
      msaa_state |= STATE_SYSTEM_READONLY;
      break;
    default:
      // READONLY state is complex on Windows.  We set STATE_SYSTEM_READONLY
      // on *some* document structure roles such as paragraph, heading or list
      // even if the node data isn't marked as read only, as long as the
      // node is not editable.
      if (!HasState(ax::mojom::State::kRichlyEditable) &&
          ShouldHaveReadonlyStateByDefault(GetRole())) {
        msaa_state |= STATE_SYSTEM_READONLY;
      }
      break;
  }

  // Windowless plugins should have STATE_SYSTEM_UNAVAILABLE.
  //
  // (All of our plugins are windowless.)
  if (GetRole() == ax::mojom::Role::kPluginObject ||
      GetRole() == ax::mojom::Role::kEmbeddedObject) {
    msaa_state |= STATE_SYSTEM_UNAVAILABLE;
  }

  //
  // Handle STATE_SYSTEM_FOCUSED
  //
  gfx::NativeViewAccessible focus = GetDelegate()->GetFocus();
  if (focus == const_cast<AXPlatformNodeWin*>(this)->GetNativeViewAccessible())
    msaa_state |= STATE_SYSTEM_FOCUSED;

  // In focused single selection UI menus and listboxes, mirror item selection
  // to focus. This helps NVDA read the selected option as it changes.
  if ((GetRole() == ax::mojom::Role::kListBoxOption || IsMenuItem(GetRole())) &&
      GetBoolAttribute(ax::mojom::BoolAttribute::kSelected)) {
    AXPlatformNodeBase* container = FromNativeViewAccessible(GetParent());
    if (container && container->GetParent() == focus) {
      if ((container->GetRole() == ax::mojom::Role::kListBox ||
           container->GetRole() == ax::mojom::Role::kMenu) &&
          !container->HasState(ax::mojom::State::kMultiselectable)) {
        msaa_state |= STATE_SYSTEM_FOCUSED;
      }
    }
  }

  // On Windows, the "focus" bit should be set on certain containers, like
  // menu bars, when visible.
  //
  // Note: this should probably check if focus is actually inside
  // the menu bar, but we don't currently track focus inside menu pop-ups,
  // and Chrome only has one menu visible at a time so this works for now.
  if (GetRole() == ax::mojom::Role::kMenuBar && !IsInvisibleOrIgnored())
    msaa_state |= STATE_SYSTEM_FOCUSED;

  // Handle STATE_SYSTEM_LINKED
  if (GetRole() == ax::mojom::Role::kLink)
    msaa_state |= STATE_SYSTEM_LINKED;

  // Special case for indeterminate progressbar.
  if (GetRole() == ax::mojom::Role::kProgressIndicator &&
      !HasStringAttribute(ax::mojom::StringAttribute::kValue) &&
      !HasFloatAttribute(ax::mojom::FloatAttribute::kValueForRange)) {
    msaa_state |= STATE_SYSTEM_MIXED;
  }

  return msaa_state;
}

// static
std::optional<DWORD> AXPlatformNodeWin::MojoEventToMSAAEvent(
    ax::mojom::Event event) {
  switch (event) {
    case ax::mojom::Event::kAlert:
      return EVENT_SYSTEM_ALERT;
    case ax::mojom::Event::kActiveDescendantChanged:
      return IA2_EVENT_ACTIVE_DESCENDANT_CHANGED;
    case ax::mojom::Event::kCheckedStateChanged:
    case ax::mojom::Event::kExpandedChanged:
    case ax::mojom::Event::kStateChanged:
      return EVENT_OBJECT_STATECHANGE;
    case ax::mojom::Event::kFocus:
    case ax::mojom::Event::kFocusContext:
    case ax::mojom::Event::kFocusAfterMenuClose:
      return EVENT_OBJECT_FOCUS;
    case ax::mojom::Event::kLiveRegionChanged:
      return EVENT_OBJECT_LIVEREGIONCHANGED;
    case ax::mojom::Event::kMenuStart:
      return EVENT_SYSTEM_MENUSTART;
    case ax::mojom::Event::kMenuEnd:
      return EVENT_SYSTEM_MENUEND;
    case ax::mojom::Event::kMenuPopupStart:
      return EVENT_SYSTEM_MENUPOPUPSTART;
    case ax::mojom::Event::kMenuPopupEnd:
      return EVENT_SYSTEM_MENUPOPUPEND;
    case ax::mojom::Event::kSelection:
      return EVENT_OBJECT_SELECTION;
    case ax::mojom::Event::kSelectionAdd:
      return EVENT_OBJECT_SELECTIONADD;
    case ax::mojom::Event::kSelectionRemove:
      return EVENT_OBJECT_SELECTIONREMOVE;
    case ax::mojom::Event::kTextChanged:
      return EVENT_OBJECT_NAMECHANGE;
    case ax::mojom::Event::kTextSelectionChanged:
      return IA2_EVENT_TEXT_CARET_MOVED;
    case ax::mojom::Event::kTooltipClosed:
      return EVENT_OBJECT_HIDE;
    case ax::mojom::Event::kTooltipOpened:
      return EVENT_OBJECT_SHOW;
    case ax::mojom::Event::kValueChanged:
      return EVENT_OBJECT_VALUECHANGE;
    default:
      return std::nullopt;
  }
}

// static
std::optional<EVENTID> AXPlatformNodeWin::MojoEventToUIAEvent(
    ax::mojom::Event event) {
  if (!AXPlatform::GetInstance().IsUiaProviderEnabled()) {
    return std::nullopt;
  }

  switch (event) {
    case ax::mojom::Event::kAlert:
      return UIA_SystemAlertEventId;
    case ax::mojom::Event::kFocus:
    case ax::mojom::Event::kFocusContext:
    case ax::mojom::Event::kFocusAfterMenuClose:
      return UIA_AutomationFocusChangedEventId;
    case ax::mojom::Event::kLiveRegionChanged:
      return UIA_LiveRegionChangedEventId;
    case ax::mojom::Event::kSelection:
      return UIA_SelectionItem_ElementSelectedEventId;
    case ax::mojom::Event::kSelectionAdd:
      return UIA_SelectionItem_ElementAddedToSelectionEventId;
    case ax::mojom::Event::kSelectionRemove:
      return UIA_SelectionItem_ElementRemovedFromSelectionEventId;
    case ax::mojom::Event::kTextSelectionChanged:
      return UIA_Text_TextSelectionChangedEventId;
    case ax::mojom::Event::kTooltipClosed:
      return UIA_ToolTipClosedEventId;
    case ax::mojom::Event::kTooltipOpened:
      return UIA_ToolTipOpenedEventId;
    default:
      return std::nullopt;
  }
}

// static
std::optional<PROPERTYID> AXPlatformNodeWin::MojoEventToUIAProperty(
    ax::mojom::Event event) {
  if (!AXPlatform::GetInstance().IsUiaProviderEnabled()) {
    return std::nullopt;
  }

  switch (event) {
    case ax::mojom::Event::kControlsChanged:
      return UIA_ControllerForPropertyId;
    case ax::mojom::Event::kCheckedStateChanged:
      return UIA_ToggleToggleStatePropertyId;
    case ax::mojom::Event::kExpandedChanged:
    case ax::mojom::Event::kRowCollapsed:
    case ax::mojom::Event::kRowExpanded:
      return UIA_ExpandCollapseExpandCollapseStatePropertyId;
    case ax::mojom::Event::kSelection:
    case ax::mojom::Event::kSelectionAdd:
    case ax::mojom::Event::kSelectionRemove:
      return UIA_SelectionItemIsSelectedPropertyId;
    case ax::mojom::Event::kTextChanged:
      return UIA_NamePropertyId;
    default:
      return std::nullopt;
  }
}

// static
BSTR AXPlatformNodeWin::GetValueAttributeAsBstr(AXPlatformNodeWin* target) {
  if (target->IsPlatformDocument()) {
    std::wstring url =
        base::UTF8ToWide(target->GetDelegate()->GetTreeData().url);
    BSTR value = SysAllocString(url.c_str());
    DCHECK(value);
    return value;
  }

  if (IsLink(target->GetRole())) {
    std::wstring url = base::UTF8ToWide(
        target->GetStringAttribute(ax::mojom::StringAttribute::kUrl));
    BSTR value = SysAllocString(url.c_str());
    DCHECK(value);
    return value;
  }

  BSTR value = SysAllocString(base::as_wcstr(target->GetValueForControl()));
  DCHECK(value);
  return value;
}

HRESULT AXPlatformNodeWin::GetStringAttributeAsBstr(
    ax::mojom::StringAttribute attribute,
    BSTR* value_bstr) const {
  std::u16string str;

  if (!GetString16Attribute(attribute, &str))
    return S_FALSE;

  *value_bstr = SysAllocString(base::as_wcstr(str));
  DCHECK(*value_bstr);

  return S_OK;
}

HRESULT AXPlatformNodeWin::GetNameAsBstr(BSTR* value_bstr) const {
  std::wstring str = base::UTF8ToWide(GetName());

  *value_bstr = SysAllocString(str.c_str());
  DCHECK(*value_bstr);
  return S_OK;
}

HRESULT AXPlatformNodeWin::ComputeListItemNameAsBstr(BSTR* value_bstr) const {
  DCHECK_EQ(GetRole(), ax::mojom::Role::kListItem);
  DCHECK(!HasStringAttribute(ax::mojom::StringAttribute::kName));
  std::wstring str;
  // If the name is specified by the author.
  if (GetNameFrom() == ax::mojom::NameFrom::kAttribute) {
    str = base::UTF8ToWide(GetName());
  } else {
    str = GetDelegate()->ComputeListItemNameFromContent();
  }

  *value_bstr = SysAllocString(str.c_str());
  DCHECK(*value_bstr);
  return S_OK;
}

void AXPlatformNodeWin::AddAlertTarget() {
  g_alert_targets.Get().insert(this);
}

void AXPlatformNodeWin::RemoveAlertTarget() {
  if (g_alert_targets.Get().find(this) != g_alert_targets.Get().end())
    g_alert_targets.Get().erase(this);
}

void AXPlatformNodeWin::HandleSpecialTextOffset(LONG* offset) {
  if (*offset == IA2_TEXT_OFFSET_LENGTH) {
    *offset = static_cast<LONG>(GetHypertext().length());
  } else if (*offset == IA2_TEXT_OFFSET_CARET) {
    int selection_start, selection_end;
    GetSelectionOffsets(&selection_start, &selection_end);
    // TODO(nektar): Deprecate selection_start and selection_end in favor of
    // anchor_offset/focus_offset. See https://crbug.com/645596.
    if (selection_end < 0)
      *offset = 0;
    else
      *offset = static_cast<LONG>(selection_end);
  }
}

LONG AXPlatformNodeWin::FindBoundary(IA2TextBoundaryType ia2_boundary,
                                     LONG start_offset,
                                     ax::mojom::MoveDirection direction) {
  HandleSpecialTextOffset(&start_offset);

  // If the |start_offset| is equal to the location of the caret, then use the
  // focus affinity, otherwise default to downstream affinity.
  ax::mojom::TextAffinity affinity = ax::mojom::TextAffinity::kDownstream;
  int selection_start, selection_end;
  GetSelectionOffsets(&selection_start, &selection_end);
  if (selection_end >= 0 && start_offset == selection_end)
    affinity = GetDelegate()->GetTreeData().sel_focus_affinity;

  ax::mojom::TextBoundary boundary = FromIA2TextBoundary(ia2_boundary);
  return static_cast<LONG>(
      FindTextBoundary(boundary, start_offset, direction, affinity));
}

AXPlatformNodeWin* AXPlatformNodeWin::GetTargetFromChildID(
    const VARIANT& var_id) {
  if (V_VT(&var_id) != VT_I4)
    return nullptr;

  LONG child_id = V_I4(&var_id);
  if (child_id == CHILDID_SELF)
    return this;

  if (child_id >= 1 &&
      static_cast<size_t>(child_id) <= GetDelegate()->GetChildCount()) {
    // Positive child ids are a 1-based child index, used by clients
    // that want to enumerate all immediate children.
    AXPlatformNodeBase* base = FromNativeViewAccessible(
        GetDelegate()->ChildAtIndex(static_cast<size_t>(child_id - 1)));
    return static_cast<AXPlatformNodeWin*>(base);
  }

  if (child_id >= 0)
    return nullptr;

  // Negative child ids can be used to map to any descendant.
  AXPlatformNode* node = GetFromUniqueId(-child_id);
  if (!node)
    return nullptr;

  AXPlatformNodeBase* base =
      FromNativeViewAccessible(node->GetNativeViewAccessible());
  if (base && !IsDescendant(base))
    base = nullptr;

  return static_cast<AXPlatformNodeWin*>(base);
}

bool AXPlatformNodeWin::IsInTreeGrid() {
  AXPlatformNodeBase* container = FromNativeViewAccessible(GetParent());

  // If parent was a rowgroup, we need to look at the grandparent
  if (container && container->GetRole() == ax::mojom::Role::kRowGroup)
    container = FromNativeViewAccessible(container->GetParent());

  if (!container)
    return false;

  return container->GetRole() == ax::mojom::Role::kTreeGrid;
}

HRESULT AXPlatformNodeWin::AllocateComArrayFromVector(
    std::vector<LONG>& results,
    LONG max,
    LONG** selected,
    LONG* n_selected) {
  DCHECK_GT(max, 0);
  DCHECK(selected);
  DCHECK(n_selected);

  auto count = std::min((LONG)results.size(), max);
  *n_selected = count;
  *selected = static_cast<LONG*>(CoTaskMemAlloc(sizeof(LONG) * count));

  for (LONG i = 0; i < count; i++)
    (*selected)[i] = results[i];
  return S_OK;
}

bool AXPlatformNodeWin::IsHyperlink() {
  int32_t hyperlink_index = -1;
  AXPlatformNodeWin* parent = GetParentPlatformNodeWin();
  if (parent) {
    hyperlink_index = parent->GetHyperlinkIndexFromChild(this);
  }

  if (hyperlink_index >= 0)
    return true;
  return false;
}

void AXPlatformNodeWin::ResetComputedHypertext() {
  hypertext_ = AXLegacyHypertext();
}

double AXPlatformNodeWin::GetHorizontalScrollPercent() {
  if (!IsHorizontallyScrollable())
    return UIA_ScrollPatternNoScroll;

  float x_min = GetIntAttribute(ax::mojom::IntAttribute::kScrollXMin);
  float x_max = GetIntAttribute(ax::mojom::IntAttribute::kScrollXMax);
  float x = GetIntAttribute(ax::mojom::IntAttribute::kScrollX);
  return 100.0 * (x - x_min) / (x_max - x_min);
}

double AXPlatformNodeWin::GetVerticalScrollPercent() {
  if (!IsVerticallyScrollable())
    return UIA_ScrollPatternNoScroll;

  float y_min = GetIntAttribute(ax::mojom::IntAttribute::kScrollYMin);
  float y_max = GetIntAttribute(ax::mojom::IntAttribute::kScrollYMax);
  float y = GetIntAttribute(ax::mojom::IntAttribute::kScrollY);
  return 100.0 * (y - y_min) / (y_max - y_min);
}

BSTR AXPlatformNodeWin::GetFontNameAttributeAsBSTR() const {
  const std::wstring string = base::UTF8ToWide(
      GetInheritedStringAttribute(ax::mojom::StringAttribute::kFontFamily));

  return SysAllocString(string.c_str());
}

BSTR AXPlatformNodeWin::GetStyleNameAttributeAsBSTR() const {
  std::u16string style_name =
      GetDelegate()->GetStyleNameAttributeAsLocalizedString();

  return SysAllocString(base::as_wcstr(style_name));
}

TextDecorationLineStyle AXPlatformNodeWin::GetUIATextDecorationStyle(
    const ax::mojom::IntAttribute int_attribute) const {
  const ax::mojom::TextDecorationStyle text_decoration_style =
      static_cast<ax::mojom::TextDecorationStyle>(
          GetIntAttribute(int_attribute));

  switch (text_decoration_style) {
    case ax::mojom::TextDecorationStyle::kNone:
      return TextDecorationLineStyle::TextDecorationLineStyle_None;
    case ax::mojom::TextDecorationStyle::kDotted:
      return TextDecorationLineStyle::TextDecorationLineStyle_Dot;
    case ax::mojom::TextDecorationStyle::kDashed:
      return TextDecorationLineStyle::TextDecorationLineStyle_Dash;
    case ax::mojom::TextDecorationStyle::kSolid:
      return TextDecorationLineStyle::TextDecorationLineStyle_Single;
    case ax::mojom::TextDecorationStyle::kDouble:
      return TextDecorationLineStyle::TextDecorationLineStyle_Double;
    case ax::mojom::TextDecorationStyle::kWavy:
      return TextDecorationLineStyle::TextDecorationLineStyle_Wavy;
  }
}

// IRawElementProviderSimple support methods.

AXPlatformNodeWin::PatternProviderFactoryMethod
AXPlatformNodeWin::GetPatternProviderFactoryMethod(PATTERNID pattern_id) {
  switch (pattern_id) {
    case UIA_AnnotationPatternId:
      if (IsStructuredAnnotation()) {
        return &PatternProvider<IAnnotationProvider>;
      }
      break;
    case UIA_ExpandCollapsePatternId:
      if (GetData().SupportsExpandCollapse()) {
        return &PatternProvider<IExpandCollapseProvider>;
      }
      break;

    case UIA_GridPatternId:
      if (IsUIATableLike(GetRole())) {
        return &PatternProvider<IGridProvider>;
      }
      break;

    case UIA_GridItemPatternId:
      // Griditem pattern should not be exposed unless the
      // the gridpattern role is exposed on an ancestor.
      if (IsUIACellOrTableHeader(GetRole()) && GetUIATableAncestor()) {
        return &PatternProvider<IGridItemProvider>;
      }
      break;

    case UIA_InvokePatternId:
      if (IsInvokeSupported()) {
        return &PatternProvider<IInvokeProvider>;
      }
      break;

    case UIA_RangeValuePatternId:
      if (GetData().IsRangeValueSupported()) {
        return &PatternProvider<IRangeValueProvider>;
      }
      break;

    case UIA_ScrollPatternId:
      if (IsScrollable()) {
        return &PatternProvider<IScrollProvider>;
      }
      break;

    case UIA_ScrollItemPatternId:
      return &PatternProvider<IScrollItemProvider>;

    case UIA_SelectionItemPatternId:
      if (IsSelectionItemSupported()) {
        return &PatternProvider<ISelectionItemProvider>;
      }
      break;

    case UIA_SelectionPatternId:
      if (IsContainerWithSelectableChildren(GetRole())) {
        return &PatternProvider<ISelectionProvider>;
      }
      break;

    case UIA_TablePatternId:
      // Despite the following documentation (as of April 2022)
      //     https://docs.microsoft.com/en-us/windows/win32/api/uiautomationcore/nn-uiautomationcore-itableprovider
      // which mentions that ITableProvider must expose column and/or row
      // headers, we should expose the Table pattern on all table-like roles.
      // This will allow clients to detect such constructs as tables and expose
      // row/column counts and navigation along with Table semantics.
      //
      // On UIA, we don't want to expose the ITableProvider for layout tables
      // because it can cause extraneous, confusing announcements for users. We
      // initially exposed it, but decided to re-evaluate our decision after
      // hearing from users.
      if (IsUIATableLike(GetRole()))
        return &PatternProvider<ITableProvider>;
      break;

    case UIA_TableItemPatternId:
      // Despite the following documentation (as of April 2022)
      //     https://docs.microsoft.com/en-us/windows/win32/api/uiautomationcore/nn-uiautomationcore-itableprovider
      // which mentions that ITableProvider must expose column and/or row
      // headers, we should expose the Table pattern on all table-like roles.
      // This will allow clients to detect such constructs as tables and expose
      // row/column counts and navigation along with Table semantics.
      //
      // On UIA, we don't want to expose the ITableProvider for layout tables
      // because it can cause extraneous, confusing announcements for users. We
      // initially exposed it, but decided to re-evaluate our decision after
      // hearing from users.
      if (IsUIACellOrTableHeader(GetRole())) {
        return &PatternProvider<ITableProvider>;
      }
      break;

    case UIA_TextChildPatternId:
      if (AXPlatformNodeTextChildProviderWin::GetTextContainer(this)) {
        return &AXPlatformNodeTextChildProviderWin::CreateIUnknown;
      }
      break;

    case UIA_TextEditPatternId:
    case UIA_TextPatternId:
      if (IsPlatformDocument() || IsTextField() || IsText())
        return &AXPlatformNodeTextProviderWin::CreateIUnknown;
      break;

    case UIA_TogglePatternId:
      if (IsToggleSupported()) {
        return &PatternProvider<IToggleProvider>;
      }
      break;

    case UIA_ValuePatternId:
      if (IsValuePatternSupported(GetDelegate())) {
        return &PatternProvider<IValueProvider>;
      }
      break;

    case UIA_WindowPatternId:
      if (HasBoolAttribute(ax::mojom::BoolAttribute::kModal)) {
        return &PatternProvider<IWindowProvider>;
      }
      break;

    // Not currently implemented.
    case UIA_CustomNavigationPatternId:
    case UIA_DockPatternId:
    case UIA_DragPatternId:
    case UIA_DropTargetPatternId:
    case UIA_ItemContainerPatternId:
    case UIA_MultipleViewPatternId:
    case UIA_ObjectModelPatternId:
    case UIA_SpreadsheetPatternId:
    case UIA_SpreadsheetItemPatternId:
    case UIA_StylesPatternId:
    case UIA_SynchronizedInputPatternId:
    case UIA_TextPattern2Id:
    case UIA_TransformPatternId:
    case UIA_TransformPattern2Id:
    case UIA_VirtualizedItemPatternId:
      break;

    // Provided by UIA Core; we should not implement.
    case UIA_LegacyIAccessiblePatternId:
      break;
  }
  return nullptr;
}

AXPlatformNodeWin* AXPlatformNodeWin::GetLowestAccessibleElementForUIA() {
  AXPlatformNodeWin* node = this;
  while (node) {
    if (!node->IsInaccessibleForUIA())
      return node;
    node = node->GetParentPlatformNodeWin();
  }

  NOTREACHED_IN_MIGRATION();
  return nullptr;
}

AXPlatformNodeWin* AXPlatformNodeWin::GetFirstTextOnlyDescendant() {
  for (auto* child = static_cast<AXPlatformNodeWin*>(GetFirstChild()); child;
       child = static_cast<AXPlatformNodeWin*>(child->GetNextSibling())) {
    if (child->IsText())
      return child;
    if (AXPlatformNodeWin* descendant = child->GetFirstTextOnlyDescendant())
      return descendant;
  }
  return nullptr;
}

void AXPlatformNodeWin::SanitizeTextAttributeValue(const std::string& input,
                                                   std::string* output) const {
  SanitizeStringAttributeForIA2(input, output);
}

void AXPlatformNodeWin::NotifyObserverForMSAAUsage() const {
  for (WinAccessibilityAPIUsageObserver& observer :
       GetWinAccessibilityAPIUsageObserverList()) {
    observer.OnMSAAUsed();
  }
}

void AXPlatformNodeWin::NotifyAddAXModeFlagsForIA2(
    const uint32_t ax_modes) const {
  if (features::IsAccessibilityRestrictiveIA2AXModesEnabled()) {
    // Non-web content is always enabled, if a client isn't looking for web
    // content, don't enable.
    if (!GetDelegate() || !GetDelegate()->IsWebContent()) {
      return;
    }
  }

  AXPlatformNode::NotifyAddAXModeFlags(ax_modes);
}

void AXPlatformNodeWin::NotifyAPIObserverForPatternRequest(
    PATTERNID pattern_id) const {
  if (!GetDelegate() || !GetDelegate()->IsWebContent()) {
    return;
  }

  bool probable_advanced_client_detected = false;
  bool text_pattern_support_needed = false;
  switch (pattern_id) {
    case UIA_TextPatternId:
    case UIA_TextChildPatternId:
      // These properties require information gated behind the kInlineTextBoxes
      // AXMode. See kInlineTextBoxes for details.
      text_pattern_support_needed = true;
      break;
    // These properties require more advanced accessibility features to be
    // enabled See kScreenReader for details.
    case UIA_RangeValuePatternId:
    case UIA_TableItemPatternId:
      probable_advanced_client_detected = true;
      break;
  }

  for (WinAccessibilityAPIUsageObserver& observer :
       GetWinAccessibilityAPIUsageObserverList()) {
    if (probable_advanced_client_detected)
      observer.OnAdvancedUIAutomationUsed();
    if (text_pattern_support_needed)
      observer.OnTextPatternRequested();
  }
}

void AXPlatformNodeWin::NotifyAPIObserverForPropertyRequest(
    PROPERTYID property_id) const {
  // Non-web content is always enabled, if a client isn't looking for web
  // content, don't enable.
  if (!GetDelegate() || !GetDelegate()->IsWebContent())
    return;

  bool probable_advanced_client_detected = false;
  bool probable_screen_reader_detected = false;
  bool uiautomation_id_requested = false;
  switch (property_id) {
    // These properties are used by non-screenreader UIA clients. They should
    // not cause additional enablement.
    case UIA_HasKeyboardFocusPropertyId:
    case UIA_FrameworkIdPropertyId:
    case UIA_IsEnabledPropertyId:
      break;
    //  These properties are not currently implemented and should not cause
    //  enablement.
    case UIA_AnnotationTypesPropertyId:
    case UIA_CenterPointPropertyId:
    case UIA_FillColorPropertyId:
    case UIA_FillTypePropertyId:
    case UIA_HeadingLevelPropertyId:
    case UIA_ItemTypePropertyId:
    case UIA_OutlineColorPropertyId:
    case UIA_OutlineThicknessPropertyId:
    case UIA_RotationPropertyId:
    case UIA_SizePropertyId:
    case UIA_VisualEffectsPropertyId:
      break;
    // These properties are provided by UIA Core; we should not implement, and
    // they should not cause enablement.
    case UIA_BoundingRectanglePropertyId:
    case UIA_NativeWindowHandlePropertyId:
    case UIA_ProcessIdPropertyId:
    case UIA_ProviderDescriptionPropertyId:
    case UIA_RuntimeIdPropertyId:
      break;
    // These properties require the screenreader mode to get correct results
    case UIA_LabeledByPropertyId:
    case UIA_LiveSettingPropertyId:
    case UIA_LevelPropertyId:
    case UIA_DescribedByPropertyId:
    case UIA_AutomationIdPropertyId:
    // These properties are indicative of a screenreader.
    case UIA_AriaRolePropertyId:
    case UIA_ControlTypePropertyId:
    case UIA_LocalizedControlTypePropertyId:
    case UIA_NamePropertyId:
    case UIA_AcceleratorKeyPropertyId:
    case UIA_AccessKeyPropertyId:
    case UIA_IsKeyboardFocusablePropertyId:
    case UIA_ClassNamePropertyId:
    case UIA_HelpTextPropertyId:
    case UIA_ClickablePointPropertyId:
    case UIA_CulturePropertyId:
    case UIA_IsControlElementPropertyId:
    case UIA_IsContentElementPropertyId:
    case UIA_IsPasswordPropertyId:
    case UIA_IsOffscreenPropertyId:
    case UIA_OrientationPropertyId:
    case UIA_IsRequiredForFormPropertyId:
    case UIA_ItemStatusPropertyId:
    case UIA_ExpandCollapseExpandCollapseStatePropertyId:
    case UIA_SelectionItemIsSelectedPropertyId:
    case UIA_ToggleToggleStatePropertyId:
    case UIA_AriaPropertiesPropertyId:
    case UIA_IsDataValidForFormPropertyId:
    case UIA_ControllerForPropertyId:
    case UIA_FlowsToPropertyId:
    case UIA_OptimizeForVisualContentPropertyId:
    case UIA_FlowsFromPropertyId:
    case UIA_IsPeripheralPropertyId:
    case UIA_PositionInSetPropertyId:
    case UIA_SizeOfSetPropertyId:
    case UIA_AnnotationObjectsPropertyId:
    case UIA_LandmarkTypePropertyId:
    case UIA_LocalizedLandmarkTypePropertyId:
    case UIA_FullDescriptionPropertyId:
    case UIA_IsDialogPropertyId:
      uiautomation_id_requested = true;
      probable_screen_reader_detected = true;
      probable_advanced_client_detected = true;
      break;
  }

  for (WinAccessibilityAPIUsageObserver& observer :
       GetWinAccessibilityAPIUsageObserverList()) {
    if (probable_advanced_client_detected)
      observer.OnAdvancedUIAutomationUsed();
    if (probable_screen_reader_detected)
      observer.OnProbableUIAutomationScreenReaderDetected();
    if (uiautomation_id_requested)
      observer.OnUIAutomationIdRequested();
  }
}

AXPlatformNodeWin* AXPlatformNodeWin::GetUIATableAncestor() const {
  AXPlatformNodeWin* parent = const_cast<AXPlatformNodeWin*>(this);
  while (parent) {
    if (IsUIATableLike(parent->GetRole())) {
      return parent;
    }
    parent = parent->GetParentPlatformNodeWin();
  }

  return nullptr;
}

// static
void AXPlatformNodeWin::SanitizeStringAttributeForIA2(const std::string& input,
                                                      std::string* output) {
  DCHECK(output);
  // According to the IA2 Spec, these characters need to be escaped with a
  // backslash: backslash, colon, comma, equals and semicolon.
  // Note that backslash must be replaced first.
  base::ReplaceChars(input, "\\", "\\\\", output);
  base::ReplaceChars(*output, ":", "\\:", output);
  base::ReplaceChars(*output, ",", "\\,", output);
  base::ReplaceChars(*output, "=", "\\=", output);
  base::ReplaceChars(*output, ";", "\\;", output);
}

bool AXPlatformNodeWin::IsSelectionItemSupported() const {
  switch (GetRole()) {
    // An ARIA 1.1+ role of "row" inside an ARIA 1.1 role of "table", should not
    // be selectable. ARIA "table" is not interactable, ARIA "grid" is.
    case ax::mojom::Role::kColumnHeader:
    case ax::mojom::Role::kRow:
    case ax::mojom::Role::kRowHeader: {
      // An ARIA grid subwidget is only selectable if explicitly marked as
      // selected (or not) with the aria-selected property.
      if (!HasBoolAttribute(ax::mojom::BoolAttribute::kSelected)) {
        return false;
      }

      AXPlatformNodeBase* table = GetTable();
      if (!table) {
        return false;
      }

      return table->GetRole() == ax::mojom::Role::kGrid ||
             table->GetRole() == ax::mojom::Role::kListGrid ||
             table->GetRole() == ax::mojom::Role::kTreeGrid;
    }
    // https://www.w3.org/TR/core-aam-1.1/#mapping_state-property_table
    // SelectionItem.IsSelected is exposed when aria-checked is True or False,
    // for 'radio' and 'menuitemradio' roles.
    case ax::mojom::Role::kRadioButton:
    case ax::mojom::Role::kMenuItemRadio: {
      if (GetData().GetCheckedState() == ax::mojom::CheckedState::kTrue ||
          GetData().GetCheckedState() == ax::mojom::CheckedState::kFalse) {
        return true;
      }
      return false;
    }
    // https://www.w3.org/TR/wai-aria-1.1/#aria-selected
    // SelectionItem.IsSelected is exposed when aria-select is True or False.
    case ax::mojom::Role::kListBoxOption:
    case ax::mojom::Role::kListItem:
    case ax::mojom::Role::kMenuListOption:
    case ax::mojom::Role::kTreeItem:
      return HasBoolAttribute(ax::mojom::BoolAttribute::kSelected);
    case ax::mojom::Role::kGridCell:
    case ax::mojom::Role::kTab:
      // According to the UIA documentation, this role should always support the
      // SelectionItem control pattern:
      // https://learn.microsoft.com/en-us/windows/win32/winauto/uiauto-supporttabitemcontroltype#required-control-patterns.
      return true;
    default:
      return false;
  }
}

bool AXPlatformNodeWin::IsToggleSupported() const {
  ax::mojom::Role role = GetRole();

  // As per the spec [1], RadioButton control does not implement
  // IToggleProvider, as it is not capable of cycling through its valid states.
  //
  // [1]:https://docs.microsoft.com/en-us/dotnet/framework/ui-automation/implementing-the-ui-automation-toggle-control-pattern
  if (IsRadio(role)) {
    return false;
  }

  // The documentation[2] and test case[3] seem to indicate that the Toggle
  // control pattern should not be exposed when the ExpandCollapse control
  // pattern is already exposed for a button. Please note that this last
  // requirement only applies to buttons, not all elements; it's possible for an
  // other role (e.g., treeitem) to support both the Toggle and the
  // ExpandCollapse control patterns.
  //
  // [2]:https://learn.microsoft.com/en-us/windows/win32/winauto/uiauto-supportbuttoncontroltype#required-control-patterns
  // [3]:https://github.com/microsoft/axe-windows/blob/main/src/Rules/Library/ButtonInvokeAndExpandeCollapsePatterns.cs
  if (GetData().SupportsExpandCollapse() && IsButton(role)) {
    return false;
  }

  // According to the CoreAAM spec [4], TogglePattern should be exposed for
  // all aria-checkable roles.
  //
  // [4]:https://w3c.github.io/core-aam/#mapping_state-property_table
  return IsPlatformCheckable() || SupportsToggle(role);
}

bool AXPlatformNodeWin::IsInvokeSupported() const {
  ax::mojom::Role role = GetRole();

  // As per the documentation [1], tab item should never support
  // IInvokeProvider.
  //
  // [1]:https://learn.microsoft.com/en-us/windows/win32/winauto/uiauto-supporttabitemcontroltype#required-control-patterns
  if (role == ax::mojom::Role::kTab) {
    return false;
  }

  // According to this Accessibility Insights test [2], the UIA Invoke and
  // Toggle patterns should never be used together for buttons.
  //
  // Note: It's not specified which pattern should be exposed when both could
  // technically be supported. However, priority is given to Toggle Pattern
  // here, as its the more specialized one.
  //
  // [2]:https://github.com/microsoft/axe-windows/blob/main/src/Rules/Library/ButtonInvokeAndTogglePatterns.cs
  if (IsToggleSupported() && IsButton(role)) {
    return false;
  }

  // According to the Accessibility Insights rules [3] and UIA documentation
  // [4], the Invoke control pattern should not be supported on the following
  // control types because another control pattern will always be available to
  // support the same invocable behavior:
  //   - UIA_AppBarControlTypeId
  //   - UIA_TabItemControlTypeId
  //
  // [3]:https://github.com/microsoft/axe-windows/blob/main/src/Rules/Library/ControlShouldNotSupportInvokePattern.cs
  // [4]:https://learn.microsoft.com/en-us/dotnet/framework/ui-automation/implementing-the-ui-automation-invoke-control-pattern
  //
  // TODO(accessibility): Add the condition for the UIA_AppBarControlTypeId if
  // we ever start exposing this control type in Chromium.
  return GetData().IsInvocable();
}

}  // namespace ui