From 3412e50b54e3daac8745234e21ab6e72be0ed165 Mon Sep 17 00:00:00 2001
From: Thomas Bruederli <thomas@roundcube.net>
Date: Wed, 04 Jun 2014 11:20:33 -0400
Subject: [PATCH] Fix attachment menu structure and aria-attributes

---
 program/js/app.js |  255 +++++++++++++++++++++++++++++++++-----------------
 1 files changed, 169 insertions(+), 86 deletions(-)

diff --git a/program/js/app.js b/program/js/app.js
index 06008b2..b1feeb4 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -342,7 +342,16 @@
             .addEventListener('initrow', function(o) { ref.triggerEvent('insertrow', { cid:o.uid, row:o }); })
             .addEventListener('select', function(o) { ref.compose_recipient_select(o); })
             .addEventListener('dblclick', function(o) { ref.compose_add_recipient('to'); })
-            .addEventListener('keypress', function(o) { if (o.key_pressed == o.ENTER_KEY) ref.compose_add_recipient('to'); })
+            .addEventListener('keypress', function(o) {
+              if (o.key_pressed == o.ENTER_KEY) {
+                if (!ref.compose_add_recipient('to')) {
+                  // execute link action on <enter> if not a recipient entry
+                  if (o.last_selected && String(o.last_selected).charAt(0) == 'G') {
+                    $(o.rows[o.last_selected].obj).find('a').first().click();
+                  }
+                }
+              }
+            })
             .init();
         }
 
@@ -444,9 +453,14 @@
 
         if (this.gui_objects.identitieslist) {
           this.identity_list = new rcube_list_widget(this.gui_objects.identitieslist,
-            {multiselect:false, draggable:false, keyboard:false});
+            {multiselect:false, draggable:false, keyboard:true});
           this.identity_list
             .addEventListener('select', function(o) { ref.identity_select(o); })
+            .addEventListener('keypress', function(o) {
+              if (o.key_pressed == o.ENTER_KEY) {
+                ref.identity_select(o);
+              }
+            })
             .init()
             .focus();
 
@@ -454,9 +468,10 @@
             this.identity_list.highlight_row(this.env.iid);
         }
         else if (this.gui_objects.sectionslist) {
-          this.sections_list = new rcube_list_widget(this.gui_objects.sectionslist, {multiselect:false, draggable:false, keyboard:false});
+          this.sections_list = new rcube_list_widget(this.gui_objects.sectionslist, {multiselect:false, draggable:false, keyboard:true});
           this.sections_list
             .addEventListener('select', function(o) { ref.section_select(o); })
+            .addEventListener('keypress', function(o) { if (o.key_pressed == o.ENTER_KEY) ref.section_select(o); })
             .init()
             .focus();
         }
@@ -464,7 +479,7 @@
           this.init_subscription_list();
         }
         else if (this.gui_objects.responseslist) {
-          this.responses_list = new rcube_list_widget(this.gui_objects.responseslist, {multiselect:false, draggable:false, keyboard:false});
+          this.responses_list = new rcube_list_widget(this.gui_objects.responseslist, {multiselect:false, draggable:false, keyboard:true});
           this.responses_list
             .addEventListener('select', function(list) {
               var win, id = list.get_single_selection();
@@ -602,7 +617,7 @@
   {
     var ret, uid, cid, url, flag, aborted = false;
 
-    if (obj && obj.blur)
+    if (obj && obj.blur && !(event || rcube_event.is_keyboard(event)))
       obj.blur();
 
     // do nothing if interface is locked by other command (with exception for searching reset)
@@ -1609,9 +1624,13 @@
     if ($(target).closest('.ui-dialog, .ui-widget-overlay').length)
       return;
 
-    list = this.message_list || this.contact_list;
-    if (list && !rcube_mouse_is_over(e, list.list.parentNode))
-      list.blur();
+    // remove focus from list widgets
+    if (window.rcube_list_widget && rcube_list_widget._instances.length) {
+      $.each(rcube_list_widget._instances, function(i,list){
+        if (list && !rcube_mouse_is_over(e, list.list.parentNode))
+          list.blur();
+      });
+    }
 
     // reset 'pressed' buttons
     if (this.buttons_sel) {
@@ -1623,7 +1642,7 @@
 
     // reset popup menus; delayed to have updated menu_stack data
     window.setTimeout(function(e){
-      var obj, skip, config, id, i;
+      var obj, skip, config, id, i, parents = $(target).parents();
       for (i = ref.menu_stack.length - 1; i >= 0; i--) {
         id = ref.menu_stack[i];
         obj = $('#' + id);
@@ -1631,6 +1650,7 @@
         if (obj.is(':visible')
           && target != obj.data('opener')
           && target != obj.get(0)  // check if scroll bar was clicked (#1489832)
+          && !parents.is(obj.data('opener'))
           && id != skip
           && (obj.attr('data-editable') != 'true' || !$(target).parents('#' + id).length)
           && (obj.attr('data-sticky') != 'true' || !rcube_mouse_is_over(e, obj.get(0)))
@@ -1647,9 +1667,12 @@
   {
     // Helper method to move focus to the next/prev active menu item
     var focus_menu_item = function(dir) {
-      var obj, mod = dir < 0 ? 'prevAll' : 'nextAll', limit = dir < 0 ? 'last' : 'first';
+      var obj, item, mod = dir < 0 ? 'prevAll' : 'nextAll', limit = dir < 0 ? 'last' : 'first';
       if (ref.focused_menu && (obj = $('#'+ref.focused_menu))) {
-        return obj.find(':focus').closest('li')[mod](':has(:not([aria-disabled=true]))').find('a,input')[limit]().focus().length;
+        item = obj.find(':focus').closest('li')[mod](':has(:not([aria-disabled=true]))').find('a,input')[limit]();
+        if (!item.length)
+          item = obj.find(':focus').closest('ul')[mod](':has(:not([aria-disabled=true]))').find('a,input')[limit]();
+        return item.focus().length;
       }
 
       return 0;
@@ -1974,7 +1997,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 +2013,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 +2069,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 +2093,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 +2132,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;';
       }
@@ -2383,7 +2425,6 @@
   this.clear_message_list = function()
   {
     this.env.messages = {};
-    this.last_selected = 0;
 
     this.show_contentframe(false);
     if (this.message_list)
@@ -2676,7 +2717,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 +2725,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);
     }
   };
 
@@ -3381,7 +3439,7 @@
     this.env.recipients_delimiter = this.env.recipients_separator + ' ';
 
     obj.keydown(function(e) { return ref.ksearch_keydown(e, this, props); })
-      .attr('autocomplete', 'off');
+      .attr({ 'autocomplete': 'off', 'aria-autocomplete': 'list', 'aria-expanded': 'false', 'role': 'combobox' });
   };
 
   this.submit_messageform = function(draft)
@@ -3452,6 +3510,8 @@
       input.val(oldval + recipients.join(delim + ' ') + delim + ' ');
       this.triggerEvent('add-recipient', { field:field, recipients:recipients });
     }
+
+    return recipients.length;
   };
 
   // checks the input fields before sending a message
@@ -4471,7 +4531,7 @@
 
         var dir = key==38 ? 1 : 0;
 
-        highlight = document.getElementById('rcmksearchSelected');
+        highlight = document.getElementById('rcmkSearchItem' + this.ksearch_selected);
         if (!highlight)
           highlight = this.ksearch_pane.__ul.firstChild;
 
@@ -4519,14 +4579,14 @@
 
   this.ksearch_select = function(node)
   {
-    var current = $('#rcmksearchSelected');
-    if (current[0] && node) {
-      current.removeAttr('id').removeClass('selected');
+    if (this.ksearch_pane && node) {
+      this.ksearch_pane.find('li.selected').removeClass('selected').removeAttr('aria-selected');
     }
 
     if (node) {
-      $(node).attr('id', 'rcmksearchSelected').addClass('selected');
+      $(node).addClass('selected').attr('aria-selected', 'true');
       this.ksearch_selected = node._rcm_id;
+      $(this.ksearch_input).attr('aria-activedescendant', 'rcmkSearchItem' + this.ksearch_selected);
     }
   };
 
@@ -4657,16 +4717,20 @@
       return;
 
     // display search results
-    var i, len, ul, li, text, type, init,
+    var i, id, len, ul, text, type, init,
       value = this.ksearch_value,
       maxlen = this.env.autocomplete_max ? this.env.autocomplete_max : 15;
 
     // create results pane if not present
     if (!this.ksearch_pane) {
       ul = $('<ul>');
-      this.ksearch_pane = $('<div>').attr('id', 'rcmKSearchpane')
+      this.ksearch_pane = $('<div>').attr('id', 'rcmKSearchpane').attr('role', 'listbox')
         .css({ position:'absolute', 'z-index':30000 }).append(ul).appendTo(document.body);
       this.ksearch_pane.__ul = ul[0];
+
+      // register (delegate) event handlers
+      ul.on('mouseover', 'li', function(e){ ref.ksearch_select(e.target); })
+        .on('onmouseup', 'li', function(e){ ref.ksearch_click(e.target); })
     }
 
     ul = this.ksearch_pane.__ul;
@@ -4691,23 +4755,29 @@
       for (i=0; i < len && maxlen > 0; i++) {
         text = typeof results[i] === 'object' ? results[i].name : results[i];
         type = typeof results[i] === 'object' ? results[i].type : '';
-        li = document.createElement('LI');
-        li.innerHTML = text.replace(new RegExp('('+RegExp.escape(value)+')', 'ig'), '##$1%%').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/##([^%]+)%%/g, '<b>$1</b>');
-        li.onmouseover = function(){ ref.ksearch_select(this); };
-        li.onmouseup = function(){ ref.ksearch_click(this) };
-        li._rcm_id = this.env.contacts.length + i;
-        if (type) li.className = type;
-        ul.appendChild(li);
+        id = i + this.env.contacts.length;
+        $('<li>').attr('id', 'rcmkSearchItem' + id)
+          .attr('role', 'option')
+          .html(this.quote_html(text.replace(new RegExp('('+RegExp.escape(value)+')', 'ig'), '##$1%%')).replace(/##([^%]+)%%/g, '<b>$1</b>'))
+          .addClass(type || '')
+          .appendTo(ul)
+          .get(0)._rcm_id = id;
         maxlen -= 1;
       }
     }
 
     if (ul.childNodes.length) {
+      // set the right aria-* attributes to the input field
+      $(this.ksearch_input)
+        .attr('aria-haspopup', 'true')
+        .attr('aria-expanded', 'true')
+        .attr('aria-owns', 'rcmKSearchpane');
+
       this.ksearch_pane.show();
+
       // select the first
       if (!this.env.contacts.length) {
-        $('li:first', ul).attr('id', 'rcmksearchSelected').addClass('selected');
-        this.ksearch_selected = 0;
+        this.ksearch_select($('li:first', ul).get(0));
       }
     }
 
@@ -4743,6 +4813,12 @@
 
     if (this.ksearch_pane)
       this.ksearch_pane.hide();
+
+    $(this.ksearch_input)
+      .attr('aria-haspopup', 'false')
+      .attr('aria-expanded', 'false')
+      .removeAttr('aria-activedescendant')
+      .removeAttr('aria-owns');
 
     this.ksearch_destroy();
   };
@@ -4948,6 +5024,7 @@
       // add link to pop back to parent group
       if (this.env.address_group_stack.length > 1) {
         $('<a href="#list">...</a>')
+          .attr('title', this.gettext('uponelevel'))
           .addClass('poplink')
           .appendTo(boxtitle)
           .click(function(e){ return ref.command('popgroup','',this); });
@@ -5484,6 +5561,7 @@
 
       if (appendcontainer.length && appendcontainer.get(0).nodeName == 'FIELDSET') {
         var input, colprop = this.env.coltypes[col],
+          input_id = 'ff_' + col + (colprop.count || 0),
           row = $('<div>').addClass('row'),
           cell = $('<div>').addClass('contactfieldcontent data'),
           label = $('<div>').addClass('contactfieldlabel label');
@@ -5491,13 +5569,13 @@
         if (colprop.subtypes_select)
           label.html(colprop.subtypes_select);
         else
-          label.html(colprop.label);
+          label.html('<label for="' + input_id + '">' + colprop.label + '</label>');
 
         var name_suffix = colprop.limit != 1 ? '[]' : '';
         if (colprop.type == 'text' || colprop.type == 'date') {
           input = $('<input>')
             .addClass('ff_'+col)
-            .attr({type: 'text', name: '_'+col+name_suffix, size: colprop.size})
+            .attr({type: 'text', name: '_'+col+name_suffix, size: colprop.size, id: input_id})
             .appendTo(cell);
 
           this.init_edit_field(col, input);
@@ -5508,7 +5586,7 @@
         else if (colprop.type == 'textarea') {
           input = $('<textarea>')
             .addClass('ff_'+col)
-            .attr({ name: '_'+col+name_suffix, cols:colprop.size, rows:colprop.rows })
+            .attr({ name: '_'+col+name_suffix, cols:colprop.size, rows:colprop.rows, id: input_id })
             .appendTo(cell);
 
           this.init_edit_field(col, input);
@@ -5543,7 +5621,7 @@
         else if (colprop.type == 'select') {
           input = $('<select>')
             .addClass('ff_'+col)
-            .attr('name', '_'+col+name_suffix)
+            .attr({ 'name': '_'+col+name_suffix, id: input_id })
             .appendTo(cell);
 
           var options = input.attr('options');
@@ -5850,7 +5928,7 @@
     this.last_sub_rx = RegExp('['+delim+']?[^'+delim+']+$');
 
     this.subscription_list = new rcube_list_widget(this.gui_objects.subscriptionlist,
-      {multiselect:false, draggable:true, keyboard:false, toggleselect:true});
+      {multiselect:false, draggable:true, keyboard:true, toggleselect:true});
     this.subscription_list
       .addEventListener('select', function(o){ ref.subscription_select(o); })
       .addEventListener('dragstart', function(o){ ref.drag_active = true; })
@@ -5859,7 +5937,8 @@
         row.obj.onmouseover = function() { ref.focus_subscription(row.id); };
         row.obj.onmouseout = function() { ref.unfocus_subscription(row.id); };
       })
-      .init();
+      .init()
+      .focus();
 
     $('#mailboxroot')
       .mouseover(function(){ ref.focus_subscription(this.id); })
@@ -6581,10 +6660,8 @@
       this.treelist.select(name);
     }
     else if (this.gui_objects.folderlist) {
-      $('li.selected', this.gui_objects.folderlist)
-        .removeClass('selected').addClass('unfocused');
-      $(this.get_folder_li(name, prefix, encode))
-        .removeClass('unfocused').addClass('selected');
+      $('li.selected', this.gui_objects.folderlist).removeClass('selected');
+      $(this.get_folder_li(name, prefix, encode)).addClass('selected');
 
       // trigger event hook
       this.triggerEvent('selectfolder', { folder:name, prefix:prefix });
@@ -6634,7 +6711,7 @@
         tr = document.createElement('tr');
 
         for (c=0, len=repl.length; c < len; c++) {
-          cell = document.createElement('td');
+          cell = document.createElement('th');
           cell.innerHTML = repl[c].html || '';
           if (repl[c].id) cell.id = repl[c].id;
           if (repl[c].className) cell.className = repl[c].className;
@@ -6876,13 +6953,7 @@
         container.data('callback')($(this).data('id'));
         return false;
       });
-/*
-      // hide selector on click out of selector element
-      var fn = function(e) { if (e.target != container.get(0)) container.hide(); };
-      $(document.body).on('mouseup', fn);
-      $('iframe').contents().on('mouseup', fn)
-        .load(function(e) { try { $(this).contents().on('mouseup', fn); } catch(e) {}; });
-*/
+
       this.folder_selector_element = container;
     }
 
@@ -6906,6 +6977,10 @@
       keyboard = rcube_event.is_keyboard(event),
       align = obj.attr('data-align') || '',
       stack = false;
+
+    // find "real" button element
+    if (ref.get(0).tagName != 'A' && ref.closest('a').length)
+      ref = ref.closest('a');
 
     if (typeof prop == 'string')
       prop = { menu:name };
@@ -6960,8 +7035,8 @@
           this.hide_menu(this.menu_stack[i]);
       }
       if (stack && this.menu_stack.length) {
-        obj.data('parent', this.menu_stack.last());
-        obj.css('z-index', ($('#'+this.menu_stack.last()).css('z-index') || 0) + 1);
+        obj.data('parent', $.last(this.menu_stack));
+        obj.css('z-index', ($('#'+$.last(this.menu_stack)).css('z-index') || 0) + 1);
       }
       else if (!stack && this.menu_stack.length) {
         this.hide_menu(this.menu_stack[0], event);
@@ -7011,7 +7086,7 @@
     // focus previous menu in stack
     if (this.menu_stack.length && keyboard) {
       this.menu_keyboard_active = true;
-      this.focused_menu = this.menu_stack.last();
+      this.focused_menu = $.last(this.menu_stack);
       if (!obj || !obj.data('opener'))
         $('#'+this.focused_menu).find('a,input:not(:disabled)').not('[aria-disabled=true]').first().focus();
     }
@@ -7354,7 +7429,8 @@
           this.enable_command('set-listmode', this.env.threads && !is_multifolder);
 
           if ((response.action == 'list' || response.action == 'search') && this.message_list) {
-            this.message_list.focus();
+            if (this.message_list.rowcount > 0)
+              this.message_list.focus();
             this.msglist_select(this.message_list);
             this.triggerEvent('listupdate', { folder:this.env.mailbox, rowcount:this.message_list.rowcount });
           }
@@ -7366,11 +7442,18 @@
             this.enable_command('search-create', this.env.source == '');
             this.enable_command('search-delete', this.env.search_id);
             this.update_group_commands();
-            this.contact_list.focus();
+            if (this.contact_list.rowcount > 0)
+              this.contact_list.focus();
             this.triggerEvent('listupdate', { folder:this.env.source, rowcount:this.contact_list.rowcount });
           }
         }
         break;
+
+      case 'list-contacts':
+      case 'search-contacts':
+        if (this.contact_list && this.contact_list.rowcount > 0)
+          this.contact_list.focus();
+        break;
     }
 
     if (response.unlock)

--
Gitblit v1.9.1