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

Commit f7ea94e

Browse files
author
Chris Yang
committed
Only send new semantics object when it is changed in layout update
fix
1 parent 063ddc7 commit f7ea94e

File tree

2 files changed

+89
-13
lines changed

2 files changed

+89
-13
lines changed

shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,9 +211,15 @@ void PostAccessibilityNotification(UIAccessibilityNotifications notification,
211211
}
212212

213213
if (layoutChanged) {
214+
SemanticsObject* next = FindNextFocusableIfNecessary();
215+
SemanticsObject* lastFocused =
216+
[objects_.get() objectForKey:@(last_focused_semantics_object_id_)];
217+
// Only specify the focus item if the new focus is different, avoiding double focuses on the
218+
// same item. See: https://github.com/flutter/flutter/issues/104176. If there is a route
219+
// change, we always refocus.
214220
ios_delegate_->PostAccessibilityNotification(
215221
UIAccessibilityLayoutChangedNotification,
216-
FindNextFocusableIfNecessary().nativeAccessibility);
222+
(routeChanged || next != lastFocused) ? next.nativeAccessibility : NULL);
217223
} else if (scrollOccured) {
218224
// TODO(chunhtai): figure out what string to use for notification. At this
219225
// point, it is guarantee the previous focused object is still in the tree
@@ -327,11 +333,9 @@ static bool DidFlagChange(const flutter::SemanticsNode& oldNode,
327333

328334
SemanticsObject* AccessibilityBridge::FindFirstFocusable(SemanticsObject* parent) {
329335
SemanticsObject* currentObject = parent ?: objects_.get()[@(kRootNodeId)];
330-
;
331336
if (!currentObject) {
332337
return nil;
333338
}
334-
335339
if (currentObject.isAccessibilityElement) {
336340
return currentObject;
337341
}

shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -743,10 +743,83 @@ - (void)testLayoutChangeDoesCallNativeAccessibility {
743743
bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/new_actions);
744744

745745
XCTAssertEqual([accessibility_notifications count], 1ul);
746-
SemanticsObject* focusObject = accessibility_notifications[0][@"argument"];
747-
// Make sure refocus event is sent with the nativeAccessibility of root node
748-
// which is a FlutterSemanticsScrollView.
749-
XCTAssertTrue([focusObject isKindOfClass:[FlutterSemanticsScrollView class]]);
746+
id focusObject = accessibility_notifications[0][@"argument"];
747+
748+
// Make sure the focused item is not specificed when it stays the same.
749+
// See: https://github.com/flutter/flutter/issues/104176
750+
XCTAssertEqualObjects(focusObject, [NSNull null]);
751+
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
752+
UIAccessibilityLayoutChangedNotification);
753+
}
754+
755+
- (void)testLayoutChangeDoesCallNativeAccessibilityWhenFocusChanged {
756+
flutter::MockDelegate mock_delegate;
757+
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
758+
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
759+
/*platform=*/thread_task_runner,
760+
/*raster=*/thread_task_runner,
761+
/*ui=*/thread_task_runner,
762+
/*io=*/thread_task_runner);
763+
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
764+
/*delegate=*/mock_delegate,
765+
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
766+
/*platform_views_controller=*/nil,
767+
/*task_runners=*/runners);
768+
id mockFlutterView = OCMClassMock([FlutterView class]);
769+
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
770+
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
771+
772+
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
773+
[[[NSMutableArray alloc] init] autorelease];
774+
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
775+
ios_delegate->on_PostAccessibilityNotification_ =
776+
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
777+
[accessibility_notifications addObject:@{
778+
@"notification" : @(notification),
779+
@"argument" : argument ? argument : [NSNull null],
780+
}];
781+
};
782+
__block auto bridge =
783+
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
784+
/*platform_view=*/platform_view.get(),
785+
/*platform_views_controller=*/nil,
786+
/*ios_delegate=*/std::move(ios_delegate));
787+
788+
flutter::CustomAccessibilityActionUpdates actions;
789+
flutter::SemanticsNodeUpdates nodes;
790+
791+
flutter::SemanticsNode node1;
792+
node1.id = 1;
793+
node1.label = "node1";
794+
nodes[node1.id] = node1;
795+
flutter::SemanticsNode root_node;
796+
root_node.id = kRootNodeId;
797+
root_node.label = "root";
798+
root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kHasImplicitScrolling);
799+
root_node.childrenInTraversalOrder = {1};
800+
root_node.childrenInHitTestOrder = {1};
801+
nodes[root_node.id] = root_node;
802+
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
803+
804+
// Simulates the focusing on the node 1.
805+
bridge->AccessibilityObjectDidBecomeFocused(1);
806+
807+
// Remove node 1 to trigger a layout change notification, and focus should be one root
808+
flutter::CustomAccessibilityActionUpdates new_actions;
809+
flutter::SemanticsNodeUpdates new_nodes;
810+
811+
flutter::SemanticsNode new_root_node;
812+
new_root_node.id = kRootNodeId;
813+
new_root_node.label = "root";
814+
new_root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kHasImplicitScrolling);
815+
new_nodes[new_root_node.id] = new_root_node;
816+
bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/new_actions);
817+
818+
XCTAssertEqual([accessibility_notifications count], 1ul);
819+
SemanticsObject* focusObject2 = accessibility_notifications[0][@"argument"];
820+
821+
// Bridge should ask accessibility to focus on root because node 1 is moved from screen.
822+
XCTAssertTrue([focusObject2 isKindOfClass:[FlutterSemanticsScrollView class]]);
750823
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
751824
UIAccessibilityLayoutChangedNotification);
752825
}
@@ -896,7 +969,6 @@ - (void)testAnnouncesRouteChangesAndLayoutChangeInOneUpdate {
896969
XCTAssertEqual([accessibility_notifications[1][@"notification"] unsignedIntValue],
897970
UIAccessibilityScreenChangedNotification);
898971
SemanticsObject* focusObject = accessibility_notifications[2][@"argument"];
899-
// It should still focus the root.
900972
XCTAssertEqual([focusObject uid], 0);
901973
XCTAssertEqual([accessibility_notifications[2][@"notification"] unsignedIntValue],
902974
UIAccessibilityLayoutChangedNotification);
@@ -1211,7 +1283,7 @@ - (void)testAnnouncesLayoutChangeWithNilIfLastFocusIsRemoved {
12111283
UIAccessibilityLayoutChangedNotification);
12121284
}
12131285

1214-
- (void)testAnnouncesLayoutChangeWithLastFocused {
1286+
- (void)testAnnouncesLayoutChangeWithTheSameItemFocused {
12151287
flutter::MockDelegate mock_delegate;
12161288
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
12171289
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
@@ -1276,10 +1348,10 @@ - (void)testAnnouncesLayoutChangeWithLastFocused {
12761348
new_root_node.childrenInHitTestOrder = {1};
12771349
second_update[root_node.id] = new_root_node;
12781350
bridge->UpdateSemantics(/*nodes=*/second_update, /*actions=*/actions);
1279-
SemanticsObject* focusObject = accessibility_notifications[0][@"argument"];
1280-
// Since we have focused on the node 1 right before the layout changed, the bridge should refocus
1281-
// the node 1.
1282-
XCTAssertEqual([focusObject uid], 1);
1351+
id focusObject = accessibility_notifications[0][@"argument"];
1352+
// Since we have focused on the node 1 right before the layout changed, the bridge should not ask
1353+
// to refocus again on the same node.
1354+
XCTAssertEqualObjects(focusObject, [NSNull null]);
12831355
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
12841356
UIAccessibilityLayoutChangedNotification);
12851357
}

0 commit comments

Comments
 (0)