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-13 04:04:45 +00:00
Родитель 266d9ad46c
Коммит ae837aea6e
11 изменённых файлов: 234 добавлений и 8 удалений

Просмотреть файл

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

Просмотреть файл

@ -428,10 +428,20 @@ interface nsIAccessibleEvent : nsISupports
*/
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.
*/
const unsigned long EVENT_LAST_ENTRY = 0x005A;
const unsigned long EVENT_LAST_ENTRY = 0x005C;
/**
* The type of event, based on the enumerated event values

Просмотреть файл

@ -5,7 +5,7 @@
* 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/. */
#include "DocAccessible.h"
#include "DocAccessibleWrap.h"
#include "nsObjCExceptions.h"
#include "nsCocoaUtils.h"
@ -30,7 +30,36 @@ using namespace mozilla;
using namespace mozilla::a11y;
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() {}
@ -120,11 +149,17 @@ nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) {
nsresult rv = Accessible::HandleAccEvent(aEvent);
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()) {
return NS_OK;
}
uint32_t eventType = aEvent->GetEventType();
Accessible* eventTarget = nullptr;
switch (eventType) {
@ -222,6 +257,8 @@ nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) {
case nsIAccessibleEvent::EVENT_SELECTION:
case nsIAccessibleEvent::EVENT_SELECTION_ADD:
case nsIAccessibleEvent::EVENT_SELECTION_REMOVE:
case nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED:
case nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED:
[nativeAcc handleAccessibleEvent:eventType];
break;

Просмотреть файл

@ -20,9 +20,23 @@ class DocAccessibleWrap : public DocAccessible {
public:
DocAccessibleWrap(dom::Document* aDocument, PresShell* aPresShell);
virtual ~DocAccessibleWrap();
virtual void Shutdown() override;
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:
nsTHashtable<nsVoidPtrHashKey> mNewLiveRegions;
};
} // namespace a11y

Просмотреть файл

@ -23,3 +23,80 @@ void DocAccessibleWrap::Shutdown() {
}
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.PutEntry(aAccessible->UniqueID());
}
void DocAccessibleWrap::ProcessNewLiveRegions() {
for (auto iter = mNewLiveRegions.Iter(); !iter.Done(); iter.Next()) {
if (Accessible* liveRegion =
GetAccessibleByUniqueID(const_cast<void*>(iter.Get()->GetKey()))) {
FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED, liveRegion);
}
}
mNewLiveRegions.Clear();
}
void DocAccessibleWrap::DoInitialUpdate() {
DocAccessible::DoInitialUpdate();
ProcessNewLiveRegions();
}

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -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;
}
);

Просмотреть файл

@ -42,6 +42,8 @@ const EVENT_VIRTUALCURSOR_CHANGED =
const EVENT_ALERT = nsIAccessibleEvent.EVENT_ALERT;
const 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 = {
enabled: false,

Просмотреть файл

@ -102,5 +102,7 @@ static const uint32_t gWinEventMap[] = {
EVENT_OBJECT_VALUECHANGE, // nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE
kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_SCROLLING
kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_ANNOUNCEMENT
kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED
kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED
// clang-format on
};

Просмотреть файл

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