Skip to content

Commit 09343ca

Browse files
authored
Make scrollable unfocusable when voiceover is running (flutter#27118)
1 parent 358a8e8 commit 09343ca

File tree

7 files changed

+71
-8
lines changed

7 files changed

+71
-8
lines changed

shell/platform/darwin/ios/framework/Source/FlutterViewController.mm

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1228,7 +1228,8 @@ - (void)onAccessibilityStatusChanged:(NSNotification*)notification {
12281228
platformView->SetSemanticsEnabled(true);
12291229
platformView->SetAccessibilityFeatures(flags);
12301230
#else
1231-
bool enabled = UIAccessibilityIsVoiceOverRunning() || UIAccessibilityIsSwitchControlRunning();
1231+
_isVoiceOverRunning = UIAccessibilityIsVoiceOverRunning();
1232+
bool enabled = _isVoiceOverRunning || UIAccessibilityIsSwitchControlRunning();
12321233
if (enabled)
12331234
flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kAccessibleNavigation);
12341235
platformView->SetSemanticsEnabled(enabled || UIAccessibilityIsSpeakScreenEnabled());

shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ extern NSNotificationName const FlutterViewControllerShowHomeIndicator;
2626
@interface FlutterViewController ()
2727

2828
@property(nonatomic, readonly) BOOL isPresentingViewController;
29+
@property(nonatomic, readonly) BOOL isVoiceOverRunning;
2930
- (fml::WeakPtr<FlutterViewController>)getWeakPtr;
3031
- (std::shared_ptr<flutter::FlutterPlatformViewsController>&)platformViewsController;
3132
- (FlutterRestorationPlugin*)restorationPlugin;

shell/platform/darwin/ios/framework/Source/SemanticsObject.mm

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -193,12 +193,6 @@ - (void)accessibilityBridgeDidFinishUpdate {
193193
[self setFrame:[_semanticsObject accessibilityFrame]];
194194
[self setContentSize:[self contentSizeInternal]];
195195
[self setContentOffset:[self contentOffsetInternal] animated:NO];
196-
if (self.contentSize.width > self.frame.size.width ||
197-
self.contentSize.height > self.frame.size.height) {
198-
self.isAccessibilityElement = YES;
199-
} else {
200-
self.isAccessibilityElement = NO;
201-
}
202196
}
203197

204198
- (void)setChildren:(NSArray<SemanticsObject*>*)children {
@@ -219,6 +213,18 @@ - (id)accessibilityContainer {
219213
return _container.get();
220214
}
221215

216+
- (BOOL)isAccessibilityElement {
217+
if (![_semanticsObject isAccessibilityBridgeAlive]) {
218+
return NO;
219+
}
220+
if (self.contentSize.width > self.frame.size.width ||
221+
self.contentSize.height > self.frame.size.height) {
222+
return !_semanticsObject.bridge->isVoiceOverRunning();
223+
} else {
224+
return NO;
225+
}
226+
}
227+
222228
// private methods
223229

224230
- (float)scrollExtentMax {

shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
window_ = [[UIWindow alloc] initWithFrame:kScreenSize];
3232
[window_ addSubview:view_];
3333
}
34+
bool isVoiceOverRunning() const override { return isVoiceOverRunningValue; }
3435
UIView* view() const override { return view_; }
3536
UIView<UITextInput>* textInputView() override { return nil; }
3637
void DispatchSemanticsAction(int32_t id, SemanticsAction action) override {
@@ -49,6 +50,7 @@ void AccessibilityObjectDidLoseFocus(int32_t id) override {}
4950
return nil;
5051
}
5152
std::vector<SemanticsActionObservation> observations;
53+
bool isVoiceOverRunningValue;
5254

5355
private:
5456
UIView* view_;
@@ -344,6 +346,30 @@ - (void)testFlutterScrollableSemanticsObjectIsNotHittestable {
344346
XCTAssertEqual([scrollable hitTest:CGPointMake(10, 10) withEvent:nil], nil);
345347
}
346348

349+
- (void)testFlutterScrollableSemanticsObjectIsHiddenWhenVoiceOverIsRunning {
350+
flutter::MockAccessibilityBridge* mock = new flutter::MockAccessibilityBridge();
351+
mock->isVoiceOverRunningValue = false;
352+
fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(mock);
353+
fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
354+
355+
flutter::SemanticsNode node;
356+
node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kHasImplicitScrolling);
357+
node.actions = flutter::kHorizontalScrollSemanticsActions;
358+
node.rect = SkRect::MakeXYWH(0, 0, 100, 200);
359+
node.scrollExtentMax = 100.0;
360+
node.scrollPosition = 0.0;
361+
362+
FlutterSemanticsObject* delegate = [[FlutterSemanticsObject alloc] initWithBridge:bridge uid:0];
363+
FlutterScrollableSemanticsObject* scrollable =
364+
[[FlutterScrollableSemanticsObject alloc] initWithSemanticsObject:delegate];
365+
SemanticsObject* scrollable_object = static_cast<SemanticsObject*>(scrollable);
366+
[scrollable_object setSemanticsNode:&node];
367+
[scrollable_object accessibilityBridgeDidFinishUpdate];
368+
XCTAssertTrue(scrollable_object.isAccessibilityElement);
369+
mock->isVoiceOverRunningValue = true;
370+
XCTAssertFalse(scrollable_object.isAccessibilityElement);
371+
}
372+
347373
- (void)testSemanticsObjectBuildsAttributedString {
348374
fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
349375
new flutter::MockAccessibilityBridge());

shell/platform/darwin/ios/framework/Source/accessibility_bridge.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
#include "flutter/lib/ui/semantics/custom_accessibility_action.h"
1919
#include "flutter/lib/ui/semantics/semantics_node.h"
2020
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h"
21-
#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h"
2221
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h"
2322
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterView.h"
23+
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"
2424
#import "flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h"
2525
#import "flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_ios.h"
2626
#include "third_party/skia/include/core/SkRect.h"
@@ -68,6 +68,8 @@ class AccessibilityBridge final : public AccessibilityBridgeIos {
6868

6969
UIView* view() const override { return view_controller_.view; }
7070

71+
bool isVoiceOverRunning() const override { return view_controller_.isVoiceOverRunning; }
72+
7173
fml::WeakPtr<AccessibilityBridge> GetWeakPtr();
7274

7375
std::shared_ptr<FlutterPlatformViewsController> GetPlatformViewsController() const override {

shell/platform/darwin/ios/framework/Source/accessibility_bridge_ios.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class AccessibilityBridgeIos {
2121
public:
2222
virtual ~AccessibilityBridgeIos() = default;
2323
virtual UIView* view() const = 0;
24+
virtual bool isVoiceOverRunning() const = 0;
2425
virtual UIView<UITextInput>* textInputView() = 0;
2526
virtual void DispatchSemanticsAction(int32_t id, flutter::SemanticsAction action) = 0;
2627
virtual void DispatchSemanticsAction(int32_t id,

shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,32 @@ - (void)testUpdateSemanticsOneNode {
224224
OCMVerifyAll(mockFlutterView);
225225
}
226226

227+
- (void)testIsVoiceOverRunning {
228+
flutter::MockDelegate mock_delegate;
229+
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
230+
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
231+
/*platform=*/thread_task_runner,
232+
/*raster=*/thread_task_runner,
233+
/*ui=*/thread_task_runner,
234+
/*io=*/thread_task_runner);
235+
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
236+
/*delegate=*/mock_delegate,
237+
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
238+
/*platform_views_controller=*/nil,
239+
/*task_runners=*/runners);
240+
id mockFlutterView = OCMClassMock([FlutterView class]);
241+
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
242+
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
243+
OCMStub([mockFlutterViewController isVoiceOverRunning]).andReturn(YES);
244+
245+
__block auto bridge =
246+
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
247+
/*platform_view=*/platform_view.get(),
248+
/*platform_views_controller=*/nil);
249+
250+
XCTAssertTrue(bridge->isVoiceOverRunning());
251+
}
252+
227253
- (void)testSemanticsDeallocated {
228254
@autoreleasepool {
229255
flutter::MockDelegate mock_delegate;

0 commit comments

Comments
 (0)