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:
parent
97b7700dae
commit
99e1a0652c
@ -46,17 +46,22 @@ enum class SemanticsAction : int32_t {
|
|||||||
kScrollToOffset = 1 << 23,
|
kScrollToOffset = 1 << 23,
|
||||||
};
|
};
|
||||||
|
|
||||||
const int kVerticalScrollSemanticsActions =
|
constexpr int kVerticalScrollSemanticsActions =
|
||||||
static_cast<int32_t>(SemanticsAction::kScrollUp) |
|
static_cast<int32_t>(SemanticsAction::kScrollUp) |
|
||||||
static_cast<int32_t>(SemanticsAction::kScrollDown);
|
static_cast<int32_t>(SemanticsAction::kScrollDown);
|
||||||
|
|
||||||
const int kHorizontalScrollSemanticsActions =
|
constexpr int kHorizontalScrollSemanticsActions =
|
||||||
static_cast<int32_t>(SemanticsAction::kScrollLeft) |
|
static_cast<int32_t>(SemanticsAction::kScrollLeft) |
|
||||||
static_cast<int32_t>(SemanticsAction::kScrollRight);
|
static_cast<int32_t>(SemanticsAction::kScrollRight);
|
||||||
|
|
||||||
const int kScrollableSemanticsActions =
|
constexpr int kScrollableSemanticsActions =
|
||||||
kVerticalScrollSemanticsActions | kHorizontalScrollSemanticsActions;
|
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
|
/// C/C++ representation of `SemanticsRole` defined in
|
||||||
/// `lib/ui/semantics.dart`.
|
/// `lib/ui/semantics.dart`.
|
||||||
///\warning This must match the `SemanticsRole` enum in
|
///\warning This must match the `SemanticsRole` enum in
|
||||||
|
@ -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
|
@end
|
||||||
|
|
||||||
@implementation FlutterSemanticsObject
|
@implementation FlutterSemanticsObject
|
||||||
|
@ -757,6 +757,162 @@ fml::RefPtr<fml::TaskRunner> CreateNewThread(const std::string& name) {
|
|||||||
XCTAssertNil(rootNode.accessibilityValue);
|
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 {
|
- (void)testLayoutChangeWithNonAccessibilityElement {
|
||||||
flutter::MockDelegate mock_delegate;
|
flutter::MockDelegate mock_delegate;
|
||||||
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
|
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
|
||||||
|
Loading…
x
Reference in New Issue
Block a user