зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
266d9ad46c
Коммит
ae837aea6e
|
@ -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"),
|
||||
|
|
Загрузка…
Ссылка в новой задаче