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

Commit c346a96

Browse files
authored
[ios] Fix text input edit rotor accessibility (#54351)
Fixes text input edit rotor accessibility missing edit actions. It also fixes a few edit items that is displayed where it's not supposed to. (e.g. displaying select item while nothing to select). *List which issues are fixed by this PR. You must list at least one issue.* Fixes flutter/flutter#151029 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
1 parent 5fc0122 commit c346a96

File tree

4 files changed

+126
-0
lines changed

4 files changed

+126
-0
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,6 +1187,8 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
11871187
} else if (action == @selector(copy:) || action == @selector(cut:) ||
11881188
action == @selector(delete:)) {
11891189
return [self textInRange:_selectedTextRange].length > 0;
1190+
} else if (action == @selector(selectAll:)) {
1191+
return self.hasText;
11901192
}
11911193
return [super canPerformAction:action withSender:sender];
11921194
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,19 @@ - (void)testStandardEditActions {
527527
XCTAssertEqualObjects(substring, @"bbbbaaaabbbbaaaa");
528528
}
529529

530+
- (void)testCanPerformActionForSelectActions {
531+
NSDictionary* config = self.mutableTemplateCopy;
532+
[self setClientId:123 configuration:config];
533+
NSArray<FlutterTextInputView*>* inputFields = self.installedInputViews;
534+
FlutterTextInputView* inputView = inputFields[0];
535+
536+
XCTAssertFalse([inputView canPerformAction:@selector(selectAll:) withSender:nil]);
537+
538+
[inputView insertText:@"aaaa"];
539+
540+
XCTAssertTrue([inputView canPerformAction:@selector(selectAll:) withSender:nil]);
541+
}
542+
530543
- (void)testDeletingBackward {
531544
NSDictionary* config = self.mutableTemplateCopy;
532545
[self setClientId:123 configuration:config];

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

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
const float kFloatCompareEpsilon = 0.001;
1818

19+
@interface TextInputSemanticsObject (Test)
20+
- (UIView<UITextInput>*)textInputSurrogate;
21+
@end
22+
1923
@interface SemanticsObjectTest : XCTestCase
2024
@end
2125

@@ -1069,4 +1073,83 @@ - (void)testTextInputSemanticsObject {
10691073
XCTAssertEqual([object accessibilityTraits], UIAccessibilityTraitNone);
10701074
}
10711075

1076+
- (void)testTextInputSemanticsObject_canPerformAction {
1077+
fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
1078+
new flutter::testing::MockAccessibilityBridge());
1079+
fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
1080+
1081+
flutter::SemanticsNode node;
1082+
node.label = "foo";
1083+
node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kIsTextField) |
1084+
static_cast<int32_t>(flutter::SemanticsFlags::kIsReadOnly);
1085+
TextInputSemanticsObject* object = [[TextInputSemanticsObject alloc] initWithBridge:bridge uid:0];
1086+
[object setSemanticsNode:&node];
1087+
[object accessibilityBridgeDidFinishUpdate];
1088+
1089+
id textInputSurrogate = OCMClassMock([UIResponder class]);
1090+
id partialSemanticsObject = OCMPartialMock(object);
1091+
OCMStub([partialSemanticsObject textInputSurrogate]).andReturn(textInputSurrogate);
1092+
1093+
OCMExpect([textInputSurrogate canPerformAction:[OCMArg anySelector] withSender:OCMOCK_ANY])
1094+
.andReturn(YES);
1095+
XCTAssertTrue([partialSemanticsObject canPerformAction:@selector(copy:) withSender:nil]);
1096+
1097+
OCMExpect([textInputSurrogate canPerformAction:[OCMArg anySelector] withSender:OCMOCK_ANY])
1098+
.andReturn(NO);
1099+
XCTAssertFalse([partialSemanticsObject canPerformAction:@selector(copy:) withSender:nil]);
1100+
}
1101+
1102+
- (void)testTextInputSemanticsObject_editActions {
1103+
fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
1104+
new flutter::testing::MockAccessibilityBridge());
1105+
fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
1106+
1107+
flutter::SemanticsNode node;
1108+
node.label = "foo";
1109+
node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kIsTextField) |
1110+
static_cast<int32_t>(flutter::SemanticsFlags::kIsReadOnly);
1111+
TextInputSemanticsObject* object = [[TextInputSemanticsObject alloc] initWithBridge:bridge uid:0];
1112+
[object setSemanticsNode:&node];
1113+
[object accessibilityBridgeDidFinishUpdate];
1114+
1115+
id textInputSurrogate = OCMClassMock([UIResponder class]);
1116+
id partialSemanticsObject = OCMPartialMock(object);
1117+
OCMStub([partialSemanticsObject textInputSurrogate]).andReturn(textInputSurrogate);
1118+
1119+
XCTestExpectation* copyExpectation =
1120+
[self expectationWithDescription:@"Surrogate's copy method is called."];
1121+
XCTestExpectation* cutExpectation =
1122+
[self expectationWithDescription:@"Surrogate's cut method is called."];
1123+
XCTestExpectation* pasteExpectation =
1124+
[self expectationWithDescription:@"Surrogate's paste method is called."];
1125+
XCTestExpectation* selectAllExpectation =
1126+
[self expectationWithDescription:@"Surrogate's selectAll method is called."];
1127+
XCTestExpectation* deleteExpectation =
1128+
[self expectationWithDescription:@"Surrogate's delete method is called."];
1129+
1130+
OCMStub([textInputSurrogate copy:OCMOCK_ANY]).andDo(^(NSInvocation* invocation) {
1131+
[copyExpectation fulfill];
1132+
});
1133+
OCMStub([textInputSurrogate cut:OCMOCK_ANY]).andDo(^(NSInvocation* invocation) {
1134+
[cutExpectation fulfill];
1135+
});
1136+
OCMStub([textInputSurrogate paste:OCMOCK_ANY]).andDo(^(NSInvocation* invocation) {
1137+
[pasteExpectation fulfill];
1138+
});
1139+
OCMStub([textInputSurrogate selectAll:OCMOCK_ANY]).andDo(^(NSInvocation* invocation) {
1140+
[selectAllExpectation fulfill];
1141+
});
1142+
OCMStub([textInputSurrogate delete:OCMOCK_ANY]).andDo(^(NSInvocation* invocation) {
1143+
[deleteExpectation fulfill];
1144+
});
1145+
1146+
[partialSemanticsObject copy:nil];
1147+
[partialSemanticsObject cut:nil];
1148+
[partialSemanticsObject paste:nil];
1149+
[partialSemanticsObject selectAll:nil];
1150+
[partialSemanticsObject delete:nil];
1151+
1152+
[self waitForExpectationsWithTimeout:1 handler:nil];
1153+
}
1154+
10721155
@end

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,4 +462,32 @@ - (BOOL)hasText {
462462
return [[self textInputSurrogate] hasText];
463463
}
464464

465+
#pragma mark - UIResponder overrides
466+
467+
- (void)cut:(id)sender {
468+
[[self textInputSurrogate] cut:sender];
469+
}
470+
471+
- (void)copy:(id)sender {
472+
[[self textInputSurrogate] copy:sender];
473+
}
474+
475+
- (void)paste:(id)sender {
476+
[[self textInputSurrogate] paste:sender];
477+
}
478+
479+
// TODO(hellohuanlin): should also support `select:`, which is not implemented by the surrogate yet.
480+
// See: https://github.com/flutter/flutter/issues/107578.
481+
- (void)selectAll:(id)sender {
482+
[[self textInputSurrogate] selectAll:sender];
483+
}
484+
485+
- (void)delete:(id)sender {
486+
[[self textInputSurrogate] delete:sender];
487+
}
488+
489+
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
490+
return [[self textInputSurrogate] canPerformAction:action withSender:sender];
491+
}
492+
465493
@end

0 commit comments

Comments
 (0)