From eb95518ef9b1bf9160f0e95d45811a4ef8c0e1fb Mon Sep 17 00:00:00 2001
From: Thomas Bruederli <thomas@roundcube.net>
Date: Thu, 01 May 2014 04:54:45 -0400
Subject: [PATCH] Enable keyboard navigation on treelist widgets with cursor keys

---
 program/js/treelist.js |   93 ++++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 89 insertions(+), 4 deletions(-)

diff --git a/program/js/treelist.js b/program/js/treelist.js
index 0dbedd2..fa39078 100644
--- a/program/js/treelist.js
+++ b/program/js/treelist.js
@@ -52,6 +52,7 @@
     indexbyid = {},
     selection = null,
     drag_active = false,
+    has_focus = false,
     box_coords = {},
     item_coords = [],
     autoexpand_timer,
@@ -105,7 +106,18 @@
     }
   });
 
+  container.on('focusin', function(e){
+      // TODO: only accept focus on virtual nodes from keyboard events
+      has_focus = true;
+    })
+    .on('focusout', function(e){
+      has_focus = false;
+    });
+
   container.attr('role', 'tree');
+
+  $(document.body)
+    .bind('keydown', keypress);
 
 
   /////// private methods
@@ -156,13 +168,13 @@
   function select(id)
   {
     if (selection) {
-      id2dom(selection).removeClass('selected');
+      id2dom(selection).removeClass('selected').removeAttr('aria-selected');
       selection = null;
     }
 
     var li = id2dom(id);
     if (li.length) {
-      li.addClass('selected');
+      li.addClass('selected').attr('aria-selected', 'true');
       selection = id;
       // TODO: expand all parent nodes if collapsed
       scroll_to_node(li);
@@ -368,6 +380,7 @@
 
     var li = $('<li>')
       .attr('id', p.id_prefix + (p.id_encode ? p.id_encode(node.id) : node.id))
+      .attr('role', 'treeitem')
       .addClass((node.classes || []).join(' '));
 
     if (replace)
@@ -388,7 +401,7 @@
     // add child list and toggle icon
     if (node.children && node.children.length) {
       $('<div class="treetoggle '+(node.collapsed ? 'collapsed' : 'expanded') + '">&nbsp;</div>').appendTo(li);
-      var ul = $('<ul>').appendTo(li).attr('class', node.childlistclass);
+      var ul = $('<ul>').appendTo(li).attr('class', node.childlistclass).attr('role', 'tree');
       if (node.collapsed)
         ul.hide();
 
@@ -424,15 +437,23 @@
         node.collapsed = sublist.css('display') == 'none';
       }
       if (li.hasClass('selected')) {
+        li.attr('aria-selected', 'true');
         selection = node.id;
       }
 
       // declare list item as treeitem
       li.attr('role', 'treeitem');
 
+      // allow virtual nodes to receive focus
+      if (node.virtual) {
+        li.children('a:first').attr('tabindex', '0');
+      }
+
       result.push(node);
       indexbyid[node.id] = node;
-    })
+    });
+
+    ul.attr('role', 'tree');
 
     return result;
   }
@@ -481,6 +502,70 @@
       scroller.scrollTop(rel_offset + current_offset);
   }
 
+  /**
+   * Handler for keyboard events on treelist
+   */
+  function keypress(e)
+  {
+    var target = e.target || {},
+      keyCode = rcube_event.get_keycode(e);
+
+    if (!has_focus || target.nodeName == 'INPUT' || target.nodeName == 'TEXTAREA' || target.nodeName == 'SELECT')
+      return true;
+
+    switch (keyCode) {
+      case 38:
+      case 40:
+      case 63232: // 'up', in safari keypress
+      case 63233: // 'down', in safari keypress
+        var li = container.find(':focus').closest('li');
+        if (li.length) {
+          focus_next(li, (mod = keyCode == 38 || keyCode == 63232 ? -1 : 1));
+        }
+        break;
+
+      case 37: // Left arrow key
+      case 39: // Right arrow key
+        var id, node, li = container.find(':focus').closest('li');
+        if (li.length) {
+          id = dom2id(li);
+          node = indexbyid[id];
+          if (node && node.children.length)
+            toggle(id, rcube_event.get_modifier(e) == SHIFT_KEY);  // toggle subtree
+        }
+        return false;
+    }
+
+    return true;
+  }
+
+  function focus_next(li, dir, from_child)
+  {
+    var mod = dir < 0 ? 'prev' : 'next',
+      next = li[mod](), limit, parent;
+
+    if (dir > 0 && !from_child && li.children('ul[role=tree]:visible').length) {
+      li.children('ul').children('li:first').children('a:first').focus();
+    }
+    else if (dir < 0 && !from_child && next.children('ul[role=tree]:visible').length) {
+      next.children('ul').children('li:last').children('a:last').focus();
+    }
+    else if (next.length && next.children('a:first')) {
+      next.children('a:first').focus();
+    }
+    else {
+      parent = li.parent().closest('li[role=treeitem]');
+      if (parent.length)
+        if (dir < 0) {
+          parent.children('a:first').focus();
+        }
+        else {
+          focus_next(parent, dir, true);
+        }
+    }
+  }
+
+
   ///// drag & drop support
 
   /**

--
Gitblit v1.9.1