diff --git a/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm index 2bb83ef0a8dd9..e4c1bd81c8df8 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm @@ -59,6 +59,80 @@ [engine shutDownEngine]; } +TEST(FlutterPlatformNodeDelegateMac, SelectableTextHasCorrectSemantics) { + FlutterEngine* engine = CreateTestEngine(); + engine.semanticsEnabled = YES; + auto bridge = engine.accessibilityBridge.lock(); + // Initialize ax node data. + FlutterSemanticsNode root; + root.id = 0; + root.flags = + static_cast(FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField | + FlutterSemanticsFlag::kFlutterSemanticsFlagIsReadOnly); + root.actions = static_cast(0); + root.text_selection_base = 1; + root.text_selection_extent = 3; + root.label = ""; + root.hint = ""; + // Selectable text store its text in value + root.value = "selectable text"; + root.increased_value = ""; + root.decreased_value = ""; + root.child_count = 0; + root.custom_accessibility_actions_count = 0; + bridge->AddFlutterSemanticsNodeUpdate(&root); + + bridge->CommitUpdates(); + + auto root_platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock(); + // Verify the accessibility attribute matches. + NSAccessibilityElement* native_accessibility = + root_platform_node_delegate->GetNativeViewAccessible(); + std::string value = [native_accessibility.accessibilityValue UTF8String]; + EXPECT_EQ(value, "selectable text"); + EXPECT_EQ(native_accessibility.accessibilityRole, NSAccessibilityStaticTextRole); + EXPECT_EQ([native_accessibility.accessibilityChildren count], 0u); + NSRange selection = native_accessibility.accessibilitySelectedTextRange; + EXPECT_EQ(selection.location, 1u); + EXPECT_EQ(selection.length, 2u); + std::string selected_text = [native_accessibility.accessibilitySelectedText UTF8String]; + EXPECT_EQ(selected_text, "el"); +} + +TEST(FlutterPlatformNodeDelegateMac, SelectableTextWithoutSelectionReturnZeroRange) { + FlutterEngine* engine = CreateTestEngine(); + engine.semanticsEnabled = YES; + auto bridge = engine.accessibilityBridge.lock(); + // Initialize ax node data. + FlutterSemanticsNode root; + root.id = 0; + root.flags = + static_cast(FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField | + FlutterSemanticsFlag::kFlutterSemanticsFlagIsReadOnly); + root.actions = static_cast(0); + root.text_selection_base = -1; + root.text_selection_extent = -1; + root.label = ""; + root.hint = ""; + // Selectable text store its text in value + root.value = "selectable text"; + root.increased_value = ""; + root.decreased_value = ""; + root.child_count = 0; + root.custom_accessibility_actions_count = 0; + bridge->AddFlutterSemanticsNodeUpdate(&root); + + bridge->CommitUpdates(); + + auto root_platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock(); + // Verify the accessibility attribute matches. + NSAccessibilityElement* native_accessibility = + root_platform_node_delegate->GetNativeViewAccessible(); + NSRange selection = native_accessibility.accessibilitySelectedTextRange; + EXPECT_TRUE(selection.location == NSNotFound); + EXPECT_EQ(selection.length, 0u); +} + TEST(FlutterPlatformNodeDelegateMac, CanPerformAction) { FlutterEngine* engine = CreateTestEngine(); engine.semanticsEnabled = YES; diff --git a/third_party/accessibility/ax/platform/ax_platform_node_mac.mm b/third_party/accessibility/ax/platform/ax_platform_node_mac.mm index 8cb1fb12d430b..44441841a499a 100644 --- a/third_party/accessibility/ax/platform/ax_platform_node_mac.mm +++ b/third_party/accessibility/ax/platform/ax_platform_node_mac.mm @@ -600,8 +600,19 @@ - (id)AXValueInternal { if (role == ax::mojom::Role::kTab) return [self AXSelectedInternal]; - if (ui::IsNameExposedInAXValueForRole(role)) + if (ui::IsNameExposedInAXValueForRole(role)) { + if (role == ax::mojom::Role::kStaticText) { + // Static texts may store their texts in the value attributes. For + // example, the selectable text stores its text in value instead of + // name. + NSString* value = [self getName]; + if (value.length == 0) { + value = [self getStringAttribute:ax::mojom::StringAttribute::kValue]; + } + return value; + } return [self getName]; + } if (_node->IsPlatformCheckable()) { // Mixed checkbox state not currently supported in views, but could be. @@ -699,6 +710,11 @@ - (NSValue*)AXSelectedTextRangeInternal { start = _node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart); end = _node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd); } + NSAssert((start >= 0 && end >= 0) || (start == -1 && end == -1), @"selection is invalid"); + + if (start == -1 && end == -1) { + return [NSValue valueWithRange:{NSNotFound, 0}]; + } // NSRange cannot represent the direction the text was selected in. return [NSValue valueWithRange:{static_cast(std::min(start, end)),