Bug 1536123 - Move virtual cursor to caret offset. r=yzen a=pascalc
authorEitan Isaacson <eitan@monotonous.org>
Thu, 21 Mar 2019 16:20:51 +0000
changeset 525750 5565a1128640e9a736a42683012506b02ec2cb5b
parent 525749 0feaf41d4e517439650f4f751a3df0d234d4971d
child 525751 affaab9df5b77f8ff5571748e149d39dfa25e27c
push id2032
push userffxbld-merge
push dateMon, 13 May 2019 09:36:57 +0000
treeherdermozilla-release@455c1065dcbe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersyzen, pascalc
bugs1536123
milestone67.0
Bug 1536123 - Move virtual cursor to caret offset. r=yzen a=pascalc Differential Revision: https://phabricator.services.mozilla.com/D23911
accessible/jsat/EventManager.jsm
accessible/jsat/Utils.jsm
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt
--- a/accessible/jsat/EventManager.jsm
+++ b/accessible/jsat/EventManager.jsm
@@ -76,16 +76,28 @@ this.EventManager.prototype = {
         // NS_ERROR_FAILURE. Checking for aEvent.accessibleDocument.docType ==
         // 'window' does not currently work.
         (aEvent.accessibleDocument.DOMDocument.doctype &&
          aEvent.accessibleDocument.DOMDocument.doctype.name === "window")) {
       return;
     }
 
     switch (aEvent.eventType) {
+      case Events.TEXT_CARET_MOVED:
+      {
+        if (aEvent.accessible != aEvent.accessibleDocument &&
+            !aEvent.isFromUserInput) {
+          // If caret moves in document without direct user
+          // we are probably stepping through results in find-in-page.
+          let acc = Utils.getTextLeafForOffset(aEvent.accessible,
+            aEvent.QueryInterface(Ci.nsIAccessibleCaretMoveEvent).caretOffset);
+          this.contentControl.autoMove(acc);
+        }
+        break;
+      }
       case Events.NAME_CHANGE:
       {
         // XXX: Port to Android
         break;
       }
       case Events.SCROLLING_START:
       {
         this.contentControl.autoMove(aEvent.accessible);
--- a/accessible/jsat/Utils.jsm
+++ b/accessible/jsat/Utils.jsm
@@ -334,16 +334,36 @@ var Utils = { // jshint ignore:line
     let parent = aStaticText.parent;
     if (aExcludeOrdered && parent.parent.DOMNode.nodeName === "OL") {
       return false;
     }
 
     return parent.role === Roles.LISTITEM && parent.childCount > 1 &&
       aStaticText.indexInParent === 0;
   },
+
+  getTextLeafForOffset: function getTextLeafForOffset(aAccessible, aOffset) {
+    let ht = aAccessible.QueryInterface(Ci.nsIAccessibleHyperText);
+    let offset = 0;
+    for (let child = aAccessible.firstChild; child; child = child.nextSibling) {
+      if (ht.getLinkIndexAtOffset(offset) != -1) {
+        // This is an embedded character, increment by one.
+        offset++;
+      } else {
+        offset += child.name.length;
+      }
+
+      if (offset >= aOffset) {
+        return child;
+      }
+    }
+
+    // This is probably a single child.
+    return aAccessible.lastChild;
+  },
 };
 
 /**
  * State object used internally to process accessible's states.
  * @param {Number} aBase     Base state.
  * @param {Number} aExtended Extended state.
  */
 function State(aBase, aExtended) {
--- 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
@@ -248,16 +248,53 @@ class AccessibilityTest : BaseSessionTes
                     assertThat("Hint has field name",
                             node.extras.getString("AccessibilityNodeInfo.hint"),
                             equalTo("Name description"))
                 }
             }
         })
     }
 
+    @Test fun testMoveCaretAccessibilityFocus() {
+        sessionRule.session.loadString("<p>Hello <a href='foo'>sweet</a>, sweet <span>world</span>", "text/html")
+        waitForInitialFocus(false)
+
+        mainSession.evaluateJS("""
+            function select(node, start, end) {
+                let r = new Range();
+                r.setStart(node, start);
+                r.setEnd(node, end);
+                let s = getSelection();
+                s.removeAllRanges();
+                s.addRange(r);
+            }
+            select($('p').childNodes[2], 2, 6);
+        """.trimIndent())
+
+        sessionRule.waitUntilCalled(object : EventDelegate {
+            @AssertCalled(count = 1)
+            override fun onAccessibilityFocused(event: AccessibilityEvent) {
+                val node = createNodeInfo(getSourceId(event))
+                assertThat("Text node should match text", node.text as String, equalTo(", sweet "))
+            }
+        })
+
+        mainSession.evaluateJS("""
+            select($('p').lastElementChild.firstChild, 1, 2);
+        """.trimIndent())
+
+        sessionRule.waitUntilCalled(object : EventDelegate {
+            @AssertCalled(count = 1)
+            override fun onAccessibilityFocused(event: AccessibilityEvent) {
+                val node = createNodeInfo(getSourceId(event))
+                assertThat("Text node should match text", node.text as String, equalTo("world"))
+            }
+        })
+    }
+
     private fun waitUntilTextSelectionChanged(fromIndex: Int, toIndex: Int) {
         var eventFromIndex = 0;
         var eventToIndex = 0;
         do {
             sessionRule.waitUntilCalled(object : EventDelegate {
                 override fun onTextSelectionChanged(event: AccessibilityEvent) {
                     eventFromIndex = event.fromIndex;
                     eventToIndex = event.toIndex;