From e8bcf08c72a18b3bf396e6448d6658227ecb46f2 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Wed, 30 Apr 2014 16:21:29 +0200 Subject: [PATCH] 1. Prepare core and Larry skin for improved accessibility 2. Implement full keyboard navigation in main mail view --- .../legacy_browser/skins/larry/ie7hacks.css | 1 + program/include/rcmail_output_html.php | 9 ++ program/js/app.js | 31 +++--- program/js/common.js | 20 ++++ program/js/list.js | 60 ++++++++++-- program/js/treelist.js | 5 + program/lib/Roundcube/html.php | 5 +- skins/larry/includes/header.html | 4 +- skins/larry/includes/mailtoolbar.html | 46 ++++----- skins/larry/mail.css | 22 ++++- skins/larry/styles.css | 58 ++++++++++- skins/larry/templates/login.html | 6 +- skins/larry/templates/mail.html | 20 ++-- 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 2a174001e..85ebaf239 100644 --- a/plugins/legacy_browser/skins/larry/ie7hacks.css +++ b/plugins/legacy_browser/skins/larry/ie7hacks.css @@ -37,6 +37,7 @@ input.button { 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 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 = ''+cols.subject+''; + 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 = ''+cols.subject+''; } // 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. ) + */ +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') + .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; } } diff --git a/skins/larry/includes/header.html b/skins/larry/includes/header.html index 69e8b8aa6..8ce784b02 100644 --- a/skins/larry/includes/header.html +++ b/skins/larry/includes/header.html @@ -1,4 +1,4 @@ -