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 | |
parent | 2d8f31da736550a0df3ccde31bf85a146aea45c0 (diff) |
1. Prepare core and Larry skin for improved accessibility
2. Implement full keyboard navigation in main mail view
-rw-r--r-- | plugins/legacy_browser/skins/larry/ie7hacks.css | 1 | ||||
-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 | ||||
-rw-r--r-- | skins/larry/includes/header.html | 4 | ||||
-rw-r--r-- | skins/larry/includes/mailtoolbar.html | 46 | ||||
-rw-r--r-- | skins/larry/mail.css | 22 | ||||
-rw-r--r-- | skins/larry/styles.css | 58 | ||||
-rw-r--r-- | skins/larry/templates/login.html | 6 | ||||
-rw-r--r-- | skins/larry/templates/mail.html | 20 | ||||
-rw-r--r-- | 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 = '<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; } } 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 @@ -<div id="header"> +<div id="header" role="banner"> <div id="topline"> <div class="topleft"> <roundcube:container name="topline-left" id="topline-left" /> @@ -21,7 +21,7 @@ <roundcube:if condition="!env:extwin && !env:framed" /> <div id="topnav"> - <div id="taskbar" class="topright"> + <div id="taskbar" class="topright" role="navigation" aria-label="Application Tasks"> <roundcube:button command="mail" label="mail" class="button-mail" classSel="button-mail button-selected" innerClass="button-inner" /> <roundcube:button command="addressbook" label="addressbook" class="button-addressbook" classSel="button-addressbook button-selected" innerClass="button-inner" /> <roundcube:container name="taskbar" id="taskbar" /> diff --git a/skins/larry/includes/mailtoolbar.html b/skins/larry/includes/mailtoolbar.html index ac08a3200..5efea7cf7 100644 --- a/skins/larry/includes/mailtoolbar.html +++ b/skins/larry/includes/mailtoolbar.html @@ -3,11 +3,11 @@ <roundcube:button command="reply" type="link" class="button reply disabled" classAct="button reply" classSel="button reply pressed" label="reply" title="replytomessage" /> <span class="dropbutton"> <roundcube:button command="reply-all" type="link" class="button reply-all disabled" classAct="button reply-all" classSel="button reply-all pressed" label="replyall" title="replytoallmessage" /> - <span class="dropbuttontip" id="replyallmenulink" onclick="UI.show_popup('replyallmenu');return false"></span> + <span class="dropbuttontip" id="replyallmenulink" onclick="UI.toggle_popup('replyallmenu',event);return false"></span> </span> <span class="dropbutton"> <roundcube:button command="forward" type="link" class="button forward disabled" classAct="button forward" classSel="button forward pressed" label="forward" title="forwardmessage" /> - <span class="dropbuttontip" id="forwardmenulink" onclick="UI.show_popup('forwardmenu');return false"></span> + <span class="dropbuttontip" id="forwardmenulink" onclick="UI.toggle_popup('forwardmenu',event);return false"></span> </span> <roundcube:button command="delete" type="link" class="button delete disabled" classAct="button delete" classSel="button delete pressed" label="delete" title="deletemessage" /> <roundcube:if condition="template:name == 'message'" /> @@ -15,44 +15,44 @@ <roundcube:button command="print" type="link" class="button print disabled" classAct="button print" classSel="button print pressed" label="print" title="printmessage" /> <roundcube:endif /> <roundcube:container name="toolbar" id="mailtoolbar" /> -<roundcube:button name="markmenulink" id="markmessagemenulink" type="link" class="button markmessage" label="mark" title="markmessages" onclick="UI.show_popup('markmessagemenu');return false" /> -<roundcube:button name="messagemenulink" id="messagemenulink" type="link" class="button more" label="more" title="moreactions" onclick="UI.show_popup('messagemenu');return false" /> +<roundcube:button name="markmenulink" id="markmessagemenulink" type="link" class="button markmessage" label="mark" title="markmessages" onclick="UI.toggle_popup('markmessagemenu',event);return false" aria-haspopup="true" aria-owns="markmessagemenu" /> +<roundcube:button name="messagemenulink" id="messagemenulink" type="link" class="button more" label="more" title="moreactions" onclick="UI.toggle_popup('messagemenu',event);return false" aria-haspopup="true" aria-owns="messagemenu" /> <div id="forwardmenu" class="popupmenu"> - <ul class="toolbarmenu"> - <li><roundcube:button command="forward-inline" label="forwardinline" prop="sub" classAct="forwardlink active" class="forwardlink" /></li> - <li><roundcube:button command="forward-attachment" label="forwardattachment" prop="sub" classAct="forwardattachmentlink active" class="forwardattachmentlink" /></li> + <ul class="toolbarmenu" role="menu"> + <li role="menuitem"><roundcube:button command="forward-inline" label="forwardinline" prop="sub" classAct="forwardlink active" class="forwardlink" /></li> + <li role="menuitem"><roundcube:button command="forward-attachment" label="forwardattachment" prop="sub" classAct="forwardattachmentlink active" class="forwardattachmentlink" /></li> <roundcube:container name="forwardmenu" id="forwardmenu" /> </ul> </div> <div id="replyallmenu" class="popupmenu"> - <ul class="toolbarmenu"> - <li><roundcube:button command="reply-all" label="replyall" prop="sub" class="replyalllink" classAct="replyalllink active" /></li> - <li><roundcube:button command="reply-list" label="replylist" prop="sub" class="replylistlink" classAct="replylistlink active" /></li> + <ul class="toolbarmenu" role="menu"> + <li role="menuitem"><roundcube:button command="reply-all" label="replyall" prop="sub" class="replyalllink" classAct="replyalllink active" /></li> + <li role="menuitem"><roundcube:button command="reply-list" label="replylist" prop="sub" class="replylistlink" classAct="replylistlink active" /></li> <roundcube:container name="replyallmenu" id="replyallmenu" /> </ul> </div> <div id="messagemenu" class="popupmenu"> - <ul class="toolbarmenu iconized"> - <li><roundcube:button command="print" label="printmessage" class="icon" classAct="icon active" innerclass="icon print" /></li> - <li><roundcube:button command="download" label="emlsave" class="icon" classAct="icon active" innerclass="icon download" /></li> - <li><roundcube:button command="edit" prop="new" label="editasnew" class="icon" classAct="icon active" innerclass="icon edit" /></li> - <li><roundcube:button command="viewsource" label="viewsource" class="icon" classAct="icon active" innerclass="icon viewsource" /></li> - <li><roundcube:button command="move" label="moveto" class="icon" classAct="icon active" innerclass="icon move folder-selector-link" /></li> - <li><roundcube:button command="copy" label="copyto" class="icon" classAct="icon active" innerclass="icon copy folder-selector-link" /></li> - <li><roundcube:button command="open" label="openinextwin" target="_blank" class="icon" classAct="icon active" innerclass="icon extwin" /></li> + <ul class="toolbarmenu iconized" role="menu"> + <li role="menuitem"><roundcube:button command="print" label="printmessage" class="icon" classAct="icon active" innerclass="icon print" /></li> + <li role="menuitem"><roundcube:button command="download" label="emlsave" class="icon" classAct="icon active" innerclass="icon download" /></li> + <li role="menuitem"><roundcube:button command="edit" prop="new" label="editasnew" class="icon" classAct="icon active" innerclass="icon edit" /></li> + <li role="menuitem"><roundcube:button command="viewsource" label="viewsource" class="icon" classAct="icon active" innerclass="icon viewsource" /></li> + <li role="menuitem"><roundcube:button command="move" label="moveto" class="icon" classAct="icon active" innerclass="icon move folder-selector-link" /></li> + <li role="menuitem"><roundcube:button command="copy" label="copyto" class="icon" classAct="icon active" innerclass="icon copy folder-selector-link" /></li> + <li role="menuitem"><roundcube:button command="open" label="openinextwin" target="_blank" class="icon" classAct="icon active" innerclass="icon extwin" /></li> <roundcube:container name="messagemenu" id="messagemenu" /> </ul> </div> <div id="markmessagemenu" class="popupmenu"> - <ul class="toolbarmenu iconized"> - <li><roundcube:button command="mark" prop="read" label="markread" classAct="icon active" class="icon" innerclass="icon read" /></li> - <li><roundcube:button command="mark" prop="unread" label="markunread" classAct="icon active" class="icon" innerclass="icon unread" /></li> - <li><roundcube:button command="mark" prop="flagged" label="markflagged" classAct="icon active" class="icon" innerclass="icon flagged" /></li> - <li><roundcube:button command="mark" prop="unflagged" label="markunflagged" classAct="icon active" class="icon" innerclass="icon unflagged" /></li> + <ul class="toolbarmenu iconized" role="menu"> + <li role="menuitem"><roundcube:button command="mark" prop="read" label="markread" classAct="icon active" class="icon" innerclass="icon read" /></li> + <li role="menuitem"><roundcube:button command="mark" prop="unread" label="markunread" classAct="icon active" class="icon" innerclass="icon unread" /></li> + <li role="menuitem"><roundcube:button command="mark" prop="flagged" label="markflagged" classAct="icon active" class="icon" innerclass="icon flagged" /></li> + <li role="menuitem"><roundcube:button command="mark" prop="unflagged" label="markunflagged" classAct="icon active" class="icon" innerclass="icon unflagged" /></li> <roundcube:container name="markmenu" id="markmessagemenu" /> </ul> </div> diff --git a/skins/larry/mail.css b/skins/larry/mail.css index b9f24b8d0..a3f7e7ba8 100644 --- a/skins/larry/mail.css +++ b/skins/larry/mail.css @@ -158,6 +158,7 @@ a.iconbutton.threadmode.selected { padding-right: 36px; } +#mailboxlist li.mailbox > a:focus, #mailboxlist li.mailbox.selected > a { background-position: 6px -21px; } @@ -166,6 +167,7 @@ a.iconbutton.threadmode.selected { background-position: 6px -189px; } +#mailboxlist li.mailbox.inbox > a:focus, #mailboxlist li.mailbox.inbox.selected > a { background-position: 6px -213px; } @@ -174,6 +176,7 @@ a.iconbutton.threadmode.selected { background-position: 6px -238px; } +#mailboxlist li.mailbox.drafts > a:focus, #mailboxlist li.mailbox.drafts.selected > a { background-position: 6px -262px; } @@ -182,6 +185,7 @@ a.iconbutton.threadmode.selected { background-position: 6px -286px; } +#mailboxlist li.mailbox.sent > a:focus, #mailboxlist li.mailbox.sent.selected > a { background-position: 6px -310px; } @@ -190,6 +194,7 @@ a.iconbutton.threadmode.selected { background-position: 6px -334px; } +#mailboxlist li.mailbox.junk > a:focus, #mailboxlist li.mailbox.junk.selected > a { background-position: 6px -358px; } @@ -198,6 +203,7 @@ a.iconbutton.threadmode.selected { background-position: 6px -382px; } +#mailboxlist li.mailbox.trash > a:focus, #mailboxlist li.mailbox.trash.selected > a { background-position: 6px -406px; } @@ -206,6 +212,7 @@ a.iconbutton.threadmode.selected { background-position: 6px -1924px; } +#mailboxlist li.mailbox.trash.empty > a:focus, #mailboxlist li.mailbox.trash.empty.selected > a { background-position: 6px -1948px; } @@ -214,6 +221,7 @@ a.iconbutton.threadmode.selected { background-position: 6px -1699px; } +#mailboxlist li.mailbox.archive > a:focus, #mailboxlist li.mailbox.archive.selected > a { background-position: 6px -1723px; } @@ -222,6 +230,7 @@ a.iconbutton.threadmode.selected { background-position: 23px -238px; } +#mailboxlist li.mailbox ul li.drafts > a:focus, #mailboxlist li.mailbox ul li.drafts.selected > a { background-position: 23px -262px; } @@ -230,6 +239,7 @@ a.iconbutton.threadmode.selected { background-position: 23px -286px; } +#mailboxlist li.mailbox ul li.sent > a:focus, #mailboxlist li.mailbox ul li.sent.selected > a { background-position: 23px -310px; } @@ -238,6 +248,7 @@ a.iconbutton.threadmode.selected { background-position: 23px -334px; } +#mailboxlist li.mailbox ul li.junk > a:focus, #mailboxlist li.mailbox ul li.junk.selected > a { background-position: 23px -358px; } @@ -246,6 +257,7 @@ a.iconbutton.threadmode.selected { background-position: 23px -382px; } +#mailboxlist li.mailbox ul li.trash > a:focus, #mailboxlist li.mailbox ul li.trash.selected > a { background-position: 23px -406px; } @@ -254,6 +266,7 @@ a.iconbutton.threadmode.selected { background-position: 23px -1924px; } +#mailboxlist li.mailbox ul li.trash.empty > a:focus, #mailboxlist li.mailbox ul li.trash.empty.selected > a { background-position: 23px -1948px; } @@ -262,6 +275,7 @@ a.iconbutton.threadmode.selected { background-position: 23px -1699px; } +#mailboxlist li.mailbox ul li.archive > a:focus, #mailboxlist li.mailbox ul li.archive.selected > a { background-position: 23px -1723px; } @@ -300,6 +314,7 @@ a.iconbutton.threadmode.selected { padding-left: 52px; /* 36 + 1 x 16 */ background-position: 22px -93px; /* 6 + 1 x 16 */ } +#mailboxlist li.mailbox ul li > a:focus, #mailboxlist li.mailbox ul li.selected > a { background-position: 22px -117px; } @@ -312,6 +327,7 @@ a.iconbutton.threadmode.selected { padding-left: 68px; /* 2x */ background-position: 38px -93px; } +#mailboxlist li.mailbox ul ul li > a:focus, #mailboxlist li.mailbox ul ul li.selected > a { background-position: 38px -117px; } @@ -323,6 +339,7 @@ a.iconbutton.threadmode.selected { padding-left: 84px; /* 3x */ background-position: 54px -93px; } +#mailboxlist li.mailbox ul ul ul li > a:focus, #mailboxlist li.mailbox ul ul ul li.selected > a { background-position: 54px -117px; } @@ -334,6 +351,7 @@ a.iconbutton.threadmode.selected { padding-left: 100px; /* 4x */ background-position: 70px -93px; } +#mailboxlist li.mailbox ul ul ul ul li > a:focus, #mailboxlist li.mailbox ul ul ul ul li.selected > a { background-position: 70px -117px; } @@ -544,7 +562,9 @@ table.messagelist.fixedcopy { .messagelist thead tr td.sortedDESC a { color: #004458; text-decoration: underline; - background: url(images/listicons.png) right -912px no-repeat; + background-image: url(images/listicons.png); + background-repeat: no-repeat; + background-position: right -912px; } .messagelist thead tr td.sortedASC a { diff --git a/skins/larry/styles.css b/skins/larry/styles.css index 660daa943..4ef57e722 100644 --- a/skins/larry/styles.css +++ b/skins/larry/styles.css @@ -945,6 +945,13 @@ a.iconlink.upload { background: url(images/buttons.png) -1000px 0 no-repeat; } +#taskbar a:focus { + color: #fff; + text-shadow: 0px 1px 1px #666; + background-color: #3da0c2; + outline: none; +} + #taskbar a.button-selected { color: #3cf; background-color: #2c2c2c; @@ -1240,6 +1247,13 @@ ul.treelist li a { text-overflow: ellipsis; } +ul.treelist li a:focus { + color: #fff; + background: #4db0d2; + text-shadow: 0px 1px 1px #666; + outline: none; +} + ul.treelist ul li a { padding-left: 38px; } @@ -1320,6 +1334,13 @@ ul.treelist li.selected > div.expanded { margin-top: 1px; } +.boxfooter a.listbutton:focus { + color: #fff; + background: #4db0d2; + text-shadow: 0px 1px 1px #666; + outline: none; +} + .uibox .boxfooter .listbutton:first-child { border-radius: 0 0 0 4px; } @@ -1329,7 +1350,9 @@ ul.treelist li.selected > div.expanded { width: 48px; height: 35px; text-indent: -5000px; - background: url(images/buttons.png) -1000px 0 no-repeat; + background-image: url(images/buttons.png); + background-position: -1000px 0; + background-repeat: no-repeat; } .boxfooter .listbutton.add .inner { @@ -1473,6 +1496,13 @@ table.records-table { text-overflow: ellipsis; } +.records-table thead td a:focus { + color: #fff; + background: #4db0d2; + text-shadow: 0px 1px 1px #666; + outline: none; +} + .records-table tbody td { padding: 2px 7px; border-bottom: 1px solid #ddd; @@ -1492,12 +1522,12 @@ table.records-table { } /* because of border-collapse, we make the left border twice what we want it to be - half will be hidden to the left */ -.records-table tbody tr.focused > td:first-child { +.records-table.focus tbody tr.focused > td:first-child { border-left: 2px solid #b0ccd7; padding-left: 4px; } -.records-table tbody tr.selected.focused > td:first-child { +.records-table.focus tbody tr.selected.focused > td:first-child { border-left-color: #49b3d2; } @@ -1908,6 +1938,13 @@ ul.proplist li { border-radius: 0; } +.toolbar a.button:focus { + color: #fff; + text-shadow: 0px 1px 1px #666; + background-color: #4db0d2; + border-radius: 4px; +} + .toolbar a.button.disabled { opacity: 0.4; filter: alpha(opacity=40); @@ -2119,6 +2156,19 @@ select.decorated option { } +a.menuselector:focus, +a.menuselector.focus, +a.iconbutton:focus, +.pagenav a.button:focus { + border-color: #4fadd5; + -webkit-box-shadow: 0 0 4px 2px rgba(71,135,177, 0.8); + -moz-box-shadow: 0 0 4px 2px rgba(71,135,177, 0.8); + -o-box-shadow: 0 0 4px 2px rgba(71,135,177, 0.8); + box-shadow: 0 0 4px 2px rgba(71,135,177, 0.8); + outline: none; +} + + /*** quota indicator ***/ #quotadisplay { @@ -2211,6 +2261,7 @@ ul.toolbarmenu li a.active { .googie_list td.googie_list_onhover, ul.toolbarmenu li a.active:hover, +ul.toolbarmenu li a.active:focus, #rcmKSearchpane ul li.selected, select.decorated option:hover, select.decorated option[selected='selected'] { @@ -2220,6 +2271,7 @@ select.decorated option[selected='selected'] { background: -o-linear-gradient(top, #00aad6 0%, #008fc9 100%); background: -ms-linear-gradient(top, #00aad6 0%, #008fc9 100%); background: linear-gradient(top, #00aad6 0%, #008fc9 100%); + outline: none; } ul.toolbarmenu.iconized li a, diff --git a/skins/larry/templates/login.html b/skins/larry/templates/login.html index 64ff6be92..b14d1965e 100644 --- a/skins/larry/templates/login.html +++ b/skins/larry/templates/login.html @@ -8,7 +8,7 @@ <body> <div id="login-form"> -<div class="box-inner"> +<div class="box-inner" role="main"> <roundcube:object name="logo" src="/images/roundcube_logo.png" id="logo" /> <roundcube:form name="form" method="post"> @@ -17,14 +17,14 @@ </div> -<div class="box-bottom"> +<div class="box-bottom" role="complementary"> <roundcube:object name="message" id="message" /> <noscript> <p class="noscriptwarning"><roundcube:label name="noscriptwarning" /></p> </noscript> </div> -<div id="bottomline"> +<div id="bottomline" role="contentinfo"> <roundcube:var name="config:product_name"> <roundcube:object name="version" condition="config:display_version" /> <roundcube:if condition="config:support_url" /> ● <a href="<roundcube:var name='config:support_url' />" target="_blank" class="support-link"><roundcube:label name="support" /></a> diff --git a/skins/larry/templates/mail.html b/skins/larry/templates/mail.html index 1e4a3ce8c..d92324fe8 100644 --- a/skins/larry/templates/mail.html +++ b/skins/larry/templates/mail.html @@ -18,7 +18,7 @@ <div id="mainscreen"> <!-- toolbar --> -<div id="messagetoolbar" class="toolbar"> +<div id="messagetoolbar" class="toolbar" role="toolbar"> <roundcube:button command="checkmail" type="link" class="button checkmail disabled" classAct="button checkmail" classSel="button checkmail pressed" label="refresh" title="checkmail" /> <roundcube:include file="/includes/mailtoolbar.html" /> </div> @@ -27,13 +27,13 @@ <!-- search filter --> <div id="searchfilter"> - <roundcube:object name="searchfilter" class="searchfilter decorated" /> + <roundcube:object name="searchfilter" class="searchfilter decorated" aria-controls="messagelist" /> </div> <!-- search box --> -<div id="quicksearchbar" class="searchbox"> +<div id="quicksearchbar" class="searchbox" role="search" aria-label="Email message search form"> <roundcube:object name="searchform" id="quicksearchbox" /> -<roundcube:button name="searchmenulink" id="searchmenulink" class="iconbutton searchoptions" onclick="UI.show_popup('searchmenu');return false" title="searchmod" content=" " /> +<roundcube:button name="searchmenulink" id="searchmenulink" class="iconbutton searchoptions" onclick="UI.toggle_popup('searchmenu',event);return false" title="searchmod" content=" " /> <roundcube:button command="reset-search" id="searchreset" class="iconbutton reset" title="resetsearch" content=" " /> </div> @@ -43,12 +43,12 @@ <div id="mailview-left"> <!-- folders list --> -<div id="mailboxcontainer" class="uibox listbox"> +<div id="mailboxcontainer" class="uibox listbox" role="navigation" aria-label="Email folder selection"> <div id="folderlist-content" class="scroller withfooter"> <roundcube:object name="mailboxlist" id="mailboxlist" class="treelist listing" folder_filter="mail" unreadwrap="%s" /> </div> <div id="folderlist-footer" class="boxfooter"> - <roundcube:button name="mailboxmenulink" id="mailboxmenulink" type="link" title="folderactions" class="listbutton groupactions" onclick="UI.show_popup('mailboxmenu');return false" innerClass="inner" content="⚙" /> + <roundcube:button name="mailboxmenulink" id="mailboxmenulink" type="link" title="folderactions" class="listbutton groupactions" onclick="UI.toggle_popup('mailboxmenu',event);return false" innerClass="inner" content="⚙" /> <roundcube:if condition="env:quota" /> <roundcube:object name="quotaDisplay" id="quotadisplay" class="countdisplay" display="text" /> <roundcube:endif /> @@ -57,7 +57,7 @@ </div> -<div id="mailview-right"> +<div id="mailview-right" role="main"> <roundcube:if condition="config:preview_pane == true" /> <div id="mailview-top" class="uibox"> @@ -81,9 +81,9 @@ </div> <div id="listselectors"> - <a href="#select" id="listselectmenulink" class="menuselector" onclick="UI.show_popup('listselectmenu');return false"><span class="handle"><roundcube:label name="select" /></span></a> + <a href="#select" id="listselectmenulink" class="menuselector" onclick="UI.toggle_popup('listselectmenu', event);return false" aria-haspopup="true" aria-owns="listselectmenu"><span class="handle"><roundcube:label name="select" /></span></a> <roundcube:if condition="env:threads" /> - <a href="#threads" id="threadselectmenulink" class="menuselector" onclick="UI.show_popup('threadselectmenu');return false"><span class="handle"><roundcube:label name="threads" /></span></a> + <a href="#threads" id="threadselectmenulink" class="menuselector" onclick="UI.toggle_popup('threadselectmenu', event);return false" aria-haspopup="true" aria-owns="threadselectmenu"><span class="handle"><roundcube:label name="threads" /></span></a> <roundcube:endif /> </div> @@ -99,7 +99,7 @@ <roundcube:container name="listcontrols" id="listcontrols" /> - <a href="#preview" id="mailpreviewtoggle" title="<roundcube:label name='previewpane' />"></a> + <a href="#preview" id="mailpreviewtoggle" class="iconbutton" title="<roundcube:label name='previewpane' />" role="button" tabindex="0"></a> </div> </div><!-- end mailview-top --> diff --git a/skins/larry/ui.js b/skins/larry/ui.js index 0e8afc652..add02b85e 100644 --- a/skins/larry/ui.js +++ b/skins/larry/ui.js @@ -33,6 +33,8 @@ function rcube_mail_ui() var mailviewsplit; var compose_headers = {}; var prefs; + var focused_popup; + var popup_keyboard_active = false; // export public methods this.set = setenv; @@ -40,6 +42,7 @@ function rcube_mail_ui() this.init_tabs = init_tabs; this.show_about = show_about; this.show_popup = show_popup; + this.toggle_popup = toggle_popup; this.add_popup = add_popup; this.set_searchmod = set_searchmod; this.set_searchscope = set_searchscope; @@ -333,6 +336,10 @@ function rcube_mail_ui() var val = $('option:selected', this).text(); $(this).next().children().text(val); }); + + select + .on('focus', function(e){ overlay.addClass('focus'); }) + .on('blur', function(e){ overlay.removeClass('focus'); }); }); // set min-width to show all toolbar buttons @@ -343,14 +350,7 @@ function rcube_mail_ui() $(document.body) .bind('mouseup', body_mouseup) - .bind('keyup', function(e){ - if (e.keyCode == 27) { - for (var id in popups) { - if (popups[id].is(':visible')) - show_popup(id, false); - } - } - }); + .bind('keydown', popup_keypress); $('iframe').load(function(e){ // this = iframe @@ -586,13 +586,21 @@ function rcube_mail_ui() /** * Trigger for popup menus */ - function show_popup(popup, show, config) + function toggle_popup(popup, e, config) + { + show_popup(popup, undefined, config, rcube_event.is_keyboard(e)); + } + + /** + * (Deprecated) trigger for popup menus + */ + function show_popup(popup, show, config, keyboard) { // auto-register menu object if (config || !popupconfig[popup]) add_popup(popup, config); - var visible = show_popupmenu(popup, show), + var visible = show_popupmenu(popup, show, keyboard), config = popupconfig[popup]; if (typeof config.callback == 'function') config.callback(visible); @@ -601,7 +609,7 @@ function rcube_mail_ui() /** * Show/hide a specific popup menu */ - function show_popupmenu(popup, show) + function show_popupmenu(popup, show, keyboard) { var obj = popups[popup], config = popupconfig[popup], @@ -638,12 +646,76 @@ function rcube_mail_ui() obj.css({ left:pos.left, top:(pos.top + (above ? -obj.height() : ref.offsetHeight)) }); } + else if (!show && keyboard && ref.length) { + ref.focus(); + } obj[show?'show':'hide'](); + popup_keyboard_active = show && keyboard; + if (popup_keyboard_active) { + focused_popup = popup; + obj.find('a,input').not('[aria-disabled=true]').first().focus(); + } + else { + focused_popup = null; + } + return show; } + /** + * Handler for keyboard events on active popups + */ + function popup_keypress(e) + { + var target = e.target || {}, + keyCode = rcube_event.get_keycode(e); + + if (e.keyCode != 27 && (!popup_keyboard_active || target.nodeName == 'TEXTAREA' || target.nodeName == 'SELECT')) + return true; + + switch (keyCode) { + case 38: + case 40: + case 63232: // "up", in safari keypress + case 63233: // "down", in safari keypress + popup_focus_item(mod = keyCode == 38 || keyCode == 63232 ? -1 : 1); + break; + + case 9: // tab + if (focused_popup) { + var mod = rcube_event.get_modifier(e); + if (!popup_focus_item(mod == SHIFT_KEY ? -1 : 1)) { + show_popup(focused_popup, false, undefined, true); + } + } + return rcube_event.cancel(e); + + case 27: // esc + for (var id in popups) { + if (popups[id].is(':visible')) + show_popup(id, false, undefined, true); + } + break; + } + + return true; + } + + /** + * Helper method to move focus to the next/prev popup menu item + */ + function popup_focus_item(dir) + { + var obj, mod = dir < 0 ? 'prevAll' : 'nextAll', limit = dir < 0 ? 'last' : 'first'; + if (focused_popup && (obj = popups[focused_popup])) { + return obj.find(':focus').closest('li')[mod](':has(:not([aria-disabled=true]))').find('a,input')[limit]().focus().length; + } + + return 0; + } + /** * */ @@ -734,8 +806,8 @@ function rcube_mail_ui() function switch_view_mode(mode, force) { if (force || !$('#mail'+mode+'mode').hasClass('disabled')) { - $('#maillistmode, #mailthreadmode').removeClass('selected'); - $('#mail'+mode+'mode').addClass('selected'); + $('#maillistmode, #mailthreadmode').removeClass('selected').attr('tabindex', '0').attr('aria-disabled', 'false'); + $('#mail'+mode+'mode').addClass('selected').attr('tabindex', '-1').attr('aria-disabled', 'true'); } } |