Bug 1479042 - Handle text insertion and name change events as live regions and announce. r=yzen

Differential Revision: https://phabricator.services.mozilla.com/D21612

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Eitan Isaacson 2019-03-15 23:10:42 +00:00
Родитель 6d8e53ccd1
Коммит c35dbb6950
3 изменённых файлов: 186 добавлений и 0 удалений

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

@ -13,8 +13,10 @@
#include "SessionAccessibility.h"
#include "nsAccessibilityService.h"
#include "nsPersistentProperties.h"
#include "nsIAccessibleAnnouncementEvent.h"
#include "nsIStringBundle.h"
#include "nsAccUtils.h"
#include "nsTextEquivUtils.h"
#include "mozilla/a11y/PDocAccessibleChild.h"
@ -84,6 +86,8 @@ nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) {
nsresult rv = Accessible::HandleAccEvent(aEvent);
NS_ENSURE_SUCCESS(rv, rv);
accessible->HandleLiveRegionEvent(aEvent);
if (IPCAccessibilityActive()) {
return NS_OK;
}
@ -619,3 +623,77 @@ mozilla::java::GeckoBundle::LocalRef AccessibleWrap::ToBundle(
return nodeInfo;
}
void AccessibleWrap::GetTextEquiv(nsString& aText) {
if (nsTextEquivUtils::HasNameRule(this, eNameFromSubtreeIfReqRule)) {
// This is an accessible that normally doesn't get its name from its
// subtree, so we collect the text equivalent explicitly.
nsTextEquivUtils::GetTextEquivFromSubtree(this, aText);
} else {
Name(aText);
}
}
bool AccessibleWrap::HandleLiveRegionEvent(AccEvent* aEvent) {
auto eventType = aEvent->GetEventType();
if (eventType != nsIAccessibleEvent::EVENT_TEXT_INSERTED &&
eventType != nsIAccessibleEvent::EVENT_NAME_CHANGE) {
// XXX: Right now only announce text inserted events. aria-relevant=removals
// is potentially on the chopping block[1]. We also don't support editable
// text because we currently can't descern the source of the change[2].
// 1. https://github.com/w3c/aria/issues/712
// 2. https://bugzilla.mozilla.org/show_bug.cgi?id=1531189
return false;
}
if (aEvent->IsFromUserInput()) {
return false;
}
nsCOMPtr<nsIPersistentProperties> attributes = Attributes();
nsString live;
nsresult rv =
attributes->GetStringProperty(NS_LITERAL_CSTRING("container-live"), live);
if (!NS_SUCCEEDED(rv)) {
return false;
}
uint16_t priority = live.EqualsIgnoreCase("assertive")
? nsIAccessibleAnnouncementEvent::ASSERTIVE
: nsIAccessibleAnnouncementEvent::POLITE;
nsString atomic;
rv = attributes->GetStringProperty(NS_LITERAL_CSTRING("container-atomic"),
atomic);
Accessible* announcementTarget = this;
nsAutoString announcement;
if (atomic.EqualsIgnoreCase("true")) {
Accessible* atomicAncestor = nullptr;
for (Accessible* parent = announcementTarget; parent;
parent = parent->Parent()) {
Element* element = parent->Elm();
if (element &&
element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::aria_atomic,
nsGkAtoms::_true, eCaseMatters)) {
atomicAncestor = parent;
break;
}
}
if (atomicAncestor) {
announcementTarget = atomicAncestor;
static_cast<AccessibleWrap*>(atomicAncestor)->GetTextEquiv(announcement);
}
} else {
GetTextEquiv(announcement);
}
announcement.CompressWhitespace();
if (announcement.IsEmpty()) {
return false;
}
announcementTarget->Announce(announcement, priority);
return true;
}

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

@ -79,6 +79,10 @@ class AccessibleWrap : public Accessible {
virtual role WrapperRole() { return Role(); }
void GetTextEquiv(nsString& aText);
bool HandleLiveRegionEvent(AccEvent* aEvent);
static void GetRoleDescription(role aRole,
nsIPersistentProperties* aAttributes,
nsAString& aGeckoRole,

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

@ -103,6 +103,7 @@ class AccessibilityTest : BaseSessionTest() {
fun onTextTraversal(event: AccessibilityEvent) { }
fun onWinContentChanged(event: AccessibilityEvent) { }
fun onWinStateChanged(event: AccessibilityEvent) { }
fun onAnnouncement(event: AccessibilityEvent) { }
}
@Before fun setup() {
@ -134,6 +135,7 @@ class AccessibilityTest : BaseSessionTest() {
AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY -> newDelegate.onTextTraversal(event)
AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED -> newDelegate.onWinContentChanged(event)
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> newDelegate.onWinStateChanged(event)
AccessibilityEvent.TYPE_ANNOUNCEMENT -> newDelegate.onAnnouncement(event)
else -> {}
}
return false
@ -591,6 +593,9 @@ class AccessibilityTest : BaseSessionTest() {
createNodeInfo(rootNode.getChildId(0)).childCount, equalTo(1))
mainSession.evaluateJS("$('#to_show').style.display = 'none';")
sessionRule.waitUntilCalled(object : EventDelegate {
@AssertCalled(count = 0)
override fun onAnnouncement(event: AccessibilityEvent) { }
@AssertCalled(count = 1)
override fun onWinContentChanged(event: AccessibilityEvent) { }
})
@ -599,6 +604,105 @@ class AccessibilityTest : BaseSessionTest() {
createNodeInfo(rootNode.getChildId(0)).childCount, equalTo(0))
}
@Test fun testLiveRegion() {
sessionRule.session.loadString(
"<div id=\"to_change\"aria-live=\"polite\"></div>","text/html")
waitForInitialFocus()
mainSession.evaluateJS("$('#to_change').textContent = 'Hello';")
sessionRule.waitUntilCalled(object : EventDelegate {
@AssertCalled(count = 1)
override fun onAnnouncement(event: AccessibilityEvent) {
assertThat("Announcement is correct", event.text[0].toString(), equalTo("Hello"))
}
@AssertCalled(count = 1)
override fun onWinContentChanged(event: AccessibilityEvent) { }
})
}
@Test fun testLiveRegionDescendant() {
sessionRule.session.loadString(
"<div aria-live='polite'><p id='to_show'>I will be shown</p></div>","text/html")
waitForInitialFocus()
mainSession.evaluateJS("$('#to_show').style.display = 'none';")
sessionRule.waitUntilCalled(object : EventDelegate {
@AssertCalled(count = 0)
override fun onAnnouncement(event: AccessibilityEvent) { }
@AssertCalled(count = 1)
override fun onWinContentChanged(event: AccessibilityEvent) { }
})
mainSession.evaluateJS("$('#to_show').style.display = 'block';")
sessionRule.waitUntilCalled(object : EventDelegate {
@AssertCalled(count = 1)
override fun onAnnouncement(event: AccessibilityEvent) {
assertThat("Announcement is correct", event.text[0].toString(), equalTo("I will be shown"))
}
@AssertCalled(count = 1)
override fun onWinContentChanged(event: AccessibilityEvent) { }
})
}
@Test fun testLiveRegionAtomic() {
sessionRule.session.loadString(
"<div aria-live='polite' aria-atomic='true' id='container'>The time is <p>3pm</p></div>","text/html")
waitForInitialFocus()
mainSession.evaluateJS("$('p').textContent = '4pm';")
sessionRule.waitUntilCalled(object : EventDelegate {
@AssertCalled(count = 1)
override fun onAnnouncement(event: AccessibilityEvent) {
assertThat("Announcement is correct", event.text[0].toString(), equalTo("The time is 4pm"))
}
@AssertCalled(count = 1)
override fun onWinContentChanged(event: AccessibilityEvent) { }
})
mainSession.evaluateJS("$('#container').removeAttribute('aria-atomic'); $('p').textContent = '5pm';")
sessionRule.waitUntilCalled(object : EventDelegate {
@AssertCalled(count = 1)
override fun onAnnouncement(event: AccessibilityEvent) {
assertThat("Announcement is correct", event.text[0].toString(), equalTo("5pm"))
}
@AssertCalled(count = 1)
override fun onWinContentChanged(event: AccessibilityEvent) { }
})
}
@Test fun testLiveRegionImage() {
sessionRule.session.loadString(
"<div aria-live='polite' aria-atomic='true'>This picture is <img src='data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==' alt='happy'></div>","text/html")
waitForInitialFocus()
mainSession.evaluateJS("console.log('eeejay', $('img').naturalWidth); $('img').alt = 'sad';")
sessionRule.waitUntilCalled(object : EventDelegate {
@AssertCalled(count = 1)
override fun onAnnouncement(event: AccessibilityEvent) {
assertThat("Announcement is correct", event.text[0].toString(), equalTo("This picture is sad"))
}
})
}
@Test fun testLiveRegionImageLabeledBy() {
sessionRule.session.loadString(
"<img src='data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==' aria-live='polite' aria-labelledby='l1'><span id='l1'>Hello</span><span id='l2'>Goodbye</span>","text/html")
waitForInitialFocus()
mainSession.evaluateJS("$('img').setAttribute('aria-labelledby', 'l2');")
sessionRule.waitUntilCalled(object : EventDelegate {
@AssertCalled(count = 1)
override fun onAnnouncement(event: AccessibilityEvent) {
assertThat("Announcement is correct", event.text[0].toString(), equalTo("Goodbye"))
}
})
}
private fun screenContainsNode(nodeId: Int): Boolean {
var node = createNodeInfo(nodeId)
var nodeBounds = Rect()