Skip to content

Commit

Permalink
Makes iOS VoiceOver treat radio buttons as regualr buttons (flutter#4…
Browse files Browse the repository at this point in the history
…1036)

Makes iOS VoiceOver treat radio buttons as regualr buttons
  • Loading branch information
chunhtai authored Apr 12, 2023
1 parent b36ff84 commit e7586d7
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,11 @@ - (NSString*)accessibilityValue {
return @([self node].value.data());
}

// iOS does not announce values of native radio buttons.
if ([self node].HasFlag(flutter::SemanticsFlags::kIsInMutuallyExclusiveGroup)) {
return nil;
}

// FlutterSwitchSemanticsObject should supercede these conditionals.
if ([self node].HasFlag(flutter::SemanticsFlags::kHasToggledState) ||
[self node].HasFlag(flutter::SemanticsFlags::kHasCheckedState)) {
Expand Down Expand Up @@ -794,7 +799,7 @@ - (UIAccessibilityTraits)accessibilityTraits {
[self node].HasAction(flutter::SemanticsAction::kDecrease)) {
traits |= UIAccessibilityTraitAdjustable;
}
// FlutterSwitchSemanticsObject should supercede these conditionals.
// This should also capture radio buttons.
if ([self node].HasFlag(flutter::SemanticsFlags::kHasToggledState) ||
[self node].HasFlag(flutter::SemanticsFlags::kHasCheckedState)) {
traits |= UIAccessibilityTraitButton;
Expand Down
18 changes: 18 additions & 0 deletions shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,24 @@ - (void)testFlutterSwitchSemanticsObjectMatchesUISwitch {
XCTAssertEqual(object.accessibilityValue, nativeSwitch.accessibilityValue);
}

- (void)testFlutterSemanticsObjectOfRadioButton {
fml::WeakPtrFactory<flutter::MockAccessibilityBridge> factory(
new flutter::MockAccessibilityBridge());
fml::WeakPtr<flutter::MockAccessibilityBridge> bridge = factory.GetWeakPtr();
FlutterSemanticsObject* object = [[FlutterSemanticsObject alloc] initWithBridge:bridge uid:0];

// Handle initial setting of node with header.
flutter::SemanticsNode node;
node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kIsInMutuallyExclusiveGroup) |
static_cast<int32_t>(flutter::SemanticsFlags::kHasCheckedState) |
static_cast<int32_t>(flutter::SemanticsFlags::kHasEnabledState) |
static_cast<int32_t>(flutter::SemanticsFlags::kIsEnabled);
node.label = "foo";
[object setSemanticsNode:&node];
XCTAssertTrue((object.accessibilityTraits & UIAccessibilityTraitButton) > 0);
XCTAssertNil(object.accessibilityValue);
}

- (void)testFlutterSwitchSemanticsObjectMatchesUISwitchDisabled {
fml::WeakPtrFactory<flutter::MockAccessibilityBridge> factory(
new flutter::MockAccessibilityBridge());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,9 @@ static void ReplaceSemanticsObject(SemanticsObject* oldObject,
!node.HasFlag(flutter::SemanticsFlags::kIsReadOnly)) {
// Text fields are backed by objects that implement UITextInput.
return [[[TextInputSemanticsObject alloc] initWithBridge:weak_ptr uid:node.id] autorelease];
} else if (node.HasFlag(flutter::SemanticsFlags::kHasToggledState) ||
node.HasFlag(flutter::SemanticsFlags::kHasCheckedState)) {
} else if (!node.HasFlag(flutter::SemanticsFlags::kIsInMutuallyExclusiveGroup) &&
(node.HasFlag(flutter::SemanticsFlags::kHasToggledState) ||
node.HasFlag(flutter::SemanticsFlags::kHasCheckedState))) {
return [[[FlutterSwitchSemanticsObject alloc] initWithBridge:weak_ptr uid:node.id] autorelease];
} else if (node.HasFlag(flutter::SemanticsFlags::kHasImplicitScrolling)) {
return [[[FlutterScrollableSemanticsObject alloc] initWithBridge:weak_ptr
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,51 @@ - (void)testAnnouncesRouteChanges {
UIAccessibilityScreenChangedNotification);
}

- (void)testRadioButtonIsNotSwitchButton {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners);
id engine = OCMClassMock([FlutterEngine class]);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
FlutterView* flutterView = [[FlutterView alloc] initWithDelegate:engine
opaque:YES
enableWideGamut:NO];
OCMStub([mockFlutterViewController view]).andReturn(flutterView);
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil,
/*ios_delegate=*/std::move(ios_delegate));

flutter::CustomAccessibilityActionUpdates actions;
flutter::SemanticsNodeUpdates nodes;

flutter::SemanticsNode root_node;
root_node.id = kRootNodeId;
root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kIsInMutuallyExclusiveGroup) |
static_cast<int32_t>(flutter::SemanticsFlags::kIsEnabled) |
static_cast<int32_t>(flutter::SemanticsFlags::kHasCheckedState) |
static_cast<int32_t>(flutter::SemanticsFlags::kHasEnabledState);
nodes[root_node.id] = root_node;
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);

SemanticsObjectContainer* rootContainer = flutterView.accessibilityElements[0];
FlutterSemanticsObject* rootNode = [rootContainer accessibilityElementAtIndex:0];

XCTAssertTrue((rootNode.accessibilityTraits & UIAccessibilityTraitButton) > 0);
XCTAssertNil(rootNode.accessibilityValue);
}

- (void)testLayoutChangeWithNonAccessibilityElement {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
Expand Down

0 comments on commit e7586d7

Please sign in to comment.