Bug 526703 - Screen readers announce ARIA list items, tree items, and listbox items as 'not selected', r=marcoz
authorAlexander Surkov <surkov.alexander@gmail.com>
Sat, 21 Jan 2012 12:06:26 +0100
changeset 86285 8c9477207cf661be358fb23daa92540d6fd41878
parent 86284 62b602a493940a8cd8b2e4007674a97c79be303a
child 86286 8ff77543b294b308930d36c245cdd56907445f38
push id805
push userakeybl@mozilla.com
push dateWed, 01 Feb 2012 18:17:35 +0000
treeherdermozilla-aurora@6fb3bf232436 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmarcoz
bugs526703
milestone12.0a1
Bug 526703 - Screen readers announce ARIA list items, tree items, and listbox items as 'not selected', r=marcoz
accessible/src/base/nsAccessible.cpp
accessible/tests/mochitest/states/Makefile.in
accessible/tests/mochitest/states/test_aria_tabs.html
accessible/tests/mochitest/states/test_aria_widgetitems.html
--- a/accessible/src/base/nsAccessible.cpp
+++ b/accessible/src/base/nsAccessible.cpp
@@ -1468,33 +1468,44 @@ nsAccessible::State()
 {
   if (IsDefunct())
     return states::DEFUNCT;
 
   PRUint64 state = NativeState();
   // Apply ARIA states to be sure accessible states will be overridden.
   ApplyARIAState(&state);
 
-  if (mRoleMapEntry && mRoleMapEntry->role == roles::PAGETAB &&
-      !(state & states::SELECTED) &&
+  // If this is an ARIA item of the selectable widget and if it's focused and
+  // not marked unselected explicitly (i.e. aria-selected="false") then expose
+  // it as selected to make ARIA widget authors life easier.
+  if (mRoleMapEntry && !(state & states::SELECTED) &&
       !mContent->AttrValueIs(kNameSpaceID_None,
                              nsGkAtoms::aria_selected,
                              nsGkAtoms::_false, eCaseMatters)) {
-    // Special case: for tabs, focused implies selected, unless explicitly
-    // false, i.e. aria-selected="false".
-    if (state & states::FOCUSED) {
-      state |= states::SELECTED;
-    } else {
-      // If focus is in a child of the tab panel surely the tab is selected!
-      Relation rel = RelationByType(nsIAccessibleRelation::RELATION_LABEL_FOR);
-      nsAccessible* relTarget = nsnull;
-      while ((relTarget = rel.Next())) {
-        if (relTarget->Role() == roles::PROPERTYPAGE &&
-            FocusMgr()->IsFocusWithin(relTarget))
-          state |= states::SELECTED;
+    // Special case for tabs: focused tab or focus inside related tab panel
+    // implies selected state.
+    if (mRoleMapEntry->role == roles::PAGETAB) {
+      if (state & states::FOCUSED) {
+        state |= states::SELECTED;
+      } else {
+        // If focus is in a child of the tab panel surely the tab is selected!
+        Relation rel = RelationByType(nsIAccessibleRelation::RELATION_LABEL_FOR);
+        nsAccessible* relTarget = nsnull;
+        while ((relTarget = rel.Next())) {
+          if (relTarget->Role() == roles::PROPERTYPAGE &&
+              FocusMgr()->IsFocusWithin(relTarget))
+            state |= states::SELECTED;
+        }
+      }
+    } else if (state & states::FOCUSED) {
+      nsAccessible* container = nsAccUtils::GetSelectableContainer(this, state);
+      if (container &&
+          !nsAccUtils::HasDefinedARIAToken(container->GetContent(),
+                                           nsGkAtoms::aria_multiselectable)) {
+        state |= states::SELECTED;
       }
     }
   }
 
   const PRUint32 kExpandCollapseStates = states::COLLAPSED | states::EXPANDED;
   if ((state & kExpandCollapseStates) == kExpandCollapseStates) {
     // Cannot be both expanded and collapsed -- this happens in ARIA expanded
     // combobox because of limitation of nsARIAMap.
--- a/accessible/tests/mochitest/states/Makefile.in
+++ b/accessible/tests/mochitest/states/Makefile.in
@@ -43,17 +43,17 @@ VPATH		= @srcdir@
 relativesrcdir  = accessible/states
 
 include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
 _TEST_FILES =\
 		test_aria.html \
 		test_aria_imgmap.html \
-		test_aria_tabs.html \
+		test_aria_widgetitems.html \
 		test_buttons.html \
 		test_doc.html \
 		test_docarticle.html \
 		test_editablebody.html \
 		test_expandable.xul \
 		test_frames.html \
 		test_inputs.html \
 		test_inputs.xul \
rename from accessible/tests/mochitest/states/test_aria_tabs.html
rename to accessible/tests/mochitest/states/test_aria_widgetitems.html
--- a/accessible/tests/mochitest/states/test_aria_tabs.html
+++ b/accessible/tests/mochitest/states/test_aria_widgetitems.html
@@ -13,123 +13,148 @@
   <script type="application/javascript"
           src="../common.js"></script>
   <script type="application/javascript"
           src="../states.js"></script>
   <script type="application/javascript"
           src="../events.js"></script>
 
   <script type="application/javascript">
-    function focusARIATab(aID, aIsSelected)
+    function focusARIAItem(aID, aIsSelected)
     {
       this.DOMNode = getNode(aID);
 
-      this.invoke = function focusARIATab_invoke()
+      this.invoke = function focusARIAItem_invoke()
       {
         this.DOMNode.focus();
       }
 
-      this.check = function focusARIATab_check(aEvent)
+      this.check = function focusARIAItem_check(aEvent)
       {
         testStates(this.DOMNode, aIsSelected ? STATE_SELECTED : 0, 0,
                    aIsSelected ? 0 : STATE_SELECTED);
       }
 
-      this.getID = function focusARIATab_getID()
+      this.getID = function focusARIAItem_getID()
       {
-        return "Focused ARIA Tab with aria-selected=" +
-                (aIsSelected ? "true, should" : "false, shouldn't") +
+        return "Focused ARIA widget item with aria-selected='" +
+                (aIsSelected ? "true', should" : "false', shouldn't") +
                 " have selected state on " + prettyName(aID);
       }
     }
 
-    function focusActiveDescendantTab(aTabID, aTabListID, aIsSelected)
+    function focusActiveDescendantItem(aItemID, aWidgetID, aIsSelected)
     {
-      this.DOMNode = getNode(aTabID);
-      this.tabListDOMNode = getNode(aTabListID);
+      this.DOMNode = getNode(aItemID);
+      this.widgetDOMNode = getNode(aWidgetID);
 
-      this.invoke = function focusActiveDescendantTab_invoke()
+      this.invoke = function focusActiveDescendantItem_invoke()
       {
-        this.tabListDOMNode.setAttribute("aria-activedescendant", aTabID);
-        this.tabListDOMNode.focus();
+        this.widgetDOMNode.setAttribute("aria-activedescendant", aItemID);
+        this.widgetDOMNode.focus();
       }
 
-      this.check = function focusActiveDescendantTab_check(aEvent)
+      this.check = function focusActiveDescendantItem_check(aEvent)
       {
         testStates(this.DOMNode, aIsSelected ? STATE_SELECTED : 0, 0,
                    aIsSelected ? 0 : STATE_SELECTED);
       }
 
       this.getID = function tabActiveDescendant_getID()
       {
-        return "ARIA Tab with activedescendant " +
+        return "ARIA widget item managed by activedescendant " +
                 (aIsSelected ? "should" : "shouldn't") +
-                " have the selected state on " + prettyName(aTabID);
+                " have the selected state on " + prettyName(aItemID);
       }
     }
 
     ////////////////////////////////////////////////////////////////////////////
     // Test
 
     //gA11yEventDumpID = "eventdump"; // debug stuff
     //gA11yEventDumpToConsole = true;
 
     var gQueue = null;
 
     function doTest()
     {
-      // simple tabs
+      // aria-selected
       testStates("aria_tab1", 0, 0, STATE_SELECTED);
       testStates("aria_tab2", STATE_SELECTED);
+      testStates("aria_tab3", 0, 0, STATE_SELECTED);
+      testStates("aria_option1", 0, 0, STATE_SELECTED);
+      testStates("aria_option2", STATE_SELECTED);
+      testStates("aria_option3", 0, 0, STATE_SELECTED);
+      testStates("aria_treeitem1", 0, 0, STATE_SELECTED);
+      testStates("aria_treeitem2", STATE_SELECTED);
+      testStates("aria_treeitem3", 0, 0, STATE_SELECTED);
 
-      // To make sure our focus != selected is truly under test, we need to
-      // make sure our cache of what currently has focus is correct, which
-      // we update asyncronously.
+      // selected state when widget item is focused
       gQueue = new eventQueue(EVENT_FOCUS);
 
-      gQueue.push(new focusARIATab("aria_tab1", true));
-      gQueue.push(new focusARIATab("aria_tab3", false));
-      gQueue.push(new focusARIATab("aria_tab2", true));
+      gQueue.push(new focusARIAItem("aria_tab1", true));
+      gQueue.push(new focusARIAItem("aria_tab2", true));
+      gQueue.push(new focusARIAItem("aria_tab3", false));
+      gQueue.push(new focusARIAItem("aria_option1", true));
+      gQueue.push(new focusARIAItem("aria_option2", true));
+      gQueue.push(new focusARIAItem("aria_option3", false));
+      gQueue.push(new focusARIAItem("aria_treeitem1", true));
+      gQueue.push(new focusARIAItem("aria_treeitem2", true));
+      gQueue.push(new focusARIAItem("aria_treeitem3", false));
 
-      // selection through aria-activedescendant
-      // Make sure initially setting it selects the tab.
-      gQueue.push(new focusActiveDescendantTab("aria_tab5", "aria_tablist2", true));
-
-      // Now, make sure if one is selected selection gets transferred properly.
-      gQueue.push(new focusActiveDescendantTab("aria_tab6", "aria_tablist2", true));
-
-      // Now, make sure the focused but explicitly unselected one behaves.
-      gQueue.push(new focusActiveDescendantTab("aria_tab4", "aria_tablist2", false));
+      // selected state when widget item is focused (by aria-activedescendant)
+      gQueue.push(new focusActiveDescendantItem("aria_tab5", "aria_tablist2", true));
+      gQueue.push(new focusActiveDescendantItem("aria_tab6", "aria_tablist2", true));
+      gQueue.push(new focusActiveDescendantItem("aria_tab4", "aria_tablist2", false));
 
       gQueue.invoke(); // SimpleTest.finish() will be called in the end
     }
 
     SimpleTest.waitForExplicitFinish();
     addA11yLoadEvent(doTest);
   </script>
 </head>
 <body>
 
   <a target="_blank"
      href="https://bugzilla.mozilla.org/show_bug.cgi?id=653601"
      title="aria-selected ignored for ARIA tabs">
     Mozilla Bug 653601
   </a>
+  <a target="_blank"
+     href="https://bugzilla.mozilla.org/show_bug.cgi?id=526703"
+     title="Focused widget item should expose selected state by default">
+    Mozilla Bug 526703
+  </a>
   <p id="display"></p>
   <div id="content" style="display: none"></div>
   <pre id="test">
   </pre>
 
   <!-- tab -->
   <div id="aria_tablist" role="tablist">
     <div id="aria_tab1" role="tab" tabindex="0">unselected tab</div>
     <div id="aria_tab2" role="tab" tabindex="0" aria-selected="true">selected tab</div>
     <div id="aria_tab3" role="tab" tabindex="0" aria-selected="false">focused explicitly unselected tab</div>
   </div>
-  
-  <!-- test activeDescendant -->
+
+  <!-- listbox -->
+  <div id="aria_listbox" role="listbox">
+    <div id="aria_option1" role="option" tabindex="0">unselected option</div>
+    <div id="aria_option2" role="option" tabindex="0" aria-selected="true">selected option</div>
+    <div id="aria_option3" role="option" tabindex="0" aria-selected="false">focused explicitly unselected option</div>
+  </div>
+
+  <!-- tree -->
+  <div id="aria_tree" role="tree">
+    <div id="aria_treeitem1" role="treeitem" tabindex="0">unselected treeitem</div>
+    <div id="aria_treeitem2" role="treeitem" tabindex="0" aria-selected="true">selected treeitem</div>
+    <div id="aria_treeitem3" role="treeitem" tabindex="0" aria-selected="false">focused explicitly unselected treeitem</div>
+  </div>
+
+  <!-- tab managed by active-descendant -->
   <div id="aria_tablist2" role="tablist" tabindex="0">
     <div id="aria_tab4" role="tab" aria-selected="false">focused explicitly unselected tab</div>
     <div id="aria_tab5" role="tab">initially selected tab</div>
     <div id="aria_tab6" role="tab">later selected tab</div>
   </div>
 </body>
 </html>