Bug 864224 - Support nested ARIA listitems structured by role='group', r=tbsaunde
authorAlexander Surkov <surkov.alexander@gmail.com>
Thu, 16 May 2013 15:38:17 +0900
changeset 143566 5a3f4463fcbb3a4cf98a57216fac169ff08a1270
parent 143565 6358dfe4de6981021acff29abe653317523d1bbb
child 143567 537f5deb09cd24c9db5fd620f861d365e89e2edf
push id2697
push userbbajaj@mozilla.com
push dateMon, 05 Aug 2013 18:49:53 +0000
treeherdermozilla-beta@dfec938c7b63 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstbsaunde
bugs864224
milestone24.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 864224 - Support nested ARIA listitems structured by role='group', r=tbsaunde
accessible/src/base/AccGroupInfo.cpp
accessible/src/generic/Accessible.cpp
accessible/tests/mochitest/attributes/test_obj_group.html
accessible/tests/mochitest/relations/test_general.html
--- a/accessible/src/base/AccGroupInfo.cpp
+++ b/accessible/src/base/AccGroupInfo.cpp
@@ -105,62 +105,79 @@ AccGroupInfo::AccGroupInfo(Accessible* a
 
   if (mParent)
     return;
 
   roles::Role parentRole = parent->Role();
   if (IsConceptualParent(aRole, parentRole))
     mParent = parent;
 
-  // In the case of ARIA tree (not ARIA treegrid) a tree can be arranged by
-  // using ARIA groups to organize levels. In this case the parent of the tree
-  // item will be a group and the previous treeitem of that should be the tree
-  // item parent.
-  if (parentRole != roles::GROUPING || aRole != roles::OUTLINEITEM)
-    return;
-
-  Accessible* parentPrevSibling = parent->PrevSibling();
-  if (!parentPrevSibling)
+  // ARIA tree and list can be arranged by using ARIA groups to organize levels.
+  if (parentRole != roles::GROUPING)
     return;
 
-  roles::Role parentPrevSiblingRole = parentPrevSibling->Role();
-  if (parentPrevSiblingRole == roles::TEXT_LEAF) {
-    // XXX Sometimes an empty text accessible is in the hierarchy here,
-    // although the text does not appear to be rendered, GetRenderedText()
-    // says that it is so we need to skip past it to find the true
-    // previous sibling.
-    parentPrevSibling = parentPrevSibling->PrevSibling();
-    if (parentPrevSibling)
-      parentPrevSiblingRole = parentPrevSibling->Role();
+  // Way #1 for ARIA tree (not ARIA treegrid): previous sibling of a group is a
+  // parent. In other words the parent of the tree item will be a group and
+  // the previous tree item of the group is a conceptual parent of the tree
+  // item.
+  if (aRole == roles::OUTLINEITEM) {
+    Accessible* parentPrevSibling = parent->PrevSibling();
+    if (parentPrevSibling && parentPrevSibling->Role() == aRole) {
+      mParent = parentPrevSibling;
+      return;
+    }
   }
 
-  // Previous sibling of parent group is a tree item, this is the
-  // conceptual tree item parent.
-  if (parentPrevSiblingRole == roles::OUTLINEITEM)
-    mParent = parentPrevSibling;
+  // Way #2 for ARIA list and tree: group is a child of an item. In other words
+  // the parent of the item will be a group and containing item of the group is
+  // a conceptual parent of the item.
+  if (aRole == roles::LISTITEM || aRole == roles::OUTLINEITEM) {
+    Accessible* grandParent = parent->Parent();
+    if (grandParent && grandParent->Role() == aRole)
+      mParent = grandParent;
+  }
 }
 
 Accessible*
 AccGroupInfo::FirstItemOf(Accessible* aContainer)
 {
-  // ARIA trees can be arranged by ARIA groups, otherwise aria-level works.
+  // ARIA tree can be arranged by ARIA groups case #1 (previous sibling of a
+  // group is a parent) or by aria-level.
   a11y::role containerRole = aContainer->Role();
   Accessible* item = aContainer->NextSibling();
   if (item) {
     if (containerRole == roles::OUTLINEITEM && item->Role() == roles::GROUPING)
       item = item->FirstChild();
 
-    AccGroupInfo* itemGroupInfo = item->GetGroupInfo();
-    if (itemGroupInfo && itemGroupInfo->ConceptualParent() == aContainer)
-      return item;
+    if (item) {
+      AccGroupInfo* itemGroupInfo = item->GetGroupInfo();
+      if (itemGroupInfo && itemGroupInfo->ConceptualParent() == aContainer)
+        return item;
+    }
+  }
+
+  // ARIA list and tree can be arranged by ARIA groups case #2 (group is
+  // a child of an item).
+  item = aContainer->LastChild();
+  if (!item)
+    return nullptr;
+
+  if (item->Role() == roles::GROUPING &&
+      (containerRole == roles::LISTITEM || containerRole == roles::OUTLINEITEM)) {
+    item = item->FirstChild();
+    if (item) {
+      AccGroupInfo* itemGroupInfo = item->GetGroupInfo();
+      if (itemGroupInfo && itemGroupInfo->ConceptualParent() == aContainer)
+        return item;
+    }
   }
 
   // Otherwise it can be a direct child.
   item = aContainer->FirstChild();
-  if (item && IsConceptualParent(BaseRole(item->Role()), containerRole))
+  if (IsConceptualParent(BaseRole(item->Role()), containerRole))
     return item;
 
   return nullptr;
 }
 
 Accessible*
 AccGroupInfo::NextItemTo(Accessible* aItem)
 {
--- a/accessible/src/generic/Accessible.cpp
+++ b/accessible/src/generic/Accessible.cpp
@@ -2019,17 +2019,18 @@ Accessible::RelationByType(uint32_t aTyp
     }
 
     case nsIAccessibleRelation::RELATION_NODE_CHILD_OF: {
       Relation rel(new RelatedAccIterator(Document(), mContent,
                                           nsGkAtoms::aria_owns));
 
       // This is an ARIA tree or treegrid that doesn't use owns, so we need to
       // get the parent the hard way.
-      if (mRoleMapEntry && (mRoleMapEntry->role == roles::OUTLINEITEM || 
+      if (mRoleMapEntry && (mRoleMapEntry->role == roles::OUTLINEITEM ||
+                            mRoleMapEntry->role == roles::LISTITEM ||
                             mRoleMapEntry->role == roles::ROW)) {
         rel.AppendTarget(GetGroupInfo()->ConceptualParent());
       }
 
       // If accessible is in its own Window, or is the root of a document,
       // then we should provide NODE_CHILD_OF relation so that MSAA clients
       // can easily get to true parent instead of getting to oleacc's
       // ROLE_WINDOW accessible which will prevent us from going up further
@@ -2050,18 +2051,20 @@ Accessible::RelationByType(uint32_t aTyp
 
     case nsIAccessibleRelation::RELATION_NODE_PARENT_OF: {
       Relation rel(new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_owns));
 
       // ARIA tree or treegrid can do the hierarchy by @aria-level, ARIA trees
       // also can be organized by groups.
       if (mRoleMapEntry &&
           (mRoleMapEntry->role == roles::OUTLINEITEM ||
+           mRoleMapEntry->role == roles::LISTITEM ||
            mRoleMapEntry->role == roles::ROW ||
            mRoleMapEntry->role == roles::OUTLINE ||
+           mRoleMapEntry->role == roles::LIST ||
            mRoleMapEntry->role == roles::TREE_TABLE)) {
         rel.AppendIter(new ItemIterator(this));
       }
 
       return rel;
     }
 
     case nsIAccessibleRelation::RELATION_CONTROLLED_BY:
@@ -3251,45 +3254,48 @@ Accessible::GetLevelInternal()
       if (parentRole == roles::OUTLINE)
         break;
       if (parentRole == roles::GROUPING)
         ++ level;
 
     }
 
   } else if (role == roles::LISTITEM) {
-    // Expose 'level' attribute on nested lists. We assume nested list is a last
-    // child of listitem of parent list. We don't handle the case when nested
-    // lists have more complex structure, for example when there are accessibles
-    // between parent listitem and nested list.
+    // Expose 'level' attribute on nested lists. We support two hierarchies:
+    // a) list -> listitem -> list -> listitem (nested list is a last child
+    //   of listitem of the parent list);
+    // b) list -> listitem -> group -> listitem (nested listitems are contained
+    //   by group that is a last child of the parent listitem).
 
     // Calculate 'level' attribute based on number of parent listitems.
     level = 0;
     Accessible* parent = this;
     while ((parent = parent->Parent())) {
       roles::Role parentRole = parent->Role();
 
       if (parentRole == roles::LISTITEM)
         ++ level;
-      else if (parentRole != roles::LIST)
+      else if (parentRole != roles::LIST && parentRole != roles::GROUPING)
         break;
-
     }
 
     if (level == 0) {
       // If this listitem is on top of nested lists then expose 'level'
       // attribute.
       parent = Parent();
       uint32_t siblingCount = parent->ChildCount();
       for (uint32_t siblingIdx = 0; siblingIdx < siblingCount; siblingIdx++) {
         Accessible* sibling = parent->GetChildAt(siblingIdx);
 
         Accessible* siblingChild = sibling->LastChild();
-        if (siblingChild && siblingChild->Role() == roles::LIST)
-          return 1;
+        if (siblingChild) {
+          roles::Role lastChildRole = siblingChild->Role();
+          if (lastChildRole == roles::LIST || lastChildRole == roles::GROUPING)
+            return 1;
+        }
       }
     } else {
       ++ level; // level is 1-index based
     }
   }
 
   return level;
 }
--- a/accessible/tests/mochitest/attributes/test_obj_group.html
+++ b/accessible/tests/mochitest/attributes/test_obj_group.html
@@ -63,26 +63,35 @@
 
       //////////////////////////////////////////////////////////////////////////
       // ARIA list
       testGroupAttrs("li7", 1, 3);
       testGroupAttrs("li8", 2, 3);
       testGroupAttrs("li9", 3, 3);
 
       //////////////////////////////////////////////////////////////////////////
-      // ARIA list (nested lists)
+      // ARIA list (nested lists: list -> listitem -> list -> listitem)
       testGroupAttrs("li10", 1, 3, 1);
       testGroupAttrs("li11", 2, 3, 1);
       testGroupAttrs("li12", 3, 3, 1);
 
       testGroupAttrs("n_li10", 1, 3, 2);
       testGroupAttrs("n_li11", 2, 3, 2);
       testGroupAttrs("n_li12", 3, 3, 2);
 
       //////////////////////////////////////////////////////////////////////////
+      // ARIA list (nested lists: list -> listitem -> group -> listitem)
+      testGroupAttrs("lgt_li1", 1, 2, 1);
+      testGroupAttrs("lgt_li1_nli1", 1, 2, 2);
+      testGroupAttrs("lgt_li1_nli2", 2, 2, 2);
+      testGroupAttrs("lgt_li2", 2, 2, 1);
+      testGroupAttrs("lgt_li2_nli1", 1, 2, 2);
+      testGroupAttrs("lgt_li2_nli2", 2, 2, 2);
+
+      //////////////////////////////////////////////////////////////////////////
       // ARIA menu (menuitem, separator, menuitemradio and menuitemcheckbox)
       testGroupAttrs("menu_item1", 1, 2);
       testGroupAttrs("menu_item2", 2, 2);
       testGroupAttrs("menu_item1.1", 1, 2);
       testGroupAttrs("menu_item1.2", 2, 2);
       testGroupAttrs("menu_item1.3", 1, 3);
       testGroupAttrs("menu_item1.4", 2, 3);
       testGroupAttrs("menu_item1.5", 3, 3);
@@ -106,16 +115,34 @@
       testGroupAttrs("ti3", 2, 2, 2);
       testGroupAttrs("ti4", 2, 3, 1);
       testGroupAttrs("ti5", 1, 3, 2);
       testGroupAttrs("ti6", 2, 3, 2);
       testGroupAttrs("ti7", 3, 3, 2);
       testGroupAttrs("ti8", 3, 3, 1);
 
       //////////////////////////////////////////////////////////////////////////
+      // ARIA tree (tree -> treeitem -> group -> treeitem)
+      testGroupAttrs("tree2_ti1", 1, 2, 1);
+      testGroupAttrs("tree2_ti1a", 1, 2, 2);
+      testGroupAttrs("tree2_ti1b", 2, 2, 2);
+      testGroupAttrs("tree2_ti2", 2, 2, 1);
+      testGroupAttrs("tree2_ti2a", 1, 2, 2);
+      testGroupAttrs("tree2_ti2b", 2, 2, 2);
+
+      //////////////////////////////////////////////////////////////////////////
+      // ARIA tree (tree -> treeitem, group -> treeitem)
+      testGroupAttrs("tree3_ti1", 1, 2, 1);
+      testGroupAttrs("tree3_ti1a", 1, 2, 2);
+      testGroupAttrs("tree3_ti1b", 2, 2, 2);
+      testGroupAttrs("tree3_ti2", 2, 2, 1);
+      testGroupAttrs("tree3_ti2a", 1, 2, 2);
+      testGroupAttrs("tree3_ti2b", 2, 2, 2);
+
+      //////////////////////////////////////////////////////////////////////////
       // ARIA grid
       testGroupAttrs("grid_row1", 1, 2);
       testGroupAttrs("grid_cell1", 1, 2);
       testGroupAttrs("grid_cell2", 2, 2);
 
       testGroupAttrs("grid_row2", 2, 2);
       testGroupAttrs("grid_cell3", 1, 2);
       testGroupAttrs("grid_cell4", 2, 2);
@@ -159,16 +186,21 @@
 </head>
 <body>
 
   <a target="_blank"
      href="https://bugzilla.mozilla.org/show_bug.cgi?id=468418"
      title="Expose level for nested lists in HTML">
     Mozilla Bug 468418
   </a>
+  <a target="_blank"
+     href="https://bugzilla.mozilla.org/show_bug.cgi?id=864224"
+     title="Support nested ARIA listitems structured by role='group'">
+    Bug 864224
+  </a>
 
   <p id="display"></p>
   <div id="content" style="display: none"></div>
   <pre id="test">
   </pre>
 
   <select size="4">
     <option id="opt1">option1</option>
@@ -223,16 +255,31 @@
       <span role="list">
         <span role="listitem" id="n_li10">Oranges</span>
         <span role="listitem" id="n_li11">Apples</span>
         <span role="listitem" id="n_li12">Bananas</span>
       </span>
     </span>
   </span>
 
+  <div role="list">
+    <div role="listitem" id="lgt_li1">Item 1
+      <div role="group">
+        <div role="listitem" id="lgt_li1_nli1">Item 1A</div>
+        <div role="listitem" id="lgt_li1_nli2">Item 1B</div>
+      </div>
+    </div>
+    <div role="listitem" id="lgt_li2">Item 2
+      <div role="group">
+        <div role="listitem" id="lgt_li2_nli1">Item 2A</div>
+        <div role="listitem" id="lgt_li2_nli2">Item 2B</div>
+      </div>
+    </div>
+  </div>
+
   <ul role="menubar">
     <li role="menuitem" aria-haspopup="true" id="menu_item1">File
       <ul role="menu">
         <li role="menuitem" id="menu_item1.1">New</li>
         <li role="menuitem" id="menu_item1.2">Open…</li>
         <li role="separator">-----</li>
         <li role="menuitem" id="menu_item1.3">Item</li>
         <li role="menuitemradio" id="menu_item1.4">Radio</li>
@@ -278,16 +325,44 @@
     <tr role="presentation">
       <td role="treeitem" aria-level="2" id="ti7">Audi</td>
     </tr>
     <tr role="presentation">
       <td role="treeitem" aria-level="1" id="ti8">people</td>
     </tr>
   </table>
 
+  <ul role="tree">
+    <li role="treeitem" id="tree2_ti1">Item 1
+      <ul role="group">
+        <li role="treeitem" id="tree2_ti1a">Item 1A</li>
+        <li role="treeitem" id="tree2_ti1b">Item 1B</li>
+      </ul>
+    </li>
+    <li role="treeitem" id="tree2_ti2">Item 2
+      <ul role="group">
+        <li role="treeitem" id="tree2_ti2a">Item 2A</li>
+        <li role="treeitem" id="tree2_ti2b">Item 2B</li>
+      </ul>
+    </li>
+  </div>
+
+  <div role="tree">
+    <div role="treeitem" id="tree3_ti1">Item 1</div>
+    <div role="group">
+      <li role="treeitem" id="tree3_ti1a">Item 1A</li>
+      <li role="treeitem" id="tree3_ti1b">Item 1B</li>
+    </div>
+    <div role="treeitem" id="tree3_ti2">Item 2</div>
+    <div role="group">
+      <div role="treeitem" id="tree3_ti2a">Item 2A</div>
+      <div role="treeitem" id="tree3_ti2b">Item 2B</div>
+    </div>
+  </div>
+
   <table role="grid">
     <tr role="row" id="grid_row1">
       <td role="gridcell" id="grid_cell1">cell1</td>
       <td role="gridcell" id="grid_cell2">cell2</td>
     </tr>
     <tr role="row" id="grid_row2">
       <td role="gridcell" id="grid_cell3">cell3</td>
       <td role="gridcell" id="grid_cell4">cell4</td>
--- a/accessible/tests/mochitest/relations/test_general.html
+++ b/accessible/tests/mochitest/relations/test_general.html
@@ -68,43 +68,60 @@
       testRelation("treeitem2", RELATION_NODE_CHILD_OF, "tree");
 
       // 'node child of' relation for outlineitem role
       testRelation("treeitem3", RELATION_NODE_CHILD_OF, "tree");
       testRelation("treeitem4", RELATION_NODE_CHILD_OF, "tree");
       testRelation("treeitem5", RELATION_NODE_CHILD_OF, "treeitem4");
       testRelation("treeitem6", RELATION_NODE_CHILD_OF, "tree");
       testRelation("treeitem7", RELATION_NODE_CHILD_OF, "treeitem6");
+      testRelation("tree2_ti1", RELATION_NODE_CHILD_OF, "tree2");
+      testRelation("tree2_ti1a", RELATION_NODE_CHILD_OF, "tree2_ti1");
+      testRelation("tree2_ti1b", RELATION_NODE_CHILD_OF, "tree2_ti1");
 
       // 'node child of' relation for row role of treegrid
       testRelation("treegridrow1", RELATION_NODE_CHILD_OF, "treegrid");
       testRelation("treegridrow2", RELATION_NODE_CHILD_OF, "treegrid");
       testRelation("treegridrow3", RELATION_NODE_CHILD_OF, "treegridrow2");
 
+      // 'node child of' relation for lists organized by groups
+      testRelation("listitem1", RELATION_NODE_CHILD_OF, "list");
+      testRelation("listitem1.1", RELATION_NODE_CHILD_OF, "listitem1");
+      testRelation("listitem1.2", RELATION_NODE_CHILD_OF, "listitem1");
+
       // 'node child of' relation for the document having window, returns
       // direct accessible parent (fixed in bug 419770).
       var iframeElmObj = {};
       var iframeAcc = getAccessible("iframe", null, iframeElmObj);
       var iframeDoc = iframeElmObj.value.contentDocument;
       var iframeDocAcc = getAccessible(iframeDoc);
       testRelation(iframeDocAcc, RELATION_NODE_CHILD_OF, iframeAcc);
 
       // 'node parent of' relation on ARIA tree and treegrid.
       testRelation("tree", RELATION_NODE_PARENT_OF,
                     ["treeitem1", "treeitem2", // aria-owns
                      "treeitem3", "treeitem4", "treeitem6"]); // children
       testRelation("treeitem4", RELATION_NODE_PARENT_OF,
                    "treeitem5"); // aria-level
       testRelation("treeitem6", RELATION_NODE_PARENT_OF,
                    "treeitem7"); // // group role
+      testRelation("tree2", RELATION_NODE_PARENT_OF, "tree2_ti1"); // group role
+      testRelation("tree2_ti1", RELATION_NODE_PARENT_OF,
+                   ["tree2_ti1a", "tree2_ti1b"]); // group role
 
       testRelation("treegridrow2", RELATION_NODE_PARENT_OF, "treegridrow3");
       testRelation("treegrid", RELATION_NODE_PARENT_OF,
                    ["treegridrow1", "treegridrow2"]);
 
+      // 'node parent of' relation on ARIA list structured by groups
+      testRelation("list", RELATION_NODE_PARENT_OF,
+                   "listitem1");
+      testRelation("listitem1", RELATION_NODE_PARENT_OF,
+                   [ "listitem1.1", "listitem1.2" ]);
+
       // aria-controls
       getAccessible("tab");
       todo(false,
            "Getting an accessible tab, otherwise relations for tabpanel aren't cached. Bug 606924 will fix that.");
       testRelation("tabpanel", RELATION_CONTROLLED_BY, "tab");
       testRelation("tab", RELATION_CONTROLLER_FOR, "tabpanel");
 
       // aria-controls, multiple relations
@@ -173,16 +190,21 @@
      title="Ignore implicit label association when it's associated explicitly">
     Bug 682790
   </a>
   <a target="_blank"
      href="https://bugzilla.mozilla.org/show_bug.cgi?id=687393"
      title="HTML select options gets relation from containing label">
     Bug 687393
   </a>
+  <a target="_blank"
+     href="https://bugzilla.mozilla.org/show_bug.cgi?id=864224"
+     title="Support nested ARIA listitems structured by role='group'">
+    Bug 864224
+  </a>
   <p id="display"></p>
   <div id="content" style="display: none"></div>
   <pre id="test">
   </pre>
 
   <label id="label1_1" for="control1_1">label</label>
   <input id="control1_1">
 
@@ -248,28 +270,46 @@
     <div role="treeitem" id="treeitem4" aria-level="1">Green</div>
     <div role="treeitem" id="treeitem5" aria-level="2">Light green</div>
     <div role="treeitem" id="treeitem6" aria-level="1">Green2</div>
     <div role="group">
       <div role="treeitem" id="treeitem7">Super light green</div>
     </div>
   </div>
 
+  <ul role="tree" id="tree2">
+    <li role="treeitem" id="tree2_ti1">Item 1
+      <ul role="group">
+        <li role="treeitem" id="tree2_ti1a">Item 1A</li>
+        <li role="treeitem" id="tree2_ti1b">Item 1B</li>
+      </ul>
+    </li>
+  </ul>
+
   <div role="treegrid" id="treegrid">
     <div role="row" id="treegridrow1">
       <span role="gridcell">cell1</span><span role="gridcell">cell2</span>
     </div>
     <div role="row" id="treegridrow2" aria-level="1">
       <span role="gridcell">cell3</span><span role="gridcell">cell4</span>
     </div>
     <div role="row" id="treegridrow3" aria-level="2">
       <span role="gridcell">cell5</span><span role="gridcell">cell6</span>
     </div>
   </div>
 
+  <div role="list" id="list">
+    <div role="listitem" id="listitem1">Item 1
+      <div role="group">
+        <div role="listitem" id="listitem1.1">Item 1A</div>
+        <div role="listitem" id="listitem1.2">Item 1B</div>
+      </div>
+    </div>
+  </div>
+
   <iframe id="iframe"></iframe>
 
   <div id="tablist" role="tablist">
     <div id="tab" role="tab" aria-controls="tabpanel">tab</div>
   </div>
   <div id="tabpanel" role="tabpanel">tabpanel</div>
 
   <div id="lr1" aria-live="assertive">1</div>