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 @@
#import "ui/gfx/mac/coordinate_conversion.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 {
// Same length as web content/WebKit.
static int kLiveRegionDebounceMillis = 20;
using RoleMap = std::map<ax::mojom::Role, NSString*>;
using EventMap = std::map<ax::mojom::Event, NSString*>;
using ActionList = std::vector<std::pair<ax::mojom::Action, NSString*>>;
using AnnouncementSpec = std::pair<base::scoped_nsobject<NSString>, bool>;
RoleMap BuildRoleMap() {
const RoleMap::value_type roles[] = {
......@@ -317,10 +311,12 @@ const ActionList& GetActionList() {
return *action_map;
}
void PostAnnouncementNotification(NSString* announcement) {
void PostAnnouncementNotification(NSString* announcement, bool is_polite) {
NSAccessibilityPriorityLevel priority =
is_polite ? NSAccessibilityPriorityMedium : NSAccessibilityPriorityHigh;
NSDictionary* notification_info = @{
NSAccessibilityAnnouncementKey : announcement,
NSAccessibilityPriorityKey : @(NSAccessibilityPriorityHigh)
NSAccessibilityPriorityKey : @(priority)
};
NSAccessibilityPostNotificationWithUserInfo(
[NSApp mainWindow], NSAccessibilityAnnouncementRequestedNotification,
......@@ -350,8 +346,27 @@ bool AlsoUseShowMenuActionForDefaultAction(const ui::AXNodeData& data) {
} // 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 {
ui::AXPlatformNodeBase* node_; // Weak. Retains us.
std::unique_ptr<AnnouncementSpec> pendingAnnouncement_;
}
@synthesize node = node_;
......@@ -408,25 +423,49 @@ bool AlsoUseShowMenuActionForDefaultAction(const ui::AXNodeData& data) {
return [value isKindOfClass:[NSString class]] ? value : nil;
}
- (NSString*)announcementTextForEvent:(ax::mojom::Event)eventType {
if (eventType == ax::mojom::Event::kAlert &&
node_->GetData().role == ax::mojom::Role::kAlert) {
// If there's no explicitly set accessible name, fall back to
// the inner text.
NSString* name =
[self getStringAttribute:ax::mojom::StringAttribute::kName];
return [name length] > 0 ? name
- (std::unique_ptr<AnnouncementSpec>)announcementForEvent:
(ax::mojom::Event)eventType {
// Only alerts and live region changes should be announced.
DCHECK(eventType == ax::mojom::Event::kAlert ||
eventType == ax::mojom::Event::kLiveRegionChanged);
std::string liveStatus =
node_->GetStringAttribute(ax::mojom::StringAttribute::kLiveStatus);
// If live status is explicitly set to off, don't announce.
if (liveStatus == "off")
return nullptr;
NSString* name = [self getStringAttribute:ax::mojom::StringAttribute::kName];
NSString* announcementText =
[name length] > 0 ? name
: base::SysUTF16ToNSString(node_->GetInnerText());
} else if (eventType == ax::mojom::Event::kLiveRegionChanged &&
node_->GetData().HasStringAttribute(
ax::mojom::StringAttribute::kContainerLiveStatus)) {
// Live regions announce their inner text.
return base::SysUTF16ToNSString(node_->GetInnerText());
}
// Only alerts and live regions have something to announce.
return nil;
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;
}
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.
- (BOOL)accessibilityIsIgnored {
......@@ -1163,10 +1202,14 @@ gfx::NativeViewAccessible AXPlatformNodeMac::GetNativeViewAccessible() {
void AXPlatformNodeMac::NotifyAccessibilityEvent(ax::mojom::Event event_type) {
GetNativeViewAccessible();
// Handle special cases.
NSString* announcement_text =
[native_node_ announcementTextForEvent:event_type];
if (announcement_text) {
PostAnnouncementNotification(announcement_text);
// Alerts and live regions go through the announcement API instead of the
// regular NSAccessibility notification system.
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;
}
if (event_type == ax::mojom::Event::kSelection) {
......@@ -1193,7 +1236,7 @@ void AXPlatformNodeMac::NotifyAccessibilityEvent(ax::mojom::Event event_type) {
}
void AXPlatformNodeMac::AnnounceText(const base::string16& text) {
PostAnnouncementNotification(base::SysUTF16ToNSString(text));
PostAnnouncementNotification(base::SysUTF16ToNSString(text), false);
}
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