Fix extra numbers showing up when enabling VoiceControl (#163593)

Fixes https://github.com/flutter/flutter/issues/158477 and
https://github.com/flutter/flutter/issues/156368.

The excess numbers in both PRs are caused by all `SemanticObjects`
returning `YES` for `accessibilityRespondsToUserInteraction`, even if it
has no semantic actions. For example, a SemanticObject with just a label
has semantic information (the label) but no action. This PR adds a
check, ensuring that an `SemanticObjects` has at least one accessible
action before returning `YES`

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.
This commit is contained in:
LouiseHsu 2025-02-25 16:59:54 -08:00 committed by GitHub
parent 97b7700dae
commit 99e1a0652c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 177 additions and 3 deletions

View File

@ -46,17 +46,22 @@ enum class SemanticsAction : int32_t {
kScrollToOffset = 1 << 23,
};
const int kVerticalScrollSemanticsActions =
constexpr int kVerticalScrollSemanticsActions =
static_cast<int32_t>(SemanticsAction::kScrollUp) |
static_cast<int32_t>(SemanticsAction::kScrollDown);
const int kHorizontalScrollSemanticsActions =
constexpr int kHorizontalScrollSemanticsActions =
static_cast<int32_t>(SemanticsAction::kScrollLeft) |
static_cast<int32_t>(SemanticsAction::kScrollRight);
const int kScrollableSemanticsActions =
constexpr int kScrollableSemanticsActions =
kVerticalScrollSemanticsActions | kHorizontalScrollSemanticsActions;
/// The following actions are not user-initiated.
constexpr int kSystemActions =
static_cast<int32_t>(SemanticsAction::kDidGainAccessibilityFocus) |
static_cast<int32_t>(SemanticsAction::kDidLoseAccessibilityFocus);
/// C/C++ representation of `SemanticsRole` defined in
/// `lib/ui/semantics.dart`.
///\warning This must match the `SemanticsRole` enum in

View File

@ -781,6 +781,19 @@ CGRect ConvertRectToGlobal(SemanticsObject* reference, CGRect local_rect) {
}
}
- (BOOL)accessibilityRespondsToUserInteraction {
// Return true only if the node contains actions other than system actions.
if ((self.node.actions & ~flutter::kSystemActions) != 0) {
return true;
}
if (!self.node.customAccessibilityActions.empty()) {
return true;
}
return false;
}
@end
@implementation FlutterSemanticsObject

View File

@ -757,6 +757,162 @@ fml::RefPtr<fml::TaskRunner> CreateNewThread(const std::string& name) {
XCTAssertNil(rootNode.accessibilityValue);
}
- (void)testSemanticObjectWithNoAccessibilityFlagNotMarkedAsResponsiveToUserInteraction {
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=*/mock_delegate.settings_.enable_impeller
? flutter::IOSRenderingAPI::kMetal
: flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners,
/*worker_task_runner=*/nil,
/*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
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;
nodes[root_node.id] = root_node;
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
SemanticsObjectContainer* rootContainer = flutterView.accessibilityElements[0];
FlutterSemanticsObject* rootNode = [rootContainer accessibilityElementAtIndex:0];
XCTAssertFalse(rootNode.accessibilityRespondsToUserInteraction);
}
- (void)testSemanticObjectWithAccessibilityFlagsMarkedAsResponsiveToUserInteraction {
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=*/mock_delegate.settings_.enable_impeller
? flutter::IOSRenderingAPI::kMetal
: flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners,
/*worker_task_runner=*/nil,
/*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
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.actions = static_cast<int32_t>(flutter::SemanticsAction::kTap);
nodes[root_node.id] = root_node;
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
SemanticsObjectContainer* rootContainer = flutterView.accessibilityElements[0];
FlutterSemanticsObject* rootNode = [rootContainer accessibilityElementAtIndex:0];
XCTAssertTrue(rootNode.accessibilityRespondsToUserInteraction);
}
// Regression test for:
// https://github.com/flutter/flutter/issues/158477
- (void)testLabeledParentAndChildNotInteractive {
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);
FlutterPlatformViewsController* flutterPlatformViewsController =
[[FlutterPlatformViewsController alloc] init];
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/mock_delegate.settings_.enable_impeller
? flutter::IOSRenderingAPI::kMetal
: flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/flutterPlatformViewsController,
/*task_runners=*/runners,
/*worker_task_runner=*/nil,
/*is_gpu_disabled_sync_switch=*/std::make_shared<fml::SyncSwitch>());
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);
@autoreleasepool {
auto bridge = std::make_unique<flutter::AccessibilityBridge>(
/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/flutterPlatformViewsController);
flutter::SemanticsNodeUpdates nodes;
flutter::SemanticsNode parent;
parent.id = 0;
parent.rect = SkRect::MakeXYWH(0, 0, 100, 200);
parent.label = "parent_label";
flutter::SemanticsNode node;
node.id = 1;
node.rect = SkRect::MakeXYWH(0, 0, 100, 200);
node.label = "child_label";
parent.childrenInTraversalOrder.push_back(1);
parent.childrenInHitTestOrder.push_back(1);
nodes[0] = parent;
nodes[1] = node;
flutter::CustomAccessibilityActionUpdates actions;
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
SemanticsObjectContainer* parentContainer = flutterView.accessibilityElements[0];
FlutterSemanticsObject* parentNode = [parentContainer accessibilityElementAtIndex:0];
FlutterSemanticsObject* childNode = [parentContainer accessibilityElementAtIndex:1];
XCTAssertTrue([parentNode.accessibilityLabel isEqualToString:@"parent_label"]);
XCTAssertTrue([childNode.accessibilityLabel isEqualToString:@"child_label"]);
XCTAssertFalse(parentNode.accessibilityRespondsToUserInteraction);
XCTAssertFalse(childNode.accessibilityRespondsToUserInteraction);
}
}
- (void)testLayoutChangeWithNonAccessibilityElement {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");