From e8bcf08c72a18b3bf396e6448d6658227ecb46f2 Mon Sep 17 00:00:00 2001
From: Thomas Bruederli <thomas@roundcube.net>
Date: Wed, 30 Apr 2014 10:21:29 -0400
Subject: [PATCH] 1. Prepare core and Larry skin for improved accessibility 2. Implement full keyboard navigation in main mail view

---
 program/js/list.js                              |   60 +++++++-
 program/include/rcmail_output_html.php          |    9 +
 program/js/treelist.js                          |    5 
 skins/larry/includes/mailtoolbar.html           |   46 +++---
 program/js/common.js                            |   20 ++
 program/lib/Roundcube/html.php                  |    5 
 skins/larry/templates/mail.html                 |   20 +-
 skins/larry/styles.css                          |   58 +++++++
 skins/larry/includes/header.html                |    4 
 skins/larry/templates/login.html                |    6 
 plugins/legacy_browser/skins/larry/ie7hacks.css |    1 
 program/js/app.js                               |   31 ++-
 skins/larry/mail.css                            |   22 +++
 skins/larry/ui.js                               |   98 ++++++++++++-
 14 files changed, 307 insertions(+), 78 deletions(-)

diff --git a/plugins/legacy_browser/skins/larry/ie7hacks.css b/plugins/legacy_browser/skins/larry/ie7hacks.css
index 2a17400..85ebaf2 100644
--- a/plugins/legacy_browser/skins/larry/ie7hacks.css
+++ b/plugins/legacy_browser/skins/larry/ie7hacks.css
@@ -37,6 +37,7 @@
 a.iconbutton,
 a.deletebutton,
 .boxpagenav a.icon,
+a.button span.icon,
 .pagenav a.button span.inner,
 .boxfooter .listbutton .inner,
 .attachmentslist li a.delete,
diff --git a/program/include/rcmail_output_html.php b/program/include/rcmail_output_html.php
index eb4a52d..a57165f 100644
--- a/program/include/rcmail_output_html.php
+++ b/program/include/rcmail_output_html.php
@@ -1130,6 +1130,15 @@
             $attrib['alt'] = html::quote($this->app->gettext($attrib['alt'], $attrib['domain']));
         }
 
+        // set accessibility attributes
+        if (!$attrib['role']) {
+            $attrib['role'] = 'button';
+        }
+        if (!empty($attrib['class']) && !empty($attrib['classact']) || !empty($attrib['imagepas']) && !empty($attrib['imageact'])) {
+            $attrib['tabindex'] = '-1';  // disable button by default
+            $attrib['aria-disabled'] = 'true';
+        }
+
         // set title to alt attribute for IE browsers
         if ($this->browser->ie && !$attrib['title'] && $attrib['alt']) {
             $attrib['title'] = $attrib['alt'];
diff --git a/program/js/app.js b/program/js/app.js
index 2451a6d..b2c9209 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -199,6 +199,9 @@
     this.enable_command('close', 'logout', 'mail', 'addressbook', 'settings', 'save-pref',
       'compose', 'undo', 'about', 'switch-task', 'menu-open', 'menu-save', true);
 
+    // set active task button
+    this.set_button(this.task, 'sel');
+
     if (this.env.permaurl)
       this.enable_command('permaurl', 'extwin', true);
 
@@ -233,7 +236,7 @@
           });
 
           document.onmouseup = function(e){ return ref.doc_mouse_up(e); };
-          this.gui_objects.messagelist.parentNode.onmousedown = function(e){ return ref.click_on_list(e); };
+          this.gui_objects.messagelist.parentNode.onclick = function(e){ return ref.click_on_list(e || window.event); };
 
           this.enable_command('toggle_status', 'toggle_flag', 'sort', true);
           this.enable_command('set-listmode', this.env.threads && !this.is_multifolder_listing());
@@ -1605,9 +1608,9 @@
       this.gui_objects.qsearchbox.blur();
 
     if (this.message_list)
-      this.message_list.focus();
+      this.message_list.focus(e);
     else if (this.contact_list)
-      this.contact_list.focus();
+      this.contact_list.focus(e);
 
     return true;
   };
@@ -1953,10 +1956,12 @@
 
     // build subject link
     if (cols.subject) {
-      var action = flags.mbox == this.env.drafts_mailbox ? 'compose' : 'show';
-      var uid_param = flags.mbox == this.env.drafts_mailbox ? '_draft_uid' : '_uid';
-      cols.subject = '<a href="./?_task=mail&_action='+action+'&_mbox='+urlencode(flags.mbox)+'&'+uid_param+'='+urlencode(uid)+'"'+
-        ' onclick="return rcube_event.cancel(event)" onmouseover="rcube_webmail.long_subject_title(this,'+(message.depth+1)+')"><span>'+cols.subject+'</span></a>';
+      var action  = flags.mbox == this.env.drafts_mailbox ? 'compose' : 'show',
+        uid_param = flags.mbox == this.env.drafts_mailbox ? '_draft_uid' : '_uid',
+        query = { _mbox: flags.mbox };
+      query[uid_param] = uid;
+      cols.subject = '<a href="' + this.url(action, query) + '" onclick="return rcube_event.keyboard_only(event)"' +
+        ' onmouseover="rcube_webmail.long_subject_title(this,'+(message.depth+1)+')" tabindex="-1"><span>'+cols.subject+'</span></a>';
     }
 
     // add each submitted col
@@ -6182,9 +6187,6 @@
         init_button(cmd, this.buttons[cmd][i]);
       }
     }
-
-    // set active task button
-    this.set_button(this.task, 'sel');
   };
 
   // set button to a specific state
@@ -6197,7 +6199,7 @@
       button = a_buttons[n];
       obj = document.getElementById(button.id);
 
-      if (!obj)
+      if (!obj || button.status == state)
         continue;
 
       // get default/passive setting of the button
@@ -6226,7 +6228,13 @@
         obj.disabled = state == 'pas';
       }
       else if (button.type == 'uibutton') {
+        button.status = state;
         $(obj).button('option', 'disabled', state == 'pas');
+      }
+      else {
+        $(obj)
+          .attr('tabindex', state == 'pas' || state == 'sel' ? '-1' : '0')
+          .attr('aria-disabled', state == 'pas' || state == 'sel' ? 'true' : 'false');
       }
     }
   };
@@ -7116,6 +7124,7 @@
           this.enable_command('set-listmode', this.env.threads && !is_multifolder);
 
           if ((response.action == 'list' || response.action == 'search') && this.message_list) {
+            this.message_list.focus();
             this.msglist_select(this.message_list);
             this.triggerEvent('listupdate', { folder:this.env.mailbox, rowcount:this.message_list.rowcount });
           }
diff --git a/program/js/common.js b/program/js/common.js
index ff5f9b9..e15c34a 100644
--- a/program/js/common.js
+++ b/program/js/common.js
@@ -281,6 +281,26 @@
   return false;
 },
 
+/**
+ * Determine whether the given event was trigered from keyboard
+ */
+is_keyboard: function(e)
+{
+  return e && (
+      (e.mozInputSource && e.mozInputSource == e.MOZ_SOURCE_KEYBOARD) ||
+      (!e.pageX && (e.pageY || 0) <= 0 && !e.clientX && (e.clientY || 0) <= 0)
+    );
+},
+
+/**
+ * Accept event if triggered from keyboard action (e.g. <Enter>)
+ */
+keyboard_only: function(e)
+{
+  console.log(e);
+  return rcube_event.is_keyboard(e) ? true : rcube_event.cancel(e);
+},
+
 touchevent: function(e)
 {
   return { pageX:e.pageX, pageY:e.pageY, offsetX:e.pageX - e.target.offsetLeft, offsetY:e.pageY - e.target.offsetTop, target:e.target, istouch:true };
diff --git a/program/js/list.js b/program/js/list.js
index 560ee0d..b4b7755 100644
--- a/program/js/list.js
+++ b/program/js/list.js
@@ -98,7 +98,7 @@
     this.rows = {};
     this.rowcount = 0;
 
-    var r, len, rows = this.tbody.childNodes;
+    var r, len, rows = this.tbody.childNodes, me = this;
 
     for (r=0, len=rows.length; r<len; r++) {
       this.rowcount += this.init_row(rows[r]) ? 1 : 0;
@@ -108,8 +108,19 @@
     this.frame = this.list.parentNode;
 
     // set body events
-    if (this.keyboard)
+    if (this.keyboard) {
       rcube_event.add_listener({event:'keydown', object:this, method:'key_press'});
+
+      // install a link element to receive focus.
+      // this helps to maintain the natural tab order when moving focus with keyboard
+      this.focus_elem = $('<a>')
+        .attr('tabindex', '0')
+        .attr('style', 'display:block; width:1px; height:1px; line-height:1px; overflow:hidden; position:absolute; top:-1000px')
+        .html('Select List')
+        .insertAfter(this.list)
+        .on('focus', function(e){ me.focus(e); })
+        .on('blur', function(e){ me.blur(e); });
+    }
   }
 
   return this;
@@ -175,9 +186,9 @@
 
     if (this.fixed_header) {  // copy (modified) fixed header back to the actual table
       $(this.list.tHead).replaceWith($(this.fixed_header).find('thead').clone());
-      $(this.list.tHead).find('tr td').attr('style', '');  // remove fixed widths
+      $(this.list.tHead).find('tr td').attr('style', '').find('a.sortcol').attr('tabindex', '-1');  // remove fixed widths
     }
-    else if (!bw.touch && this.list.className.indexOf('fixedheader') >= 0) {
+    else if (!bw.touch && this.list.className.indexOf('fixedheader') >= 0 && 0) {
       this.init_fixed_header();
     }
 
@@ -219,6 +230,12 @@
   else {
     $(this.fixed_header).find('thead').replaceWith(clone);
   }
+
+  // avoid scrolling header links being focused
+  $(this.list.tHead).find('a.sortcol').attr('tabindex', '-1');
+
+  // set tabindex to fixed header sort links
+  clone.find('a.sortcol').attr('tabindex', '0');
 
   this.thead = clone.get(0);
   this.resize();
@@ -265,6 +282,8 @@
 
   if (sel)
     this.clear_selection();
+  else
+    this.last_selected = 0;
 
   // reset scroll position (in Opera)
   if (this.frame)
@@ -370,6 +389,9 @@
  */
 focus: function(e)
 {
+  if (this.focused)
+    return;
+
   var n, id;
   this.focused = true;
 
@@ -380,20 +402,26 @@
     }
   }
 
+  if (e)
+    rcube_event.cancel(e);
+
   // Un-focus already focused elements (#1487123, #1487316, #1488600, #1488620)
   // It looks that window.focus() does the job for all browsers, but not Firefox (#1489058)
-  $('iframe,:focus:not(body)').blur();
-  window.focus();
+  // We now fix this by explicitly assigning focus to a dedicated link element
+  this.focus_elem.focus();
 
-  if (e || (e = window.event))
-    rcube_event.cancel(e);
+  $(this.list).addClass('focus');
+
+  // set internal focus pointer to first row
+  if (!this.last_selected)
+    this.select_first(CONTROL_KEY);
 },
 
 
 /**
  * remove focus from the list
  */
-blur: function()
+blur: function(e)
 {
   var n, id;
   this.focused = false;
@@ -403,6 +431,8 @@
       $(this.rows[id].obj).removeClass('selected focused').addClass('unfocused');
     }
   }
+  
+  $(this.list).removeClass('focus');
 },
 
 
@@ -1101,8 +1131,10 @@
     this.selection = [];
   }
 
-  if (num_select && !this.selection.length && !no_event)
+  if (num_select && !this.selection.length && !no_event) {
     this.triggerEvent('select');
+    this.last_selected = 0;
+  }
 },
 
 
@@ -1311,9 +1343,17 @@
   }
 
   if (new_row) {
+    // simulate ctr-key if no rows are selected
+    if (!mod_key && !this.selection.length)
+      mod_key = CONTROL_KEY;
+
     this.select_row(new_row.uid, mod_key, false);
     this.scrollto(new_row.uid);
   }
+  else if (!new_row && !selected_row) {
+    // select the first row if none selected yet
+    this.select_first(CONTROL_KEY);
+  }
 
   return false;
 },
diff --git a/program/js/treelist.js b/program/js/treelist.js
index 353eb6b..0dbedd2 100644
--- a/program/js/treelist.js
+++ b/program/js/treelist.js
@@ -105,6 +105,8 @@
     }
   });
 
+  container.attr('role', 'tree');
+
 
   /////// private methods
 
@@ -425,6 +427,9 @@
         selection = node.id;
       }
 
+      // declare list item as treeitem
+      li.attr('role', 'treeitem');
+
       result.push(node);
       indexbyid[node.id] = node;
     })
diff --git a/program/lib/Roundcube/html.php b/program/lib/Roundcube/html.php
index f47ef29..5e07a78 100644
--- a/program/lib/Roundcube/html.php
+++ b/program/lib/Roundcube/html.php
@@ -32,7 +32,7 @@
 
     public static $doctype = 'xhtml';
     public static $lc_tags = true;
-    public static $common_attrib = array('id','class','style','title','align','unselectable');
+    public static $common_attrib = array('id','class','style','title','align','unselectable','tabindex','role');
     public static $containers = array('iframe','div','span','p','h1','h2','h3','ul','form','textarea','table','thead','tbody','tr','th','td','style','script');
 
 
@@ -286,7 +286,8 @@
             // ignore not allowed attributes
             if (!empty($allowed)) {
                 $is_data_attr = @substr_compare($key, 'data-', 0, 5) === 0;
-                if (!isset($allowed_f[$key]) && (!$is_data_attr || !isset($allowed_f['data-*']))) {
+                $is_aria_attr = @substr_compare($key, 'aria-', 0, 5) === 0;
+                if (!$is_aria_attr && !isset($allowed_f[$key]) && (!$is_data_attr || !isset($allowed_f['data-*']))) {
                     continue;
                 }
             }
diff --git a/skins/larry/includes/header.html b/skins/larry/includes/header.html
index 69e8b8a..8ce784b 100644
--- a/skins/larry/includes/header.html
+++ b/skins/larry/includes/header.html
@@ -1,4 +1,4 @@
-<div id="header">
+<div id="header" role="banner">
 <div id="topline">
 	<div class="topleft">
 		<roundcube:container name="topline-left" id="topline-left" />
@@ -21,7 +21,7 @@
 
 <roundcube:if condition="!env:extwin &amp;&amp; !env:framed" />
 <div id="topnav">
-	<div id="taskbar" class="topright">
+	<div id="taskbar" class="topright" role="navigation" aria-label="Application Tasks">
 	<roundcube:button command="mail" label="mail" class="button-mail" classSel="button-mail button-selected" innerClass="button-inner" />
 	<roundcube:button command="addressbook" label="addressbook" class="button-addressbook" classSel="button-addressbook button-selected" innerClass="button-inner" />
 	<roundcube:container name="taskbar" id="taskbar" />
diff --git a/skins/larry/includes/mailtoolbar.html b/skins/larry/includes/mailtoolbar.html
index ac08a32..5efea7c 100644
--- a/skins/larry/includes/mailtoolbar.html
+++ b/skins/larry/includes/mailtoolbar.html
@@ -3,11 +3,11 @@
 <roundcube:button command="reply" type="link" class="button reply disabled" classAct="button reply" classSel="button reply pressed" label="reply" title="replytomessage" />
 <span class="dropbutton">
 	<roundcube:button command="reply-all" type="link" class="button reply-all disabled" classAct="button reply-all" classSel="button reply-all pressed" label="replyall" title="replytoallmessage" />
-	<span class="dropbuttontip" id="replyallmenulink" onclick="UI.show_popup('replyallmenu');return false"></span>
+	<span class="dropbuttontip" id="replyallmenulink" onclick="UI.toggle_popup('replyallmenu',event);return false"></span>
 </span>
 <span class="dropbutton">
 	<roundcube:button command="forward" type="link" class="button forward disabled" classAct="button forward" classSel="button forward pressed" label="forward" title="forwardmessage" />
-	<span class="dropbuttontip" id="forwardmenulink" onclick="UI.show_popup('forwardmenu');return false"></span>
+	<span class="dropbuttontip" id="forwardmenulink" onclick="UI.toggle_popup('forwardmenu',event);return false"></span>
 </span>
 <roundcube:button command="delete" type="link" class="button delete disabled" classAct="button delete" classSel="button delete pressed" label="delete" title="deletemessage" />
 <roundcube:if condition="template:name == 'message'" />
@@ -15,44 +15,44 @@
 <roundcube:button command="print" type="link" class="button print disabled" classAct="button print" classSel="button print pressed" label="print" title="printmessage" />
 <roundcube:endif />
 <roundcube:container name="toolbar" id="mailtoolbar" />
-<roundcube:button name="markmenulink" id="markmessagemenulink" type="link" class="button markmessage" label="mark" title="markmessages" onclick="UI.show_popup('markmessagemenu');return false" />
-<roundcube:button name="messagemenulink" id="messagemenulink" type="link" class="button more" label="more" title="moreactions" onclick="UI.show_popup('messagemenu');return false" />
+<roundcube:button name="markmenulink" id="markmessagemenulink" type="link" class="button markmessage" label="mark" title="markmessages" onclick="UI.toggle_popup('markmessagemenu',event);return false" aria-haspopup="true" aria-owns="markmessagemenu" />
+<roundcube:button name="messagemenulink" id="messagemenulink" type="link" class="button more" label="more" title="moreactions" onclick="UI.toggle_popup('messagemenu',event);return false" aria-haspopup="true" aria-owns="messagemenu" />
 
 <div id="forwardmenu" class="popupmenu">
-	<ul class="toolbarmenu">
-		<li><roundcube:button command="forward-inline" label="forwardinline" prop="sub" classAct="forwardlink active" class="forwardlink" /></li>
-		<li><roundcube:button command="forward-attachment" label="forwardattachment" prop="sub" classAct="forwardattachmentlink active" class="forwardattachmentlink" /></li>
+	<ul class="toolbarmenu" role="menu">
+		<li role="menuitem"><roundcube:button command="forward-inline" label="forwardinline" prop="sub" classAct="forwardlink active" class="forwardlink" /></li>
+		<li role="menuitem"><roundcube:button command="forward-attachment" label="forwardattachment" prop="sub" classAct="forwardattachmentlink active" class="forwardattachmentlink" /></li>
 		<roundcube:container name="forwardmenu" id="forwardmenu" />
 	</ul>
 </div>
 
 <div id="replyallmenu" class="popupmenu">
-	<ul class="toolbarmenu">
-		<li><roundcube:button command="reply-all" label="replyall" prop="sub" class="replyalllink" classAct="replyalllink active" /></li>
-		<li><roundcube:button command="reply-list" label="replylist" prop="sub" class="replylistlink" classAct="replylistlink active" /></li>
+	<ul class="toolbarmenu" role="menu">
+		<li role="menuitem"><roundcube:button command="reply-all" label="replyall" prop="sub" class="replyalllink" classAct="replyalllink active" /></li>
+		<li role="menuitem"><roundcube:button command="reply-list" label="replylist" prop="sub" class="replylistlink" classAct="replylistlink active" /></li>
 		<roundcube:container name="replyallmenu" id="replyallmenu" />
 	</ul>
 </div>
 
 <div id="messagemenu" class="popupmenu">
-  <ul class="toolbarmenu iconized">
-	<li><roundcube:button command="print" label="printmessage" class="icon" classAct="icon active" innerclass="icon print" /></li>
-	<li><roundcube:button command="download" label="emlsave" class="icon" classAct="icon active" innerclass="icon download" /></li>
-	<li><roundcube:button command="edit" prop="new" label="editasnew" class="icon" classAct="icon active" innerclass="icon edit" /></li>
-	<li><roundcube:button command="viewsource" label="viewsource" class="icon" classAct="icon active" innerclass="icon viewsource" /></li>
-	<li><roundcube:button command="move" label="moveto" class="icon" classAct="icon active" innerclass="icon move folder-selector-link" /></li>
-	<li><roundcube:button command="copy" label="copyto" class="icon" classAct="icon active" innerclass="icon copy folder-selector-link" /></li>
-	<li><roundcube:button command="open" label="openinextwin" target="_blank" class="icon" classAct="icon active" innerclass="icon extwin" /></li>
+  <ul class="toolbarmenu iconized" role="menu">
+	<li role="menuitem"><roundcube:button command="print" label="printmessage" class="icon" classAct="icon active" innerclass="icon print" /></li>
+	<li role="menuitem"><roundcube:button command="download" label="emlsave" class="icon" classAct="icon active" innerclass="icon download" /></li>
+	<li role="menuitem"><roundcube:button command="edit" prop="new" label="editasnew" class="icon" classAct="icon active" innerclass="icon edit" /></li>
+	<li role="menuitem"><roundcube:button command="viewsource" label="viewsource" class="icon" classAct="icon active" innerclass="icon viewsource" /></li>
+	<li role="menuitem"><roundcube:button command="move" label="moveto" class="icon" classAct="icon active" innerclass="icon move folder-selector-link" /></li>
+	<li role="menuitem"><roundcube:button command="copy" label="copyto" class="icon" classAct="icon active" innerclass="icon copy folder-selector-link" /></li>
+	<li role="menuitem"><roundcube:button command="open" label="openinextwin" target="_blank" class="icon" classAct="icon active" innerclass="icon extwin" /></li>
 	<roundcube:container name="messagemenu" id="messagemenu" />
   </ul>
 </div>
 
 <div id="markmessagemenu" class="popupmenu">
-  <ul class="toolbarmenu iconized">
-	<li><roundcube:button command="mark" prop="read" label="markread" classAct="icon active" class="icon" innerclass="icon read" /></li>
-	<li><roundcube:button command="mark" prop="unread" label="markunread" classAct="icon active" class="icon" innerclass="icon unread" /></li>
-	<li><roundcube:button command="mark" prop="flagged" label="markflagged" classAct="icon active" class="icon" innerclass="icon flagged" /></li>
-	<li><roundcube:button command="mark" prop="unflagged" label="markunflagged" classAct="icon active" class="icon" innerclass="icon unflagged" /></li>
+  <ul class="toolbarmenu iconized" role="menu">
+	<li role="menuitem"><roundcube:button command="mark" prop="read" label="markread" classAct="icon active" class="icon" innerclass="icon read" /></li>
+	<li role="menuitem"><roundcube:button command="mark" prop="unread" label="markunread" classAct="icon active" class="icon" innerclass="icon unread" /></li>
+	<li role="menuitem"><roundcube:button command="mark" prop="flagged" label="markflagged" classAct="icon active" class="icon" innerclass="icon flagged" /></li>
+	<li role="menuitem"><roundcube:button command="mark" prop="unflagged" label="markunflagged" classAct="icon active" class="icon" innerclass="icon unflagged" /></li>
 	<roundcube:container name="markmenu" id="markmessagemenu" />
   </ul>
 </div>
diff --git a/skins/larry/mail.css b/skins/larry/mail.css
index b9f24b8..a3f7e7b 100644
--- a/skins/larry/mail.css
+++ b/skins/larry/mail.css
@@ -158,6 +158,7 @@
 	padding-right: 36px;
 }
 
+#mailboxlist li.mailbox > a:focus,
 #mailboxlist li.mailbox.selected > a {
 	background-position: 6px -21px;
 }
@@ -166,6 +167,7 @@
 	background-position: 6px -189px;
 }
 
+#mailboxlist li.mailbox.inbox > a:focus,
 #mailboxlist li.mailbox.inbox.selected > a {
 	background-position: 6px -213px;
 }
@@ -174,6 +176,7 @@
 	background-position: 6px -238px;
 }
 
+#mailboxlist li.mailbox.drafts > a:focus,
 #mailboxlist li.mailbox.drafts.selected > a {
 	background-position: 6px -262px;
 }
@@ -182,6 +185,7 @@
 	background-position: 6px -286px;
 }
 
+#mailboxlist li.mailbox.sent > a:focus,
 #mailboxlist li.mailbox.sent.selected > a {
 	background-position: 6px -310px;
 }
@@ -190,6 +194,7 @@
 	background-position: 6px -334px;
 }
 
+#mailboxlist li.mailbox.junk > a:focus,
 #mailboxlist li.mailbox.junk.selected > a {
 	background-position: 6px -358px;
 }
@@ -198,6 +203,7 @@
 	background-position: 6px -382px;
 }
 
+#mailboxlist li.mailbox.trash > a:focus,
 #mailboxlist li.mailbox.trash.selected > a {
 	background-position: 6px -406px;
 }
@@ -206,6 +212,7 @@
 	background-position: 6px -1924px;
 }
 
+#mailboxlist li.mailbox.trash.empty > a:focus,
 #mailboxlist li.mailbox.trash.empty.selected > a {
 	background-position: 6px -1948px;
 }
@@ -214,6 +221,7 @@
 	background-position: 6px -1699px;
 }
 
+#mailboxlist li.mailbox.archive > a:focus,
 #mailboxlist li.mailbox.archive.selected > a {
 	background-position: 6px -1723px;
 }
@@ -222,6 +230,7 @@
 	background-position: 23px -238px;
 }
 
+#mailboxlist li.mailbox ul li.drafts > a:focus,
 #mailboxlist li.mailbox ul li.drafts.selected > a {
 	background-position: 23px -262px;
 }
@@ -230,6 +239,7 @@
 	background-position: 23px -286px;
 }
 
+#mailboxlist li.mailbox ul li.sent > a:focus,
 #mailboxlist li.mailbox ul li.sent.selected > a {
 	background-position: 23px -310px;
 }
@@ -238,6 +248,7 @@
 	background-position: 23px -334px;
 }
 
+#mailboxlist li.mailbox ul li.junk > a:focus,
 #mailboxlist li.mailbox ul li.junk.selected > a {
 	background-position: 23px -358px;
 }
@@ -246,6 +257,7 @@
 	background-position: 23px -382px;
 }
 
+#mailboxlist li.mailbox ul li.trash > a:focus,
 #mailboxlist li.mailbox ul li.trash.selected > a {
 	background-position: 23px -406px;
 }
@@ -254,6 +266,7 @@
 	background-position: 23px -1924px;
 }
 
+#mailboxlist li.mailbox ul li.trash.empty > a:focus,
 #mailboxlist li.mailbox ul li.trash.empty.selected > a {
 	background-position: 23px -1948px;
 }
@@ -262,6 +275,7 @@
 	background-position: 23px -1699px;
 }
 
+#mailboxlist li.mailbox ul li.archive > a:focus,
 #mailboxlist li.mailbox ul li.archive.selected > a {
 	background-position: 23px -1723px;
 }
@@ -300,6 +314,7 @@
 	padding-left: 52px;  /* 36 + 1 x 16 */
 	background-position: 22px -93px;  /* 6 + 1 x 16 */
 }
+#mailboxlist li.mailbox ul li > a:focus,
 #mailboxlist li.mailbox ul li.selected > a {
 	background-position: 22px -117px;
 }
@@ -312,6 +327,7 @@
 	padding-left: 68px;  /* 2x */
 	background-position: 38px -93px;
 }
+#mailboxlist li.mailbox ul ul li > a:focus,
 #mailboxlist li.mailbox ul ul li.selected > a {
 	background-position: 38px -117px;
 }
@@ -323,6 +339,7 @@
 	padding-left: 84px;  /* 3x */
 	background-position: 54px -93px;
 }
+#mailboxlist li.mailbox ul ul ul li > a:focus,
 #mailboxlist li.mailbox ul ul ul li.selected > a {
 	background-position: 54px -117px;
 }
@@ -334,6 +351,7 @@
 	padding-left: 100px;  /* 4x */
 	background-position: 70px -93px;
 }
+#mailboxlist li.mailbox ul ul ul ul li > a:focus,
 #mailboxlist li.mailbox ul ul ul ul li.selected > a {
 	background-position: 70px -117px;
 }
@@ -544,7 +562,9 @@
 .messagelist thead tr td.sortedDESC a {
 	color: #004458;
 	text-decoration: underline;
-	background: url(images/listicons.png) right -912px no-repeat;
+	background-image: url(images/listicons.png);
+	background-repeat: no-repeat;
+	background-position: right -912px;
 }
 
 .messagelist thead tr td.sortedASC a {
diff --git a/skins/larry/styles.css b/skins/larry/styles.css
index 660daa9..4ef57e7 100644
--- a/skins/larry/styles.css
+++ b/skins/larry/styles.css
@@ -945,6 +945,13 @@
 	background: url(images/buttons.png) -1000px 0 no-repeat;
 }
 
+#taskbar a:focus {
+	color: #fff;
+	text-shadow: 0px 1px 1px #666;
+	background-color: #3da0c2;
+	outline: none;
+}
+
 #taskbar a.button-selected {
 	color: #3cf;
 	background-color: #2c2c2c;
@@ -1240,6 +1247,13 @@
 	text-overflow: ellipsis;
 }
 
+ul.treelist li a:focus {
+	color: #fff;
+	background: #4db0d2;
+	text-shadow: 0px 1px 1px #666;
+	outline: none;
+}
+
 ul.treelist ul li a {
 	padding-left: 38px;
 }
@@ -1320,6 +1334,13 @@
 	margin-top: 1px;
 }
 
+.boxfooter a.listbutton:focus {
+	color: #fff;
+	background: #4db0d2;
+	text-shadow: 0px 1px 1px #666;
+	outline: none;
+}
+
 .uibox .boxfooter .listbutton:first-child {
 	border-radius: 0 0 0 4px;
 }
@@ -1329,7 +1350,9 @@
 	width: 48px;
 	height: 35px;
 	text-indent: -5000px;
-	background: url(images/buttons.png) -1000px 0 no-repeat;
+	background-image: url(images/buttons.png);
+	background-position: -1000px 0;
+	background-repeat: no-repeat;
 }
 
 .boxfooter .listbutton.add .inner {
@@ -1473,6 +1496,13 @@
 	text-overflow: ellipsis;
 }
 
+.records-table thead td a:focus {
+	color: #fff;
+	background: #4db0d2;
+	text-shadow: 0px 1px 1px #666;
+	outline: none;
+}
+
 .records-table tbody td {
 	padding: 2px 7px;
 	border-bottom: 1px solid #ddd;
@@ -1492,12 +1522,12 @@
 }
 
 /* because of border-collapse, we make the left border twice what we want it to be - half will be hidden to the left */
-.records-table tbody tr.focused > td:first-child {
+.records-table.focus tbody tr.focused > td:first-child {
 	border-left: 2px solid #b0ccd7;
 	padding-left: 4px;
 }
 
-.records-table tbody tr.selected.focused > td:first-child {
+.records-table.focus tbody tr.selected.focused > td:first-child {
 	border-left-color: #49b3d2;
 }
 
@@ -1908,6 +1938,13 @@
 	border-radius: 0;
 }
 
+.toolbar a.button:focus {
+	color: #fff;
+	text-shadow: 0px 1px 1px #666;
+	background-color: #4db0d2;
+	border-radius: 4px;
+}
+
 .toolbar a.button.disabled {
 	opacity: 0.4;
 	filter: alpha(opacity=40);
@@ -2119,6 +2156,19 @@
 }
 
 
+a.menuselector:focus,
+a.menuselector.focus,
+a.iconbutton:focus,
+.pagenav a.button:focus {
+	border-color: #4fadd5;
+	-webkit-box-shadow: 0 0 4px 2px rgba(71,135,177, 0.8);
+	   -moz-box-shadow: 0 0 4px 2px rgba(71,135,177, 0.8);
+	     -o-box-shadow: 0 0 4px 2px rgba(71,135,177, 0.8);
+	        box-shadow: 0 0 4px 2px rgba(71,135,177, 0.8);
+	outline: none;
+}
+
+
 /*** quota indicator ***/
 
 #quotadisplay {
@@ -2211,6 +2261,7 @@
 
 .googie_list td.googie_list_onhover,
 ul.toolbarmenu li a.active:hover,
+ul.toolbarmenu li a.active:focus,
 #rcmKSearchpane ul li.selected,
 select.decorated option:hover,
 select.decorated option[selected='selected'] {
@@ -2220,6 +2271,7 @@
 	background: -o-linear-gradient(top, #00aad6 0%, #008fc9 100%);
 	background: -ms-linear-gradient(top, #00aad6 0%, #008fc9 100%);
 	background: linear-gradient(top, #00aad6 0%, #008fc9 100%);
+	outline: none;
 }
 
 ul.toolbarmenu.iconized li a,
diff --git a/skins/larry/templates/login.html b/skins/larry/templates/login.html
index 64ff6be..b14d196 100644
--- a/skins/larry/templates/login.html
+++ b/skins/larry/templates/login.html
@@ -8,7 +8,7 @@
 <body>
 
 <div id="login-form">
-<div class="box-inner">
+<div class="box-inner" role="main">
 <roundcube:object name="logo" src="/images/roundcube_logo.png" id="logo" />
 
 <roundcube:form name="form" method="post">
@@ -17,14 +17,14 @@
 
 </div>
 
-<div class="box-bottom">
+<div class="box-bottom" role="complementary">
 	<roundcube:object name="message" id="message" />
 	<noscript>
 		<p class="noscriptwarning"><roundcube:label name="noscriptwarning" /></p>
 	</noscript>
 </div>
 
-<div id="bottomline">
+<div id="bottomline" role="contentinfo">
 	<roundcube:var name="config:product_name"> <roundcube:object name="version" condition="config:display_version" />
 	<roundcube:if condition="config:support_url" />
 		&nbsp;&#9679;&nbsp; <a href="<roundcube:var name='config:support_url' />" target="_blank" class="support-link"><roundcube:label name="support" /></a>
diff --git a/skins/larry/templates/mail.html b/skins/larry/templates/mail.html
index 1e4a3ce..d92324f 100644
--- a/skins/larry/templates/mail.html
+++ b/skins/larry/templates/mail.html
@@ -18,7 +18,7 @@
 <div id="mainscreen">
 
 <!-- toolbar -->
-<div id="messagetoolbar" class="toolbar">
+<div id="messagetoolbar" class="toolbar" role="toolbar">
 	<roundcube:button command="checkmail" type="link" class="button checkmail disabled" classAct="button checkmail" classSel="button checkmail pressed" label="refresh" title="checkmail" />
 	<roundcube:include file="/includes/mailtoolbar.html" />
 </div>
@@ -27,13 +27,13 @@
 
 <!-- search filter -->
 <div id="searchfilter">
-	<roundcube:object name="searchfilter" class="searchfilter decorated" />
+	<roundcube:object name="searchfilter" class="searchfilter decorated" aria-controls="messagelist" />
 </div>
 
 <!-- search box -->
-<div id="quicksearchbar" class="searchbox">
+<div id="quicksearchbar" class="searchbox" role="search" aria-label="Email message search form">
 <roundcube:object name="searchform" id="quicksearchbox" />
-<roundcube:button name="searchmenulink" id="searchmenulink" class="iconbutton searchoptions" onclick="UI.show_popup('searchmenu');return false" title="searchmod" content=" " />
+<roundcube:button name="searchmenulink" id="searchmenulink" class="iconbutton searchoptions" onclick="UI.toggle_popup('searchmenu',event);return false" title="searchmod" content=" " />
 <roundcube:button command="reset-search" id="searchreset" class="iconbutton reset" title="resetsearch" content=" " />
 </div>
 
@@ -43,12 +43,12 @@
 <div id="mailview-left">
 
 <!-- folders list -->
-<div id="mailboxcontainer" class="uibox listbox">
+<div id="mailboxcontainer" class="uibox listbox" role="navigation" aria-label="Email folder selection">
 <div id="folderlist-content" class="scroller withfooter">
 <roundcube:object name="mailboxlist" id="mailboxlist" class="treelist listing" folder_filter="mail" unreadwrap="%s" />
 </div>
 <div id="folderlist-footer" class="boxfooter">
-	<roundcube:button name="mailboxmenulink" id="mailboxmenulink" type="link" title="folderactions" class="listbutton groupactions" onclick="UI.show_popup('mailboxmenu');return false" innerClass="inner" content="&#9881;" />
+	<roundcube:button name="mailboxmenulink" id="mailboxmenulink" type="link" title="folderactions" class="listbutton groupactions" onclick="UI.toggle_popup('mailboxmenu',event);return false" innerClass="inner" content="&#9881;" />
 	<roundcube:if condition="env:quota" />
 		<roundcube:object name="quotaDisplay" id="quotadisplay" class="countdisplay" display="text" />
 	<roundcube:endif />
@@ -57,7 +57,7 @@
 
 </div>
 
-<div id="mailview-right">
+<div id="mailview-right" role="main">
 
 <roundcube:if condition="config:preview_pane == true" />
 <div id="mailview-top" class="uibox">
@@ -81,9 +81,9 @@
 	</div>
 	
 	<div id="listselectors">
-	<a href="#select" id="listselectmenulink" class="menuselector" onclick="UI.show_popup('listselectmenu');return false"><span class="handle"><roundcube:label name="select" /></span></a>
+	<a href="#select" id="listselectmenulink" class="menuselector" onclick="UI.toggle_popup('listselectmenu', event);return false" aria-haspopup="true" aria-owns="listselectmenu"><span class="handle"><roundcube:label name="select" /></span></a>
 	<roundcube:if condition="env:threads" />
-		&nbsp; <a href="#threads" id="threadselectmenulink" class="menuselector" onclick="UI.show_popup('threadselectmenu');return false"><span class="handle"><roundcube:label name="threads" /></span></a>
+		&nbsp; <a href="#threads" id="threadselectmenulink" class="menuselector" onclick="UI.toggle_popup('threadselectmenu', event);return false" aria-haspopup="true" aria-owns="threadselectmenu"><span class="handle"><roundcube:label name="threads" /></span></a>
 	<roundcube:endif />
 	</div>
 
@@ -99,7 +99,7 @@
 
 	<roundcube:container name="listcontrols" id="listcontrols" />
 
-	<a href="#preview" id="mailpreviewtoggle" title="<roundcube:label name='previewpane' />"></a>
+	<a href="#preview" id="mailpreviewtoggle" class="iconbutton" title="<roundcube:label name='previewpane' />" role="button" tabindex="0"></a>
 </div>
 
 </div><!-- end mailview-top -->
diff --git a/skins/larry/ui.js b/skins/larry/ui.js
index 0e8afc6..add02b8 100644
--- a/skins/larry/ui.js
+++ b/skins/larry/ui.js
@@ -33,6 +33,8 @@
   var mailviewsplit;
   var compose_headers = {};
   var prefs;
+  var focused_popup;
+  var popup_keyboard_active = false;
 
   // export public methods
   this.set = setenv;
@@ -40,6 +42,7 @@
   this.init_tabs = init_tabs;
   this.show_about = show_about;
   this.show_popup = show_popup;
+  this.toggle_popup = toggle_popup;
   this.add_popup = add_popup;
   this.set_searchmod = set_searchmod;
   this.set_searchscope = set_searchscope;
@@ -333,6 +336,10 @@
           var val = $('option:selected', this).text();
           $(this).next().children().text(val);
         });
+
+      select
+        .on('focus', function(e){ overlay.addClass('focus'); })
+        .on('blur', function(e){ overlay.removeClass('focus'); });
     });
 
     // set min-width to show all toolbar buttons
@@ -343,14 +350,7 @@
 
     $(document.body)
       .bind('mouseup', body_mouseup)
-      .bind('keyup', function(e){
-        if (e.keyCode == 27) {
-          for (var id in popups) {
-            if (popups[id].is(':visible'))
-              show_popup(id, false);
-          }
-        }
-      });
+      .bind('keydown', popup_keypress);
 
     $('iframe').load(function(e){
       // this = iframe
@@ -586,13 +586,21 @@
   /**
    * Trigger for popup menus
    */
-  function show_popup(popup, show, config)
+  function toggle_popup(popup, e, config)
+  {
+    show_popup(popup, undefined, config, rcube_event.is_keyboard(e));
+  }
+
+  /**
+   * (Deprecated) trigger for popup menus
+   */
+  function show_popup(popup, show, config, keyboard)
   {
     // auto-register menu object
     if (config || !popupconfig[popup])
       add_popup(popup, config);
 
-    var visible = show_popupmenu(popup, show),
+    var visible = show_popupmenu(popup, show, keyboard),
       config = popupconfig[popup];
     if (typeof config.callback == 'function')
       config.callback(visible);
@@ -601,7 +609,7 @@
   /**
    * Show/hide a specific popup menu
    */
-  function show_popupmenu(popup, show)
+  function show_popupmenu(popup, show, keyboard)
   {
     var obj = popups[popup],
       config = popupconfig[popup],
@@ -638,10 +646,74 @@
 
       obj.css({ left:pos.left, top:(pos.top + (above ? -obj.height() : ref.offsetHeight)) });
     }
+    else if (!show && keyboard && ref.length) {
+      ref.focus();
+    }
 
     obj[show?'show':'hide']();
 
+    popup_keyboard_active = show && keyboard;
+    if (popup_keyboard_active) {
+      focused_popup = popup;
+      obj.find('a,input').not('[aria-disabled=true]').first().focus();
+    }
+    else {
+      focused_popup = null;
+    }
+
     return show;
+  }
+
+  /** 
+   * Handler for keyboard events on active popups
+   */
+  function popup_keypress(e)
+  {
+    var target = e.target || {},
+      keyCode = rcube_event.get_keycode(e);
+
+    if (e.keyCode != 27 && (!popup_keyboard_active || 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
+        popup_focus_item(mod = keyCode == 38 || keyCode == 63232 ? -1 : 1);
+        break;
+
+      case 9:   // tab
+        if (focused_popup) {
+          var mod = rcube_event.get_modifier(e);
+          if (!popup_focus_item(mod == SHIFT_KEY ? -1 : 1)) {
+            show_popup(focused_popup, false, undefined, true);
+          }
+        }
+        return rcube_event.cancel(e);
+
+      case 27:  // esc
+        for (var id in popups) {
+          if (popups[id].is(':visible'))
+            show_popup(id, false, undefined, true);
+        }
+        break;
+    }
+
+    return true;
+  }
+
+  /**
+   * Helper method to move focus to the next/prev popup menu item
+   */
+  function popup_focus_item(dir)
+  {
+    var obj, mod = dir < 0 ? 'prevAll' : 'nextAll', limit = dir < 0 ? 'last' : 'first';
+    if (focused_popup && (obj = popups[focused_popup])) {
+      return obj.find(':focus').closest('li')[mod](':has(:not([aria-disabled=true]))').find('a,input')[limit]().focus().length;
+    }
+
+    return 0;
   }
 
   /**
@@ -734,8 +806,8 @@
   function switch_view_mode(mode, force)
   {
     if (force || !$('#mail'+mode+'mode').hasClass('disabled')) {
-      $('#maillistmode, #mailthreadmode').removeClass('selected');
-      $('#mail'+mode+'mode').addClass('selected');
+      $('#maillistmode, #mailthreadmode').removeClass('selected').attr('tabindex', '0').attr('aria-disabled', 'false');
+      $('#mail'+mode+'mode').addClass('selected').attr('tabindex', '-1').attr('aria-disabled', 'true');
     }
   }
 

--
Gitblit v1.9.1