chromium/ui/accessibility/platform/ax_platform_node_ui_kit_element.mm

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

#import "ui/accessibility/platform/ax_platform_node_ui_kit_element.h"

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

#import "base/memory/raw_ptr.h"
#import "base/strings/sys_string_conversions.h"
#import "ui/accessibility/ax_common.h"
#import "ui/accessibility/ax_enum_util.h"
#import "ui/accessibility/ax_enums.mojom.h"
#import "ui/accessibility/ax_role_properties.h"
#import "ui/accessibility/platform/ax_platform_node_ios.h"
#import "ui/accessibility/platform/ax_platform_tree_manager_delegate.h"
#import "ui/accessibility/platform/child_iterator_base.h"

@implementation AXPlatformNodeUIKitElement {
  // The AXPlatformNode corresponding to this wrapper instance.
  raw_ptr<AXPlatformNodeIOS> _node;
  // An array of children of this object. Cached to avoid re-computing.
  NSMutableArray* _children;
  // Whether the children have changed and need to be updated.
  BOOL _needsToUpdateChildren;
  // Whether _children is currently being computed.
  BOOL _gettingChildren;
}

- (instancetype)initWithPlatformNode:(AXPlatformNodeIOS*)platformNode {
  id container = platformNode->GetParent();
  // TODO(crbug.com/336611337): Sometimes container is null for new subframes.
  // We need a way to retry after the AXTreeManager is connected to its parent.
  if (!container) {
    return nil;
  }
  if (self = [super initWithAccessibilityContainer:platformNode->GetParent()]) {
    _node = platformNode;
    _needsToUpdateChildren = YES;
    _gettingChildren = NO;
  }
  return self;
}

- (void)childrenChanged {
  if (!_node || _gettingChildren) {
    return;
  }
  _needsToUpdateChildren = YES;
  if (![self isIncludedInPlatformTree]) {
    AXPlatformNode* parentNode =
        AXPlatformNode::FromNativeViewAccessible(_node->GetParent());
    if (parentNode) {
      [parentNode->GetNativeViewAccessible() childrenChanged];
    }
  }
}

#pragma mark - AXPlatformNodeUIKit

- (void)detach {
  _node = nullptr;
}

- (AXPlatformNodeIOS*)node {
  return _node.get();
}

#pragma mark - UIAccessibilityContainer

- (NSArray*)accessibilityElements {
  if (!_node) {
    return nil;
  }
  if (_needsToUpdateChildren) {
    base::AutoReset<BOOL> set_getting_children(&_gettingChildren, YES);
    uint32_t childCount = _node->GetChildCount();
    _children = [[NSMutableArray alloc] initWithCapacity:childCount];
    for (auto it = _node->GetDelegate()->ChildrenBegin();
         *it != *_node->GetDelegate()->ChildrenEnd(); ++(*it)) {
      AXPlatformNodeUIKitElement* child = it->GetNativeViewAccessible();
      if ([child isIncludedInPlatformTree]) {
        [_children addObject:child];
      } else {
        [_children addObjectsFromArray:[child accessibilityElements]];
      }
    }

    // Also, add indirect children (if any).
    const std::vector<int32_t>& indirectChildIds = _node->GetIntListAttribute(
        ax::mojom::IntListAttribute::kIndirectChildIds);
    for (uint32_t i = 0; i < indirectChildIds.size(); ++i) {
      int32_t child_id = indirectChildIds[i];
      AXPlatformNode* child = _node->GetDelegate()->GetFromNodeID(child_id);

      if (child) {
        [_children addObject:child->GetNativeViewAccessible()];
      }
    }
    _needsToUpdateChildren = NO;
  }
  return _children;
}

#pragma mark - UIAccessibility

- (UIAccessibilityTraits)accessibilityTraits {
  // TODO(crbug.com/336611337): Choose appropriate traits based on node's role.
  return UIAccessibilityTraitLink;
}

- (CGRect)accessibilityFrame {
  if (!_node) {
    return CGRectZero;
  }

  gfx::Rect rect = _node->GetDelegate()->GetBoundsRect(
      AXCoordinateSystem::kScreenDIPs, AXClippingBehavior::kClipped);
  rect = ScaleToRoundedRect(
      rect, 1.f / _node->GetIOSDelegate()->GetDeviceScaleFactor());

  return rect.ToCGRect();
}

- (id)accessibilityFocusedUIElement {
  if (!_node) {
    return nil;
  }

  return _node->GetFocus();
}

- (BOOL)isAccessibilityElement {
  if (!_node) {
    return NO;
  }

  if (_node->GetRole() == ax::mojom::Role::kImage &&
      _node->GetNameFrom() == ax::mojom::NameFrom::kAttributeExplicitlyEmpty) {
    return NO;
  }

  // TODO(crbug.com/336611337): If a node is an accessibility element, then
  // VoiceOver will not visit its accessibilityElements (that is, its children).
  // If we ever need both a node and its descendants to be interactable using
  // VoiceOver, we will need to restructure the UIAccessibility tree by
  // inserting an additional node to act as a container.
  if ([self accessibilityElements].count) {
    return NO;
  }

  return (_node->GetRole() != ax::mojom::Role::kUnknown &&
          !_node->GetDelegate()->IsIgnored());
}

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

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

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

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

  return @"";
}

#pragma mark - UIAccessibilityAction

- (NSArray<UIAccessibilityCustomAction*>*)accessibilityCustomActions {
  // TODO(crbug.com/336611337): Wire up custom actions for each node, like
  // being able to tap or otherwise interact.
  return nil;
}

#pragma mark - Private

- (BOOL)isIncludedInPlatformTree {
  return _node && _node->GetRole() != ax::mojom::Role::kUnknown &&
         !_node->IsInvisibleOrIgnored();
}

- (BOOL)isImage {
  return IsImage(_node->GetRole()) &&
         !_node->GetBoolAttribute(
             ax::mojom::BoolAttribute::kCanvasHasFallback) &&
         !_node->GetChildCount() &&
         _node->GetNameFrom() != ax::mojom::NameFrom::kAttributeExplicitlyEmpty;
}

@end