Repository navigation
[iOS] Give hidden semantics nodes in scrollables a non-empty accessib… - #193723
uditswaroopa09 wants to merge 1 commit into
Conversation
…ility frame Since iOS 27, VoiceOver no longer treats elements whose accessibilityFrame is CGRectZero as candidates for the next element. Off-screen children of a scrollable are flagged isHidden and reported CGRectZero, so a one-finger swipe stopped at the last visible item instead of triggering accessibilityScrollToVisible and an implicit scroll. Hidden nodes inside a FlutterScrollableSemanticsObject now report a 1pt strip at the leading or trailing edge of the scrollable's viewport, depending on which side the node lives on. VoiceOver handles them like clipped elements, and the existing showOnScreen path scrolls them in. Fixes flutter#192774
There was a problem hiding this comment.
Code Review
This pull request updates SemanticsObject.mm to calculate and return a non-empty frame for hidden nodes inside scrollable viewports, preventing VoiceOver from skipping them, and adds corresponding tests in SemanticsObjectTest.mm. The review feedback highlights memory leaks in the new tests due to manual heap allocation of MockAccessibilityBridge and suggests using std::unique_ptr for lifetime management. It also recommends replacing XCTAssertTrue with XCTAssertEqual in assertions to provide clearer failure messages.
| fml::WeakPtrFactory<flutter::testing::MockAccessibilityBridge> factory( | ||
| new flutter::testing::MockAccessibilityBridge()); | ||
| fml::WeakPtr<flutter::testing::MockAccessibilityBridge> bridge = factory.GetWeakPtr(); |
There was a problem hiding this comment.
The test allocates MockAccessibilityBridge on the heap using new but never deletes it, leading to a memory leak. Since fml::WeakPtrFactory does not take ownership of the pointer, we should manage its lifetime using std::unique_ptr to prevent memory leaks.
auto bridge_impl = std::make_unique<flutter::testing::MockAccessibilityBridge>();\n fml::WeakPtrFactory<flutter::testing::MockAccessibilityBridge> factory(bridge_impl.get());\n fml::WeakPtr<flutter::testing::MockAccessibilityBridge> bridge = factory.GetWeakPtr();
References
- C++ code should follow the Google C++ Style Guide, which recommends using std::unique_ptr to manage ownership of dynamically allocated objects. (link)
| XCTAssertTrue(bridge->observations.size() == 1); | ||
| XCTAssertTrue(bridge->observations[0].id == 3); | ||
| XCTAssertTrue(bridge->observations[0].action == flutter::SemanticsAction::kShowOnScreen); |
There was a problem hiding this comment.
Using XCTAssertEqual instead of XCTAssertTrue with an equality operator provides much better failure messages because it prints both the expected and actual values when the assertion fails.
XCTAssertEqual(bridge->observations.size(), 1U);\n XCTAssertEqual(bridge->observations[0].id, 3);\n XCTAssertEqual(bridge->observations[0].action, flutter::SemanticsAction::kShowOnScreen);
| fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory( | ||
| new flutter::testing::MockAccessibilityBridge()); | ||
| fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr(); |
There was a problem hiding this comment.
The test allocates MockAccessibilityBridge on the heap using new but never deletes it, leading to a memory leak. Since fml::WeakPtrFactory does not take ownership of the pointer, we should manage its lifetime using std::unique_ptr to prevent memory leaks.
auto bridge_impl = std::make_unique<flutter::testing::MockAccessibilityBridge>();\n fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(bridge_impl.get());\n fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
References
- C++ code should follow the Google C++ Style Guide, which recommends using std::unique_ptr to manage ownership of dynamically allocated objects. (link)
| fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory( | ||
| new flutter::testing::MockAccessibilityBridge()); | ||
| fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr(); |
There was a problem hiding this comment.
The test allocates MockAccessibilityBridge on the heap using new but never deletes it, leading to a memory leak. Since fml::WeakPtrFactory does not take ownership of the pointer, we should manage its lifetime using std::unique_ptr to prevent memory leaks.
auto bridge_impl = std::make_unique<flutter::testing::MockAccessibilityBridge>();\n fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(bridge_impl.get());\n fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
References
- C++ code should follow the Google C++ Style Guide, which recommends using std::unique_ptr to manage ownership of dynamically allocated objects. (link)
Thanks. These follow the existing convention in SemanticsObjectTest.mm — |
|
Hi @LongCatIsLooong @LouiseHsu Do you have an estimated timeline for approving this pull request? We urgently need this fix. Thanks! |
I looked at this PR but I'm still debugging the issue to figure out if there's a way to fix the root cause. The plan is to put a fix in the upcoming 3.50 release. If you need a fix right now the fastest way is to apply the patch locally (https://github.com/flutter/flutter/blob/master/docs/releases/Flutter-build-release-channels.md#will-a-particular-bug-fix-be-provided-in-a-hotfix-release). |
|
Hi, any news on this ticket? This is causing significant issues for impaired iOS 27 users who leverage Voiceover. Thanks! |
|
(keeping this open in case the other PR isn't a good cherry-pick candidate). |
Since iOS 27, VoiceOver no longer treats elements whose
accessibilityFrameis
CGRectZeroas candidates for the next focusable element. Flutter'sscrollables flag off-screen children as
isHiddenand reportCGRectZerofor their frame, causing single-finger swipe to stop at the last visible
item instead of triggering
accessibilityScrollToVisibleand scrollingimplicitly.
Hidden nodes inside a
FlutterScrollableSemanticsObjectnow report a 1ptstrip at the leading or trailing edge of the scrollable's viewport (depending
on which side the node lives on). VoiceOver treats these like clipped elements
and fires the existing
showOnScreenpath to scroll them into view.Vertical and horizontal scrollables are both handled. Hidden nodes with no
scrollable ancestor retain their
CGRectZeroframe — no behaviour change forthose.
Related Issues
Fixes #192774
Tests
Three new XCTest cases in
SemanticsObjectTest.mm:testHiddenNodeInVerticalScrollableHasNonEmptyFrameAtViewportEdgetestHiddenNodeInHorizontalScrollableHasNonEmptyFrameAtViewportEdgetestHiddenNodeOutsideScrollableKeepsEmptyFrameChecklist