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

Commit ccaee70

Browse files
authored
Missing default focus when navigating to a page with no SemanticsNode that sets namesRoute:true (#20516)
1 parent 3930ac1 commit ccaee70

File tree

2 files changed

+46
-0
lines changed

2 files changed

+46
-0
lines changed

shell/platform/android/io/flutter/view/AccessibilityBridge.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1548,6 +1548,17 @@ private void sendWindowChangeEvent(@NonNull SemanticsNode route) {
15481548
AccessibilityEvent event =
15491549
obtainAccessibilityEvent(route.id, AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
15501550
String routeName = route.getRouteName();
1551+
if (routeName == null) {
1552+
// The routeName will be null when there is no semantics node that represnets namesRoute in
1553+
// the scopeRoute. The TYPE_WINDOW_STATE_CHANGED only works the route name is not null and not
1554+
// empty. Gives it a whitespace will make it focus the first semantics node without
1555+
// pronouncing any word.
1556+
//
1557+
// The other way to trigger a focus change is to send a TYPE_VIEW_FOCUSED to the
1558+
// rootAccessibilityView. However, it is less predictable which semantics node it will focus
1559+
// next.
1560+
routeName = " ";
1561+
}
15511562
event.getText().add(routeName);
15521563
sendAccessibilityEvent(event);
15531564
}

shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,41 @@ public void itUnfocusesPlatformViewWhenPlatformViewGoesAway() {
132132
assertEquals(event.getEventType(), AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
133133
}
134134

135+
@Test
136+
public void itAnnouncesWhiteSpaceWhenNoNamesRoute() {
137+
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
138+
AccessibilityManager mockManager = mock(AccessibilityManager.class);
139+
View mockRootView = mock(View.class);
140+
Context context = mock(Context.class);
141+
when(mockRootView.getContext()).thenReturn(context);
142+
when(context.getPackageName()).thenReturn("test");
143+
AccessibilityBridge accessibilityBridge =
144+
setUpBridge(mockRootView, mockManager, mockViewEmbedder);
145+
ViewParent mockParent = mock(ViewParent.class);
146+
when(mockRootView.getParent()).thenReturn(mockParent);
147+
when(mockManager.isEnabled()).thenReturn(true);
148+
149+
// Sent a11y tree with scopeRoute without namesRoute.
150+
TestSemanticsNode root = new TestSemanticsNode();
151+
root.id = 0;
152+
TestSemanticsNode scopeRoute = new TestSemanticsNode();
153+
scopeRoute.id = 1;
154+
scopeRoute.addFlag(AccessibilityBridge.Flag.SCOPES_ROUTE);
155+
root.children.add(scopeRoute);
156+
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
157+
accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings);
158+
159+
ArgumentCaptor<AccessibilityEvent> eventCaptor =
160+
ArgumentCaptor.forClass(AccessibilityEvent.class);
161+
verify(mockParent, times(2))
162+
.requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture());
163+
AccessibilityEvent event = eventCaptor.getAllValues().get(0);
164+
assertEquals(event.getEventType(), AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
165+
List<CharSequence> sentences = event.getText();
166+
assertEquals(sentences.size(), 1);
167+
assertEquals(sentences.get(0).toString(), " ");
168+
}
169+
135170
@Test
136171
public void itHoverOverOutOfBoundsDoesNotCrash() {
137172
// SementicsNode.hitTest() returns null when out of bounds.

0 commit comments

Comments
 (0)