Bug 1198336 - P1: Introduce live region added/removed events. r=Jamie,morgan

VoiceOver pre-caches live region data and does its own deltas to
know what to parts of a subtree changed, and what to announce
based on AXARIAAtomic and AXARIARelevant.

I added a removed event as well. This will help us cache a "live region"
flag in the main process and avoid sync round trips for attributes when not needed.

Differential Revision: https://phabricator.services.mozilla.com/D96291
This commit is contained in:
Eitan Isaacson 2020-11-10 23:07:20 +00:00
parent d14d817428
commit 85740d386d
10 changed files with 228 additions and 7 deletions

View File

@ -498,6 +498,8 @@ static const char kEventTypeNames[][40] = {
"text value change", // EVENT_TEXT_VALUE_CHANGE "text value change", // EVENT_TEXT_VALUE_CHANGE
"scrolling", // EVENT_SCROLLING "scrolling", // EVENT_SCROLLING
"announcement", // EVENT_ANNOUNCEMENT "announcement", // EVENT_ANNOUNCEMENT
"live region added", // EVENT_LIVE_REGION_ADDED
"live region removed", // EVENT_LIVE_REGION_REMOVED
}; };
#endif #endif

View File

@ -428,10 +428,20 @@ interface nsIAccessibleEvent : nsISupports
*/ */
const unsigned long EVENT_ANNOUNCEMENT = 0x0059; const unsigned long EVENT_ANNOUNCEMENT = 0x0059;
/**
* A live region has been introduced. Mac only.
*/
const unsigned long EVENT_LIVE_REGION_ADDED = 0x005A;
/**
* A live region has been removed (aria-live attribute changed). Mac Only.
*/
const unsigned long EVENT_LIVE_REGION_REMOVED = 0x005B;
/** /**
* Help make sure event map does not get out-of-line. * Help make sure event map does not get out-of-line.
*/ */
const unsigned long EVENT_LAST_ENTRY = 0x005A; const unsigned long EVENT_LAST_ENTRY = 0x005C;
/** /**
* The type of event, based on the enumerated event values * The type of event, based on the enumerated event values

View File

@ -5,7 +5,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "DocAccessible.h" #include "DocAccessibleWrap.h"
#include "nsObjCExceptions.h" #include "nsObjCExceptions.h"
#include "nsCocoaUtils.h" #include "nsCocoaUtils.h"
@ -30,7 +30,36 @@ using namespace mozilla;
using namespace mozilla::a11y; using namespace mozilla::a11y;
AccessibleWrap::AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc) AccessibleWrap::AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc)
: Accessible(aContent, aDoc), mNativeObject(nil), mNativeInited(false) {} : Accessible(aContent, aDoc), mNativeObject(nil), mNativeInited(false) {
if (aContent && aContent->IsElement() && aDoc) {
// 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 = aContent->AsElement()->FindAttrValueIn(
kNameSpaceID_None, 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) {
doc->QueueNewLiveRegion(this);
}
} else if (nsStaticAtom* value = GetAccService()->MarkupAttribute(
aContent, nsGkAtoms::live)) {
// HTML element defines it as a live region. It's live!
if (value == nsGkAtoms::polite || value == nsGkAtoms::assertive) {
doc->QueueNewLiveRegion(this);
}
}
}
}
AccessibleWrap::~AccessibleWrap() {} AccessibleWrap::~AccessibleWrap() {}
@ -120,11 +149,17 @@ nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) {
nsresult rv = Accessible::HandleAccEvent(aEvent); nsresult rv = Accessible::HandleAccEvent(aEvent);
NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_SUCCESS(rv, rv);
uint32_t eventType = aEvent->GetEventType();
if (eventType == nsIAccessibleEvent::EVENT_SHOW) {
DocAccessibleWrap* doc = static_cast<DocAccessibleWrap*>(Document());
doc->ProcessNewLiveRegions();
}
if (IPCAccessibilityActive()) { if (IPCAccessibilityActive()) {
return NS_OK; return NS_OK;
} }
uint32_t eventType = aEvent->GetEventType();
Accessible* eventTarget = nullptr; Accessible* eventTarget = nullptr;
switch (eventType) { switch (eventType) {
@ -222,6 +257,8 @@ nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) {
case nsIAccessibleEvent::EVENT_SELECTION: case nsIAccessibleEvent::EVENT_SELECTION:
case nsIAccessibleEvent::EVENT_SELECTION_ADD: case nsIAccessibleEvent::EVENT_SELECTION_ADD:
case nsIAccessibleEvent::EVENT_SELECTION_REMOVE: case nsIAccessibleEvent::EVENT_SELECTION_REMOVE:
case nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED:
case nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED:
[nativeAcc handleAccessibleEvent:eventType]; [nativeAcc handleAccessibleEvent:eventType];
break; break;

View File

@ -23,6 +23,20 @@ class DocAccessibleWrap : public DocAccessible {
virtual void Shutdown() override; virtual void Shutdown() override;
virtual ~DocAccessibleWrap(); virtual ~DocAccessibleWrap();
virtual void AttributeChanged(dom::Element* aElement, int32_t aNameSpaceID,
nsAtom* aAttribute, int32_t aModType,
const nsAttrValue* aOldValue) override;
void QueueNewLiveRegion(Accessible* aAccessible);
void ProcessNewLiveRegions();
protected:
virtual void DoInitialUpdate() override;
private:
AccessibleHashtable mNewLiveRegions;
}; };
} // namespace a11y } // namespace a11y

View File

@ -23,3 +23,77 @@ void DocAccessibleWrap::Shutdown() {
} }
DocAccessibleWrap::~DocAccessibleWrap() {} DocAccessibleWrap::~DocAccessibleWrap() {}
void DocAccessibleWrap::AttributeChanged(dom::Element* aElement,
int32_t aNameSpaceID,
nsAtom* aAttribute, int32_t aModType,
const nsAttrValue* aOldValue) {
DocAccessible::AttributeChanged(aElement, aNameSpaceID, aAttribute, aModType,
aOldValue);
if (aAttribute == nsGkAtoms::aria_live) {
Accessible* accessible =
mContent != aElement ? GetAccessible(aElement) : this;
if (!accessible) {
return;
}
static const dom::Element::AttrValuesArray sLiveRegionValues[] = {
nsGkAtoms::OFF, nsGkAtoms::polite, nsGkAtoms::assertive, nullptr};
int32_t attrValue =
aElement->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::aria_live,
sLiveRegionValues, eIgnoreCase);
if (attrValue > 0) {
if (!aOldValue || aOldValue->IsEmptyString() ||
aOldValue->Equals(nsGkAtoms::OFF, eIgnoreCase)) {
// This element just got an active aria-live attribute value
FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED,
accessible);
}
} else {
if (aOldValue && (aOldValue->Equals(nsGkAtoms::polite, eIgnoreCase) ||
aOldValue->Equals(nsGkAtoms::assertive, eIgnoreCase))) {
// This element lost an active live region
FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED,
accessible);
} else if (attrValue == 0) {
// aria-live="off", check if its a role-based live region that
// needs to be removed.
if (const nsRoleMapEntry* roleMap = accessible->ARIARoleMap()) {
// aria role defines it as a live region. It's live!
if (roleMap->liveAttRule == ePoliteLiveAttr) {
FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED,
accessible);
}
} else if (nsStaticAtom* value = GetAccService()->MarkupAttribute(
aElement, nsGkAtoms::live)) {
// HTML element defines it as a live region. It's live!
if (value == nsGkAtoms::polite || value == nsGkAtoms::assertive) {
FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED,
accessible);
}
}
}
}
}
}
void DocAccessibleWrap::QueueNewLiveRegion(Accessible* aAccessible) {
if (!aAccessible) {
return;
}
mNewLiveRegions.Put(aAccessible->UniqueID(), RefPtr{aAccessible});
}
void DocAccessibleWrap::ProcessNewLiveRegions() {
for (auto iter = mNewLiveRegions.Iter(); !iter.Done(); iter.Next()) {
FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED, iter.Data());
}
mNewLiveRegions.Clear();
}
void DocAccessibleWrap::DoInitialUpdate() {
DocAccessible::DoInitialUpdate();
ProcessNewLiveRegions();
}

View File

@ -77,14 +77,15 @@ void ProxyDestroyed(ProxyAccessible* aProxy) {
} }
void ProxyEvent(ProxyAccessible* aProxy, uint32_t aEventType) { void ProxyEvent(ProxyAccessible* aProxy, uint32_t aEventType) {
// ignore everything but focus-changed, value-changed, caret, // Ignore event that we don't escape below, they aren't yet supported.
// selection, and document load complete events for now.
if (aEventType != nsIAccessibleEvent::EVENT_FOCUS && if (aEventType != nsIAccessibleEvent::EVENT_FOCUS &&
aEventType != nsIAccessibleEvent::EVENT_VALUE_CHANGE && aEventType != nsIAccessibleEvent::EVENT_VALUE_CHANGE &&
aEventType != nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE && aEventType != nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE &&
aEventType != nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED && aEventType != nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED &&
aEventType != nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE && aEventType != nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE &&
aEventType != nsIAccessibleEvent::EVENT_REORDER) aEventType != nsIAccessibleEvent::EVENT_REORDER &&
aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED &&
aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED)
return; return;
mozAccessible* wrapper = GetNativeFromGeckoAccessible(aProxy); mozAccessible* wrapper = GetNativeFromGeckoAccessible(aProxy);

View File

@ -38,3 +38,4 @@ skip-if = os == 'mac' && debug # Bug 1664577
[browser_text_selection.js] [browser_text_selection.js]
[browser_navigate.js] [browser_navigate.js]
[browser_hierarchy.js] [browser_hierarchy.js]
[browser_live_regions.js]

View File

@ -0,0 +1,79 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/**
* Test live region creation and removal.
*/
addAccessibleTask(
`
<div id="polite">Polite region</div>
<div id="assertive" aria-live="assertive">Assertive region</div>
`,
async (browser, accDoc) => {
let liveRegionAdded = waitForEvent(EVENT_LIVE_REGION_ADDED, "polite");
await SpecialPowers.spawn(browser, [], () => {
content.document
.getElementById("polite")
.setAttribute("aria-atomic", "true");
content.document
.getElementById("polite")
.setAttribute("aria-live", "polite");
});
await liveRegionAdded;
let liveRegionRemoved = waitForEvent(
EVENT_LIVE_REGION_REMOVED,
"assertive"
);
await SpecialPowers.spawn(browser, [], () => {
content.document.getElementById("assertive").removeAttribute("aria-live");
});
await liveRegionRemoved;
liveRegionAdded = waitForEvent(EVENT_LIVE_REGION_ADDED, "new-region");
await SpecialPowers.spawn(browser, [], () => {
let newRegionElm = content.document.createElement("div");
newRegionElm.id = "new-region";
newRegionElm.setAttribute("aria-live", "assertive");
content.document.body.appendChild(newRegionElm);
});
await liveRegionAdded;
let loadComplete = Promise.all([
waitForMacEvent("AXLoadComplete"),
waitForEvent(EVENT_LIVE_REGION_ADDED, "region-1"),
waitForEvent(EVENT_LIVE_REGION_ADDED, "region-2"),
waitForEvent(EVENT_LIVE_REGION_ADDED, "status"),
waitForEvent(EVENT_LIVE_REGION_ADDED, "output"),
]);
await SpecialPowers.spawn(browser, [], () => {
content.location = `data:text/html;charset=utf-8,
<div id="region-1" aria-live="polite"></div>
<div id="region-2" aria-live="assertive"></div>
<div id="region-3" aria-live="off"></div>
<div id="status" role="status"></div>
<output id="output"></output>`;
});
await loadComplete;
liveRegionRemoved = waitForEvent(EVENT_LIVE_REGION_REMOVED, "status");
await SpecialPowers.spawn(browser, [], () => {
content.document
.getElementById("status")
.setAttribute("aria-live", "off");
});
await liveRegionRemoved;
liveRegionRemoved = waitForEvent(EVENT_LIVE_REGION_REMOVED, "output");
await SpecialPowers.spawn(browser, [], () => {
content.document
.getElementById("output")
.setAttribute("aria-live", "off");
});
await liveRegionRemoved;
}
);

View File

@ -42,6 +42,8 @@ const EVENT_VIRTUALCURSOR_CHANGED =
const EVENT_ALERT = nsIAccessibleEvent.EVENT_ALERT; const EVENT_ALERT = nsIAccessibleEvent.EVENT_ALERT;
const EVENT_TEXT_SELECTION_CHANGED = const EVENT_TEXT_SELECTION_CHANGED =
nsIAccessibleEvent.EVENT_TEXT_SELECTION_CHANGED; nsIAccessibleEvent.EVENT_TEXT_SELECTION_CHANGED;
const EVENT_LIVE_REGION_ADDED = nsIAccessibleEvent.EVENT_LIVE_REGION_ADDED;
const EVENT_LIVE_REGION_REMOVED = nsIAccessibleEvent.EVENT_LIVE_REGION_REMOVED;
const EventsLogger = { const EventsLogger = {
enabled: false, enabled: false,

View File

@ -2294,6 +2294,7 @@ STATIC_ATOMS = [
Atom("aria_rowindextext", "aria-rowindextext"), Atom("aria_rowindextext", "aria-rowindextext"),
Atom("aria_rowspan", "aria-rowspan"), Atom("aria_rowspan", "aria-rowspan"),
Atom("aria_valuetext", "aria-valuetext"), Atom("aria_valuetext", "aria-valuetext"),
Atom("assertive", "assertive"),
Atom("auto_generated", "auto-generated"), Atom("auto_generated", "auto-generated"),
Atom("banner", "banner"), Atom("banner", "banner"),
Atom("checkable", "checkable"), Atom("checkable", "checkable"),