diff options
author | Thomas Bruederli <thomas@roundcube.net> | 2014-04-30 16:21:29 +0200 |
---|---|---|
committer | Thomas Bruederli <thomas@roundcube.net> | 2014-04-30 16:21:29 +0200 |
commit | e8bcf08c72a18b3bf396e6448d6658227ecb46f2 (patch) | |
tree | dd4dc60d87f2340114eb9dcaaf7a567e09cdc995 /program | |
parent | 2d8f31da736550a0df3ccde31bf85a146aea45c0 (diff) |
1. Prepare core and Larry skin for improved accessibility
2. Implement full keyboard navigation in main mail view
Diffstat (limited to 'program')
-rw-r--r-- | program/include/rcmail_output_html.php | 9 | ||||
-rw-r--r-- | program/js/app.js | 31 | ||||
-rw-r--r-- | program/js/common.js | 20 | ||||
-rw-r--r-- | program/js/list.js | 60 | ||||
-rw-r--r-- | program/js/treelist.js | 5 | ||||
-rw-r--r-- | program/lib/Roundcube/html.php | 5 |
6 files changed, 107 insertions, 23 deletions
diff --git a/program/include/rcmail_output_html.php b/program/include/rcmail_output_html.php index eb4a52d04..a57165f03 100644 --- a/program/include/rcmail_output_html.php +++ b/program/include/rcmail_output_html.php @@ -1130,6 +1130,15 @@ EOF; $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 2451a6d3d..b2c9209a7 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -199,6 +199,9 @@ function rcube_webmail() 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 @@ function rcube_webmail() }); 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 @@ function rcube_webmail() 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 @@ function rcube_webmail() // 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 @@ function rcube_webmail() 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 @@ function rcube_webmail() 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,8 +6228,14 @@ function rcube_webmail() 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 @@ function rcube_webmail() 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 ff5f9b9bd..e15c34a3b 100644 --- a/program/js/common.js +++ b/program/js/common.js @@ -281,6 +281,26 @@ cancel: function(evt) 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 560ee0d9b..b4b775566 100644 --- a/program/js/list.js +++ b/program/js/list.js @@ -98,7 +98,7 @@ init: function() 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 @@ init: function() 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 @@ init_header: function() 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(); } @@ -220,6 +231,12 @@ init_fixed_header: function() $(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 @@ clear: function(sel) if (sel) this.clear_selection(); + else + this.last_selected = 0; // reset scroll position (in Opera) if (this.frame) @@ -370,6 +389,9 @@ update_row: function(id, cols, newid, select) */ focus: function(e) { + if (this.focused) + return; + var n, id; this.focused = true; @@ -380,20 +402,26 @@ focus: function(e) } } + 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 @@ blur: function() $(this.rows[id].obj).removeClass('selected focused').addClass('unfocused'); } } + + $(this.list).removeClass('focus'); }, @@ -1101,8 +1131,10 @@ clear_selection: function(id, no_event) 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 @@ use_arrow_key: function(keyCode, mod_key) } 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 353eb6be7..0dbedd256 100644 --- a/program/js/treelist.js +++ b/program/js/treelist.js @@ -105,6 +105,8 @@ function rcube_treelist_widget(node, p) } }); + container.attr('role', 'tree'); + /////// private methods @@ -425,6 +427,9 @@ function rcube_treelist_widget(node, p) 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 f47ef299a..5e07a7806 100644 --- a/program/lib/Roundcube/html.php +++ b/program/lib/Roundcube/html.php @@ -32,7 +32,7 @@ class html 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 @@ class html // 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; } } |