diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm index 87be4ee9bf093..61419ed9cd307 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm @@ -1187,6 +1187,8 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { } else if (action == @selector(copy:) || action == @selector(cut:) || action == @selector(delete:)) { return [self textInRange:_selectedTextRange].length > 0; + } else if (action == @selector(selectAll:)) { + return self.hasText; } return [super canPerformAction:action withSender:sender]; } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm index 46bffc9228c35..cd75d64dc903f 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm @@ -527,6 +527,19 @@ - (void)testStandardEditActions { XCTAssertEqualObjects(substring, @"bbbbaaaabbbbaaaa"); } +- (void)testCanPerformActionForSelectActions { + NSDictionary* config = self.mutableTemplateCopy; + [self setClientId:123 configuration:config]; + NSArray* inputFields = self.installedInputViews; + FlutterTextInputView* inputView = inputFields[0]; + + XCTAssertFalse([inputView canPerformAction:@selector(selectAll:) withSender:nil]); + + [inputView insertText:@"aaaa"]; + + XCTAssertTrue([inputView canPerformAction:@selector(selectAll:) withSender:nil]); +} + - (void)testDeletingBackward { NSDictionary* config = self.mutableTemplateCopy; [self setClientId:123 configuration:config]; diff --git a/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm b/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm index 328d32b6914e0..e95078a419cd6 100644 --- a/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm +++ b/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm @@ -16,6 +16,10 @@ const float kFloatCompareEpsilon = 0.001; +@interface TextInputSemanticsObject (Test) +- (UIView*)textInputSurrogate; +@end + @interface SemanticsObjectTest : XCTestCase @end @@ -1069,4 +1073,83 @@ - (void)testTextInputSemanticsObject { XCTAssertEqual([object accessibilityTraits], UIAccessibilityTraitNone); } +- (void)testTextInputSemanticsObject_canPerformAction { + fml::WeakPtrFactory factory( + new flutter::testing::MockAccessibilityBridge()); + fml::WeakPtr bridge = factory.GetWeakPtr(); + + flutter::SemanticsNode node; + node.label = "foo"; + node.flags = static_cast(flutter::SemanticsFlags::kIsTextField) | + static_cast(flutter::SemanticsFlags::kIsReadOnly); + TextInputSemanticsObject* object = [[TextInputSemanticsObject alloc] initWithBridge:bridge uid:0]; + [object setSemanticsNode:&node]; + [object accessibilityBridgeDidFinishUpdate]; + + id textInputSurrogate = OCMClassMock([UIResponder class]); + id partialSemanticsObject = OCMPartialMock(object); + OCMStub([partialSemanticsObject textInputSurrogate]).andReturn(textInputSurrogate); + + OCMExpect([textInputSurrogate canPerformAction:[OCMArg anySelector] withSender:OCMOCK_ANY]) + .andReturn(YES); + XCTAssertTrue([partialSemanticsObject canPerformAction:@selector(copy:) withSender:nil]); + + OCMExpect([textInputSurrogate canPerformAction:[OCMArg anySelector] withSender:OCMOCK_ANY]) + .andReturn(NO); + XCTAssertFalse([partialSemanticsObject canPerformAction:@selector(copy:) withSender:nil]); +} + +- (void)testTextInputSemanticsObject_editActions { + fml::WeakPtrFactory factory( + new flutter::testing::MockAccessibilityBridge()); + fml::WeakPtr bridge = factory.GetWeakPtr(); + + flutter::SemanticsNode node; + node.label = "foo"; + node.flags = static_cast(flutter::SemanticsFlags::kIsTextField) | + static_cast(flutter::SemanticsFlags::kIsReadOnly); + TextInputSemanticsObject* object = [[TextInputSemanticsObject alloc] initWithBridge:bridge uid:0]; + [object setSemanticsNode:&node]; + [object accessibilityBridgeDidFinishUpdate]; + + id textInputSurrogate = OCMClassMock([UIResponder class]); + id partialSemanticsObject = OCMPartialMock(object); + OCMStub([partialSemanticsObject textInputSurrogate]).andReturn(textInputSurrogate); + + XCTestExpectation* copyExpectation = + [self expectationWithDescription:@"Surrogate's copy method is called."]; + XCTestExpectation* cutExpectation = + [self expectationWithDescription:@"Surrogate's cut method is called."]; + XCTestExpectation* pasteExpectation = + [self expectationWithDescription:@"Surrogate's paste method is called."]; + XCTestExpectation* selectAllExpectation = + [self expectationWithDescription:@"Surrogate's selectAll method is called."]; + XCTestExpectation* deleteExpectation = + [self expectationWithDescription:@"Surrogate's delete method is called."]; + + OCMStub([textInputSurrogate copy:OCMOCK_ANY]).andDo(^(NSInvocation* invocation) { + [copyExpectation fulfill]; + }); + OCMStub([textInputSurrogate cut:OCMOCK_ANY]).andDo(^(NSInvocation* invocation) { + [cutExpectation fulfill]; + }); + OCMStub([textInputSurrogate paste:OCMOCK_ANY]).andDo(^(NSInvocation* invocation) { + [pasteExpectation fulfill]; + }); + OCMStub([textInputSurrogate selectAll:OCMOCK_ANY]).andDo(^(NSInvocation* invocation) { + [selectAllExpectation fulfill]; + }); + OCMStub([textInputSurrogate delete:OCMOCK_ANY]).andDo(^(NSInvocation* invocation) { + [deleteExpectation fulfill]; + }); + + [partialSemanticsObject copy:nil]; + [partialSemanticsObject cut:nil]; + [partialSemanticsObject paste:nil]; + [partialSemanticsObject selectAll:nil]; + [partialSemanticsObject delete:nil]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/TextInputSemanticsObject.mm b/shell/platform/darwin/ios/framework/Source/TextInputSemanticsObject.mm index 682799155649c..3695947b3ccdc 100644 --- a/shell/platform/darwin/ios/framework/Source/TextInputSemanticsObject.mm +++ b/shell/platform/darwin/ios/framework/Source/TextInputSemanticsObject.mm @@ -462,4 +462,32 @@ - (BOOL)hasText { return [[self textInputSurrogate] hasText]; } +#pragma mark - UIResponder overrides + +- (void)cut:(id)sender { + [[self textInputSurrogate] cut:sender]; +} + +- (void)copy:(id)sender { + [[self textInputSurrogate] copy:sender]; +} + +- (void)paste:(id)sender { + [[self textInputSurrogate] paste:sender]; +} + +// TODO(hellohuanlin): should also support `select:`, which is not implemented by the surrogate yet. +// See: https://github.com/flutter/flutter/issues/107578. +- (void)selectAll:(id)sender { + [[self textInputSurrogate] selectAll:sender]; +} + +- (void)delete:(id)sender { + [[self textInputSurrogate] delete:sender]; +} + +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender { + return [[self textInputSurrogate] canPerformAction:action withSender:sender]; +} + @end