From c35dbb695061f0fbc3b801e6827150a3e3d0ba53 Mon Sep 17 00:00:00 2001 From: Eitan Isaacson Date: Fri, 15 Mar 2019 23:10:42 +0000 Subject: [PATCH] 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 --- accessible/android/AccessibleWrap.cpp | 78 +++++++++++++ accessible/android/AccessibleWrap.h | 4 + .../geckoview/test/AccessibilityTest.kt | 104 ++++++++++++++++++ 3 files changed, 186 insertions(+) diff --git a/accessible/android/AccessibleWrap.cpp b/accessible/android/AccessibleWrap.cpp index b352378fb066..63c0913accc3 100644 --- a/accessible/android/AccessibleWrap.cpp +++ b/accessible/android/AccessibleWrap.cpp @@ -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 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(atomicAncestor)->GetTextEquiv(announcement); + } + } else { + GetTextEquiv(announcement); + } + + announcement.CompressWhitespace(); + if (announcement.IsEmpty()) { + return false; + } + + announcementTarget->Announce(announcement, priority); + return true; +} diff --git a/accessible/android/AccessibleWrap.h b/accessible/android/AccessibleWrap.h index 4899c79c4960..75c44261e4e8 100644 --- a/accessible/android/AccessibleWrap.h +++ b/accessible/android/AccessibleWrap.h @@ -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, diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt index 86b207369ee1..643316ee1d97 100644 --- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt @@ -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( + "
","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( + "

I will be shown

","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( + "
The time is

3pm

","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( + "
This picture is happy
","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( + "HelloGoodbye","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()