From 8fd955eb1eb7b6e3a1e0275bf8481c287dd6afc0 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Wed, 28 May 2014 17:34:38 +0200 Subject: [PATCH] Improve (message) list widget display and keyboard navigation to show sane behavior with screen readers --- program/js/app.js | 104 ++++++++++++++++++++++++++------------- program/js/list.js | 58 +++++++++++++++------- skins/classic/common.css | 3 +- skins/larry/styles.css | 2 + 4 files changed, 115 insertions(+), 52 deletions(-) diff --git a/program/js/app.js b/program/js/app.js index 41f9a3859..bf163c614 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -1974,7 +1974,7 @@ function rcube_webmail() 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 @@ function rcube_webmail() 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) { + status_class += ' replied'; + status_label += this.get_label('replied') + ' '; + } + if (flags.forwarded) { + status_class += ' forwarded'; + status_label += this.get_label('replied') + ' '; } - if (flags.answered) - css_class += ' replied'; - if (flags.forwarded) - css_class += ' forwarded'; // update selection if (message.selected && !list.in_selection(uid)) @@ -2037,7 +2046,7 @@ function rcube_webmail() row_class += ' unroot'; } - tree += ' '; + tree += ''; row.className = row_class; // build subject link @@ -2061,28 +2070,36 @@ function rcube_webmail() if (c == 'flag') { css_class = (flags.flagged ? 'flagged' : 'unflagged'); - html = ' '; + label = this.get_label(css_class); + html = ''; } else if (c == 'attachment') { + label = this.get_label('withattachment'); if (flags.attachmentClass) - html = ' '; + html = ''; else if (/application\/|multipart\/(m|signed)/.test(flags.ctype)) - html = ' '; + html = ''; else if (/multipart\/report/.test(flags.ctype)) - html = ' '; - else + html = ''; + else html = ' '; } 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 = ' '; + html = ''; } else if (c == 'threads') html = expando; @@ -2092,8 +2109,10 @@ function rcube_webmail() html = tree + cols[c]; } else if (c == 'priority') { - if (flags.prio > 0 && flags.prio < 6) - html = ' '; + if (flags.prio > 0 && flags.prio < 6) { + label = this.get_label('priority') + ' ' + flags.prio; + html = ''; + } else html = ' '; } @@ -2676,7 +2695,7 @@ function rcube_webmail() // 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 @@ function rcube_webmail() 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 62f1b1c9d..0bf5d568b 100644 --- a/program/js/list.js +++ b/program/js/list.js @@ -51,7 +51,7 @@ function rcube_list_widget(list, p) 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 @@ init: function() 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 = $('') - .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 @@ focus: function(e) 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 @@ focus: function(e) 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 @@ focus: function(e) 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 @@ select_row: function(id, mod_key, with_mouse) 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 @@ key_press: function(e) 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 4367d26cb..126408444 100644 --- a/skins/classic/common.css +++ b/skins/classic/common.css @@ -627,7 +627,8 @@ table.records-table tbody tr td 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 a84202800..d7ad74c01 100644 --- a/skins/larry/styles.css +++ b/skins/larry/styles.css @@ -1184,6 +1184,7 @@ a.iconlink.upload { padding-bottom: 5px; height: auto; min-height: 14px; + outline: none; } .webkit .listing tbody td { @@ -1555,6 +1556,7 @@ table.records-table { 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 */