Skip to content

Commit 2a12d78

Browse files
authored
Fixed ios layout change to not refocus semantics object if the focus … (flutter#21029)
* Fixed ios layout change to not refocus semantics object if the focus is outside of flutter * update * addressing comments
1 parent 5271340 commit 2a12d78

File tree

6 files changed

+119
-20
lines changed

6 files changed

+119
-20
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ - (BOOL)accessibilityPerformEscape {
453453
- (void)accessibilityElementDidBecomeFocused {
454454
if (![self isAccessibilityBridgeAlive])
455455
return;
456-
[self bridge]->AccessibilityFocusDidChange([self uid]);
456+
[self bridge]->AccessibilityObjectDidBecomeFocused([self uid]);
457457
if ([self node].HasFlag(flutter::SemanticsFlags::kIsHidden) ||
458458
[self node].HasFlag(flutter::SemanticsFlags::kIsHeader)) {
459459
[self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kShowOnScreen);
@@ -467,6 +467,7 @@ - (void)accessibilityElementDidBecomeFocused {
467467
- (void)accessibilityElementDidLoseFocus {
468468
if (![self isAccessibilityBridgeAlive])
469469
return;
470+
[self bridge]->AccessibilityObjectDidLoseFocus([self uid]);
470471
if ([self node].HasAction(flutter::SemanticsAction::kDidLoseAccessibilityFocus)) {
471472
[self bridge]->DispatchSemanticsAction([self uid],
472473
flutter::SemanticsAction::kDidLoseAccessibilityFocus);

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ void DispatchSemanticsAction(int32_t id,
3333
SemanticsActionObservation observation(id, action);
3434
observations.push_back(observation);
3535
}
36-
void AccessibilityFocusDidChange(int32_t id) override {}
36+
void AccessibilityObjectDidBecomeFocused(int32_t id) override {}
37+
void AccessibilityObjectDidLoseFocus(int32_t id) override {}
3738
FlutterPlatformViewsController* GetPlatformViewsController() const override { return nil; }
3839
std::vector<SemanticsActionObservation> observations;
3940

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ class AccessibilityBridge final : public AccessibilityBridgeIos {
6161
void DispatchSemanticsAction(int32_t id,
6262
flutter::SemanticsAction action,
6363
std::vector<uint8_t> args) override;
64-
void AccessibilityFocusDidChange(int32_t id) override;
64+
void AccessibilityObjectDidBecomeFocused(int32_t id) override;
65+
void AccessibilityObjectDidLoseFocus(int32_t id) override;
6566

6667
UIView<UITextInput>* textInputView() override;
6768

@@ -85,6 +86,9 @@ class AccessibilityBridge final : public AccessibilityBridgeIos {
8586
FlutterViewController* view_controller_;
8687
PlatformViewIOS* platform_view_;
8788
FlutterPlatformViewsController* platform_views_controller_;
89+
// If the this id is kSemanticObjectIdInvalid, it means either nothing has
90+
// been focused or the focus is currently outside of the flutter application
91+
// (i.e. the status bar or keyboard)
8892
int32_t last_focused_semantics_object_id_;
8993
fml::scoped_nsobject<NSMutableDictionary<NSNumber*, SemanticsObject*>> objects_;
9094
fml::scoped_nsprotocol<FlutterBasicMessageChannel*> accessibility_channel_;

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

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
namespace flutter {
1717
namespace {
1818

19+
constexpr int32_t kSemanticObjectIdInvalid = -1;
20+
1921
class DefaultIosDelegate : public AccessibilityBridge::IosDelegate {
2022
public:
2123
bool IsFlutterViewControllerPresentingModalViewController(
@@ -41,7 +43,7 @@ void PostAccessibilityNotification(UIAccessibilityNotifications notification,
4143
: view_controller_(view_controller),
4244
platform_view_(platform_view),
4345
platform_views_controller_(platform_views_controller),
44-
last_focused_semantics_object_id_(0),
46+
last_focused_semantics_object_id_(kSemanticObjectIdInvalid),
4547
objects_([[NSMutableDictionary alloc] init]),
4648
weak_factory_(this),
4749
previous_route_id_(0),
@@ -67,10 +69,16 @@ void PostAccessibilityNotification(UIAccessibilityNotifications notification,
6769
return [[platform_view_->GetOwnerViewController().get().engine textInputPlugin] textInputView];
6870
}
6971

70-
void AccessibilityBridge::AccessibilityFocusDidChange(int32_t id) {
72+
void AccessibilityBridge::AccessibilityObjectDidBecomeFocused(int32_t id) {
7173
last_focused_semantics_object_id_ = id;
7274
}
7375

76+
void AccessibilityBridge::AccessibilityObjectDidLoseFocus(int32_t id) {
77+
if (last_focused_semantics_object_id_ == id) {
78+
last_focused_semantics_object_id_ = kSemanticObjectIdInvalid;
79+
}
80+
}
81+
7482
void AccessibilityBridge::UpdateSemantics(flutter::SemanticsNodeUpdates nodes,
7583
flutter::CustomAccessibilityActionUpdates actions) {
7684
BOOL layoutChanged = NO;
@@ -198,22 +206,28 @@ void PostAccessibilityNotification(UIAccessibilityNotifications notification,
198206
nextToFocus);
199207
}
200208
} else if (layoutChanged) {
201-
// Tries to refocus the previous focused semantics object to avoid random jumps.
202-
SemanticsObject* nextToFocus =
203-
[objects_.get() objectForKey:@(last_focused_semantics_object_id_)];
204-
if (!nextToFocus && root) {
205-
nextToFocus = FindFirstFocusable(root);
209+
SemanticsObject* nextToFocus = nil;
210+
// This property will be -1 if the focus is outside of the flutter
211+
// application. In this case, we should not refocus anything.
212+
if (last_focused_semantics_object_id_ != kSemanticObjectIdInvalid) {
213+
// Tries to refocus the previous focused semantics object to avoid random jumps.
214+
nextToFocus = [objects_.get() objectForKey:@(last_focused_semantics_object_id_)];
215+
if (!nextToFocus && root) {
216+
nextToFocus = FindFirstFocusable(root);
217+
}
206218
}
207219
ios_delegate_->PostAccessibilityNotification(UIAccessibilityLayoutChangedNotification,
208220
nextToFocus);
209221
} else if (scrollOccured) {
210222
// TODO(chunhtai): figure out what string to use for notification. At this
211223
// point, it is guarantee the previous focused object is still in the tree
212224
// so that we don't need to worry about focus lost. (e.g. "Screen 0 of 3")
213-
SemanticsObject* nextToFocus =
214-
[objects_.get() objectForKey:@(last_focused_semantics_object_id_)];
215-
if (!nextToFocus && root) {
216-
nextToFocus = FindFirstFocusable(root);
225+
SemanticsObject* nextToFocus = nil;
226+
if (last_focused_semantics_object_id_ != kSemanticObjectIdInvalid) {
227+
nextToFocus = [objects_.get() objectForKey:@(last_focused_semantics_object_id_)];
228+
if (!nextToFocus && root) {
229+
nextToFocus = FindFirstFocusable(root);
230+
}
217231
}
218232
ios_delegate_->PostAccessibilityNotification(UIAccessibilityPageScrolledNotification,
219233
nextToFocus);

shell/platform/darwin/ios/framework/Source/accessibility_bridge_ios.h

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,17 @@ class AccessibilityBridgeIos {
2525
flutter::SemanticsAction action,
2626
std::vector<uint8_t> args) = 0;
2727
/**
28-
* A callback that is called after the accessibility focus has moved to a new
29-
* SemanticObject.
28+
* A callback that is called when a SemanticObject receives focus.
3029
*
3130
* The input id is the uid of the newly focused SemanticObject.
3231
*/
33-
virtual void AccessibilityFocusDidChange(int32_t id) = 0;
32+
virtual void AccessibilityObjectDidBecomeFocused(int32_t id) = 0;
33+
/**
34+
* A callback that is called when a SemanticObject loses focus
35+
*
36+
* The input id is the uid of the newly focused SemanticObject.
37+
*/
38+
virtual void AccessibilityObjectDidLoseFocus(int32_t id) = 0;
3439
virtual FlutterPlatformViewsController* GetPlatformViewsController() const = 0;
3540
};
3641

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

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ - (void)testAnnouncesLayoutChangeWithNilIfLastFocusIsRemoved {
450450

451451
XCTAssertEqual([accessibility_notifications count], 0ul);
452452
// Simulates the focusing on the node 1.
453-
bridge->AccessibilityFocusDidChange(1);
453+
bridge->AccessibilityObjectDidBecomeFocused(1);
454454

455455
flutter::SemanticsNodeUpdates second_update;
456456
// Simulates the removal of the node 1
@@ -520,7 +520,7 @@ - (void)testAnnouncesLayoutChangeWithLastFocused {
520520

521521
XCTAssertEqual([accessibility_notifications count], 0ul);
522522
// Simulates the focusing on the node 1.
523-
bridge->AccessibilityFocusDidChange(1);
523+
bridge->AccessibilityObjectDidBecomeFocused(1);
524524

525525
flutter::SemanticsNodeUpdates second_update;
526526
// Simulates the removal of the node 2.
@@ -539,6 +539,80 @@ - (void)testAnnouncesLayoutChangeWithLastFocused {
539539
UIAccessibilityLayoutChangedNotification);
540540
}
541541

542+
- (void)testAnnouncesLayoutChangeWhenFocusMovedOutside {
543+
flutter::MockDelegate mock_delegate;
544+
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
545+
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
546+
/*platform=*/thread_task_runner,
547+
/*raster=*/thread_task_runner,
548+
/*ui=*/thread_task_runner,
549+
/*io=*/thread_task_runner);
550+
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
551+
/*delegate=*/mock_delegate,
552+
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
553+
/*task_runners=*/runners);
554+
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
555+
id mockFlutterView = OCMClassMock([FlutterView class]);
556+
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
557+
558+
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
559+
[[[NSMutableArray alloc] init] autorelease];
560+
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
561+
ios_delegate->on_PostAccessibilityNotification_ =
562+
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
563+
[accessibility_notifications addObject:@{
564+
@"notification" : @(notification),
565+
@"argument" : argument ? argument : [NSNull null],
566+
}];
567+
};
568+
__block auto bridge =
569+
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
570+
/*platform_view=*/platform_view.get(),
571+
/*platform_views_controller=*/nil,
572+
/*ios_delegate=*/std::move(ios_delegate));
573+
574+
flutter::CustomAccessibilityActionUpdates actions;
575+
flutter::SemanticsNodeUpdates first_update;
576+
577+
flutter::SemanticsNode node_one;
578+
node_one.id = 1;
579+
node_one.label = "route1";
580+
first_update[node_one.id] = node_one;
581+
flutter::SemanticsNode node_two;
582+
node_two.id = 2;
583+
node_two.label = "route2";
584+
first_update[node_two.id] = node_two;
585+
flutter::SemanticsNode root_node;
586+
root_node.id = kRootNodeId;
587+
root_node.label = "root";
588+
root_node.childrenInTraversalOrder = {1, 2};
589+
root_node.childrenInHitTestOrder = {1, 2};
590+
first_update[root_node.id] = root_node;
591+
bridge->UpdateSemantics(/*nodes=*/first_update, /*actions=*/actions);
592+
593+
XCTAssertEqual([accessibility_notifications count], 0ul);
594+
// Simulates the focusing on the node 1.
595+
bridge->AccessibilityObjectDidBecomeFocused(1);
596+
// Simulates that the focus move outside of flutter.
597+
bridge->AccessibilityObjectDidLoseFocus(1);
598+
599+
flutter::SemanticsNodeUpdates second_update;
600+
// Simulates the removal of the node 2.
601+
flutter::SemanticsNode new_root_node;
602+
new_root_node.id = kRootNodeId;
603+
new_root_node.label = "root";
604+
new_root_node.childrenInTraversalOrder = {1};
605+
new_root_node.childrenInHitTestOrder = {1};
606+
second_update[root_node.id] = new_root_node;
607+
bridge->UpdateSemantics(/*nodes=*/second_update, /*actions=*/actions);
608+
NSNull* focusObject = accessibility_notifications[0][@"argument"];
609+
// Since the focus is moved outside of the app right before the layout
610+
// changed, the bridge should not try to refocus anything .
611+
XCTAssertEqual(focusObject, [NSNull null]);
612+
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
613+
UIAccessibilityLayoutChangedNotification);
614+
}
615+
542616
- (void)testAnnouncesScrollChangeWithLastFocused {
543617
flutter::MockDelegate mock_delegate;
544618
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
@@ -591,7 +665,7 @@ - (void)testAnnouncesScrollChangeWithLastFocused {
591665
[accessibility_notifications removeAllObjects];
592666

593667
// Simulates the focusing on the node 1.
594-
bridge->AccessibilityFocusDidChange(1);
668+
bridge->AccessibilityObjectDidBecomeFocused(1);
595669

596670
flutter::SemanticsNodeUpdates second_update;
597671
// Simulates the scrolling on the node 1.

0 commit comments

Comments
 (0)