Bug 1915262 - Fire queued live region event from content in MacOS. r=morgan

Introduce a gecko live region changed event and fire it from within content.
This way it gets coalesced in the case of many insertions/deletions.

Also, rely on text insert/delete instead of reorder because there can be cases
where the text in a leaf changes as opposed to a tree mutation.
We get text insert/delete on mutations too, so that should cover it.

Differential Revision: https://phabricator.services.mozilla.com/D224388
This commit is contained in:
Eitan Isaacson 2024-10-10 17:36:26 +00:00
parent fc07ffe892
commit 949bd3acfe
9 changed files with 84 additions and 44 deletions

View File

@ -522,6 +522,7 @@ static const char kEventTypeNames[][40] = {
"live region added", // EVENT_LIVE_REGION_ADDED
"live region removed", // EVENT_LIVE_REGION_REMOVED
"inner reorder", // EVENT_INNER_REORDER
"live region changed", // EVENT_LIVE_REGION_CHANGED
};
#endif

View File

@ -213,10 +213,15 @@ interface nsIAccessibleEvent : nsISupports
*/
const unsigned long EVENT_INNER_REORDER = 0x0028;
/**
* A live region's contents has changed. Mac Only.
*/
const unsigned long EVENT_LIVE_REGION_CHANGED = 0x0029;
/**
* Help make sure event map does not get out-of-line.
*/
const unsigned long EVENT_LAST_ENTRY = 0x0029;
const unsigned long EVENT_LAST_ENTRY = 0x002a;
/**
* The type of event, based on the enumerated event values

View File

@ -54,6 +54,8 @@ class AccessibleWrap : public LocalAccessible {
virtual nsresult HandleAccEvent(AccEvent* aEvent) override;
static bool IsLiveRegion(nsIContent* aContent);
protected:
friend class xpcAccessibleMacInterface;

View File

@ -35,34 +35,11 @@ AccessibleWrap::AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc)
: LocalAccessible(aContent, aDoc),
mNativeObject(nil),
mNativeInited(false) {
if (aContent && aContent->IsElement() && aDoc) {
if (aContent && aDoc && IsLiveRegion(aContent)) {
// Check if this accessible is a live region and queue it
// it for dispatching an event after it has been inserted.
DocAccessibleWrap* doc = static_cast<DocAccessibleWrap*>(aDoc);
static const dom::Element::AttrValuesArray sLiveRegionValues[] = {
nsGkAtoms::OFF, nsGkAtoms::polite, nsGkAtoms::assertive, nullptr};
int32_t attrValue = nsAccUtils::FindARIAAttrValueIn(
aContent->AsElement(), nsGkAtoms::aria_live, sLiveRegionValues,
eIgnoreCase);
if (attrValue == 0) {
// aria-live is "off", do nothing.
} else if (attrValue > 0) {
// aria-live attribute is polite or assertive. It's live!
doc->QueueNewLiveRegion(this);
} else if (const nsRoleMapEntry* roleMap =
aria::GetRoleMap(aContent->AsElement())) {
// aria role defines it as a live region. It's live!
if (roleMap->liveAttRule == ePoliteLiveAttr ||
roleMap->liveAttRule == eAssertiveLiveAttr) {
doc->QueueNewLiveRegion(this);
}
} else if (nsStaticAtom* value = GetAccService()->MarkupAttribute(
aContent, nsGkAtoms::aria_live)) {
// HTML element defines it as a live region. It's live!
if (value == nsGkAtoms::polite || value == nsGkAtoms::assertive) {
doc->QueueNewLiveRegion(this);
}
}
doc->QueueNewLiveRegion(this);
}
}
@ -167,11 +144,58 @@ nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) {
doc->ProcessNewLiveRegions();
}
if ((eventType == nsIAccessibleEvent::EVENT_TEXT_INSERTED ||
eventType == nsIAccessibleEvent::EVENT_TEXT_REMOVED ||
eventType == nsIAccessibleEvent::EVENT_NAME_CHANGE) &&
!aEvent->FromUserInput()) {
for (LocalAccessible* container = aEvent->GetAccessible(); container;
container = container->LocalParent()) {
if (container->HasOwnContent() && IsLiveRegion(container->GetContent())) {
// We rely on EventQueue::CoalesceEvents to remove duplicates
Document()->FireDelayedEvent(
nsIAccessibleEvent::EVENT_LIVE_REGION_CHANGED, container);
}
}
}
return NS_OK;
NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
}
bool AccessibleWrap::IsLiveRegion(nsIContent* aContent) {
if (!aContent || !aContent->IsElement()) {
return false;
}
static const dom::Element::AttrValuesArray sLiveRegionValues[] = {
nsGkAtoms::OFF, nsGkAtoms::polite, nsGkAtoms::assertive, nullptr};
int32_t attrValue = nsAccUtils::FindARIAAttrValueIn(
aContent->AsElement(), nsGkAtoms::aria_live, sLiveRegionValues,
eIgnoreCase);
if (attrValue == 0) {
// aria-live is "off", do nothing.
} else if (attrValue > 0) {
// aria-live attribute is polite or assertive. It's live!
return true;
} else if (const nsRoleMapEntry* roleMap =
aria::GetRoleMap(aContent->AsElement())) {
// aria role defines it as a live region. It's live!
if (roleMap->liveAttRule == ePoliteLiveAttr ||
roleMap->liveAttRule == eAssertiveLiveAttr) {
return true;
}
} else if (nsStaticAtom* value = GetAccService()->MarkupAttribute(
aContent, nsGkAtoms::aria_live)) {
// HTML element defines it as a live region. It's live!
if (value == nsGkAtoms::polite || value == nsGkAtoms::assertive) {
return true;
}
}
return false;
}
////////////////////////////////////////////////////////////////////////////////
// AccessibleWrap protected

View File

@ -101,5 +101,10 @@ void DocAccessibleWrap::ProcessNewLiveRegions() {
void DocAccessibleWrap::DoInitialUpdate() {
DocAccessible::DoInitialUpdate();
if (IsLiveRegion(mDocumentNode->GetBodyElement())) {
// Check if this doc's body element is a live region
QueueNewLiveRegion(this);
}
ProcessNewLiveRegions();
}

View File

@ -99,6 +99,7 @@ void PlatformEvent(Accessible* aTarget, uint32_t aEventType) {
aEventType != nsIAccessibleEvent::EVENT_REORDER &&
aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED &&
aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED &&
aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_CHANGED &&
aEventType != nsIAccessibleEvent::EVENT_NAME_CHANGE &&
aEventType != nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED) {
return;

View File

@ -43,7 +43,6 @@ using namespace mozilla::a11y;
@interface mozAccessible ()
- (BOOL)providesLabelNotTitle;
- (void)maybePostLiveRegionChanged;
- (void)maybePostA11yUtilNotification;
@end
@ -869,17 +868,6 @@ struct RoleDescrComparator {
return NO;
}
- (void)maybePostLiveRegionChanged {
id<MOXAccessible> liveRegion =
[self moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) {
return [moxAcc moxIsLiveRegion];
}];
if (liveRegion) {
[liveRegion moxPostNotification:@"AXLiveRegionChanged"];
}
}
- (void)maybePostA11yUtilNotification {
MOZ_ASSERT(mGeckoAccessible);
// Sometimes we use a special live region to make announcements to the user.
@ -1004,16 +992,15 @@ struct RoleDescrComparator {
case nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED:
mIsLiveRegion = false;
break;
case nsIAccessibleEvent::EVENT_REORDER:
[self maybePostLiveRegionChanged];
break;
case nsIAccessibleEvent::EVENT_NAME_CHANGE: {
case nsIAccessibleEvent::EVENT_NAME_CHANGE:
if (![self providesLabelNotTitle]) {
[self moxPostNotification:NSAccessibilityTitleChangedNotification];
}
[self maybePostLiveRegionChanged];
break;
}
case nsIAccessibleEvent::EVENT_LIVE_REGION_CHANGED:
MOZ_ASSERT(mIsLiveRegion);
[self moxPostNotification:@"AXLiveRegionChanged"];
break;
}
}

View File

@ -161,5 +161,19 @@ addAccessibleTask(
});
await liveRegionChanged;
ok(true, "changed aria-label");
liveRegionChanged = waitForMacEvent("AXLiveRegionChanged", "live");
await SpecialPowers.spawn(browser, [], () => {
content.document.getElementById("live").firstChild.data = "The hour is ";
});
await liveRegionChanged;
ok(true, "changed text leaf contents");
liveRegionChanged = waitForMacEvent("AXLiveRegionChanged", "live");
await SpecialPowers.spawn(browser, [], () => {
content.document.getElementById("live").firstChild.data = "";
});
await liveRegionChanged;
ok(true, "delete text leaf contents");
}
);

View File

@ -53,5 +53,6 @@ static const uint32_t gWinEventMap[] = {
kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED
kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED
kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_INNER_REORDER
kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_LIVE_REGION_CHANGED
// clang-format on
};