summaryrefslogtreecommitdiff
path: root/program
diff options
context:
space:
mode:
authorThomas Bruederli <thomas@roundcube.net>2014-04-30 16:21:29 +0200
committerThomas Bruederli <thomas@roundcube.net>2014-04-30 16:21:29 +0200
commite8bcf08c72a18b3bf396e6448d6658227ecb46f2 (patch)
treedd4dc60d87f2340114eb9dcaaf7a567e09cdc995 /program
parent2d8f31da736550a0df3ccde31bf85a146aea45c0 (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.php9
-rw-r--r--program/js/app.js31
-rw-r--r--program/js/common.js20
-rw-r--r--program/js/list.js60
-rw-r--r--program/js/treelist.js5
-rw-r--r--program/lib/Roundcube/html.php5
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;
}
}