Bug 1514687 - Allow accessibility code to focus XUL radio buttons without selecting them. r=bgrins,smaug,MarcoZ,paolo
authorJames Teh <jteh@mozilla.com>
Fri, 11 Jan 2019 04:52:50 +0000
changeset 453681 346851347c90
parent 453680 3f1fcb891684
child 453682 c01bd7f588bd
push id35369
push userncsoregi@mozilla.com
push dateMon, 14 Jan 2019 10:42:48 +0000
treeherdermozilla-central@3dc7d345da52 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins, smaug, MarcoZ, paolo
bugs1514687
milestone66.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 1514687 - Allow accessibility code to focus XUL radio buttons without selecting them. r=bgrins,smaug,MarcoZ,paolo 1. This requires exposing radiogroup's focusedItem property to C++. Unfortunately, there's no existing equivalent in nsIDOMXULSelectControlItemElement. radiogroup is the only element that needs this, so a new interface has been created for it. 2. Accessibility uses focusedItem instead of selectedItem when setting focus. 3. When an item is focused, accessibility needs to be notified. This is done using a DOMMenuItemActive event. Differential Revision: https://phabricator.services.mozilla.com/D15295
accessible/tests/mochitest/focus/a11y.ini
accessible/tests/mochitest/focus/test_focus_radio.xul
accessible/xul/XULFormControlAccessible.cpp
accessible/xul/XULFormControlAccessible.h
dom/base/Element.cpp
dom/base/Element.h
dom/interfaces/xul/moz.build
dom/interfaces/xul/nsIDOMXULRadioGroupElement.idl
toolkit/content/widgets/radio.js
--- a/accessible/tests/mochitest/focus/a11y.ini
+++ b/accessible/tests/mochitest/focus/a11y.ini
@@ -1,8 +1,9 @@
 [DEFAULT]
 support-files =
   !/accessible/tests/mochitest/*.js
 
+[test_focus_radio.xul]
 [test_focusedChild.html]
 skip-if = (os == 'win' && os_version != '6.1') # bug 845134
 [test_takeFocus.html]
 [test_takeFocus.xul]
new file mode 100644
--- /dev/null
+++ b/accessible/tests/mochitest/focus/test_focus_radio.xul
@@ -0,0 +1,85 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+                 type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        title="Tests for Accessible TakeFocus on Radio Elements">
+
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+  <script type="application/javascript"
+          src="../common.js" />
+  <script type="application/javascript"
+          src="../role.js" />
+  <script type="application/javascript"
+          src="../states.js" />
+  <script type="application/javascript"
+          src="../events.js" />
+
+  <script type="application/javascript">
+  <![CDATA[
+    async function doTests() {
+      let radio1 = getAccessible("radio1");
+      let focused = waitForEventPromise(EVENT_FOCUS, radio1);
+      radio1.takeFocus();
+      await focused;
+      // radio1 wasn't selected. Ensure that takeFocus didn't change that.
+      testStates(radio1, STATE_FOCUSED, 0, STATE_CHECKED);
+
+      // Test focusing another radio in the group while the group is still
+      // focused.
+      let radio2 = getAccessible("radio2");
+      focused = waitForEventPromise(EVENT_FOCUS, radio2);
+      radio2.takeFocus();
+      await focused;
+      testStates(radio2, STATE_FOCUSED | STATE_CHECKED);
+
+      let groupEl = document.getElementById("radiogroup");
+      // Selecting an item also focuses it.
+      focused = waitForEventPromise(EVENT_FOCUS, radio1);
+      groupEl.value = "1";
+      testStates(radio1, STATE_FOCUSED | STATE_CHECKED);
+
+      // If an item is already selected but not focused, selecting it again
+      // focuses it.
+      focused = waitForEventPromise(EVENT_FOCUS, radio2);
+      radio2.takeFocus();
+      await focused;
+      testStates(radio2, STATE_FOCUSED, 0, STATE_CHECKED);
+      // radio1 is selected but not focused.
+      // Select radio1 again, which should focus it.
+      focused = waitForEventPromise(EVENT_FOCUS, radio1);
+      groupEl.value = "1";
+      await focused;
+      testStates(radio1, STATE_FOCUSED | STATE_CHECKED);
+
+      SimpleTest.finish();
+    }
+
+    SimpleTest.waitForExplicitFinish();
+    addA11yLoadEvent(doTests);
+  ]]>
+  </script>
+
+  <hbox flex="1" style="overflow: auto;">
+    <body xmlns="http://www.w3.org/1999/xhtml">
+      <p id="display"></p>
+      <div id="content" style="display: none"></div>
+      <pre id="test">
+      </pre>
+    </body>
+
+    <vbox flex="1">
+      <radiogroup id="radiogroup" value="2">
+        <radio id="radio1" value="1"/>
+        <radio id="radio2" value="2"/>
+      </radiogroup>
+
+      <vbox id="eventdump"/>
+    </vbox>
+  </hbox>
+</window>
--- a/accessible/xul/XULFormControlAccessible.cpp
+++ b/accessible/xul/XULFormControlAccessible.cpp
@@ -13,16 +13,17 @@
 #include "Relation.h"
 #include "Role.h"
 #include "States.h"
 #include "TreeWalker.h"
 #include "XULMenuAccessible.h"
 
 #include "nsIDOMXULButtonElement.h"
 #include "nsIDOMXULMenuListElement.h"
+#include "nsIDOMXULRadioGroupElement.h"
 #include "nsIDOMXULSelectCntrlItemEl.h"
 #include "nsIEditor.h"
 #include "nsIFrame.h"
 #include "nsITextControlFrame.h"
 #include "nsMenuPopupFrame.h"
 #include "nsNameSpaceManager.h"
 #include "mozilla/dom/Element.h"
 
@@ -301,16 +302,51 @@ uint64_t XULRadioGroupAccessible::Native
 bool XULRadioGroupAccessible::IsWidget() const { return true; }
 
 bool XULRadioGroupAccessible::IsActiveWidget() const {
   return FocusMgr()->HasDOMFocus(mContent);
 }
 
 bool XULRadioGroupAccessible::AreItemsOperable() const { return true; }
 
+Accessible* XULRadioGroupAccessible::CurrentItem() const {
+  if (!mSelectControl) {
+    return nullptr;
+  }
+
+  RefPtr<Element> currentItemElm;
+  nsCOMPtr<nsIDOMXULRadioGroupElement> group =
+      mSelectControl->AsXULRadioGroup();
+  if (group) {
+    group->GetFocusedItem(getter_AddRefs(currentItemElm));
+  }
+
+  if (currentItemElm) {
+    DocAccessible* document = Document();
+    if (document) {
+      return document->GetAccessible(currentItemElm);
+    }
+  }
+
+  return nullptr;
+}
+
+void XULRadioGroupAccessible::SetCurrentItem(const Accessible* aItem) {
+  if (!mSelectControl) {
+    return;
+  }
+
+  nsCOMPtr<Element> itemElm = aItem->Elm();
+  nsCOMPtr<nsIDOMXULRadioGroupElement> group =
+      mSelectControl->AsXULRadioGroup();
+  if (group) {
+    group->SetFocusedItem(itemElm);
+  }
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // XULStatusBarAccessible
 ////////////////////////////////////////////////////////////////////////////////
 
 XULStatusBarAccessible::XULStatusBarAccessible(nsIContent* aContent,
                                                DocAccessible* aDoc)
     : AccessibleWrap(aContent, aDoc) {}
 
--- a/accessible/xul/XULFormControlAccessible.h
+++ b/accessible/xul/XULFormControlAccessible.h
@@ -115,16 +115,18 @@ class XULRadioGroupAccessible : public X
   // Accessible
   virtual mozilla::a11y::role NativeRole() const override;
   virtual uint64_t NativeInteractiveState() const override;
 
   // Widgets
   virtual bool IsWidget() const override;
   virtual bool IsActiveWidget() const override;
   virtual bool AreItemsOperable() const override;
+  virtual Accessible* CurrentItem() const override;
+  virtual void SetCurrentItem(const Accessible* aItem) override;
 };
 
 /**
  * Used for XUL statusbar element.
  */
 class XULStatusBarAccessible : public AccessibleWrap {
  public:
   XULStatusBarAccessible(nsIContent* aContent, DocAccessible* aDoc);
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
@@ -148,16 +148,17 @@
 #include "nsDOMStringMap.h"
 #include "DOMIntersectionObserver.h"
 
 #include "nsIDOMXULButtonElement.h"
 #include "nsIDOMXULContainerElement.h"
 #include "nsIDOMXULControlElement.h"
 #include "nsIDOMXULMenuListElement.h"
 #include "nsIDOMXULMultSelectCntrlEl.h"
+#include "nsIDOMXULRadioGroupElement.h"
 #include "nsIDOMXULRelatedElement.h"
 #include "nsIDOMXULMultSelectCntrlEl.h"
 #include "nsIDOMXULSelectCntrlEl.h"
 #include "nsIDOMXULSelectCntrlItemEl.h"
 #include "nsIBrowser.h"
 
 #include "nsISpeculativeConnect.h"
 #include "nsIIOService.h"
@@ -4059,16 +4060,22 @@ already_AddRefed<nsIDOMXULMenuListElemen
 
 already_AddRefed<nsIDOMXULMultiSelectControlElement>
 Element::AsXULMultiSelectControl() {
   nsCOMPtr<nsIDOMXULMultiSelectControlElement> value;
   GetCustomInterface(getter_AddRefs(value));
   return value.forget();
 }
 
+already_AddRefed<nsIDOMXULRadioGroupElement> Element::AsXULRadioGroup() {
+  nsCOMPtr<nsIDOMXULRadioGroupElement> value;
+  GetCustomInterface(getter_AddRefs(value));
+  return value.forget();
+}
+
 already_AddRefed<nsIDOMXULRelatedElement> Element::AsXULRelated() {
   nsCOMPtr<nsIDOMXULRelatedElement> value;
   GetCustomInterface(getter_AddRefs(value));
   return value.forget();
 }
 
 already_AddRefed<nsIDOMXULSelectControlElement> Element::AsXULSelectControl() {
   nsCOMPtr<nsIDOMXULSelectControlElement> value;
--- a/dom/base/Element.h
+++ b/dom/base/Element.h
@@ -63,16 +63,17 @@ class nsDOMStringMap;
 struct ServoNodeData;
 
 class nsIDOMXULButtonElement;
 class nsIDOMXULContainerElement;
 class nsIDOMXULContainerItemElement;
 class nsIDOMXULControlElement;
 class nsIDOMXULMenuListElement;
 class nsIDOMXULMultiSelectControlElement;
+class nsIDOMXULRadioGroupElement;
 class nsIDOMXULRelatedElement;
 class nsIDOMXULSelectControlElement;
 class nsIDOMXULSelectControlItemElement;
 class nsIBrowser;
 
 namespace mozilla {
 class DeclarationBlock;
 struct MutationClosureData;
@@ -1598,16 +1599,17 @@ class Element : public FragmentOrElement
   // pointer only if the element implements that interface.
   already_AddRefed<nsIDOMXULButtonElement> AsXULButton();
   already_AddRefed<nsIDOMXULContainerElement> AsXULContainer();
   already_AddRefed<nsIDOMXULContainerItemElement> AsXULContainerItem();
   already_AddRefed<nsIDOMXULControlElement> AsXULControl();
   already_AddRefed<nsIDOMXULMenuListElement> AsXULMenuList();
   already_AddRefed<nsIDOMXULMultiSelectControlElement>
   AsXULMultiSelectControl();
+  already_AddRefed<nsIDOMXULRadioGroupElement> AsXULRadioGroup();
   already_AddRefed<nsIDOMXULRelatedElement> AsXULRelated();
   already_AddRefed<nsIDOMXULSelectControlElement> AsXULSelectControl();
   already_AddRefed<nsIDOMXULSelectControlItemElement> AsXULSelectControlItem();
   already_AddRefed<nsIBrowser> AsBrowser();
 
  protected:
   /*
    * Named-bools for use with SetAttrAndNotify to make call sites easier to
--- a/dom/interfaces/xul/moz.build
+++ b/dom/interfaces/xul/moz.build
@@ -9,15 +9,16 @@ with Files("**"):
 
 XPIDL_SOURCES += [
     'nsIDOMXULButtonElement.idl',
     'nsIDOMXULCommandDispatcher.idl',
     'nsIDOMXULContainerElement.idl',
     'nsIDOMXULControlElement.idl',
     'nsIDOMXULMenuListElement.idl',
     'nsIDOMXULMultSelectCntrlEl.idl',
+    'nsIDOMXULRadioGroupElement.idl',
     'nsIDOMXULRelatedElement.idl',
     'nsIDOMXULSelectCntrlEl.idl',
     'nsIDOMXULSelectCntrlItemEl.idl',
 ]
 
 XPIDL_MODULE = 'dom_xul'
 
new file mode 100644
--- /dev/null
+++ b/dom/interfaces/xul/nsIDOMXULRadioGroupElement.idl
@@ -0,0 +1,13 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+webidl Element;
+
+[scriptable, uuid(2cc1d24b-ec9f-4e18-aa34-a298a9007f23)]
+interface nsIDOMXULRadioGroupElement : nsISupports {
+  attribute Element focusedItem;
+};
--- a/toolkit/content/widgets/radio.js
+++ b/toolkit/content/widgets/radio.js
@@ -241,46 +241,59 @@ class MozRadiogroup extends MozElements.
         children[i].removeAttribute("focused");
       }
     }
 
     var event = document.createEvent("Events");
     event.initEvent("select", false, true);
     this.dispatchEvent(event);
 
-    if (!alreadySelected && focused) {
-      // Only report if actual change
-      var myEvent;
-      if (val) {
-        myEvent = document.createEvent("Events");
-        myEvent.initEvent("RadioStateChange", true, true);
-        val.dispatchEvent(myEvent);
-      }
+    if (focused) {
+      if (alreadySelected) {
+        // Notify accessibility that this item got focus.
+        event = document.createEvent("Events");
+        event.initEvent("DOMMenuItemActive", true, true);
+        val.dispatchEvent(event);
+      } else {
+        // Only report if actual change
+        if (val) {
+          // Accessibility will fire focus for this.
+          event = document.createEvent("Events");
+          event.initEvent("RadioStateChange", true, true);
+          val.dispatchEvent(event);
+        }
 
-      if (previousItem) {
-        myEvent = document.createEvent("Events");
-        myEvent.initEvent("RadioStateChange", true, true);
-        previousItem.dispatchEvent(myEvent);
+        if (previousItem) {
+          event = document.createEvent("Events");
+          event.initEvent("RadioStateChange", true, true);
+          previousItem.dispatchEvent(event);
+        }
       }
     }
 
     return val;
   }
 
   get selectedItem() {
     var children = this._getRadioChildren();
     for (var i = 0; i < children.length; ++i) {
       if (children[i].selected)
         return children[i];
     }
     return null;
   }
 
   set focusedItem(val) {
-    if (val) val.setAttribute("focused", "true");
+    if (val) {
+      val.setAttribute("focused", "true");
+      // Notify accessibility that this item got focus.
+      let event = document.createEvent("Events");
+      event.initEvent("DOMMenuItemActive", true, true);
+      val.dispatchEvent(event);
+    }
 
     // unfocus all other group nodes
     var children = this._getRadioChildren();
     for (var i = 0; i < children.length; ++i) {
       if (children[i] != val)
         children[i].removeAttribute("focused");
     }
     return val;
@@ -370,13 +383,16 @@ class MozRadiogroup extends MozElements.
     var radio = document.createXULElement("radio");
     radio.setAttribute("label", label);
     radio.setAttribute("value", value);
     this.appendChild(radio);
     return radio;
   }
 }
 
-MozXULElement.implementCustomInterface(MozRadiogroup, [Ci.nsIDOMXULSelectControlElement]);
+MozXULElement.implementCustomInterface(MozRadiogroup, [
+  Ci.nsIDOMXULSelectControlElement,
+  Ci.nsIDOMXULRadioGroupElement,
+]);
 
 customElements.define("radiogroup", MozRadiogroup);
 
 }