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="&lt;" />
-				<roundcube:button name="resource-cal-next" id="resource-calendar-next" type="link" class="icon nextpage" title="calendar.nextslot" content="&gt;" />
+				<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