Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit aa83691

Browse files
authored
fix selectable text selections are not announced in voice over (#24933)
1 parent e3a84f9 commit aa83691

File tree

2 files changed

+91
-1
lines changed

2 files changed

+91
-1
lines changed

shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,80 @@
5959
[engine shutDownEngine];
6060
}
6161

62+
TEST(FlutterPlatformNodeDelegateMac, SelectableTextHasCorrectSemantics) {
63+
FlutterEngine* engine = CreateTestEngine();
64+
engine.semanticsEnabled = YES;
65+
auto bridge = engine.accessibilityBridge.lock();
66+
// Initialize ax node data.
67+
FlutterSemanticsNode root;
68+
root.id = 0;
69+
root.flags =
70+
static_cast<FlutterSemanticsFlag>(FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField |
71+
FlutterSemanticsFlag::kFlutterSemanticsFlagIsReadOnly);
72+
root.actions = static_cast<FlutterSemanticsAction>(0);
73+
root.text_selection_base = 1;
74+
root.text_selection_extent = 3;
75+
root.label = "";
76+
root.hint = "";
77+
// Selectable text store its text in value
78+
root.value = "selectable text";
79+
root.increased_value = "";
80+
root.decreased_value = "";
81+
root.child_count = 0;
82+
root.custom_accessibility_actions_count = 0;
83+
bridge->AddFlutterSemanticsNodeUpdate(&root);
84+
85+
bridge->CommitUpdates();
86+
87+
auto root_platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
88+
// Verify the accessibility attribute matches.
89+
NSAccessibilityElement* native_accessibility =
90+
root_platform_node_delegate->GetNativeViewAccessible();
91+
std::string value = [native_accessibility.accessibilityValue UTF8String];
92+
EXPECT_EQ(value, "selectable text");
93+
EXPECT_EQ(native_accessibility.accessibilityRole, NSAccessibilityStaticTextRole);
94+
EXPECT_EQ([native_accessibility.accessibilityChildren count], 0u);
95+
NSRange selection = native_accessibility.accessibilitySelectedTextRange;
96+
EXPECT_EQ(selection.location, 1u);
97+
EXPECT_EQ(selection.length, 2u);
98+
std::string selected_text = [native_accessibility.accessibilitySelectedText UTF8String];
99+
EXPECT_EQ(selected_text, "el");
100+
}
101+
102+
TEST(FlutterPlatformNodeDelegateMac, SelectableTextWithoutSelectionReturnZeroRange) {
103+
FlutterEngine* engine = CreateTestEngine();
104+
engine.semanticsEnabled = YES;
105+
auto bridge = engine.accessibilityBridge.lock();
106+
// Initialize ax node data.
107+
FlutterSemanticsNode root;
108+
root.id = 0;
109+
root.flags =
110+
static_cast<FlutterSemanticsFlag>(FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField |
111+
FlutterSemanticsFlag::kFlutterSemanticsFlagIsReadOnly);
112+
root.actions = static_cast<FlutterSemanticsAction>(0);
113+
root.text_selection_base = -1;
114+
root.text_selection_extent = -1;
115+
root.label = "";
116+
root.hint = "";
117+
// Selectable text store its text in value
118+
root.value = "selectable text";
119+
root.increased_value = "";
120+
root.decreased_value = "";
121+
root.child_count = 0;
122+
root.custom_accessibility_actions_count = 0;
123+
bridge->AddFlutterSemanticsNodeUpdate(&root);
124+
125+
bridge->CommitUpdates();
126+
127+
auto root_platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
128+
// Verify the accessibility attribute matches.
129+
NSAccessibilityElement* native_accessibility =
130+
root_platform_node_delegate->GetNativeViewAccessible();
131+
NSRange selection = native_accessibility.accessibilitySelectedTextRange;
132+
EXPECT_TRUE(selection.location == NSNotFound);
133+
EXPECT_EQ(selection.length, 0u);
134+
}
135+
62136
TEST(FlutterPlatformNodeDelegateMac, CanPerformAction) {
63137
FlutterEngine* engine = CreateTestEngine();
64138
engine.semanticsEnabled = YES;

third_party/accessibility/ax/platform/ax_platform_node_mac.mm

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -600,8 +600,19 @@ - (id)AXValueInternal {
600600
if (role == ax::mojom::Role::kTab)
601601
return [self AXSelectedInternal];
602602

603-
if (ui::IsNameExposedInAXValueForRole(role))
603+
if (ui::IsNameExposedInAXValueForRole(role)) {
604+
if (role == ax::mojom::Role::kStaticText) {
605+
// Static texts may store their texts in the value attributes. For
606+
// example, the selectable text stores its text in value instead of
607+
// name.
608+
NSString* value = [self getName];
609+
if (value.length == 0) {
610+
value = [self getStringAttribute:ax::mojom::StringAttribute::kValue];
611+
}
612+
return value;
613+
}
604614
return [self getName];
615+
}
605616

606617
if (_node->IsPlatformCheckable()) {
607618
// Mixed checkbox state not currently supported in views, but could be.
@@ -699,6 +710,11 @@ - (NSValue*)AXSelectedTextRangeInternal {
699710
start = _node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart);
700711
end = _node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd);
701712
}
713+
NSAssert((start >= 0 && end >= 0) || (start == -1 && end == -1), @"selection is invalid");
714+
715+
if (start == -1 && end == -1) {
716+
return [NSValue valueWithRange:{NSNotFound, 0}];
717+
}
702718

703719
// NSRange cannot represent the direction the text was selected in.
704720
return [NSValue valueWithRange:{static_cast<NSUInteger>(std::min(start, end)),

0 commit comments

Comments
 (0)