From 8fd955eb1eb7b6e3a1e0275bf8481c287dd6afc0 Mon Sep 17 00:00:00 2001
From: Thomas Bruederli <thomas@roundcube.net>
Date: Wed, 28 May 2014 11:34:38 -0400
Subject: [PATCH] Improve (message) list widget display and keyboard navigation to show sane behavior with screen readers

---
 program/js/list.js       |   58 +++++++++++++-----
 skins/classic/common.css |    3 
 skins/larry/styles.css   |    2 
 program/js/app.js        |  104 +++++++++++++++++++++++-----------
 4 files changed, 115 insertions(+), 52 deletions(-)

diff --git a/program/js/app.js b/program/js/app.js
index 41f9a38..bf163c6 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -1974,7 +1974,7 @@
       flags: flags.extra_flags
     });
 
-    var c, n, col, html, css_class,
+    var c, n, col, html, css_class, label, status_class = '', status_label = '',
       tree = '', expando = '',
       list = this.message_list,
       rows = list.rows,
@@ -1990,17 +1990,26 @@
     css_class = 'msgicon';
     if (this.env.status_col === null) {
       css_class += ' status';
-      if (flags.deleted)
-        css_class += ' deleted';
-      else if (!flags.seen)
-        css_class += ' unread';
-      else if (flags.unread_children > 0)
-        css_class += ' unreadchildren';
+      if (flags.deleted) {
+        status_class += ' deleted';
+        status_label += this.get_label('deleted') + ' ';
+      }
+      else if (!flags.seen) {
+        status_class += ' unread';
+        status_label += this.get_label('unread') + ' ';
+      }
+      else if (flags.unread_children > 0) {
+        status_class += ' unreadchildren';
+      }
     }
-    if (flags.answered)
-      css_class += ' replied';
-    if (flags.forwarded)
-      css_class += ' forwarded';
+    if (flags.answered) {
+      status_class += ' replied';
+      status_label += this.get_label('replied') + ' ';
+    }
+    if (flags.forwarded) {
+      status_class += ' forwarded';
+      status_label += this.get_label('replied') + ' ';
+    }
 
     // update selection
     if (message.selected && !list.in_selection(uid))
@@ -2037,7 +2046,7 @@
         row_class += ' unroot';
     }
 
-    tree += '<span id="msgicn'+row.id+'" class="'+css_class+'">&nbsp;</span>';
+    tree += '<span id="msgicn'+row.id+'" class="'+css_class+status_class+'" title="'+status_label+'"></span>';
     row.className = row_class;
 
     // build subject link
@@ -2061,28 +2070,36 @@
 
       if (c == 'flag') {
         css_class = (flags.flagged ? 'flagged' : 'unflagged');
-        html = '<span id="flagicn'+row.id+'" class="'+css_class+'">&nbsp;</span>';
+        label = this.get_label(css_class);
+        html = '<span id="flagicn'+row.id+'" class="'+css_class+'" title="'+label+'"></span>';
       }
       else if (c == 'attachment') {
+        label = this.get_label('withattachment');
         if (flags.attachmentClass)
-          html = '<span class="'+flags.attachmentClass+'">&nbsp;</span>';
+          html = '<span class="'+flags.attachmentClass+'" title="'+label+'"></span>';
         else if (/application\/|multipart\/(m|signed)/.test(flags.ctype))
-          html = '<span class="attachment">&nbsp;</span>';
+          html = '<span class="attachment" title="'+label+'"></span>';
         else if (/multipart\/report/.test(flags.ctype))
-          html = '<span class="report">&nbsp;</span>';
-        else
+          html = '<span class="report"></span>';
+          else
           html = '&nbsp;';
       }
       else if (c == 'status') {
-        if (flags.deleted)
+        label = '';
+        if (flags.deleted) {
           css_class = 'deleted';
-        else if (!flags.seen)
+          label = this.get_label('deleted');
+        }
+        else if (!flags.seen) {
           css_class = 'unread';
-        else if (flags.unread_children > 0)
+          label = this.get_label('unread');
+        }
+        else if (flags.unread_children > 0) {
           css_class = 'unreadchildren';
+        }
         else
           css_class = 'msgicon';
-        html = '<span id="statusicn'+row.id+'" class="'+css_class+'">&nbsp;</span>';
+        html = '<span id="statusicn'+row.id+'" class="'+css_class+status_class+'" title="'+label+'"></span>';
       }
       else if (c == 'threads')
         html = expando;
@@ -2092,8 +2109,10 @@
         html = tree + cols[c];
       }
       else if (c == 'priority') {
-        if (flags.prio > 0 && flags.prio < 6)
-          html = '<span class="prio'+flags.prio+'">&nbsp;</span>';
+        if (flags.prio > 0 && flags.prio < 6) {
+          label = this.get_label('priority') + ' ' + flags.prio;
+          html = '<span class="prio'+flags.prio+'" title="'+label+'"></span>';
+        }
         else
           html = '&nbsp;';
       }
@@ -2676,7 +2695,7 @@
   // set message icon
   this.set_message_icon = function(uid)
   {
-    var css_class,
+    var css_class, label = '',
       row = this.message_list.rows[uid];
 
     if (!row)
@@ -2684,38 +2703,55 @@
 
     if (row.icon) {
       css_class = 'msgicon';
-      if (row.deleted)
+      if (row.deleted) {
         css_class += ' deleted';
-      else if (row.unread)
+        label += this.get_label('deleted') + ' ';
+      }
+      else if (row.unread) {
         css_class += ' unread';
+        label += this.get_label('unread') + ' ';
+      }
       else if (row.unread_children)
         css_class += ' unreadchildren';
       if (row.msgicon == row.icon) {
-        if (row.replied)
+        if (row.replied) {
           css_class += ' replied';
-        if (row.forwarded)
+          label += this.get_label('replied') + ' ';
+        }
+        if (row.forwarded) {
           css_class += ' forwarded';
+          label += this.get_label('forwarded') + ' ';
+        }
         css_class += ' status';
       }
 
-      row.icon.className = css_class;
+      $(row.icon).attr('class', css_class).attr('title', label);
     }
 
     if (row.msgicon && row.msgicon != row.icon) {
+      label = '';
       css_class = 'msgicon';
-      if (!row.unread && row.unread_children)
+      if (!row.unread && row.unread_children) {
         css_class += ' unreadchildren';
-      if (row.replied)
+      }
+      if (row.replied) {
         css_class += ' replied';
-      if (row.forwarded)
+        label += this.get_label('replied') + ' ';
+      }
+      if (row.forwarded) {
         css_class += ' forwarded';
+        label += this.get_label('forwarded') + ' ';
+      }
 
-      row.msgicon.className = css_class;
+      $(row.msgicon).attr('class', css_class).attr('title', label);
     }
 
     if (row.flagicon) {
       css_class = (row.flagged ? 'flagged' : 'unflagged');
-      row.flagicon.className = css_class;
+      label = this.get_label(css_class);
+      $(row.flagicon).attr('class', css_class)
+        .attr('aria-label', label)
+        .attr('title', label);
     }
   };
 
diff --git a/program/js/list.js b/program/js/list.js
index 62f1b1c..0bf5d56 100644
--- a/program/js/list.js
+++ b/program/js/list.js
@@ -51,7 +51,7 @@
   this.rowcount = 0;
   this.colcount = 0;
 
-  this.subject_col = -1;
+  this.subject_col = 0;
   this.modkey = 0;
   this.multiselect = false;
   this.multiexpand = false;
@@ -111,15 +111,9 @@
     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:fixed; top:-1000px')
-        .html($(this.list).attr('summary') || 'Select List')
-        .insertAfter(this.list)
-        .on('focus', function(e){ me.focus(e); })
-        .on('blur', function(e){ me.blur(e); });
+      // allow the table element to receive focus.
+      $(this.list).attr('tabindex', '0')
+        .on('focus', function(e){ me.focus(e); });
     }
   }
 
@@ -398,10 +392,16 @@
   if (e)
     rcube_event.cancel(e);
 
+  var focus_elem = null;
+
+  if (this.last_selected && this.rows[this.last_selected]) {
+    focus_elem = $(this.rows[this.last_selected].obj).find(this.col_tagname()).eq(this.subject_col).attr('tabindex', '0');
+  }
+
   // Un-focus already focused elements (#1487123, #1487316, #1488600, #1488620)
-  if (this.focus_elem) {
+  if (focus_elem && focus_elem.length) {
     // We now fix this by explicitly assigning focus to a dedicated link element
-    this.focus_noscroll(this.focus_elem);
+    this.focus_noscroll(focus_elem);
   }
   else {
     // It looks that window.focus() does the job for all browsers, but not Firefox (#1489058)
@@ -409,7 +409,7 @@
     window.focus();
   }
 
-  $(this.list).addClass('focus');
+  $(this.list).addClass('focus').removeAttr('tabindex');
 
   // set internal focus pointer to first row
   if (!this.last_selected)
@@ -423,7 +423,17 @@
 blur: function(e)
 {
   this.focused = false;
-  $(this.list).removeClass('focus');
+
+  // avoid the table getting focus right again
+  var me = this;
+  setTimeout(function(){
+    $(me.list).removeClass('focus').attr('tabindex', '0');
+  }, 20);
+
+  if (this.last_selected && this.rows[this.last_selected]) {
+    $(this.rows[this.last_selected].obj)
+      .find(this.col_tagname()).eq(this.subject_col).removeAttr('tabindex');
+  }
 },
 
 /**
@@ -914,16 +924,22 @@
   if (this.selection.join(',') != select_before)
     this.triggerEvent('select');
 
-  if (this.last_selected != 0 && this.rows[this.last_selected])
-    $(this.rows[this.last_selected].obj).removeClass('focused');
+  if (this.last_selected != 0 && this.rows[this.last_selected]) {
+    $(this.rows[this.last_selected].obj).removeClass('focused')
+      .find(this.col_tagname()).eq(this.subject_col).removeAttr('tabindex');
+  }
 
   // unselect if toggleselect is active and the same row was clicked again
   if (this.toggleselect && this.last_selected == id) {
     this.clear_selection();
     id = null;
   }
-  else
+  else {
     $(this.rows[id].obj).addClass('focused');
+    // set cursor focus to link inside selected row
+    if (this.focused)
+      this.focus_noscroll($(this.rows[id].obj).find(this.col_tagname()).eq(this.subject_col).attr('tabindex', '0'));
+  }
 
   if (!this.selection.length)
     this.shift_start = null;
@@ -1287,6 +1303,14 @@
 
       return rcube_event.cancel(e);
 
+    case 9: // Tab
+      this.blur();
+      break;
+
+    case 13: // Enter
+      if (!this.selection.length)
+        this.select_row(this.last_selected, mod_key, false);
+
     default:
       this.key_pressed = keyCode;
       this.modkey = mod_key;
diff --git a/skins/classic/common.css b/skins/classic/common.css
index 4367d26..1264084 100644
--- a/skins/classic/common.css
+++ b/skins/classic/common.css
@@ -627,7 +627,8 @@
   white-space: nowrap;
   border-bottom: 1px solid #EBEBEB;
   overflow: hidden;
-  text-align: left;  
+  text-align: left;
+	outline: none;
 }
 
 table.records-table tr
diff --git a/skins/larry/styles.css b/skins/larry/styles.css
index a842028..d7ad74c 100644
--- a/skins/larry/styles.css
+++ b/skins/larry/styles.css
@@ -1184,6 +1184,7 @@
 	padding-bottom: 5px;
 	height: auto;
 	min-height: 14px;
+	outline: none;
 }
 
 .webkit .listing tbody td {
@@ -1555,6 +1556,7 @@
 	overflow: hidden;
 	text-overflow: ellipsis;
 	background-color: #fff;
+	outline: none;
 }
 
 /* This padding-left minus the focused padding left should be half of the focused border-left */

--
Gitblit v1.9.1