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

[ios] Fix text input edit rotor accessibility #54351

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,19 @@ - (void)testStandardEditActions {
XCTAssertEqualObjects(substring, @"bbbbaaaabbbbaaaa");
}

- (void)testCanPerformActionForSelectActions {
NSDictionary* config = self.mutableTemplateCopy;
[self setClientId:123 configuration:config];
NSArray<FlutterTextInputView*>* 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];
Expand Down
83 changes: 83 additions & 0 deletions shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

const float kFloatCompareEpsilon = 0.001;

@interface TextInputSemanticsObject (Test)
- (UIView<UITextInput>*)textInputSurrogate;
@end

@interface SemanticsObjectTest : XCTestCase
@end

Expand Down Expand Up @@ -1069,4 +1073,83 @@ - (void)testTextInputSemanticsObject {
XCTAssertEqual([object accessibilityTraits], UIAccessibilityTraitNone);
}

- (void)testTextInputSemanticsObject_canPerformAction {
fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
new flutter::testing::MockAccessibilityBridge());
fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();

flutter::SemanticsNode node;
node.label = "foo";
node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kIsTextField) |
static_cast<int32_t>(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<flutter::AccessibilityBridgeIos> factory(
new flutter::testing::MockAccessibilityBridge());
fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();

flutter::SemanticsNode node;
node.label = "foo";
node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kIsTextField) |
static_cast<int32_t>(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
Original file line number Diff line number Diff line change
Expand Up @@ -462,4 +462,32 @@ - (BOOL)hasText {
return [[self textInputSurrogate] hasText];
}

#pragma mark - UIResponder overrides

- (void)cut:(id)sender {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure all the gotchas, but can this class use forwardingTargetForSelector instead of directly calling every method on textInputSurrogate? It's inevitable we will miss some as they are added in future SDKs. I'm not sure it's the right behavior for this class, though, and I'm not sure if the super class UIAccessibilityElement already implements, in which case it won't work...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out that iOS determines whether to show these options in accessibility rotor by checking the presence of these functions, So we can't use forwardingTargetForSelector.

[[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