plugins/calendar plugins/libcalendaring plugins/libkolab
Thomas Brüderli
bruederli at kolabsys.com
Thu Jun 19 10:40:43 CEST 2014
plugins/calendar/calendar.php | 2
plugins/calendar/calendar_ui.js | 142 +++++++++++-------
plugins/calendar/lib/calendar_ui.php | 11 -
plugins/calendar/lib/js/fullcalendar.js | 36 ++--
plugins/calendar/localization/en_US.inc | 19 ++
plugins/calendar/skins/larry/calendar.css | 52 +++++-
plugins/calendar/skins/larry/fullcalendar.css | 4
plugins/calendar/skins/larry/templates/calendar.html | 88 ++++++-----
plugins/calendar/skins/larry/templates/eventedit.html | 29 ++-
plugins/libcalendaring/libcalendaring.js | 57 ++++---
plugins/libcalendaring/libcalendaring.php | 3
plugins/libcalendaring/localization/en_US.inc | 7
plugins/libkolab/js/folderlist.js | 17 +-
13 files changed, 315 insertions(+), 152 deletions(-)
New commits:
commit efecba6675e09abab29ce531f284d055566db801
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Jun 19 10:40:28 2014 +0200
Accessibility enhancements for the calendar module (#3084)
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 154b0a3..f863580 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -299,7 +299,7 @@ class calendar extends rcube_plugin
$this->rc->output->set_env('calendar_driver', $this->rc->config->get('calendar_driver'), false);
$this->rc->output->set_env('calendar_resources', (bool)$this->rc->config->get('calendar_resources_driver'));
$this->rc->output->set_env('mscolors', jqueryui::get_color_values());
- $this->rc->output->set_env('identities-selector', $this->ui->identity_select(array('id' => 'edit-identities-list')));
+ $this->rc->output->set_env('identities-selector', $this->ui->identity_select(array('id' => 'edit-identities-list', 'aria-label' => $this->gettext('roleorganizer'))));
$view = get_input_value('view', RCUBE_INPUT_GPC);
if (in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table')))
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 68ae94f..ff9e5b4 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -237,32 +237,33 @@ function rcube_calendar_ui(settings)
if (edit) {
rcmail.env.attachments[elem.id] = elem;
// delete icon
- content = document.createElement('A');
- content.href = '#delete';
- content.title = rcmail.gettext('delete');
- content.className = 'delete';
- $(content).click({id: elem.id}, function(e) { remove_attachment(this, e.data.id); return false; });
+ content = $('<a href="#delete" />')
+ .attr('title', rcmail.gettext('delete'))
+ .attr('aria-label', rcmail.gettext('delete') + ' ' + Q(elem.name))
+ .addClass('delete')
+ .click({id: elem.id}, function(e) { remove_attachment(this, e.data.id); return false; });
if (!rcmail.env.deleteicon)
- content.innerHTML = rcmail.gettext('delete');
+ content.html(rcmail.gettext('delete'));
else {
img = document.createElement('IMG');
img.src = rcmail.env.deleteicon;
img.alt = rcmail.gettext('delete');
- content.appendChild(img);
+ content.append(img);
}
- li.appendChild(content);
+ content.appendTo(li);
}
// name/link
- content = document.createElement('A');
- content.innerHTML = elem.name;
- content.className = 'file';
- content.href = '#load';
- $(content).click({event: event, att: elem}, function(e) {
- load_attachment(e.data.event, e.data.att); return false; });
- li.appendChild(content);
+ content = $('<a href="#load" />')
+ .html(Q(elem.name))
+ .addClass('file')
+ .click({event: event, att: elem}, function(e) {
+ load_attachment(e.data.event, e.data.att);
+ return false;
+ })
+ .appendTo(li);
ul.appendChild(li);
}
@@ -283,7 +284,7 @@ function rcube_calendar_ui(settings)
};
// event details dialog (show only)
- var event_show_dialog = function(event)
+ var event_show_dialog = function(event, ev)
{
var $dialog = $("#eventshow").attr('class', 'uidialog');
var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:false };
@@ -424,24 +425,34 @@ function rcube_calendar_ui(settings)
$dialog.dialog('close');
};
}
-
+
// open jquery UI dialog
$dialog.dialog({
modal: false,
resizable: !bw.ie6,
closeOnEscape: (!bw.ie6 && !bw.ie7), // disable for performance reasons
- title: Q(me.event_date_text(event)),
+ title: me.event_date_text(event),
open: function() {
- $dialog.parent().find('.ui-button').first().focus();
+ $dialog.attr('aria-hidden', 'false');
+ setTimeout(function(){
+ $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
+ }, 5);
},
close: function() {
- $dialog.dialog('destroy').hide();
+ $dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
},
buttons: buttons,
minWidth: 320,
width: 420
}).show();
-
+
+ // remember opener element (to be focused on close)
+ $dialog.data('opener', ev && rcube_event.is_keyboard(ev) ? ev.target : null);
+
+ // set voice title on dialog widget
+ $dialog.dialog('widget').removeAttr('aria-labelledby')
+ .attr('aria-label', me.event_date_text(event, true) + ', ', event.title);
+
// set dialog size according to content
me.dialog_resize($dialog.get(0), $dialog.height(), 420);
/*
@@ -472,8 +483,11 @@ function rcube_calendar_ui(settings)
// bring up the event dialog (jquery-ui popup)
var event_edit_dialog = function(action, event)
{
+ // copy opener element from show dialog
+ var op_elem = $("#eventshow:ui-dialog").data('opener');
+
// close show dialog first
- $("#eventshow:ui-dialog").dialog('close');
+ $("#eventshow:ui-dialog").data('opener', null).dialog('close');
var $dialog = $('<div>');
var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:action=='new' };
@@ -675,8 +689,8 @@ function rcube_calendar_ui(settings)
$('#edit-tab-attachments')[(calendar.attachments?'show':'hide')]();
// activate the first tab
- $('#eventtabs').tabs('select', 0);
-
+ $('#eventtabs').tabs('option', 'active', 0);
+
// hack: set task to 'calendar' to make all dialog actions work correctly
var comm_path_before = rcmail.env.comm_path;
rcmail.env.comm_path = comm_path_before.replace(/_task=[a-z]+/, '_task=calendar');
@@ -690,15 +704,18 @@ function rcube_calendar_ui(settings)
closeOnEscape: false,
title: rcmail.gettext((action == 'edit' ? 'edit_event' : 'new_event'), 'calendar'),
open: function() {
+ editform.attr('aria-hidden', 'false');
$dialog.parent().find('.ui-dialog-buttonset .ui-button').first().addClass('mainaction');
},
close: function() {
- editform.hide().appendTo(document.body);
+ editform.hide().attr('aria-hidden', 'true').appendTo(document.body);
$dialog.dialog("destroy").remove();
rcmail.ksearch_blur();
rcmail.ksearch_destroy();
freebusy_data = {};
rcmail.env.comm_path = comm_path_before; // restore comm_path
+ if (op_elem)
+ $(op_elem).focus();
},
buttons: buttons,
minWidth: 500,
@@ -843,12 +860,13 @@ function rcube_calendar_ui(settings)
closeOnEscape: (!bw.ie6 && !bw.ie7),
title: rcmail.gettext('scheduletime', 'calendar'),
open: function() {
- $dialog.parent().find('.ui-dialog-buttonset .ui-button').first().focus();
+ $dialog.attr('aria-hidden', 'false').find('#shedule-find-next, #shedule-find-prev').not(':disabled').first().focus();
},
close: function() {
if (bw.ie6)
$("#edit-attendees-table").css('visibility','visible');
- $dialog.dialog("destroy").hide();
+ $dialog.dialog("destroy").attr('aria-hidden', 'true').hide();
+ // TODO: focus opener button
},
resizeStop: function() {
render_freebusy_overlay();
@@ -1316,6 +1334,9 @@ function rcube_calendar_ui(settings)
var now = new Date();
$('#shedule-find-prev').button('option', 'disabled', (event.start.getTime() < now.getTime()));
+
+ // speak new selection
+ rcmail.display_message(rcmail.gettext('suggestedslot', 'calendar') + ': ' + me.event_date_text(event, true), 'voice');
}
else {
alert(rcmail.gettext('noslotfound','calendar'));
@@ -1407,7 +1428,7 @@ function rcube_calendar_ui(settings)
if (organizer && !readonly)
dispname = rcmail.env['identities-selector'];
- var select = '<select class="edit-attendee-role"' + (organizer || readonly ? ' disabled="true"' : '') + '>';
+ var select = '<select class="edit-attendee-role"' + (organizer || readonly ? ' disabled="true"' : '') + ' aria-label="' + rcmail.gettext('role','calendar') + '">';
for (var r in opts)
select += '<option value="'+ r +'" class="' + r.toLowerCase() + '"' + (data.role == r ? ' selected="selected"' : '') +'>' + Q(opts[r]) + '</option>';
select += '</select>';
@@ -1427,7 +1448,7 @@ function rcube_calendar_ui(settings)
var html = '<td class="role">' + select + '</td>' +
'<td class="name">' + dispname + '</td>' +
- '<td class="availability"><img src="./program/resources/blank.gif" class="availabilityicon ' + avail + '" data-email="' + data.email + '" /></td>' +
+ '<td class="availability"><img src="./program/resources/blank.gif" class="availabilityicon ' + avail + '" data-email="' + data.email + '" alt="" /></td>' +
'<td class="confirmstate"><span class="' + String(data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + Q(data.status || '') + '</span></td>' +
'<td class="options">' + (organizer || readonly ? '' : dellink) + '</td>';
@@ -1482,10 +1503,11 @@ function rcube_calendar_ui(settings)
url: rcmail.url('freebusy-status'),
data: { email:email, start:date2servertime(clone_date(event.start, event.allDay?1:0)), end:date2servertime(clone_date(event.end, event.allDay?2:0)), _remote: 1 },
success: function(status){
- icon.removeClass('loading').addClass(String(status).toLowerCase());
+ var avail = String(status).toLowerCase();
+ icon.removeClass('loading').addClass(avail).attr('alt', rcmail.gettext('avail' + avail, 'calendar'));
},
error: function(){
- icon.removeClass('loading').addClass('unknown');
+ icon.removeClass('loading').addClass('unknown').attr('alt', rcmail.gettext('availunknown', 'calendar'));
}
});
};
@@ -1522,11 +1544,14 @@ function rcube_calendar_ui(settings)
resizable: true,
closeOnEscape: true,
title: rcmail.gettext('findresources', 'calendar'),
+ open: function() {
+ $dialog.attr('aria-hidden', 'false');
+ },
close: function() {
- $dialog.dialog('destroy').hide();
+ $dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
},
resize: function(e) {
- var container = $(rcmail.gui_objects.resourceinfocalendar)
+ var container = $(rcmail.gui_objects.resourceinfocalendar);
container.fullCalendar('option', 'height', container.height() + 4);
},
buttons: buttons,
@@ -1593,9 +1618,11 @@ function rcube_calendar_ui(settings)
titleFormat: { day: 'dddd ' + settings['date_long'] },
currentTimeIndicator: settings.time_indicator,
eventRender: function(event, element, view) {
+ var title = rcmail.get_label(event.status, 'calendar');
element.addClass('status-' + event.status);
element.find('.fc-event-head').hide();
- element.find('.fc-event-title').text(rcmail.get_label(event.status, 'calendar'));
+ element.find('.fc-event-title').text(title);
+ element.attr('aria-label', me.event_date_text(event, true) + ': ' + title);
}
});
@@ -1971,10 +1998,12 @@ function rcube_calendar_ui(settings)
title: rcmail.gettext((action == 'remove' ? 'removeeventconfirm' : 'changeeventconfirm'), 'calendar'),
buttons: buttons,
open: function() {
- $dialog.parent().find('.ui-button').first().focus();
+ setTimeout(function(){
+ $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
+ }, 5);
},
close: function(){
- $dialog.dialog("destroy").hide();
+ $dialog.dialog("destroy").remove();
if (!rcmail.busy)
fc.fullCalendar('refetchEvents');
}
@@ -2021,6 +2050,8 @@ function rcube_calendar_ui(settings)
if (event.status) {
element.addClass('cal-event-status-' + String(event.status).toLowerCase());
}
+
+ element.attr('aria-label', event.title + ', ' + me.event_date_text(event, true));
};
@@ -2099,8 +2130,8 @@ function rcube_calendar_ui(settings)
allDayText: rcmail.gettext('all-day', 'calendar'),
currentTimeIndicator: settings.time_indicator,
eventRender: fc_event_render,
- eventClick: function(event) {
- event_show_dialog(event);
+ eventClick: function(event, ev, view) {
+ event_show_dialog(event, ev);
}
});
@@ -2743,6 +2774,7 @@ function rcube_calendar_ui(settings)
id_prefix: 'rcmlical',
selectable: true,
save_state: true,
+ keyboard: false,
searchbox: '#calendarlistsearch',
search_action: 'calendar/calendar',
search_sources: [ 'folders', 'users' ],
@@ -2773,6 +2805,12 @@ function rcube_calendar_ui(settings)
rcmail.http_post('calendar', { action:'subscribe', c:{ id:p.id, active:cal.active?1:0, permanent:cal.subscribed?1:0 } });
}
});
+ calendars_list.addEventListener('search-complete', function(data) {
+ if (data.length)
+ rcmail.display_message(rcmail.gettext('nrcalendarsfound','calendar').replace('$nr', data.length), 'voice');
+ else
+ rcmail.display_message(rcmail.gettext('nocalendarsfound','calendar'), 'info');
+ });
// init (delegate) event handler on calendar list checkboxes
$(rcmail.gui_objects.calendarslist).on('click', 'input[type=checkbox]', function(e){
@@ -2922,9 +2960,9 @@ function rcube_calendar_ui(settings)
day_clicked_ts = now;
},
// callback when a specific event is clicked
- eventClick: function(event) {
+ eventClick: function(event, ev, view) {
if (!event.temp)
- event_show_dialog(event);
+ event_show_dialog(event, ev);
},
// callback when an event was dragged and finally dropped
eventDrop: function(event, dayDelta, minuteDelta, allDay, revertFunc) {
@@ -3054,7 +3092,7 @@ function rcube_calendar_ui(settings)
// scroll to current time
var $this = $(this);
var widget = $this.autocomplete('widget');
- var menu = $this.data('autocomplete').menu;
+ var menu = $this.data('ui-autocomplete').menu;
var amregex = /^(.+)(a[.m]*)/i;
var pmregex = /^(.+)(a[.m]*)/i;
var val = $(this).val().replace(amregex, '0:$1').replace(pmregex, '1:$1');
@@ -3107,9 +3145,9 @@ function rcube_calendar_ui(settings)
return [ true, (active ? 'ui-datepicker-activerange ui-datepicker-active-' + view.name : ''), ''];
}
})) // set event handler for clicks on calendar week cell of the datepicker widget
- .click(function(e) {
+ .on('click', 'td.ui-datepicker-week-col', function(e) {
var cell = $(e.target);
- if (e.target.tagName == 'TD' && cell.hasClass('ui-datepicker-week-col')) {
+ if (e.target.tagName == 'TD' && cell.hasClass('')) {
var base_date = minical.datepicker('getDate');
if (minical.data('month'))
base_date.setMonth(minical.data('month')-1);
@@ -3126,7 +3164,9 @@ function rcube_calendar_ui(settings)
fc.fullCalendar('gotoDate', date).fullCalendar('setDate', date).fullCalendar('changeView', 'agendaWeek');
minical.datepicker('setDate', date);
}
- });
+ });
+
+ minical.find('.ui-datepicker-inline').attr('aria-labelledby', 'aria-label-minical');
if (rcmail.env.date) {
var viewdate = new Date();
@@ -3136,10 +3176,11 @@ function rcube_calendar_ui(settings)
// init event dialog
$('#eventtabs').tabs({
- show: function(event, ui) {
- if (ui.panel.id == 'event-panel-attendees' || ui.panel.id == 'event-panel-resources') {
- var tab = ui.panel.id == 'event-panel-resources' ? 'resource' : 'attendee';
- $('#edit-'+tab+'-name').select();
+ activate: function(event, ui) {
+ if (ui.newPanel.selector == '#event-panel-attendees' || ui.newPanel.selector == '#event-panel-resources') {
+ var tab = ui.newPanel.selector == '#event-panel-resources' ? 'resource' : 'attendee';
+ if (!rcube_event.is_keyboard(event))
+ $('#edit-'+tab+'-name').select();
// update free-busy status if needed
if (freebusy_ui.needsupdate && me.selected_event)
update_freebusy_status(me.selected_event);
@@ -3162,6 +3203,7 @@ function rcube_calendar_ui(settings)
.autocomplete({
delay: 100,
minLength: 1,
+ appendTo: '#eventedit',
source: autocomplete_times,
open: autocomplete_open,
change: event_times_changed,
@@ -3173,9 +3215,9 @@ function rcube_calendar_ui(settings)
.click(function() { // show drop-down upon clicks
$(this).autocomplete('search', $(this).val() ? $(this).val().replace(/\D.*/, "") : " ");
}).each(function(){
- $(this).data('autocomplete')._renderItem = function(ul, item) {
+ $(this).data('ui-autocomplete')._renderItem = function(ul, item) {
return $('<li>')
- .data('item.autocomplete', item)
+ .data('ui-autocomplete-item', item)
.append('<a>' + item[0] + item[1] + '</a>')
.appendTo(ul);
};
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index c7be56a..a788bac 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -303,10 +303,11 @@ class calendar_ui
$content = '';
if (!$activeonly || $prop['active']) {
+ $label_id = 'cl:' . $id;
$content = html::div(join(' ', $classes),
- html::span(array('class' => 'calname', 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname']) .
+ html::span(array('class' => 'calname', 'id' => $label_id, 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname']) .
($prop['virtual'] ? '' :
- html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
+ html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active'], 'aria-labelledby' => $label_id), '') .
(isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->cal->gettext('calendarsubscribe')), ' ') : '') .
html::span(array('class' => 'handle', 'style' => "background-color: #" . ($prop['color'] ?: 'f00')), ' ')
)
@@ -326,7 +327,7 @@ class calendar_ui
$select_range = new html_select(array('name' => 'listrange', 'id' => 'agenda-listrange'));
$select_range->add(1 . ' ' . preg_replace('/\(.+\)/', '', $this->cal->lib->gettext('days')), $days);
- foreach (array(2,5,7,14,30,60,90) as $days)
+ foreach (array(2,5,7,14,30,60,90,180,365) as $days)
$select_range->add($days . ' ' . preg_replace('/\(|\)/', '', $this->cal->lib->gettext('days')), $days);
$html .= html::label('agenda-listrange', $this->cal->gettext('listrange'));
@@ -334,8 +335,8 @@ class calendar_ui
$select_sections = new html_select(array('name' => 'listsections', 'id' => 'agenda-listsections'));
$select_sections->add('---', '');
- foreach (array('day' => 'days', 'week' => 'weeks', 'month' => 'months', 'smart' => 'smartsections') as $val => $label)
- $select_sections->add(preg_replace('/\(|\)/', '', ucfirst($this->cal->gettext($label))), $val);
+ foreach (array('day' => 'libcalendaring.days', 'week' => 'libcalendaring.weeks', 'month' => 'libcalendaring.months', 'smart' => 'calendar.smartsections') as $val => $label)
+ $select_sections->add(preg_replace('/\(|\)/', '', ucfirst($this->rc->gettext($label))), $val);
$html .= html::span('spacer', ' ');
$html .= html::label('agenda-listsections', $this->cal->gettext('listsections'));
diff --git a/plugins/calendar/lib/js/fullcalendar.js b/plugins/calendar/lib/js/fullcalendar.js
index 2150126..dc2fe10 100644
--- a/plugins/calendar/lib/js/fullcalendar.js
+++ b/plugins/calendar/lib/js/fullcalendar.js
@@ -1,5 +1,5 @@
/*!
- * FullCalendar v1.6.4-rcube-1.0
+ * FullCalendar v1.6.4-rcube-1.1
* Docs & License: http://arshaw.com/fullcalendar/
* (c) 2013 Adam Shaw, 2014 Kolab Systems AG
*/
@@ -813,7 +813,7 @@ function Header(calendar, options) {
var prevButton;
$.each(this.split(','), function(j, buttonName) {
if (buttonName == 'title') {
- e.append("<span class='fc-header-title'><h2> </h2></span>");
+ e.append("<span class='fc-header-title'><h2 aria-live='polite' aria-relevant='text' aria-atomic='true'> </h2></span>");
if (prevButton) {
prevButton.addClass(tm + '-corner-right');
}
@@ -833,7 +833,7 @@ function Header(calendar, options) {
var icon = options.theme ? smartProperty(options.buttonIcons, buttonName) : null; // why are we using smartProperty here?
var text = smartProperty(options.buttonText, buttonName); // why are we using smartProperty here?
var button = $(
- "<span class='fc-button fc-button-" + buttonName + " " + tm + "-state-default'>" +
+ "<span class='fc-button fc-button-" + buttonName + " " + tm + "-state-default' role='button' tabindex='0'>" +
(icon ?
"<span class='fc-icon-wrap'>" +
"<span class='ui-icon ui-icon-" + icon + "'/>" +
@@ -869,6 +869,10 @@ function Header(calendar, options) {
.removeClass(tm + '-state-down');
}
)
+ .keypress(function(ev) {
+ if (ev.keyCode == 13)
+ $(ev.target).trigger('click');
+ })
.appendTo(e);
disableTextSelection(button);
if (!prevButton) {
@@ -895,25 +899,25 @@ function Header(calendar, options) {
function activateButton(buttonName) {
element.find('span.fc-button-' + buttonName)
- .addClass(tm + '-state-active');
+ .addClass(tm + '-state-active').attr('tabindex', '-1');
}
function deactivateButton(buttonName) {
element.find('span.fc-button-' + buttonName)
- .removeClass(tm + '-state-active');
+ .removeClass(tm + '-state-active').attr('tabindex', '0');
}
function disableButton(buttonName) {
element.find('span.fc-button-' + buttonName)
- .addClass(tm + '-state-disabled');
+ .addClass(tm + '-state-disabled').attr('tabindex', '-1');
}
function enableButton(buttonName) {
element.find('span.fc-button-' + buttonName)
- .removeClass(tm + '-state-disabled');
+ .removeClass(tm + '-state-disabled').attr('tabindex', '0');
}
@@ -1760,7 +1764,7 @@ function _exclEndDay(end, allDay) {
function lazySegBind(container, segs, bindHandlers) {
- container.unbind('mouseover').mouseover(function(ev) {
+ container.unbind('mouseover focusin').bind('mouseover focusin', function(ev) {
var parent=ev.target, e,
i, seg;
while (parent != this) {
@@ -4051,7 +4055,7 @@ function AgendaEventRenderer() {
"left:" + seg.left + "px;" +
skinCss +
"'" +
- ">" +
+ " tabindex='0'>" +
"<div class='fc-event-inner fc-event-skin'" + skinCssAttr + ">" +
"<div class='fc-event-head fc-event-skin'" + skinCssAttr + ">" +
"<div class='fc-event-time'>" +
@@ -4067,7 +4071,7 @@ function AgendaEventRenderer() {
"</div>"; // close inner
if (seg.isEnd && isEventResizable(event)) {
html +=
- "<div class='ui-resizable-handle ui-resizable-s'>=</div>";
+ "<div class='ui-resizable-handle ui-resizable-s' role='presentation'>=</div>";
}
html +=
"</" + (url ? "a" : "div") + ">";
@@ -4941,7 +4945,7 @@ function ListEventRenderer() {
}
function lazySegBind(container, seg, bindHandlers) {
- container.unbind('mouseover').mouseover(function(ev) {
+ container.unbind('mouseover focusin').bind('mouseover focusin', function(ev) {
var parent = ev.target, e = parent, i, event;
while (parent != this) {
e = parent;
@@ -5132,7 +5136,7 @@ function TableEventRenderer() {
rowClasses.push('fc-today');
}
- s += "<tr class='" + rowClasses.join(' ') + "'>";
+ s += "<tr class='" + rowClasses.join(' ') + "' tabindex='0'>";
for (var col, c=0; c < tableCols.length; c++) {
col = tableCols[c];
if (col == 'handle') {
@@ -5358,7 +5362,11 @@ function View(element, calendar, viewName) {
function(ev) {
trigger('eventMouseout', this, event, ev);
}
- );
+ )
+ .keypress(function(ev) {
+ if (ev.keyCode == 13)
+ $(this).trigger('click', { pointerType:'keyboard' });
+ });
// TODO: don't fire eventMouseover/eventMouseout *while* dragging is occuring (on subject element)
// TODO: same for resizing
}
@@ -6008,7 +6016,7 @@ function DayEventRenderer() {
"left:" + segment.left + "px;" +
skinCss +
"'" +
- ">" +
+ " tabindex='0'>" +
"<div class='fc-event-inner'>";
if (!event.allDay && segment.isStart) {
html +=
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index b92f377..80d36cb 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -53,7 +53,9 @@ $labels['location'] = 'Location';
$labels['url'] = 'URL';
$labels['date'] = 'Date';
$labels['start'] = 'Start';
+$labels['starttime'] = 'Start time';
$labels['end'] = 'End';
+$labels['endtime'] = 'End time';
$labels['repeat'] = 'Repeat';
$labels['selectdate'] = 'Choose date';
$labels['freebusy'] = 'Show me as';
@@ -87,8 +89,11 @@ $labels['showurl'] = 'Show calendar URL';
$labels['showurldescription'] = 'Use the following address to access (read only) your calendar from other applications. You can copy and paste this into any calendar software that supports the iCal format.';
$labels['caldavurldescription'] = 'Copy this address to a <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> client application (e.g. Evolution or Mozilla Thunderbird) to fully synchronize this specific calendar with your computer or mobile device.';
$labels['findcalendars'] = 'Find calendars...';
+$labels['searchterms'] = 'Search terms';
$labels['calsearchresults'] = 'Available Calendars';
$labels['calendarsubscribe'] = 'List permanently';
+$labels['nocalendarsfound'] = 'No calendars found';
+$labels['nrcalendarsfound'] = '$nr calendars found';
// agenda view
$labels['listrange'] = 'Range to display:';
@@ -99,6 +104,7 @@ $labels['today'] = 'Today';
$labels['tomorrow'] = 'Tomorrow';
$labels['thisweek'] = 'This week';
$labels['nextweek'] = 'Next week';
+$labels['prevweek'] = 'Previous week';
$labels['thismonth'] = 'This month';
$labels['nextmonth'] = 'Next month';
$labels['weekofyear'] = 'Week';
@@ -140,6 +146,7 @@ $labels['onlyworkinghours'] = 'Find availability within my working hours';
$labels['reqallattendees'] = 'Required/all participants';
$labels['prevslot'] = 'Previous Slot';
$labels['nextslot'] = 'Next Slot';
+$labels['suggestedslot'] = 'Suggested Slot';
$labels['noslotfound'] = 'Unable to find a free time slot';
$labels['invitationsubject'] = 'You\'ve been invited to "$title"';
$labels['invitationmailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nPlease find attached an iCalendar file with all the event details which you can import to your calendar application.";
@@ -232,4 +239,16 @@ $labels['birthdayscalendarsources'] = 'From these address books';
$labels['birthdayeventtitle'] = '$name\'s Birthday';
$labels['birthdayage'] = 'Age $age';
+// (hidden) titles and labels for accessibility annotations
+$labels['arialabelminical'] = 'Calendar date selection';
+$labels['arialabelcalendarview'] = 'Calendar view';
+$labels['arialabelsearchform'] = 'Event search form';
+$labels['arialabelquicksearchbox'] = 'Event search input';
+$labels['arialabelcalsearchform'] = 'Calendars search form';
+$labels['calendaractions'] = 'Calendar actions';
+$labels['arialabeleventattendees'] = 'Event participants list';
+$labels['arialabeleventresources'] = 'Event resources list';
+$labels['arialabelresourcesearchform'] = 'Resources search form';
+$labels['arialabelresourceselection'] = 'Available resources';
+
?>
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index bd1d489..84169fd 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -85,6 +85,14 @@ body.attachmentwin #topnav .topright {
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#d9f1fb', endColorstr='#c5e3ee', GradientType=0);
}
+#datepicker .ui-datepicker-days-cell-over a.ui-state-default {
+ color: #fff;
+ border-color: #2fa0c0;
+ background: rgba(73,180,210,0.6);
+ text-shadow: 0px 1px 1px #666;
+ filter: none;
+}
+
#datepicker .ui-datepicker-activerange a.ui-state-active {
color: #fff;
background: #00acd4;
@@ -265,12 +273,19 @@ pre {
cursor: pointer;
}
-#calendars .treelist div:hover > a.subscribed {
- background-position: 1px -110px;
+#calendars .treelist div > a.subscribed:focus {
+ border-radius: 3px;
+ outline: 2px solid rgba(30,150,192, 0.5);
}
-#calendars .treelist div.subscribed a.subscribed {
- background-position: -15px -110px;
+#calendars .treelist div:hover > a.subscribed,
+#calendars .treelist div > a.subscribed:focus {
+ background-position: 0 -110px;
+}
+
+#calendars .treelist div.subscribed a.subscribed,
+#calendars .treelist div.subscribed a.subscribed:focus {
+ background-position: -16px -110px;
}
#calendars .treelist li input {
@@ -420,7 +435,7 @@ pre {
}
body.calendarmain #quicksearchbar {
- z-index: 200;
+ z-index: 20;
}
body.calendarmain #searchmenulink {
@@ -600,7 +615,7 @@ a.miniColors-trigger {
.calendarmain .fc-view-table td.fc-list-header,
#attendees-freebusy-table h3.boxtitle,
#schedule-freebusy-times thead th,
-.edit-attendees-table thead td
+.edit-attendees-table thead th
{
color: #69939e;
font-size: 11px;
@@ -614,6 +629,7 @@ a.miniColors-trigger {
border: 0;
border-bottom: 1px solid #ccc;
height: 18px;
+ line-height: 18px;
padding: 8px 7px 3px 7px;
}
@@ -775,21 +791,26 @@ td.topalign {
margin-top: 0.5em;
}
+.edit-attendees-table th.role,
.edit-attendees-table td.role {
width: 9em;
}
+.edit-attendees-table th.availability,
.edit-attendees-table td.availability,
+.edit-attendees-table th.confirmstate,
.edit-attendees-table td.confirmstate {
width: 4em;
}
+.edit-attendees-table th.options,
.edit-attendees-table td.options {
width: 3em;
text-align: right;
padding-right: 4px;
}
+.edit-attendees-table th.name,
.edit-attendees-table td.name {
width: auto;
white-space: nowrap;
@@ -1120,7 +1141,7 @@ a.dropdown-link:after {
left: 0;
right: 0;
height: auto;
- z-index: 200;
+ z-index: 10;
padding: 4px 5px;
border: 1px solid #c3c3c3;
border-top-color: #ddd;
@@ -1376,11 +1397,21 @@ a.dropdown-link:after {
-o-box-shadow: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
+ outline: none;
+}
+
+.calendarmain .fc-header-left .fc-button:focus {
+ color: #fff;
+ text-shadow: 0px 1px 1px #666;
+ background-color: rgba(30,150,192, 0.5);
+ border-radius: 3px;
}
.calendarmain .fc-header-left .fc-button.fc-state-active {
font-weight: bold;
color: #222;
+ text-shadow: none;
+ background-color: transparent;
}
.calendarmain .fc-header-left .fc-button-agendaDay {
@@ -1428,6 +1459,13 @@ a.dropdown-link:after {
font-size: 1em !important;
}
+.calendarmain .fc-event:focus {
+ outline: 1px solid rgba(71,135,177, 0.4);
+ -webkit-box-shadow: 0 0 2px 3px rgba(71,135,177, 0.6);
+ -moz-box-shadow: 0 0 2px 3px rgba(71,135,177, 0.6);
+ -o-box-shadow: 0 0 2px 3px rgba(71,135,177, 0.6);
+ box-shadow: 0 0 2px 3px rgba(71,135,177, 0.6);
+}
.fc-event-title {
font-weight: bold;
}
diff --git a/plugins/calendar/skins/larry/fullcalendar.css b/plugins/calendar/skins/larry/fullcalendar.css
index c6e36e7..daefdf2 100644
--- a/plugins/calendar/skins/larry/fullcalendar.css
+++ b/plugins/calendar/skins/larry/fullcalendar.css
@@ -269,6 +269,10 @@ html .fc,
cursor: default;
}
+.fc-event:focus {
+ outline: 2px solid ActiveBorder;
+ }
+
a.fc-event {
text-decoration: none;
}
diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
index 2e45fcf..6fce4ca 100644
--- a/plugins/calendar/skins/larry/templates/calendar.html
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -9,9 +9,12 @@
<roundcube:include file="/includes/header.html" />
+<h1 class="voice"><roundcube:label name="calendar.calendar" /></h1>
+
<div id="mainscreen">
<div id="calendarsidebar">
- <div id="calendartoolbar" class="toolbar">
+ <h2 id="aria-label-toolbar" class="voice"><roundcube:label name="arialabeltoolbar" /></h2>
+ <div id="calendartoolbar" class="toolbar" role="toolbar" aria-labelledby="aria-label-toolbar">
<roundcube:button command="addevent" type="link" class="button addevent disabled" classAct="button addevent" classSel="button addevent pressed" label="calendar.new_event" title="calendar.new_event" />
<roundcube:button command="print" type="link" class="button print disabled" classAct="button print" classSel="button print pressed" label="calendar.print" title="calendar.printtitle" />
<roundcube:button command="events-import" type="link" class="button import disabled" classAct="button import" classSel="button import pressed" label="import" title="calendar.importevents" />
@@ -19,35 +22,41 @@
<roundcube:container name="toolbar" id="calendartoolbar" />
</div>
- <div id="datepicker" class="uibox"></div>
+ <h2 id="aria-label-minical" class="voice"><roundcube:label name="calendar.arialabelminical" /></h2>
+ <div id="datepicker" class="uibox" role="presentation"></div>
- <div id="calendars" class="uibox listbox" style="visibility:hidden">
- <h2 class="boxtitle"><roundcube:label name="calendar.calendars" />
- <a class="iconbutton search" title="<roundcube:label name='calendar.findcalendars' />"></a>
+ <div id="calendars" class="uibox listbox" style="visibility:hidden" role="navigation" aria-labelledby="aria-label-calendarlist">
+ <h2 class="boxtitle" id="aria-label-calendarlist"><roundcube:label name="calendar.calendars" />
+ <a href="#calendars" class="iconbutton search" title="<roundcube:label name='calendar.findcalendars' />" tabindex="0"><roundcube:label name='calendar.findcalendars' /></a>
</h2>
<div class="listsearchbox">
- <div class="searchbox">
+ <div class="searchbox" role="search" aria-labelledby="aria-label-calsearchform" aria-controls="calendarslist">
+ <h3 id="aria-label-calsearchform" class="voice"><roundcube:label name="calendar.arialabelcalsearchform" /></h3>
+ <label for="calendarlistsearch" class="voice"><roundcube:label name="calendar.searchterms" /></label>
<input type="text" name="q" id="calendarlistsearch" placeholder="<roundcube:label name='calendar.findcalendars' />" />
<a class="iconbutton searchicon"></a>
- <roundcube:button command="reset-listsearch" id="calendarlistsearch-reset" class="iconbutton reset" title="resetsearch" content="x" />
+ <roundcube:button command="reset-listsearch" id="calendarlistsearch-reset" class="iconbutton reset" title="resetsearch" label="resetsearch" />
</div>
</div>
<div class="scroller withfooter">
- <roundcube:object name="plugin.calendar_list" id="calendarslist" class="treelist listing" />
+ <roundcube:object name="plugin.calendar_list" id="calendarslist" class="treelist listing" />
</div>
<div class="boxfooter">
- <roundcube:button command="calendar-create" type="link" title="calendar.createcalendar" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button name="calendaroptionslink" id="calendaroptionsmenulink" type="link" title="moreactions" class="listbutton groupactions" onclick="UI.show_popup('calendaroptionsmenu', undefined, { above:true });return false" innerClass="inner" content="⚙" />
+ <roundcube:button command="calendar-create" type="link" title="calendar.createcalendar" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button name="calendaroptionslink" id="calendaroptionsmenulink" type="link" title="moreactions" class="listbutton groupactions" onclick="return UI.toggle_popup('calendaroptionsmenu', event, { above:true })" innerClass="inner" label="calendar.calendaractions" aria-haspopup="true" aria-expanded="false" aria-owns="calendaroptionsmenu-menu" />
</div>
</div>
</div>
- <div id="quicksearchbar">
+ <div id="quicksearchbar" class="searchbox" role="search" aria-labelledby="aria-label-searchform">
+ <h2 id="aria-label-searchform" class="voice"><roundcube:label name="calendar.arialabelsearchform" /></h2>
+ <label for="quicksearchbox" class="voice"><roundcube:label name="calendar.arialabelquicksearchbox" /></label>
<roundcube:object name="plugin.searchform" id="quicksearchbox" />
- <a id="searchmenulink" class="iconbutton searchoptions" > </a>
- <roundcube:button command="reset-search" id="searchreset" class="iconbutton reset" title="resetsearch" content=" " />
+ <a id="searchmenulink" class="iconbutton searchoptions" tabindex="-1"> </a>
+ <roundcube:button command="reset-search" id="searchreset" class="iconbutton reset" title="resetsearch" label="resetsearch" />
</div>
- <div id="calendar">
+ <h2 id="aria-label-calendarview" class="voice"><roundcube:label name="calendar.arialabelcalendarview" /></h2>
+ <div id="calendar" role="main" aria-labelledby="aria-label-calendarview">
<roundcube:object name="plugin.angenda_options" class="boxfooter" id="agendaoptions" />
</div>
</div>
@@ -56,18 +65,19 @@
<roundcube:object name="message" id="messagestack" />
-<div id="calendaroptionsmenu" class="popupmenu">
- <ul class="toolbarmenu">
- <li><roundcube:button command="calendar-edit" label="calendar.edit" classAct="active" /></li>
- <li><roundcube:button command="calendar-remove" label="calendar.remove" classAct="active" /></li>
- <li><roundcube:button command="calendar-showurl" label="calendar.showurl" classAct="active" /></li>
+<div id="calendaroptionsmenu" class="popupmenu" aria-hidden="true">
+ <h3 id="aria-label-calendaroptions" class="voice"><roundcube:label name="calendar.calendaractions" /></h3>
+ <ul id="calendaroptionsmenu-menu" class="toolbarmenu" role="menu" aria-labelledby="aria-label-calendaroptions">
+ <li role="menuitem"><roundcube:button command="calendar-edit" label="calendar.edit" classAct="active" /></li>
+ <li role="menuitem"><roundcube:button command="calendar-remove" label="calendar.remove" classAct="active" /></li>
+ <li role="menuitem"><roundcube:button command="calendar-showurl" label="calendar.showurl" classAct="active" /></li>
<roundcube:if condition="env:calendar_driver == 'kolab'" />
- <li class="separator_above"><roundcube:button command="folders" task="settings" type="link" label="managefolders" classAct="active" /></li>
+ <li role="menuitem"><roundcube:button command="folders" task="settings" type="link" label="managefolders" classAct="active" /></li>
<roundcube:endif />
</ul>
</div>
-<div id="eventshow" class="uidialog">
+<div id="eventshow" class="uidialog" aria-hidden="true">
<h1 id="event-title">Event Title</h1>
<div class="event-section" id="event-location">Location</div>
<div class="event-section" id="event-date">From-To</div>
@@ -125,14 +135,17 @@
<roundcube:include file="/templates/eventedit.html" />
-<div id="eventresourcesdialog" class="uidialog">
+<div id="eventresourcesdialog" class="uidialog" aria-hidden="true">
<div id="resource-dialog-left">
- <div id="resource-selection" class="uibox listbox">
+ <div id="resource-selection" class="uibox listbox" role="navigation" aria-labelledby="aria-label-resourceselection">
+ <h2 class="voice" id="aria-label-resourceselection"><roundcube:label name="calendar.arialabelresourceselection" /></h2>
<div id="resourcequicksearch">
- <div class="searchbox">
+ <div class="searchbox" role="search" aria-labelledby="aria-label-resourcesearchform" aria-controls="resources-list">
+ <h3 id="aria-label-resourcesearchform" class="voice"><roundcube:label name="calendar.arialabelresourcesearchform" /></h3>
+ <label for="resourcesearchbox" class="voice"><roundcube:label name="calendar.searchterms" /></label>
<roundcube:object name="plugin.resources_searchform" id="resourcesearchbox" />
<a id="resourcesearchmenulink" class="iconbutton searchoptions"> </a>
- <roundcube:button command="reset-resource-search" id="resourcesearchreset" class="iconbutton reset" title="resetsearch" content=" " />
+ <roundcube:button command="reset-resource-search" id="resourcesearchreset" class="iconbutton reset" title="resetsearch" label="resetsearch" />
</div>
</div>
<div class="scroller">
@@ -142,25 +155,25 @@
</div>
<div id="resource-dialog-right">
- <div id="resource-info" class="uibox contentbox">
- <h2 class="boxtitle"><roundcube:label name="calendar.resourcedetails" /></h2>
+ <div id="resource-info" class="uibox contentbox" role="region" aria-labelledby="aria-label-resourcedetails">
+ <h2 class="boxtitle" id="aria-label-resourcedetails"><roundcube:label name="calendar.resourcedetails" /></h2>
<div class="scroller">
- <roundcube:object name="plugin.resource_info" id="resource-details" class="propform" />
+ <roundcube:object name="plugin.resource_info" id="resource-details" class="propform" aria-live="polite" aria-relevant="text" aria-atomic="true" />
</div>
</div>
- <div id="resource-availability" class="uibox contentbox">
- <h2 class="boxtitle"><roundcube:label name="calendar.resourceavailability" /></h2>
+ <div id="resource-availability" class="uibox contentbox" role="region" aria-labelledby="aria-label-resourceavailability">
+ <h2 class="boxtitle" id="aria-label-resourceavailability"><roundcube:label name="calendar.resourceavailability" /></h2>
<roundcube:object name="plugin.resource_calendar" id="resource-freebusy-calendar" />
<div class="boxpagenav">
- <roundcube:button name="resource-cal-prev" id="resource-calendar-prev" type="link" class="icon prevpage" title="calendar.prevslot" content="<" />
- <roundcube:button name="resource-cal-next" id="resource-calendar-next" type="link" class="icon nextpage" title="calendar.nextslot" content=">" />
+ <roundcube:button name="resource-cal-prev" id="resource-calendar-prev" type="link" class="icon prevpage" title="calendar.prevslot" label="calendar.prevweek" />
+ <roundcube:button name="resource-cal-next" id="resource-calendar-next" type="link" class="icon nextpage" title="calendar.nextslot" label="calendar.nextweek" />
</div>
</div>
</div>
</div>
-<div id="eventfreebusy" class="uidialog">
+<div id="eventfreebusy" class="uidialog" aria-hidden="true">
<roundcube:object name="plugin.attendees_freebusy_table" id="attendees-freebusy-table" cellpadding="0" />
<div class="schedule-options">
@@ -203,7 +216,7 @@
</div>
</div>
-<div id="calendarform" class="uidialog">
+<div id="calendarform" class="uidialog" aria-hidden="true">
<roundcube:label name="loading" />
</div>
@@ -262,6 +275,8 @@ $(document).ready(function(e){
// TODO: save state in localStorage
}
});
+
+ return false;
});
});
@@ -275,8 +290,8 @@ function calendarview_splitter(p)
this.collapsed = false;
this.dragging = false;
this.threshold = 80;
- this.lastpos = 0;
- this._lastpos = 0;
+ this.lastpos = -1;
+ this._lastpos = -1;
this._min = p.min;
var me = this;
@@ -316,6 +331,9 @@ function calendarview_splitter(p)
this.p1.resize();
this.lastpos = this.pos;
+ if (this._lastpos == -1)
+ this._lastpos = this.pos;
+
// also resize iframe covers
if (this.drag_active) {
$('iframe').each(function(i, elem) {
diff --git a/plugins/calendar/skins/larry/templates/eventedit.html b/plugins/calendar/skins/larry/templates/eventedit.html
index fb3fa20..28cfc7f 100644
--- a/plugins/calendar/skins/larry/templates/eventedit.html
+++ b/plugins/calendar/skins/larry/templates/eventedit.html
@@ -1,4 +1,4 @@
-<div id="eventedit" class="uidialog uidialog-tabbed">
+<div id="eventedit" class="uidialog uidialog-tabbed" aria-hidden="true">
<form id="eventtabs" action="#" method="post" enctype="multipart/form-data">
<ul>
<li><a href="#event-panel-summary"><roundcube:label name="calendar.tabsummary" /></a></li><li id="edit-tab-recurrence"><a href="#event-panel-recurrence"><roundcube:label name="calendar.tabrecurrence" /></a></li><li id="edit-tab-attendees"><a href="#event-panel-attendees"><roundcube:label name="calendar.tabattendees" /></a></li><li id="edit-tab-resources"><a href="#event-panel-resources"><roundcube:label name="calendar.tabresources" /></a></li><li id="edit-tab-attachments"><a href="#event-panel-attachments"><roundcube:label name="calendar.tabattachments" /></a></li>
@@ -8,7 +8,7 @@
<div class="event-section">
<label for="edit-title"><roundcube:label name="calendar.title" /></label>
<br />
- <input type="text" class="text" name="title" id="edit-title" size="40" />
+ <input type="text" class="text" name="title" id="edit-title" size="40" required="true" />
</div>
<div class="event-section">
<label for="edit-location"><roundcube:label name="calendar.location" /></label>
@@ -28,21 +28,21 @@
<div class="event-section">
<label style="float:right;padding-right:0.5em"><input type="checkbox" name="allday" id="edit-allday" value="1" /><roundcube:label name="calendar.all-day" /></label>
<label for="edit-startdate"><roundcube:label name="calendar.start" /></label>
- <input type="text" name="startdate" size="11" id="edit-startdate" />
- <input type="text" name="starttime" size="6" id="edit-starttime" />
+ <input type="text" name="startdate" size="11" id="edit-startdate" required="true" />
+ <input type="text" name="starttime" size="6" id="edit-starttime" aria-label="<roundcube:label name='calendar.starttime' />" />
</div>
<div class="event-section">
<label for="edit-enddate"><roundcube:label name="calendar.end" /></label>
- <input type="text" name="enddate" size="11" id="edit-enddate" />
- <input type="text" name="endtime" size="6" id="edit-endtime" />
+ <input type="text" name="enddate" size="11" id="edit-enddate" required="true" />
+ <input type="text" name="endtime" size="6" id="edit-endtime" aria-label="<roundcube:label name='calendar.endtime' />" />
</div>
<div class="event-section" id="edit-alarms">
<div class="edit-alarm-item first">
- <label><roundcube:label name="calendar.alarms" /></label>
- <roundcube:object name="plugin.alarm_select" />
+ <label for="edit-alarm-item"><roundcube:label name="calendar.alarms" /></label>
+ <roundcube:object name="plugin.alarm_select" id="edit-alarm-item" />
<span class="edit-alarm-buttons">
- <a href="#add" class="iconbutton add add-alarm">+</a>
- <a href="#delete" class="iconbutton remove delete-alarm">-</a>
+ <a href="#add" class="iconbutton add add-alarm"><roundcube:label name="libcalendaring.addalarm" /></a>
+ <a href="#delete" class="iconbutton remove delete-alarm"><roundcube:label name="libcalendaring.removealarm" /></a>
</span>
</div>
</div>
@@ -97,13 +97,15 @@
</div>
<!-- attendees list -->
<div id="event-panel-attendees">
- <roundcube:object name="plugin.attendees_list" id="edit-attendees-table" class="records-table edit-attendees-table" coltitle="attendee" />
+ <h3 id="aria-label-attendeestable" class="voice"><roundcube:label name="calendar.arialabeleventattendees" /></h3>
+ <roundcube:object name="plugin.attendees_list" id="edit-attendees-table" class="records-table edit-attendees-table" coltitle="attendee" aria-labelledby="aria-label-attendeestable" />
<roundcube:object name="plugin.attendees_form" id="edit-attendees-form" />
<roundcube:include file="/templates/freebusylegend.html" />
</div>
<!-- resources list -->
<div id="event-panel-resources">
- <roundcube:object name="plugin.attendees_list" id="edit-resources-table" class="records-table edit-attendees-table" coltitle="resource" />
+ <h3 id="aria-label-resourcestable" class="voice"><roundcube:label name="calendar.arialabeleventresources" /></h3>
+ <roundcube:object name="plugin.attendees_list" id="edit-resources-table" class="records-table edit-attendees-table" coltitle="resource" aria-labelledby="aria-label-resourcestable" />
<roundcube:object name="plugin.resources_form" id="edit-resources-form" />
<roundcube:include file="/templates/freebusylegend.html" />
</div>
@@ -112,7 +114,8 @@
<div id="edit-attachments">
<roundcube:object name="plugin.attachments_list" id="attachment-list" class="attachmentslist" />
</div>
- <div id="edit-attachments-form">
+ <div id="edit-attachments-form" role="region" aria-labelledby="aria-label-attachmentuploadform">
+ <h3 id="aria-label-attachmentuploadform" class="voice"><roundcube:label name="arialabelattachmentuploadform" /></h2>
<roundcube:object name="plugin.attachments_form" id="calendar-attachment-form" attachmentFieldSize="30" />
</div>
<roundcube:object name="plugin.filedroparea" id="event-panel-attachments" />
diff --git a/plugins/libcalendaring/libcalendaring.js b/plugins/libcalendaring/libcalendaring.js
index f5c20c4..0ccce4c 100644
--- a/plugins/libcalendaring/libcalendaring.js
+++ b/plugins/libcalendaring/libcalendaring.js
@@ -68,25 +68,26 @@ function rcube_libcalendaring(settings)
/**
* Create a nice human-readable string for the date/time range
*/
- this.event_date_text = function(event)
+ this.event_date_text = function(event, voice)
{
if (!event.start)
return '';
if (!event.end)
event.end = event.start;
- var fromto, duration = event.end.getTime() / 1000 - event.start.getTime() / 1000;
+ var fromto, duration = event.end.getTime() / 1000 - event.start.getTime() / 1000,
+ until = voice ? ' ' + rcmail.gettext('until','libcalendaring') + ' ' : ' â ';
if (event.allDay) {
- fromto = this.format_datetime(event.start, 1)
- + (duration > 86400 || event.start.getDay() != event.end.getDay() ? ' — ' + this.format_datetime(event.end, 1) : '');
+ fromto = this.format_datetime(event.start, 1, voice)
+ + (duration > 86400 || event.start.getDay() != event.end.getDay() ? until + this.format_datetime(event.end, 1, voice) : '');
}
else if (duration < 86400 && event.start.getDay() == event.end.getDay()) {
- fromto = this.format_datetime(event.start, 0)
- + (duration > 0 ? ' — ' + this.format_datetime(event.end, 2) : '');
+ fromto = this.format_datetime(event.start, 0, voice)
+ + (duration > 0 ? until + this.format_datetime(event.end, 2, voice) : '');
}
else {
- fromto = this.format_datetime(event.start, 0)
- + (duration > 0 ? ' — ' + this.format_datetime(event.end, 0) : '');
+ fromto = this.format_datetime(event.start, 0, voice)
+ + (duration > 0 ? until + this.format_datetime(event.end, 0, voice) : '');
}
return fromto;
@@ -178,15 +179,18 @@ function rcube_libcalendaring(settings)
/**
* Format the given date object according to user's prefs
*/
- this.format_datetime = function(date, mode)
+ this.format_datetime = function(date, mode, voice)
{
var res = '';
- if (!mode || mode == 1)
- res += $.datepicker.formatDate(datepicker_settings.dateFormat, date, datepicker_settings);
- if (!mode)
- res += ' ';
- if (!mode || mode == 2)
- res += this.format_time(date);
+ if (!mode || mode == 1) {
+ res += $.datepicker.formatDate(voice ? 'MM d yy' : datepicker_settings.dateFormat, date, datepicker_settings);
+ }
+ if (!mode) {
+ res += voice ? ' ' + rcmail.gettext('at','libcalendaring') + ' ' : ' ';
+ }
+ if (!mode || mode == 2) {
+ res += this.format_time(date, voice);
+ }
return res;
}
@@ -194,7 +198,7 @@ function rcube_libcalendaring(settings)
/**
* Clone from fullcalendar.js
*/
- this.format_time = function(date)
+ this.format_time = function(date, voice)
{
var zeroPad = function(n) { return (n < 10 ? '0' : '') + n; }
var formatters = {
@@ -212,7 +216,8 @@ function rcube_libcalendaring(settings)
TT : function(d) { return d.getHours() < 12 ? 'AM' : 'PM' }
};
- var i, i2, c, formatter, res = '', format = settings['time_format'];
+ var i, i2, c, formatter, res = '',
+ format = voice ? settings['time_format'].replace(':',' ').replace('HH','H').replace('hh','h').replace('mm','m').replace('ss','s') : settings['time_format'];
for (i=0; i < format.length; i++) {
c = format.charAt(i);
for (i2=Math.min(i+2, format.length); i2 > i; i2--) {
@@ -316,10 +321,13 @@ function rcube_libcalendaring(settings)
.replace(/\n/g, "<br/>");
};
- this.init_alarms_edit = function(prefix)
+ this.init_alarms_edit = function(prefix, index)
{
+ var edit_type = $(prefix+' select.edit-alarm-type'),
+ dom_id = edit_type.attr('id');
+
// register events on alarm fields
- $(prefix+' select.edit-alarm-type').change(function(){
+ edit_type.change(function(){
$(this).parent().find('span.edit-alarm-values')[(this.selectedIndex>0?'show':'hide')]();
});
$(prefix+' select.edit-alarm-offset').change(function(){
@@ -337,13 +345,20 @@ function rcube_libcalendaring(settings)
return false;
});
+ // set a unique id attribute and set label reference accordingly
+ if ((index || 0) > 0 && dom_id) {
+ dom_id += ':' + (new Date().getTime());
+ edit_type.attr('id', dom_id);
+ $(prefix+' label:first').attr('for', dom_id);
+ }
+
$(prefix).on('click', 'a.add-alarm', function(e){
var i = $(this).closest('.edit-alarm-item').siblings().length + 1;
var item = $(this).closest('.edit-alarm-item').clone(false)
.removeClass('first')
.appendTo(prefix);
- me.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')');
+ me.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')', i);
$('select.edit-alarm-type, select.edit-alarm-offset', item).change();
return false;
});
@@ -364,7 +379,7 @@ function rcube_libcalendaring(settings)
}
else {
domnode = $(prefix + ' .edit-alarm-item').eq(0).clone(false).removeClass('first').appendTo(prefix);
- this.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')');
+ this.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')', i);
}
$('select.edit-alarm-type', domnode).val(alarm.action);
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index 09a9c68..b84ede7 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -288,7 +288,7 @@ class libcalendaring extends rcube_plugin
public function alarm_select($attrib, $alarm_types, $absolute_time = true)
{
unset($attrib['name']);
- $select_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type'));
+ $select_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type', 'id' => $attrib['id']));
$select_type->add($this->gettext('none'), '');
foreach ($alarm_types as $type)
$select_type->add($this->gettext(strtolower("alarm{$type}option")), $type);
@@ -991,6 +991,7 @@ class libcalendaring extends rcube_plugin
'class' => 'delete',
'onclick' => sprintf("return %s.remove_from_attachment_list('rcmfile%s')", JS_OBJECT_NAME, $id),
'title' => $this->rc->gettext('delete'),
+ 'aria-label' => $this->rc->gettext('delete') . ' ' . $attachment['name'],
), $button);
$content .= Q($attachment['name']);
diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc
index f768fbf..863b7fa 100644
--- a/plugins/libcalendaring/localization/en_US.inc
+++ b/plugins/libcalendaring/localization/en_US.inc
@@ -2,6 +2,10 @@
$labels = array();
+// words for spoken dates
+$labels['until'] = 'until';
+$labels['at'] = 'at';
+
// alarms related labels
$labels['alarmemail'] = 'Send Email';
$labels['alarmdisplay'] = 'Show message';
@@ -18,7 +22,8 @@ $labels['trigger+M'] = 'minutes after';
$labels['trigger+H'] = 'hours after';
$labels['trigger+D'] = 'days after';
$labels['triggerattime'] = 'at time';
-$labels['addalarm'] = 'add alarm';
+$labels['addalarm'] = 'Add alarm';
+$labels['removealarm'] = 'Remove alarm';
$labels['alarmtitle'] = 'Upcoming events';
$labels['dismissall'] = 'Dismiss all';
diff --git a/plugins/libkolab/js/folderlist.js b/plugins/libkolab/js/folderlist.js
index 00882a8..67c7c9b 100644
--- a/plugins/libkolab/js/folderlist.js
+++ b/plugins/libkolab/js/folderlist.js
@@ -46,8 +46,9 @@ function kolab_folderlist(node, p)
if (results.length) {
// create treelist widget to present the search results
if (!search_results_widget) {
+ var list_id = (me.container.attr('id') || p.id_prefix || '0')
search_results_container = $('<div class="searchresults"></div>')
- .html(p.search_title ? '<h2 class="boxtitle">' + p.search_title + '</h2>' : '')
+ .html(p.search_title ? '<h2 class="boxtitle" id="st:' + list_id + '">' + p.search_title + '</h2>' : '')
.insertAfter(me.container);
search_results_widget = new rcube_treelist_widget('<ul>', {
@@ -55,7 +56,7 @@ function kolab_folderlist(node, p)
selectable: false
});
// copy classes from main list
- search_results_widget.container.addClass(me.container.attr('class'));
+ search_results_widget.container.addClass(me.container.attr('class')).attr('aria-labelledby', 'st:' + list_id);
// register click handler on search result's checkboxes to select the given item for listing
search_results_widget.container
@@ -87,6 +88,11 @@ function kolab_folderlist(node, p)
else {
li.remove();
}
+
+ // set focus to cloned checkbox
+ if (rcube_event.is_keyboard(e)) {
+ $(me.get_item(id, true)).find('input[type=checkbox]').first().focus();
+ }
});
}
@@ -178,7 +184,7 @@ function kolab_folderlist(node, p)
}
if (listsearch_request) {
- // ignore, let the currently runnung sequest finish
+ // ignore, let the currently running request finish
if (listsearch_request.query == search.query) {
return;
}
@@ -196,7 +202,10 @@ function kolab_folderlist(node, p)
postdata: { action:'search', q:search.query, source:'%s' },
lock: rcmail.display_message(rcmail.get_label('searching'), 'loading'),
onresponse: render_search_results,
- whendone: function(e){ listsearch_request = null; }
+ whendone: function(data){
+ listsearch_request = null;
+ me.triggerEvent('search-complete', data);
+ }
});
listsearch_request = { id:reqid, query:search.query };
More information about the commits
mailing list