Bug 1455749 - Advertise nodes as editable and focusable (1/2). r=jchen,yzen
authorEitan Isaacson <eitan@monotonous.org>
Fri, 11 May 2018 08:30:00 +0300
changeset 794524 b1089dc357a8f8cc5a940314d29a4ac54517ec56
parent 794523 ce9f1466ec789f59f904b81a3cbd9aa94b33deaf
child 794525 4345a33a957c8bda9e0a3b18f0d05e64a513cd28
push id109697
push userbmo:sledru@mozilla.com
push dateSat, 12 May 2018 10:04:34 +0000
reviewersjchen, yzen
bugs1455749
milestone62.0a1
Bug 1455749 - Advertise nodes as editable and focusable (1/2). r=jchen,yzen
accessible/jsat/EventManager.jsm
accessible/jsat/Presentation.jsm
accessible/tests/mochitest/jsat/test_text_editable_navigation.html
accessible/tests/mochitest/jsat/test_text_editing.html
accessible/tests/mochitest/jsat/test_text_navigation_focus.html
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
--- a/accessible/jsat/EventManager.jsm
+++ b/accessible/jsat/EventManager.jsm
@@ -259,17 +259,19 @@ this.EventManager.prototype = {
       {
         // Put vc where the focus is at
         let acc = aEvent.accessible;
         this._setEditingMode(aEvent);
         if (![Roles.CHROME_WINDOW,
              Roles.DOCUMENT,
              Roles.APPLICATION].includes(acc.role)) {
           this.contentControl.autoMove(acc);
-       }
+        }
+
+        this.present(Presentation.focused(acc));
 
        if (this.inTest) {
         this.sendMsgFunc("AccessFu:Focused");
        }
        break;
       }
       case Events.DOCUMENT_LOAD_COMPLETE:
       {
--- a/accessible/jsat/Presentation.jsm
+++ b/accessible/jsat/Presentation.jsm
@@ -60,39 +60,40 @@ class AndroidPresentor {
         androidEvents.push({
           eventType: AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
           text: [adjustedText.text],
           fromIndex: adjustedText.startOffset,
           toIndex: adjustedText.endOffset
         });
       }
     } else {
-      let state = Utils.getState(context.accessible);
-      androidEvents.push({eventType: (isExploreByTouch) ?
-                           AndroidEvents.VIEW_HOVER_ENTER : focusEventType,
-                         text: Utils.localize(UtteranceGenerator.genForContext(
-                           context)),
-                         bounds: context.bounds,
-                         clickable: context.accessible.actionCount > 0,
-                         checkable: state.contains(States.CHECKABLE),
-                         checked: state.contains(States.CHECKED)});
+      let info = this._infoFromContext(context);
+      let eventType = isExploreByTouch ?
+        AndroidEvents.VIEW_HOVER_ENTER : focusEventType;
+      androidEvents.push({...info, eventType});
     }
 
     try {
       context.accessibleForBounds.scrollTo(
         Ci.nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE);
     } catch (e) {}
 
     if (context.accessible) {
       this.displayedAccessibles.set(context.accessible.document.window, context);
     }
 
     return androidEvents;
   }
 
+  focused(aObject) {
+    let info = this._infoFromContext(
+      new PivotContext(aObject, null, -1, -1, true, false));
+    return [{ eventType: AndroidEvents.VIEW_FOCUSED, ...info }];
+  }
+
   /**
    * An object's action has been invoked.
    * @param {nsIAccessible} aObject the object that has been invoked.
    * @param {string} aActionName the name of the action.
    */
   actionInvoked(aObject, aActionName) {
     let state = Utils.getState(aObject);
 
@@ -294,11 +295,25 @@ class AndroidPresentor {
    * @param  {string} aModifiedText Optional modified text.
    */
   liveRegion(aAccessible, aIsPolite, aIsHide, aModifiedText) {
     let context = !aModifiedText ?
       new PivotContext(aAccessible, null, -1, -1, true, !!aIsHide) : null;
     return this.announce(
       UtteranceGenerator.genForLiveRegion(context, aIsHide, aModifiedText));
   }
+
+  _infoFromContext(aContext) {
+    let state = Utils.getState(aContext.accessible);
+    return {
+      text: Utils.localize(UtteranceGenerator.genForContext(aContext)),
+      bounds: aContext.bounds,
+      focusable: state.contains(States.FOCUSABLE),
+      focused: state.contains(States.FOCUSED),
+      clickable: aContext.accessible.actionCount > 0,
+      checkable: state.contains(States.CHECKABLE),
+      checked: state.contains(States.CHECKED),
+      editable: state.contains(States.EDITABLE),
+    };
+  }
 }
 
 const Presentation = new AndroidPresentor();
--- a/accessible/tests/mochitest/jsat/test_text_editable_navigation.html
+++ b/accessible/tests/mochitest/jsat/test_text_editable_navigation.html
@@ -31,25 +31,25 @@
       is(aSelectionEvent.toIndex, aTo, "Caret offset (toIndex)");
     }
 
     async function testEditableTextNavigation(doc, runner) {
       // Editable text tests.
       let evt;
 
       evt = await runner.focusSelector("textarea",
-        AndroidEvents.ANNOUNCEMENT,
+        AndroidEvents.VIEW_FOCUSED,
         AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED,
         AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
-      // XXX: Get rid of announcements, and send focus events instead
-      runner.eventTextMatches(evt[0], ["editing"]);
+      is(evt[0].editable, true, "focused item is editable");
       runner.eventTextMatches(evt[1],
         ["Text content test document",
          "Please refrain from Mayoneggs during this salmonella scare.",
          "text area"]);
+      is(evt[1].focused, true, "a11y focused item is focused");
       is(evt[2].fromIndex, 0, "Correct fromIndex");
       is(evt[2].toIndex, 0, "Correct toIndex");
 
       evt = await runner.activateCurrent(10,
         AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
         AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
       checkMoveCaret(...evt, 0, 10);
 
@@ -83,18 +83,18 @@
         AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
       checkMoveCaret(...evt, 38, 59);
 
       evt = await runner.moveCaretPrevious(MovementGranularity.WORD,
         AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
         AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
       checkMoveCaret(...evt, 59, 53);
 
-      evt = await runner.blur(AndroidEvents.ANNOUNCEMENT);
-      runner.eventTextMatches(evt, ["navigating"]);
+      evt = await runner.blur(AndroidEvents.VIEW_FOCUSED);
+      is(evt.editable, false, "Focused out of editable");
     }
 
     function doTest() {
       var doc = currentTabDocument();
 
       addA11yLoadEvent(async function() {
         let runner = new AccessFuContentTestRunner();
         await runner.start();
--- a/accessible/tests/mochitest/jsat/test_text_editing.html
+++ b/accessible/tests/mochitest/jsat/test_text_editing.html
@@ -32,22 +32,22 @@
         runner.eventTextMatches(testSelEvent, text);
         is(testSelEvent.toIndex, insertIndex + addedCount);
         is(testSelEvent.fromIndex, insertIndex + addedCount);
       }
 
       let evt;
 
       evt = await runner.focusSelector("input",
-        AndroidEvents.ANNOUNCEMENT,
+        AndroidEvents.VIEW_FOCUSED,
         AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED,
         AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
-      // XXX: Get rid of announcements, and send focus events instead
-      runner.eventTextMatches(evt[0], ["editing"]);
+      is(evt[0].editable, true, "focused item is editable");
       runner.eventTextMatches(evt[1], ["Text content test document", "entry"]);
+      is(evt[1].focused, true, "a11y focused item is focused");
       is(evt[2].fromIndex, 0, "Caret at start (fromIndex)");
       is(evt[2].toIndex, 0, "Caret at start (toIndex)");
 
       evt = await runner.typeKey("B",
         AndroidEvents.VIEW_TEXT_CHANGED,
         AndroidEvents.VIEW_TEXT_SELECTION_CHANGED,
         "todo.value-changed");
       checkInsert(evt[0], evt[1], ["B"], 0, 1);
@@ -101,18 +101,18 @@
       checkInsert(evt[0], evt[1], ["Bob Lobla"], 8, 1);
 
       evt = await runner.typeKey("w",
         AndroidEvents.VIEW_TEXT_CHANGED,
         AndroidEvents.VIEW_TEXT_SELECTION_CHANGED,
         "todo.value-changed");
       checkInsert(evt[0], evt[1], ["Bob Loblaw"], 9, 1);
 
-      evt = await runner.blur(AndroidEvents.ANNOUNCEMENT);
-      runner.eventTextMatches(evt, ["navigating"]);
+      evt = await runner.blur(AndroidEvents.VIEW_FOCUSED);
+      is(evt.editable, false, "Focused out of editable");
     }
 
 
     function doTest() {
       var doc = currentTabDocument();
 
       addA11yLoadEvent(async function() {
         let runner = new AccessFuContentTestRunner();
--- a/accessible/tests/mochitest/jsat/test_text_navigation_focus.html
+++ b/accessible/tests/mochitest/jsat/test_text_navigation_focus.html
@@ -23,66 +23,66 @@
   <script type="application/javascript" src="../layout.js"></script>
   <script type="application/javascript" src="jsatcommon.js"></script>
 
   <script type="application/javascript">
     async function testTextNavigationFocus(doc, runner) {
       let evt;
 
       evt = await runner.focusSelector("textarea",
-        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED,
-        AndroidEvents.ANNOUNCEMENT);
-      // XXX: Get rid of announcements, and send focus events instead
-      runner.eventTextMatches(evt[0],
+        AndroidEvents.VIEW_FOCUSED,
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      is(evt[0].editable, true, "focused item is editable");
+      is(evt[1].focused, true, "a11y focused item is focused");
+      runner.eventTextMatches(evt[1],
         ["Text content test document",
          "Please refrain from Mayoneggs during this salmonella scare.",
          "text area"]);
-      runner.eventTextMatches(evt[1], ["editing"]);
 
       evt = await runner.moveNext("Simple",
-        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED,
-        AndroidEvents.ANNOUNCEMENT);
-      runner.eventTextMatches(evt[0], ["So we don't get dessert?", "label"]);
-      runner.eventTextMatches(evt[1], ["navigating"]);
+        AndroidEvents.VIEW_FOCUSED,
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      is(evt[0].editable, false, "focused out of editable");
+      runner.eventTextMatches(evt[1], ["So we don't get dessert?", "label"]);
       runner.isFocused("html");
 
       evt = await runner.moveNext("Simple",
         AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
       runner.eventTextMatches(evt, ["entry"]);
       runner.isFocused("html");
 
       evt = await runner.activateCurrent(0,
         AndroidEvents.VIEW_CLICKED,
-        AndroidEvents.ANNOUNCEMENT,
+        AndroidEvents.VIEW_FOCUSED,
         AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
-      runner.eventTextMatches(evt[1], ["editing"]);
+      is(evt[1].editable, true, "focused item is editable");
       is(evt[2].fromIndex, 0, "Cursor at start");
       runner.isFocused("input[type=text]");
 
       evt = await runner.movePrevious("Simple",
-        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED,
-        AndroidEvents.ANNOUNCEMENT);
-      runner.eventTextMatches(evt[0], ["So we don't get dessert?", "label"]);
-      runner.eventTextMatches(evt[1], ["navigating"]);
+        AndroidEvents.VIEW_FOCUSED,
+        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
+      is(evt[0].editable, false, "focused out of editable");
+      runner.eventTextMatches(evt[1], ["So we don't get dessert?", "label"]);
       runner.isFocused("html");
 
       evt = await runner.moveNext("Simple",
         AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED);
       runner.eventTextMatches(evt, ["entry"]);
       runner.isFocused("html");
 
       // XXX: TEXT_SELECTION_CHANGED should be fired here
       evt = await runner.activateCurrent(0,
         AndroidEvents.VIEW_CLICKED,
-        AndroidEvents.ANNOUNCEMENT);
-      runner.eventTextMatches(evt[1], ["editing"]);
+        AndroidEvents.VIEW_FOCUSED);
+      is(evt[1].editable, true, "focused item is editable");
       runner.isFocused("input[type=text]");
 
-      evt = await runner.blur(AndroidEvents.ANNOUNCEMENT);
-      runner.eventTextMatches(evt, ["navigating"]);
+      evt = await runner.blur(AndroidEvents.VIEW_FOCUSED);
+      is(evt.editable, false, "Focused out of editable");
     }
 
 
     function doTest() {
       var doc = currentTabDocument();
 
       addA11yLoadEvent(async function() {
         let runner = new AccessFuContentTestRunner();
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
@@ -351,16 +351,19 @@ public class SessionAccessibility {
         event.setMaxScrollY(message.getInt("maxScrollY", -1));
     }
 
     private void populateNodeInfoFromJSON(AccessibilityNodeInfo node, final GeckoBundle message) {
         node.setEnabled(message.getBoolean("enabled", true));
         node.setCheckable(message.getBoolean("checkable"));
         node.setChecked(message.getBoolean("checked"));
         node.setPassword(message.getBoolean("password"));
+        node.setFocusable(message.getBoolean("focusable"));
+        node.setFocused(message.getBoolean("focused"));
+        node.setEditable(message.getBoolean("editable"));
 
         final String[] textArray = message.getStringArray("text");
         StringBuilder sb = new StringBuilder();
         if (textArray != null && textArray.length > 0) {
             sb.append(textArray[0] != null ? textArray[0] : "");
             for (int i = 1; i < textArray.length; i++) {
                 sb.append(' ').append(textArray[i] != null ? textArray[i] : "");
             }
@@ -413,17 +416,20 @@ public class SessionAccessibility {
                 return;
             }
 
             if (exitView.equals("movePrevious")) {
                 eventSource = View.NO_ID;
             }
         }
 
-        if (eventSource != View.NO_ID) {
+        if (eventSource != View.NO_ID &&
+                (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED ||
+                 eventType == AccessibilityEvent.TYPE_VIEW_FOCUSED ||
+                 eventType == AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)) {
             // In Jelly Bean we populate an AccessibilityNodeInfo with the minimal amount of data to have
             // it work with TalkBack.
             if (mVirtualContentNode == null) {
                 mVirtualContentNode = AccessibilityNodeInfo.obtain(mView, eventSource);
             }
             populateNodeInfoFromJSON(mVirtualContentNode, message);
         }