Commit 9c6c69f1 authored by Leonard Grey's avatar Leonard Grey Committed by Commit Bot

Mac a11y: Improve live region support in UI

- Removes restriction that live region changed events need to be fired from
nodes with the live region role, since we don't actually do this in UI code.
- Announces the node's name as the live region text, falling back to inner text
if name isn't set. Again, this tracks how we actually use the notifications.
- Debounces multiple notifications from the same node to one per 20ms

Bug: 1015002
Change-Id: Id564c85cd0ab0888877d4cf4b12fb872d1306284
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1871830
Commit-Queue: Leonard Grey <lgrey@chromium.org>
Reviewed-by: default avatarAaron Leventhal <aleventhal@chromium.org>
Cr-Commit-Position: refs/heads/master@{#711412}
parent e5a0ab25
...@@ -21,21 +21,15 @@ ...@@ -21,21 +21,15 @@
#import "ui/gfx/mac/coordinate_conversion.h" #import "ui/gfx/mac/coordinate_conversion.h"
#include "ui/strings/grit/ui_strings.h" #include "ui/strings/grit/ui_strings.h"
@interface AXPlatformNodeCocoa (Private)
// Helper function for string attributes that don't require extra processing.
- (NSString*)getStringAttribute:(ax::mojom::StringAttribute)attribute;
// Returns AXValue, or nil if AXValue isn't an NSString.
- (NSString*)getAXValueAsString;
// Returns the text that should be announced for an event with type |eventType|,
// or nil if it shouldn't be announced.
- (NSString*)announcementTextForEvent:(ax::mojom::Event)eventType;
@end
namespace { namespace {
// Same length as web content/WebKit.
static int kLiveRegionDebounceMillis = 20;
using RoleMap = std::map<ax::mojom::Role, NSString*>; using RoleMap = std::map<ax::mojom::Role, NSString*>;
using EventMap = std::map<ax::mojom::Event, NSString*>; using EventMap = std::map<ax::mojom::Event, NSString*>;
using ActionList = std::vector<std::pair<ax::mojom::Action, NSString*>>; using ActionList = std::vector<std::pair<ax::mojom::Action, NSString*>>;
using AnnouncementSpec = std::pair<base::scoped_nsobject<NSString>, bool>;
RoleMap BuildRoleMap() { RoleMap BuildRoleMap() {
const RoleMap::value_type roles[] = { const RoleMap::value_type roles[] = {
...@@ -317,10 +311,12 @@ const ActionList& GetActionList() { ...@@ -317,10 +311,12 @@ const ActionList& GetActionList() {
return *action_map; return *action_map;
} }
void PostAnnouncementNotification(NSString* announcement) { void PostAnnouncementNotification(NSString* announcement, bool is_polite) {
NSAccessibilityPriorityLevel priority =
is_polite ? NSAccessibilityPriorityMedium : NSAccessibilityPriorityHigh;
NSDictionary* notification_info = @{ NSDictionary* notification_info = @{
NSAccessibilityAnnouncementKey : announcement, NSAccessibilityAnnouncementKey : announcement,
NSAccessibilityPriorityKey : @(NSAccessibilityPriorityHigh) NSAccessibilityPriorityKey : @(priority)
}; };
NSAccessibilityPostNotificationWithUserInfo( NSAccessibilityPostNotificationWithUserInfo(
[NSApp mainWindow], NSAccessibilityAnnouncementRequestedNotification, [NSApp mainWindow], NSAccessibilityAnnouncementRequestedNotification,
...@@ -350,8 +346,27 @@ bool AlsoUseShowMenuActionForDefaultAction(const ui::AXNodeData& data) { ...@@ -350,8 +346,27 @@ bool AlsoUseShowMenuActionForDefaultAction(const ui::AXNodeData& data) {
} // namespace } // namespace
@interface AXPlatformNodeCocoa (Private)
// Helper function for string attributes that don't require extra processing.
- (NSString*)getStringAttribute:(ax::mojom::StringAttribute)attribute;
// Returns AXValue, or nil if AXValue isn't an NSString.
- (NSString*)getAXValueAsString;
// Returns the data necessary to queue an NSAccessibility announcement if
// |eventType| should be announced, or nullptr otherwise.
- (std::unique_ptr<AnnouncementSpec>)announcementForEvent:
(ax::mojom::Event)eventType;
// Ask the system to announce |announcementText|. This is debounced to happen
// at most every |kLiveRegionDebounceMillis| per node, with only the most
// recent announcement text read, to account for situations with multiple
// notifications happening one after another (for example, results for
// find-in-page updating rapidly as they come in from subframes).
- (void)scheduleLiveRegionAnnouncement:
(std::unique_ptr<AnnouncementSpec>)polite;
@end
@implementation AXPlatformNodeCocoa { @implementation AXPlatformNodeCocoa {
ui::AXPlatformNodeBase* node_; // Weak. Retains us. ui::AXPlatformNodeBase* node_; // Weak. Retains us.
std::unique_ptr<AnnouncementSpec> pendingAnnouncement_;
} }
@synthesize node = node_; @synthesize node = node_;
...@@ -408,25 +423,49 @@ bool AlsoUseShowMenuActionForDefaultAction(const ui::AXNodeData& data) { ...@@ -408,25 +423,49 @@ bool AlsoUseShowMenuActionForDefaultAction(const ui::AXNodeData& data) {
return [value isKindOfClass:[NSString class]] ? value : nil; return [value isKindOfClass:[NSString class]] ? value : nil;
} }
- (NSString*)announcementTextForEvent:(ax::mojom::Event)eventType { - (std::unique_ptr<AnnouncementSpec>)announcementForEvent:
if (eventType == ax::mojom::Event::kAlert && (ax::mojom::Event)eventType {
node_->GetData().role == ax::mojom::Role::kAlert) { // Only alerts and live region changes should be announced.
// If there's no explicitly set accessible name, fall back to DCHECK(eventType == ax::mojom::Event::kAlert ||
// the inner text. eventType == ax::mojom::Event::kLiveRegionChanged);
NSString* name = std::string liveStatus =
[self getStringAttribute:ax::mojom::StringAttribute::kName]; node_->GetStringAttribute(ax::mojom::StringAttribute::kLiveStatus);
return [name length] > 0 ? name // If live status is explicitly set to off, don't announce.
: base::SysUTF16ToNSString(node_->GetInnerText()); if (liveStatus == "off")
} else if (eventType == ax::mojom::Event::kLiveRegionChanged && return nullptr;
node_->GetData().HasStringAttribute(
ax::mojom::StringAttribute::kContainerLiveStatus)) { NSString* name = [self getStringAttribute:ax::mojom::StringAttribute::kName];
// Live regions announce their inner text. NSString* announcementText =
return base::SysUTF16ToNSString(node_->GetInnerText()); [name length] > 0 ? name
: base::SysUTF16ToNSString(node_->GetInnerText());
if ([announcementText length] == 0)
return nullptr;
return std::make_unique<AnnouncementSpec>(
base::scoped_nsobject<NSString>([announcementText retain]),
liveStatus != "assertive");
}
- (void)scheduleLiveRegionAnnouncement:
(std::unique_ptr<AnnouncementSpec>)announcement {
if (pendingAnnouncement_) {
// An announcement is already in flight, so just reset the contents. This is
// threadsafe because the dispatch is on the main queue.
pendingAnnouncement_ = std::move(announcement);
return;
} }
// Only alerts and live regions have something to announce.
return nil;
}
pendingAnnouncement_ = std::move(announcement);
dispatch_after(kLiveRegionDebounceMillis * NSEC_PER_MSEC,
dispatch_get_main_queue(), ^{
if (!pendingAnnouncement_) {
return;
}
PostAnnouncementNotification(pendingAnnouncement_->first,
pendingAnnouncement_->second);
pendingAnnouncement_.reset();
});
}
// NSAccessibility informal protocol implementation. // NSAccessibility informal protocol implementation.
- (BOOL)accessibilityIsIgnored { - (BOOL)accessibilityIsIgnored {
...@@ -1163,10 +1202,14 @@ gfx::NativeViewAccessible AXPlatformNodeMac::GetNativeViewAccessible() { ...@@ -1163,10 +1202,14 @@ gfx::NativeViewAccessible AXPlatformNodeMac::GetNativeViewAccessible() {
void AXPlatformNodeMac::NotifyAccessibilityEvent(ax::mojom::Event event_type) { void AXPlatformNodeMac::NotifyAccessibilityEvent(ax::mojom::Event event_type) {
GetNativeViewAccessible(); GetNativeViewAccessible();
// Handle special cases. // Handle special cases.
NSString* announcement_text =
[native_node_ announcementTextForEvent:event_type]; // Alerts and live regions go through the announcement API instead of the
if (announcement_text) { // regular NSAccessibility notification system.
PostAnnouncementNotification(announcement_text); if (event_type == ax::mojom::Event::kAlert ||
event_type == ax::mojom::Event::kLiveRegionChanged) {
if (auto announcement = [native_node_ announcementForEvent:event_type]) {
[native_node_ scheduleLiveRegionAnnouncement:std::move(announcement)];
}
return; return;
} }
if (event_type == ax::mojom::Event::kSelection) { if (event_type == ax::mojom::Event::kSelection) {
...@@ -1193,7 +1236,7 @@ void AXPlatformNodeMac::NotifyAccessibilityEvent(ax::mojom::Event event_type) { ...@@ -1193,7 +1236,7 @@ void AXPlatformNodeMac::NotifyAccessibilityEvent(ax::mojom::Event event_type) {
} }
void AXPlatformNodeMac::AnnounceText(const base::string16& text) { void AXPlatformNodeMac::AnnounceText(const base::string16& text) {
PostAnnouncementNotification(base::SysUTF16ToNSString(text)); PostAnnouncementNotification(base::SysUTF16ToNSString(text), false);
} }
int AXPlatformNodeMac::GetIndexInParent() { int AXPlatformNodeMac::GetIndexInParent() {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment