From 9738113517d713ca708bbbaa17b6eba6e5f359ab Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Mon, 19 Aug 2024 10:04:45 -0700 Subject: [PATCH 1/3] [ios]fix text input rotor accessiblity --- .../Source/FlutterTextInputPlugin.mm | 2 + .../Source/FlutterTextInputPluginTest.mm | 15 ++++ .../framework/Source/SemanticsObjectTest.mm | 89 +++++++++++++++++++ .../Source/TextInputSemanticsObject.mm | 30 +++++++ 4 files changed, 136 insertions(+) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm index 87be4ee9bf093..a3d43e18c149d 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(select:) || 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..404d50280c264 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm @@ -527,6 +527,21 @@ - (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(select:) withSender:nil]); + XCTAssertFalse([inputView canPerformAction:@selector(selectAll:) withSender:nil]); + + [inputView insertText:@"aaaa"]; + + XCTAssertTrue([inputView canPerformAction:@selector(select:) withSender:nil]); + 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..911a5d5225f3e 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,89 @@ - (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* selectExpectation = + [self expectationWithDescription:@"Surrogate's select 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 select:OCMOCK_ANY]).andDo(^(NSInvocation* invocation) { + [selectExpectation 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 select: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..0b35d274ab70e 100644 --- a/shell/platform/darwin/ios/framework/Source/TextInputSemanticsObject.mm +++ b/shell/platform/darwin/ios/framework/Source/TextInputSemanticsObject.mm @@ -462,4 +462,34 @@ - (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]; +} + +- (void)select:(id)sender { + [[self textInputSurrogate] select:sender]; +} + +- (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 From bf10ef477c5f0474f8ac124df66915d21ff3e749 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Fri, 23 Aug 2024 13:36:07 -0700 Subject: [PATCH 2/3] remove select: function --- .../darwin/ios/framework/Source/FlutterTextInputPlugin.mm | 2 +- .../ios/framework/Source/FlutterTextInputPluginTest.mm | 2 -- .../darwin/ios/framework/Source/TextInputSemanticsObject.mm | 6 ++---- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm index a3d43e18c149d..61419ed9cd307 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm @@ -1187,7 +1187,7 @@ - (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(select:) || action == @selector(selectAll:)) { + } 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 404d50280c264..cd75d64dc903f 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm @@ -533,12 +533,10 @@ - (void)testCanPerformActionForSelectActions { NSArray* inputFields = self.installedInputViews; FlutterTextInputView* inputView = inputFields[0]; - XCTAssertFalse([inputView canPerformAction:@selector(select:) withSender:nil]); XCTAssertFalse([inputView canPerformAction:@selector(selectAll:) withSender:nil]); [inputView insertText:@"aaaa"]; - XCTAssertTrue([inputView canPerformAction:@selector(select:) withSender:nil]); XCTAssertTrue([inputView canPerformAction:@selector(selectAll:) withSender:nil]); } diff --git a/shell/platform/darwin/ios/framework/Source/TextInputSemanticsObject.mm b/shell/platform/darwin/ios/framework/Source/TextInputSemanticsObject.mm index 0b35d274ab70e..3695947b3ccdc 100644 --- a/shell/platform/darwin/ios/framework/Source/TextInputSemanticsObject.mm +++ b/shell/platform/darwin/ios/framework/Source/TextInputSemanticsObject.mm @@ -476,10 +476,8 @@ - (void)paste:(id)sender { [[self textInputSurrogate] paste:sender]; } -- (void)select:(id)sender { - [[self textInputSurrogate] select: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]; } From 8fe402d7e0126e4ae564d59c6cfe9d7552705717 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Fri, 23 Aug 2024 14:46:16 -0700 Subject: [PATCH 3/3] also fix test --- .../darwin/ios/framework/Source/SemanticsObjectTest.mm | 6 ------ 1 file changed, 6 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm b/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm index 911a5d5225f3e..e95078a419cd6 100644 --- a/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm +++ b/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm @@ -1122,8 +1122,6 @@ - (void)testTextInputSemanticsObject_editActions { [self expectationWithDescription:@"Surrogate's cut method is called."]; XCTestExpectation* pasteExpectation = [self expectationWithDescription:@"Surrogate's paste method is called."]; - XCTestExpectation* selectExpectation = - [self expectationWithDescription:@"Surrogate's select method is called."]; XCTestExpectation* selectAllExpectation = [self expectationWithDescription:@"Surrogate's selectAll method is called."]; XCTestExpectation* deleteExpectation = @@ -1138,9 +1136,6 @@ - (void)testTextInputSemanticsObject_editActions { OCMStub([textInputSurrogate paste:OCMOCK_ANY]).andDo(^(NSInvocation* invocation) { [pasteExpectation fulfill]; }); - OCMStub([textInputSurrogate select:OCMOCK_ANY]).andDo(^(NSInvocation* invocation) { - [selectExpectation fulfill]; - }); OCMStub([textInputSurrogate selectAll:OCMOCK_ANY]).andDo(^(NSInvocation* invocation) { [selectAllExpectation fulfill]; }); @@ -1151,7 +1146,6 @@ - (void)testTextInputSemanticsObject_editActions { [partialSemanticsObject copy:nil]; [partialSemanticsObject cut:nil]; [partialSemanticsObject paste:nil]; - [partialSemanticsObject select:nil]; [partialSemanticsObject selectAll:nil]; [partialSemanticsObject delete:nil];