Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions lib/ui/semantics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -483,9 +483,12 @@ class SemanticsFlag {
/// Platforms may use this information to make polite announcements to the
/// user to inform them of updates to this node.
///
/// An example of a live region is a [SnackBar] widget. On Android, A live
/// region causes a polite announcement to be generated automatically, even
/// if the user does not have focus of the widget.
/// An example of a live region is a [SnackBar] widget. On Android and iOS,
/// live region causes a polite announcement to be generated automatically,
/// even if the widget does not have accessibility focus. This announcement
/// may not be spoken if the OS accessibility services are already
/// announcing something else, such as reading the label of a focused
/// widget or providing a system announcement.
static const SemanticsFlag isLiveRegion = SemanticsFlag._(_kIsLiveRegionIndex);

/// The semantics node has the quality of either being "on" or "off".
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ constexpr int32_t kRootNodeId = 0;
uid:(int32_t)uid NS_DESIGNATED_INITIALIZER;

- (BOOL)nodeWillCauseScroll:(const flutter::SemanticsNode*)node;
- (BOOL)nodeShouldTriggerAnnouncement:(const flutter::SemanticsNode*)node;
- (void)collectRoutes:(NSMutableArray<SemanticsObject*>*)edges;
- (NSString*)routeName;
- (BOOL)onCustomAccessibilityAction:(FlutterCustomAccessibilityAction*)action;
Expand Down
19 changes: 19 additions & 0 deletions shell/platform/darwin/ios/framework/Source/SemanticsObject.mm
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,25 @@ - (BOOL)nodeWillCauseScroll:(const flutter::SemanticsNode*)node {
[self node].scrollPosition != node->scrollPosition;
}

/**
* Whether calling `setSemanticsNode:` with `node` should trigger an
* announcement.
*/
- (BOOL)nodeShouldTriggerAnnouncement:(const flutter::SemanticsNode*)node {
// The node dropped the live region flag, if it ever had one.
if (!node || !node->HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) {
return NO;
}

// The node has gained a new live region flag, always announce.
if (![self node].HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) {
return YES;
}

// The label has updated, and the new node has a live region flag.
return [self node].label != node->label;
}

- (BOOL)hasChildren {
if (_node.IsPlatformViewNode()) {
return YES;
Expand Down
38 changes: 38 additions & 0 deletions shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,42 @@ - (void)testReplaceChildAtIndex {
XCTAssertEqualObjects(parent.children, @[ child2 ]);
}

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

// Handle nil with no node set.
XCTAssertFalse([object nodeShouldTriggerAnnouncement:nil]);

// Handle initial setting of node with liveRegion
flutter::SemanticsNode node;
node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kIsLiveRegion);
node.label = "foo";
XCTAssertTrue([object nodeShouldTriggerAnnouncement:&node]);

// Handle nil with node set.
[object setSemanticsNode:&node];
XCTAssertFalse([object nodeShouldTriggerAnnouncement:nil]);

// Handle new node, still has live region, same label.
XCTAssertFalse([object nodeShouldTriggerAnnouncement:&node]);

// Handle update node with new label, still has live region.
flutter::SemanticsNode updatedNode;
updatedNode.flags = static_cast<int32_t>(flutter::SemanticsFlags::kIsLiveRegion);
updatedNode.label = "bar";
XCTAssertTrue([object nodeShouldTriggerAnnouncement:&updatedNode]);

// Handle dropping the live region flag.
updatedNode.flags = 0;
XCTAssertFalse([object nodeShouldTriggerAnnouncement:&updatedNode]);

// Handle adding the flag when the label has not changed.
updatedNode.label = "foo";
[object setSemanticsNode:&updatedNode];
XCTAssertTrue([object nodeShouldTriggerAnnouncement:&node]);
}

@end
22 changes: 22 additions & 0 deletions shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
flutter::CustomAccessibilityActionUpdates actions) {
BOOL layoutChanged = NO;
BOOL scrollOccured = NO;
BOOL needsAnnouncement = NO;
for (const auto& entry : actions) {
const flutter::CustomAccessibilityAction& action = entry.second;
actions_[action.id] = action;
Expand All @@ -55,6 +56,7 @@
SemanticsObject* object = GetOrCreateObject(node.id, nodes);
layoutChanged = layoutChanged || [object nodeWillCauseLayoutChange:&node];
scrollOccured = scrollOccured || [object nodeWillCauseScroll:&node];
needsAnnouncement = [object nodeShouldTriggerAnnouncement:&node];
[object setSemanticsNode:&node];
NSUInteger newChildCount = node.childrenInTraversalOrder.size();
NSMutableArray* newChildren =
Expand Down Expand Up @@ -95,6 +97,26 @@
} else if (object.platformViewSemanticsContainer) {
object.platformViewSemanticsContainer = nil;
}
if (needsAnnouncement) {
// Try to be more polite - iOS 11+ supports
// UIAccessibilitySpeechAttributeQueueAnnouncement which should avoid
// interrupting system notifications or other elements.
// Expectation: roughly match the behavior of polite announcements on
// Android.
NSString* announcement =
[[[NSString alloc] initWithUTF8String:object.node.label.c_str()] autorelease];
if (@available(iOS 11.0, *)) {
UIAccessibilityPostNotification(
UIAccessibilityAnnouncementNotification,
[[[NSAttributedString alloc]
initWithString:announcement
attributes:@{
UIAccessibilitySpeechAttributeQueueAnnouncement : @YES
}] autorelease]);
} else {
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcement);
}
}
}

SemanticsObject* root = objects_.get()[@(kRootNodeId)];
Expand Down