Bug 1481922 - add support for select action and view selected event. r=eeejay, jchen
authorYura Zenevich <yura.zenevich@gmail.com>
Thu, 09 Aug 2018 11:08:41 -0400
changeset 431030 8720a95a99917395f69be6ea8a1d6c58dfd34290
parent 431029 93aabcabcdf717db6d0e0983fa1f8b2add0c42f3
child 431031 6f0f8668fb8198364c4b59b0a818d2405b6349dc
push id34419
push userbtara@mozilla.com
push dateSat, 11 Aug 2018 03:43:33 +0000
treeherdermozilla-central@7ed5ed3d4814 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerseeejay, jchen
bugs1481922
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1481922 - add support for select action and view selected event. r=eeejay, jchen MozReview-Commit-ID: FaLz7majPhz
accessible/jsat/AccessFu.jsm
accessible/jsat/ContentControl.jsm
accessible/jsat/Presentation.jsm
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
--- a/accessible/jsat/AccessFu.jsm
+++ b/accessible/jsat/AccessFu.jsm
@@ -12,26 +12,27 @@ ChromeUtils.defineModuleGetter(this, "Re
                                "resource://gre/modules/Geometry.jsm");
 
 if (Utils.MozBuildApp === "mobile/android") {
   ChromeUtils.import("resource://gre/modules/Messaging.jsm");
 }
 
 const GECKOVIEW_MESSAGE = {
   ACTIVATE: "GeckoView:AccessibilityActivate",
-  VIEW_FOCUSED: "GeckoView:AccessibilityViewFocused",
+  BY_GRANULARITY: "GeckoView:AccessibilityByGranularity",
+  CLIPBOARD: "GeckoView:AccessibilityClipboard",
+  EXPLORE_BY_TOUCH: "GeckoView:AccessibilityExploreByTouch",
   LONG_PRESS: "GeckoView:AccessibilityLongPress",
-  BY_GRANULARITY: "GeckoView:AccessibilityByGranularity",
   NEXT: "GeckoView:AccessibilityNext",
   PREVIOUS: "GeckoView:AccessibilityPrevious",
   SCROLL_BACKWARD: "GeckoView:AccessibilityScrollBackward",
   SCROLL_FORWARD: "GeckoView:AccessibilityScrollForward",
-  EXPLORE_BY_TOUCH: "GeckoView:AccessibilityExploreByTouch",
+  SELECT: "GeckoView:AccessibilitySelect",
   SET_SELECTION: "GeckoView:AccessibilitySetSelection",
-  CLIPBOARD: "GeckoView:AccessibilityClipboard",
+  VIEW_FOCUSED: "GeckoView:AccessibilityViewFocused",
 };
 
 const ACCESSFU_MESSAGE = {
   PRESENT: "AccessFu:Present",
   DOSCROLL: "AccessFu:DoScroll",
 };
 
 const FRAME_SCRIPT = "chrome://global/content/accessibility/content-script.js";
@@ -233,16 +234,19 @@ var AccessFu = {
         this.Input.moveToPoint("Simple", ...data.coordinates);
         break;
       case GECKOVIEW_MESSAGE.SET_SELECTION:
         this.Input.setSelection(data);
         break;
       case GECKOVIEW_MESSAGE.CLIPBOARD:
         this.Input.clipboard(data);
         break;
+      case GECKOVIEW_MESSAGE.SELECT:
+        this.Input.selectCurrent(data);
+        break;
     }
   },
 
   observe: function observe(aSubject, aTopic, aData) {
     switch (aTopic) {
       case "domwindowopened": {
         let win = aSubject.QueryInterface(Ci.nsIDOMWindow);
         win.addEventListener("load", () => {
@@ -343,16 +347,21 @@ var Input = {
     mm.sendAsyncMessage("AccessFu:Clipboard", aDetails);
   },
 
   activateCurrent: function activateCurrent(aData) {
     let mm = Utils.getMessageManager();
     mm.sendAsyncMessage("AccessFu:Activate", { offset: 0 });
   },
 
+  selectCurrent: function selectCurrent(aData) {
+    let mm = Utils.getMessageManager();
+    mm.sendAsyncMessage("AccessFu:Select", aData);
+  },
+
   doScroll: function doScroll(aDetails, aBrowser) {
     let horizontal = aDetails.horizontal;
     let page = aDetails.page;
     let win = aBrowser.ownerGlobal;
     let winUtils = win.windowUtils;
     let p = AccessFu.screenToClientBounds(aDetails.bounds, win).center();
     winUtils.sendWheelEvent(p.x, p.y,
       horizontal ? page : 0, horizontal ? 0 : page, 0,
--- a/accessible/jsat/ContentControl.jsm
+++ b/accessible/jsat/ContentControl.jsm
@@ -28,25 +28,26 @@ const CLIPBOARD_PASTE = 0x8000;
 const CLIPBOARD_CUT = 0x10000;
 
 function ContentControl(aContentScope) {
   this._contentScope = Cu.getWeakReference(aContentScope);
   this._childMessageSenders = new WeakMap();
 }
 
 this.ContentControl.prototype = {
-  messagesOfInterest: ["AccessFu:MoveCursor",
-                       "AccessFu:ClearCursor",
-                       "AccessFu:MoveToPoint",
+  messagesOfInterest: ["AccessFu:Activate",
+                       "AccessFu:AndroidScroll",
                        "AccessFu:AutoMove",
-                       "AccessFu:Activate",
+                       "AccessFu:ClearCursor",
+                       "AccessFu:Clipboard",
                        "AccessFu:MoveByGranularity",
-                       "AccessFu:AndroidScroll",
-                       "AccessFu:SetSelection",
-                       "AccessFu:Clipboard"],
+                       "AccessFu:MoveCursor",
+                       "AccessFu:MoveToPoint",
+                       "AccessFu:Select",
+                       "AccessFu:SetSelection"],
 
   start: function cc_start() {
     let cs = this._contentScope.get();
     for (let message of this.messagesOfInterest) {
       cs.addMessageListener(message, this);
     }
   },
 
@@ -175,16 +176,26 @@ this.ContentControl.prototype = {
     }
     this.document.activeElement.blur();
   },
 
   handleAutoMove: function cc_handleAutoMove(aMessage) {
     this.autoMove(null, aMessage.json);
   },
 
+  handleSelect: function cc_handleSelect(aMessage) {
+    const vc = this.vc;
+    if (!this.sendToChild(vc, aMessage, null, true)) {
+      const acc = vc.position;
+      if (Utils.getState(acc).contains(States.SELECTABLE)) {
+        this.handleActivate(aMessage);
+      }
+    }
+  },
+
   handleActivate: function cc_handleActivate(aMessage) {
     let activateAccessible = (aAccessible) => {
       Logger.debug(() => {
         return ["activateAccessible", Logger.accessibleToString(aAccessible)];
       });
 
       if (aAccessible.actionCount > 0) {
         aAccessible.doAction(0);
@@ -217,17 +228,17 @@ this.ContentControl.prototype = {
           node.dispatchEvent(evt);
         }
       }
 
       // Action invoked will be presented on checked/selected state change.
       if (!Utils.getState(aAccessible).contains(States.CHECKABLE) &&
           !Utils.getState(aAccessible).contains(States.SELECTABLE)) {
         this._contentScope.get().sendAsyncMessage("AccessFu:Present",
-          Presentation.actionInvoked(aAccessible, "click"));
+          Presentation.actionInvoked());
       }
     };
 
     let focusedAcc = Utils.AccService.getAccessibleFor(
       this.document.activeElement);
     if (focusedAcc && this.vc.position === focusedAcc
         && focusedAcc.role === Roles.ENTRY) {
       let accText = focusedAcc.QueryInterface(Ci.nsIAccessibleText);
--- a/accessible/jsat/Presentation.jsm
+++ b/accessible/jsat/Presentation.jsm
@@ -111,31 +111,26 @@ class AndroidPresentor {
   }
 
   /**
    * An object's select action has been invoked.
    * @param {nsIAccessible} aAccessible the object that has been invoked.
    */
   selected(aAccessible) {
     return [{
-      eventType: AndroidEvents.VIEW_CLICKED,
+      eventType: AndroidEvents.VIEW_SELECTED,
       selected: Utils.getState(aAccessible).contains(States.SELECTED)
     }];
   }
 
   /**
    * An object's action has been invoked.
-   * @param {nsIAccessible} aAccessible the object that has been invoked.
-   * @param {string} aActionName the name of the action.
    */
-  actionInvoked(aAccessible, aActionName) {
-    return [{
-      eventType: AndroidEvents.VIEW_CLICKED,
-      text: Utils.localize(UtteranceGenerator.genForAction(aAccessible, aActionName))
-    }];
+  actionInvoked() {
+    return [{ eventType: AndroidEvents.VIEW_CLICKED }];
   }
 
   /**
    * Text has changed, either by the user or by the system. TODO.
    */
   textChanged(aAccessible, aIsInserted, aStart, aLength, aText, aModifiedText) {
     let androidEvent = {
       eventType: AndroidEvents.VIEW_TEXT_CHANGED,
--- 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
@@ -59,16 +59,17 @@ class AccessibilityTest : BaseSessionTes
             return 0
         }
     }
 
     private interface EventDelegate {
         fun onAccessibilityFocused(event: AccessibilityEvent) { }
         fun onClicked(event: AccessibilityEvent) { }
         fun onFocused(event: AccessibilityEvent) { }
+        fun onSelected(event: AccessibilityEvent) { }
         fun onTextSelectionChanged(event: AccessibilityEvent) { }
         fun onTextChanged(event: AccessibilityEvent) { }
         fun onTextTraversal(event: AccessibilityEvent) { }
     }
 
     @Before fun setup() {
         // We initialize a view with a parent and grandparent so that the
         // accessibility events propagate up at least to the parent.
@@ -85,16 +86,17 @@ class AccessibilityTest : BaseSessionTes
         sessionRule.addExternalDelegateUntilTestEnd(
             EventDelegate::class,
         { newDelegate -> (view.parent as View).setAccessibilityDelegate(object : View.AccessibilityDelegate() {
             override fun onRequestSendAccessibilityEvent(host: ViewGroup, child: View, event: AccessibilityEvent): Boolean {
                 when (event.eventType) {
                     AccessibilityEvent.TYPE_VIEW_FOCUSED -> newDelegate.onFocused(event)
                     AccessibilityEvent.TYPE_VIEW_CLICKED -> newDelegate.onClicked(event)
                     AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED -> newDelegate.onAccessibilityFocused(event)
+                    AccessibilityEvent.TYPE_VIEW_SELECTED -> newDelegate.onSelected(event)
                     AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED -> newDelegate.onTextSelectionChanged(event)
                     AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED -> newDelegate.onTextChanged(event)
                     AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY -> newDelegate.onTextTraversal(event)
                     else -> {}
                 }
                 return false
             }
         }) },
@@ -199,31 +201,35 @@ class AccessibilityTest : BaseSessionTes
             @AssertCalled(count = 1)
             override fun onTextTraversal(event: AccessibilityEvent) {
               assertThat("fromIndex matches", event.fromIndex, equalTo(fromIndex))
               assertThat("toIndex matches", event.toIndex, equalTo(toIndex))
             }
         })
     }
 
-    private fun waitUntilClick(checked: Boolean? = null, selected: Boolean? = null) {
+    private fun waitUntilClick(checked: Boolean) {
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled(count = 1)
             override fun onClicked(event: AccessibilityEvent) {
                 var nodeId = getSourceId(event)
                 var node = provider.createAccessibilityNodeInfo(nodeId)
+                assertThat("Event's checked state matches", event.isChecked, equalTo(checked))
+                assertThat("Checkbox node has correct checked state", node.isChecked, equalTo(checked))
+            }
+        })
+    }
 
-                if (checked != null) {
-                    assertThat("Event's checked state matches", event.isChecked, equalTo(checked))
-                    assertThat("Checkbox node has correct checked state", node.isChecked, equalTo(checked))
-                }
-
-                if (selected != null) {
-                    assertThat("Selectable node has correct selected state", node.isSelected, equalTo(selected))
-                }
+    private fun waitUntilSelect(selected: Boolean) {
+        sessionRule.waitUntilCalled(object : EventDelegate {
+            @AssertCalled(count = 1)
+            override fun onSelected(event: AccessibilityEvent) {
+                var nodeId = getSourceId(event)
+                var node = provider.createAccessibilityNodeInfo(nodeId)
+                assertThat("Selectable node has correct selected state", node.isSelected, equalTo(selected))
             }
         })
     }
 
     private fun setSelectionArguments(start: Int, end: Int): Bundle {
         val arguments = Bundle(2)
         arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, start)
         arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, end)
@@ -402,20 +408,20 @@ class AccessibilityTest : BaseSessionTes
                 assertThat("Checkbox node is clickable", node.isClickable, equalTo(true))
                 assertThat("Checkbox node is focusable", node.isFocusable, equalTo(true))
                 assertThat("Checkbox node is not checked", node.isChecked, equalTo(false))
                 assertThat("Checkbox node has correct role", node.text.toString(), equalTo("many option check button"))
             }
         })
 
         provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_CLICK, null)
-        waitUntilClick(checked = true)
+        waitUntilClick(true)
 
         provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_CLICK, null)
-        waitUntilClick(checked = false)
+        waitUntilClick(false)
     }
 
     @Test fun testSelectable() {
         var nodeId = View.NO_ID
         sessionRule.session.loadString(
                 """<ul style="list-style-type: none;" role="listbox">
                         <li id="li" role="option" onclick="this.setAttribute('aria-selected',
                             this.getAttribute('aria-selected') == 'true' ? 'false' : 'true')">1</li>
@@ -430,14 +436,20 @@ class AccessibilityTest : BaseSessionTes
                 var node = provider.createAccessibilityNodeInfo(nodeId)
                 assertThat("Selectable node is clickable", node.isClickable, equalTo(true))
                 assertThat("Selectable node is not selected", node.isSelected, equalTo(false))
                 assertThat("Selectable node has correct role", node.text.toString(), equalTo("1 option list box"))
             }
         })
 
         provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_CLICK, null)
-        waitUntilClick(selected = true)
+        waitUntilSelect(true)
 
         provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_CLICK, null)
-        waitUntilClick(selected = false)
+        waitUntilSelect(false)
+
+        provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_SELECT, null)
+        waitUntilSelect(true)
+
+        provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_SELECT, null)
+        waitUntilSelect(false)
     }
 }
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
@@ -184,16 +184,19 @@ public class SessionAccessibility {
                             mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityLongPress", null);
                             return true;
                         case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
                             mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityScrollForward", null);
                             return true;
                         case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
                             mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityScrollBackward", null);
                             return true;
+                        case AccessibilityNodeInfo.ACTION_SELECT:
+                            mSession.getEventDispatcher().dispatch("GeckoView:AccessibilitySelect", null);
+                            return true;
                         case AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT:
                             if (mLastItem) {
                                 return false;
                             }
                             // fall-through
                         case AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT:
                             if (arguments != null) {
                                 data = new GeckoBundle(1);
@@ -478,18 +481,19 @@ public class SessionAccessibility {
             mVirtualContentNode = AccessibilityNodeInfo.obtain(mView, eventSource);
             populateNodeInfoFromJSON(mVirtualContentNode, message);
         }
 
         if (mVirtualContentNode != null) {
             // Bounds for the virtual content can be updated from any event.
             updateBounds(mVirtualContentNode, message);
 
-            // State for the virtual content can be updated when view is clicked.
-            if (eventType == AccessibilityEvent.TYPE_VIEW_CLICKED) {
+            // State for the virtual content can be updated when view is clicked/selected.
+            if (eventType == AccessibilityEvent.TYPE_VIEW_CLICKED ||
+                eventType == AccessibilityEvent.TYPE_VIEW_SELECTED) {
                 updateState(mVirtualContentNode, message);
             }
         }
 
         final AccessibilityEvent accessibilityEvent = obtainEvent(eventType, eventSource);
         populateEventFromJSON(accessibilityEvent, message);
         ((ViewParent) mView).requestSendAccessibilityEvent(mView, accessibilityEvent);
     }