5 commits - plugins/calendar plugins/libcalendaring plugins/tasklist

Thomas Brüderli bruederli at kolabsys.com
Fri Jun 20 15:12:47 CEST 2014


 plugins/calendar/calendar_ui.js                          |    7 
 plugins/calendar/drivers/kolab/kolab_driver.php          |    6 
 plugins/calendar/skins/larry/calendar.css                |    8 
 plugins/calendar/skins/larry/templates/calendar.html     |    2 
 plugins/libcalendaring/libcalendaring.php                |   25 +-
 plugins/libcalendaring/localization/en_US.inc            |    1 
 plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php |    1 
 plugins/tasklist/jquery.tagedit.js                       |  100 +++++++--
 plugins/tasklist/localization/en_US.inc                  |   17 +
 plugins/tasklist/skins/larry/tagedit.css                 |    8 
 plugins/tasklist/skins/larry/tasklist.css                |   50 ++--
 plugins/tasklist/skins/larry/templates/mainview.html     |  114 +++++-----
 plugins/tasklist/skins/larry/templates/taskedit.html     |   33 +-
 plugins/tasklist/tasklist.js                             |  166 ++++++++++++---
 plugins/tasklist/tasklist_ui.php                         |   15 -
 15 files changed, 392 insertions(+), 161 deletions(-)

New commits:
commit 5c490914fecbc257e3b3ba04ddfcdfb2656937f9
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri Jun 20 15:12:11 2014 +0200

    Accessibilty improvements and keyboard navigation for tasks module (#3085)

diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index 715fc74..1999a8e 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -1148,7 +1148,6 @@ class tasklist_kolab_driver extends tasklist_driver
             if (is_array($tab['fields']) && empty($tab['content'])) {
                 $table = new html_table(array('cols' => 2));
                 foreach ($tab['fields'] as $col => $colprop) {
-                    $colprop['id'] = '_'.$col;
                     $label = !empty($colprop['label']) ? $colprop['label'] : $this->plugin->gettext($col);
 
                     $table->add('title', html::label($colprop['id'], Q($label)));
diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index 6193105..9fd0e3e 100644
--- a/plugins/tasklist/localization/en_US.inc
+++ b/plugins/tasklist/localization/en_US.inc
@@ -8,8 +8,12 @@ $labels['tags'] = 'Tags';
 $labels['tasklistsubscribe'] = 'List permanently';
 $labels['listsearchresults'] = 'Available Tasklists';
 $labels['findlists'] = 'Find tasklists...';
+$labels['searchterms'] = 'Search terms';
+$labels['notasklistsfound'] = 'No tasklists found';
+$labels['nrtasklistsfound'] = '$nr tasklists found';
 
 $labels['newtask'] = 'New Task';
+$labels['createtask'] = 'Create Task <Enter>';
 $labels['createnewtask'] = 'Create new Task (e.g. Saturday, Mow the lawn)';
 $labels['createfrommail'] = 'Save as task';
 $labels['mark'] = 'Mark';
@@ -19,7 +23,9 @@ $labels['delete'] = 'Delete';
 $labels['title'] = 'Title';
 $labels['description'] = 'Description';
 $labels['datetime'] = 'Due';
+$labels['duetime'] = 'Due time';
 $labels['start'] = 'Start';
+$labels['starttime'] = 'Start time';
 $labels['alarms'] = 'Reminder';
 $labels['repeat'] = 'Repeat';
 $labels['status'] = 'Status';
@@ -48,6 +54,7 @@ $labels['addsubtask'] = 'Add subtask';
 $labels['deletetask'] = 'Delete task';
 $labels['deletethisonly'] = 'Delete this task only';
 $labels['deletewithchilds'] = 'Delete with all subtasks';
+$labels['taskactions'] = 'Task options...';
 
 $labels['tabsummary'] = 'Summary';
 $labels['tabrecurrence'] = 'Recurrence';
@@ -79,3 +86,13 @@ $labels['deleteparenttasktconfirm'] = 'Do you really want to delete this task an
 $labels['deletelistconfirm'] = 'Do you really want to delete this list with all its tasks?';
 $labels['deletelistconfirmrecursive'] = 'Do you really want to delete this list with all its sub-lists and tasks?';
 $labels['aclnorights'] = 'You do not have administrator rights on this task list.';
+
+// (hidden) titles and labels for accessibility annotations
+$labels['quickaddinput'] = 'New task date and title';
+$labels['arialabelquickaddbox'] = 'Quick add new task';
+$labels['arialabelsearchform'] = 'Task search form';
+$labels['arialabelquicksearchbox'] = 'Task search input';
+$labels['arialabellistsearchform'] = 'Tasklists search form';
+$labels['arialabeltaskselector'] = 'List mode';
+$labels['arialabeltasklisting'] = 'Tasks listing';
+
diff --git a/plugins/tasklist/skins/larry/tagedit.css b/plugins/tasklist/skins/larry/tagedit.css
index a26b5ab..704427e 100644
--- a/plugins/tasklist/skins/larry/tagedit.css
+++ b/plugins/tasklist/skins/larry/tagedit.css
@@ -63,6 +63,14 @@
 	color: #0d5165;
 }
 
+.tagedit-list li.tagedit-listelement-focus {
+	border-color: #4787b1;
+	-moz-box-shadow: 0 0 3px 1px rgba(71,135,177, 0.8);
+	-webkit-box-shadow: 0 0 3px 1px rgba(71,135,177, 0.8);
+	-o-box-shadow: 0 0 3px 1px rgba(71,135,177, 0.8);
+	box-shadow: 0 0 3px 1px rgba(71,135,177, 0.8);
+}
+
 .tagedit span.tag-element {
 	margin-right: 0.6em;
 	padding: 2px 6px;
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index 54372fa..0bcf4fe 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -165,7 +165,6 @@ body.attachmentwin #topnav .topright {
 	border-color: #003645;
 	border-radius: 10px;
 	text-shadow: none;
-	outline: none;
 }
 
 #taskselector li .count {
@@ -306,15 +305,17 @@ body.attachmentwin #topnav .topright {
 	background: url(sprites.png) right 20px no-repeat;
 }
 
-#tasklistsbox .treelist li span.quickview {
+#tasklistsbox .treelist li a.quickview {
 	display: inline-block;
 	position: absolute;
 	top: 6px;
-	right: 20px;
+	right: 24px;
 	width: 16px;
 	height: 16px;
-	margin-right: 4px;
+	padding: 0;
 	background: url(sprites.png) -200px 0 no-repeat;
+	overflow: hidden;
+	text-indent: -5000px;
 	cursor: pointer;
 }
 
@@ -332,6 +333,7 @@ body.attachmentwin #topnav .topright {
 	cursor: pointer;
 }
 
+#tasklistsbox .treelist div > a.subscribed:focus,
 #tasklistsbox .treelist div:hover > a.subscribed {
 	background-position: -2px -215px;
 }
@@ -340,18 +342,25 @@ body.attachmentwin #topnav .topright {
 	background-position: -20px -215px;
 }
 
-#tasklistsbox .treelist li div:hover > span.quickview {
+#tasklistsbox .treelist div > a.quickview:focus,
+#tasklistsbox .treelist li div:hover > a.quickview {
 	background-position: -20px -101px;
 }
 
-#tasklistsbox .treelist li div.focusview > span.quickview {
+#tasklistsbox .treelist li div.focusview > a.quickview {
 	background-position: -2px -101px;
 }
 
-#tasklistsbox .searchresults .treelist li span.quickview {
+#tasklistsbox .searchresults .treelist li a.quickview {
 	display: none;
 }
 
+#tasklistsbox .treelist div a.quickview:focus,
+#tasklistsbox .treelist div a.subscribed:focus {
+	border-radius: 3px;
+	outline: 2px solid rgba(30,150,192, 0.5);
+}
+
 #tasklistsbox .treelist li.selected > div > span.listname {
 	font-weight: bold;
 }
@@ -578,8 +587,10 @@ body.attachmentwin #topnav .topright {
 	overflow: hidden;
 	text-overflow: ellipsis;
 	cursor: default;
+	outline: none;
 }
 
+.taskhead:focus,
 .taskhead.droptarget {
 	border-color: #4787b1;
 	box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
@@ -598,21 +609,20 @@ body.attachmentwin #topnav .topright {
 
 .taskhead .flagged {
 	display: inline-block;
-	visibility: hidden;
 	width: 16px;
 	height: 16px;
-	background: url(sprites.png) -2px -3px no-repeat;
+	background: url(sprites.png) 1000px -3px no-repeat;
 	margin: -3px 1em 0 0;
 	vertical-align: middle;
 	cursor: pointer;
 }
 
+.taskhead .flagged:focus,
 .taskhead:hover .flagged {
-	visibility: visible;
+	background-position: -2px -3px;
 }
 
 .taskhead.flagged .flagged {
-	visibility: visible;
 	background-position: -2px -23px;
 }
 
@@ -675,28 +685,22 @@ body.attachmentwin #topnav .topright {
 	font-size: 11px;
 }
 
-.taskhead .actions,
-.taskhead .delete {
+.taskhead .actions {
 	display: block;
-	visibility: hidden;
 	position: absolute;
 	top: 3px;
 	right: 6px;
 	width: 18px;
 	height: 18px;
-	background: url(sprites.png) 0 -80px no-repeat;
-	text-indent: -1000px;
+	background: url(sprites.png) 1000px -80px no-repeat;
+	text-indent: -5000px;
 	overflow: hidden;
 	cursor: pointer;
 }
 
-.taskhead .delete {
-	background-position: 0 -40px;
-}
-
-.taskhead:hover .actions,
-.taskhead:hover .delete {
-	visibility: visible;
+.taskhead .actions:focus,
+.taskhead:hover .actions {
+	background-position: 0 -80px;
 }
 
 .taskhead.complete {
diff --git a/plugins/tasklist/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html
index 632abcb..5a2baf9 100644
--- a/plugins/tasklist/skins/larry/templates/mainview.html
+++ b/plugins/tasklist/skins/larry/templates/mainview.html
@@ -9,109 +9,123 @@
 
 <roundcube:include file="/includes/header.html" />
 
+<h1 class="voice"><roundcube:label name="tasklist.navtitle" /></h1>
+
 <div id="mainscreen">
 	<div id="sidebar">
-		<div id="taskstoolbar" class="toolbar">
+		<h2 id="aria-label-toolbar" class="voice"><roundcube:label name="arialabeltoolbar" /></h2>
+		<div id="taskstoolbar" class="toolbar" role="toolbar" aria-labelledby="aria-label-toolbar">
 			<roundcube:button command="newtask" type="link" class="button newtask disabled" classAct="button newtask" classSel="button newtask pressed" label="tasklist.newtask" title="tasklist.newtask" />
 			<roundcube:container name="toolbar" id="taskstoolbar" />
 		</div>
 
 		<div id="tagsbox" class="uibox listbox">
-			<h2 class="boxtitle"><roundcube:label name="tasklist.tags" id="taglist" /></h2>
+			<h2 class="boxtitle" id="aria-label-tagsbox"><roundcube:label name="tasklist.tags" id="taglist" /></h2>
 			<div class="scroller">
-				<roundcube:object name="plugin.tagslist" id="tagslist" class="tagcloud" />
+				<roundcube:object name="plugin.tagslist" id="tagslist" class="tagcloud" role="region" aria-labelledby="aria-label-tagsbox" aria-controls="thelist" />
 			</div>
 		</div>
 
-		<div id="tasklistsbox" class="uibox listbox">
-			<h2 class="boxtitle"><roundcube:label name="tasklist.lists" />
-				<a class="iconbutton search" title="<roundcube:label name='tasklist.findlists' />"></a>
+		<div id="tasklistsbox" class="uibox listbox" role="navigation" aria-labelledby="aria-label-tasklists">
+			<h2 class="boxtitle" id="aria-label-tasklists"><roundcube:label name="tasklist.lists" />
+				<a href="#tasklists" class="iconbutton search" title="<roundcube:label name='tasklist.findlists' />" tabindex="0"><roundcube:label name="tasklist.findlists" /></a>
 			</h2>
 			<div class="listsearchbox">
-				<div class="searchbox">
+				<div class="searchbox" role="search" aria-labelledby="aria-label-listsearchform" aria-controls="tasklists">
+					<h3 id="aria-label-listsearchform" class="voice"><roundcube:label name="tasklist.arialabellistsearchform" /></h3>
+					<label for="tasklistsearch" class="voice"><roundcube:label name="tasklist.searchterms" /></label>
 					<input type="text" name="q" id="tasklistsearch" placeholder="<roundcube:label name='tasklist.findlists' />" />
 					<a class="iconbutton searchicon"></a>
-					<roundcube:button command="reset-listsearch" id="tasklistsearch-reset" class="iconbutton reset" title="resetsearch" content="x" />
+					<roundcube:button command="reset-listsearch" id="tasklistsearch-reset" class="iconbutton reset" title="resetsearch" label="resetsearch" />
 				</div>
 			</div>
 			<div class="scroller withfooter">
 			<roundcube:object name="plugin.tasklists" id="tasklists" class="treelist listing" />
 			</div>
 			<div class="boxfooter">
-				<roundcube:button command="list-create" type="link" title="tasklist.createlist" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button name="tasklistoptionslink" id="tasklistoptionsmenulink" type="link" title="tasklist.listactions" class="listbutton groupactions" onclick="UI.show_popup('tasklistoptionsmenu', undefined, { above:true });return false" innerClass="inner" content="⚙" />
+				<roundcube:button command="list-create" type="link" title="tasklist.createlist" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" label="tasklist.createlist" /><roundcube:button name="tasklistoptionslink" id="tasklistoptionsmenulink" type="link" title="tasklist.listactions" class="listbutton groupactions" onclick="return UI.toggle_popup('tasklistoptionsmenu', event, { above:true })" innerClass="inner" label="tasklist.listactions" aria-haspopup="true" aria-expanded="false" aria-owns="tasklistoptionsmenu-menu" />
 			</div>
 		</div>
+
+		<div id="tasklistoptionsmenu" class="popupmenu" aria-hidden="true">
+			<h3 id="aria-label-tasklistoptions" class="voice"><roundcube:label name="tasklist.listactions" /></h3>
+			<ul class="toolbarmenu" id="tasklistoptionsmenu-menu" role="menu" aria-labelledby="aria-label-tasklistoptions">
+				<li role="menuitem"><roundcube:button command="list-edit" label="edit" classAct="active" /></li>
+				<li role="menuitem"><roundcube:button command="list-remove" label="delete" classAct="active" /></li>
+				<!--<li role="menuitem"><roundcube:button command="list-import" label="tasklist.import" classAct="active" /></li>-->
+				<roundcube:if condition="env:tasklist_driver == 'kolab'" />
+				<li role="menuitem"><roundcube:button command="folders" task="settings" type="link" label="managefolders" classAct="active" /></li>
+				<roundcube:endif />
+			</ul>
+		</div>
 	</div>
 	
 	<div id="mainview-right">
 	
-	<div id="quickaddbox">
+	<div id="quickaddbox" role="region" aria-labelledby="aria-label-quickaddbox">
+		<h2 id="aria-label-quickaddbox" class="voice"><roundcube:label name="tasklist.arialabelquickaddbox" /></h2>
 		<roundcube:object name="plugin.quickaddform" />
 	</div>
 
-	<div id="quicksearchbar">
+	<div id="quicksearchbar" role="search" aria-labelledby="aria-label-searchform">
+		<h2 id="aria-label-searchform" class="voice"><roundcube:label name="tasklist.arialabelsearchform" /></h2>
+		<label for="quicksearchbox" class="voice"><roundcube:label name="tasklist.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=" " />
+		<roundcube:button command="reset-search" id="searchreset" class="iconbutton reset" title="resetsearch" label="resetsearch" />
 	</div>
 
 	<div id="tasksview" class="uibox">
 		<div class="boxtitle buttonbar">
-			<ul id="taskselector">
-				<li class="all selected"><a href="#all"><roundcube:label name="tasklist.all" /><span class="count"></span></a></li>
-				<li class="overdue inactive"><a href="#overdue"><roundcube:label name="tasklist.overdue" /><span class="count"></span></a></li>
-				<li class="flagged"><a href="#flagged"><roundcube:label name="tasklist.flagged" /><span class="count"></span></a></li>
-				<li class="today"><a href="#today"><roundcube:label name="tasklist.today" /><span class="count"></span></a></li>
-				<li class="tomorrow"><a href="#tomorrow"><roundcube:label name="tasklist.tomorrow" /><span class="count"></span></a></li>
-				<li class="week"><a href="#week"><roundcube:label name="tasklist.next7days" /></a></li>
-				<li class="later"><a href="#later"><roundcube:label name="tasklist.later" /></a></li>
-				<li class="nodate"><a href="#nodate"><roundcube:label name="tasklist.nodate" ucfirst="true" /></a></li>
-				<li class="complete"><a href="#complete"><roundcube:label name="tasklist.complete" /><span class="count"></span></a></li>
+			<h2 id="aria-label-taskselector" class="voice"><roundcube:label name="tasklist.arialabeltaskselector" /></h2>
+			<ul id="taskselector" role="radiogroup" aria-labelledby="aria-label-taskselector" aria-controls="thelist">
+				<li class="all selected" role="radio" aria-checked="true" aria-labelledby="aria-radio-all"><a href="#all" id="aria-radio-all"><roundcube:label name="tasklist.all" /><span class="count"></span></a></li>
+				<li class="overdue inactive" role="radio" aria-checked="false" aria-labelledby="aria-radio-overdue"><a href="#overdue" id="aria-radio-overdue"><roundcube:label name="tasklist.overdue" /><span class="count"></span></a></li>
+				<li class="flagged" role="radio" aria-checked="false" aria-labelledby="aria-radio-flagged"><a href="#flagged" id="aria-radio-flagged"><roundcube:label name="tasklist.flagged" /><span class="count"></span></a></li>
+				<li class="today" role="radio" aria-checked="false" aria-labelledby="aria-radio-today"><a href="#today" id="aria-radio-today"><roundcube:label name="tasklist.today" /><span class="count"></span></a></li>
+				<li class="tomorrow" role="radio" aria-checked="false" aria-labelledby="aria-radio-tomorrow"><a href="#tomorrow" id="aria-radio-tomorrow"><roundcube:label name="tasklist.tomorrow" /><span class="count"></span></a></li>
+				<li class="week" role="radio" aria-checked="false" aria-labelledby="aria-radio-week"><a href="#week" id="aria-radio-week"><roundcube:label name="tasklist.next7days" /></a></li>
+				<li class="later" role="radio" aria-checked="false" aria-labelledby="aria-radio-later"><a href="#later" id="aria-radio-later"><roundcube:label name="tasklist.later" /></a></li>
+				<li class="nodate" role="radio" aria-checked="false" aria-labelledby="aria-radio-nodate"><a href="#nodate" id="aria-radio-nodate"><roundcube:label name="tasklist.nodate" ucfirst="true" /></a></li>
+				<li class="complete" role="radio" aria-checked="false" aria-labelledby="aria-radio-complete"><a href="#complete" id="aria-radio-complete"><roundcube:label name="tasklist.complete" /><span class="count"></span></a></li>
 			</ul>
 
 			<div class="buttonbar-right">
-				<roundcube:button name="taskviewmenulink" id="taskviewmenulink" type="link" title="tasklist.viewoptions" class="listmenu viewoptions" onclick="UI.show_popup('taskviewmenu');return false" innerClass="inner" content="⚙" />
+				<roundcube:button name="taskviewmenulink" id="taskviewmenulink" type="link" title="tasklist.viewoptions" class="listmenu viewoptions" onclick="return UI.toggle_popup('taskviewmenu',event)" innerClass="inner" label="tasklist.viewoptions" aria-haspopup="true" aria-expanded="false" aria-owns="taskviewmenu-menu" />
+			</div>
+
+			<div id="taskviewmenu" class="popupmenu" aria-hidden="true">
+				<h3 id="aria-label-taskviewmenu" class="voice"><roundcube:label name="tasklist.viewoptions" /></h3>
+				<ul class="toolbarmenu" id="taskviewmenu-menu" role="menu" aria-labelledby="aria-label-taskviewmenu">
+					<li role="menuitem"><roundcube:button command="expand-all" label="expand-all" class="icon" classAct="icon active" innerclass="icon expand" /></li>
+					<li role="menuitem"><roundcube:button command="collapse-all" label="collapse-all" class="icon" classAct="icon active" innerclass="icon collapse" /></li>
+				</ul>
 			</div>
 		</div>
 		
-		<div class="scroller">
-			<roundcube:object name="plugin.tasks" id="thelist" />
+		<h2 class="voice" id="aria-label-tasklisting"><roundcube:label name="tasklist.arialabeltasklisting" /></h2>
+		<div class="scroller" role="main" aria-labelledby="aria-label-tasklisting">
+			<roundcube:object name="plugin.tasks" id="thelist" role="tree" />
 			<div id="listmessagebox"></div>
 			<div id="rootdroppable"></div>
 		</div>
 	</div>
 	
+	<div id="taskitemmenu" class="popupmenu" aria-hidden="true" data-align="right">
+		<h3 id="aria-label-taskactions" class="voice"><roundcube:label name="tasklist.taskactions" /></h3>
+		<ul class="toolbarmenu iconized" id="taskitemmenu-menu" role="menu" aria-labelledby="aria-label-taskactions">
+			<li role="menuitem"><roundcube:button name="edit" type="link" onclick="rctasks.edit_task(rctasks.selected_task.id, 'edit'); return false" label="edit" class="icon active" innerclass="icon edit" /></li>
+			<li role="menuitem"><roundcube:button name="delete" type="link" onclick="rctasks.delete_task(rctasks.selected_task.id); return false" label="delete" class="icon active" innerclass="icon delete" /></li>
+			<li role="menuitem"><roundcube:button name="addchild" type="link" onclick="rctasks.add_childtask(rctasks.selected_task.id); return false" label="tasklist.addsubtask" class="icon active" innerclass="icon add" /></li>
+		</ul>
 	</div>
 
-</div>
-
-<roundcube:object name="message" id="messagestack" />
+	</div>
 
-<div id="taskitemmenu" class="popupmenu">
-	<ul class="toolbarmenu iconized">
-		<li><roundcube:button name="edit" type="link" onclick="rctasks.edit_task(rctasks.selected_task.id, 'edit'); return false" label="edit" class="icon active" innerclass="icon edit" /></li>
-		<li><roundcube:button name="delete" type="link" onclick="rctasks.delete_task(rctasks.selected_task.id); return false" label="delete" class="icon active" innerclass="icon delete" /></li>
-		<li><roundcube:button name="addchild" type="link" onclick="rctasks.add_childtask(rctasks.selected_task.id); return false" label="tasklist.addsubtask" class="icon active" innerclass="icon add" /></li>
-	</ul>
 </div>
 
-<div id="tasklistoptionsmenu" class="popupmenu">
-	<ul class="toolbarmenu">
-		<li><roundcube:button command="list-edit" label="edit" classAct="active" /></li>
-		<li><roundcube:button command="list-remove" label="delete" classAct="active" /></li>
-		<!--<li><roundcube:button command="list-import" label="tasklist.import" classAct="active" /></li>-->
-		<roundcube:if condition="env:tasklist_driver == 'kolab'" />
-		<li><roundcube:button command="folders" task="settings" type="link" label="managefolders" classAct="active" /></li>
-		<roundcube:endif />
-	</ul>
-</div>
+<roundcube:object name="message" id="messagestack" />
 
-<div id="taskviewmenu" class="popupmenu">
-	<ul class="toolbarmenu">
-		<li><roundcube:button command="expand-all" label="expand-all" class="icon" classAct="icon active" innerclass="icon expand" /></li>
-		<li><roundcube:button command="collapse-all" label="collapse-all" class="icon" classAct="icon active" innerclass="icon collapse" /></li>
-	</ul>
-</div>
 
 <div id="taskshow">
 	<div class="form-section" id="task-parent-title"></div>
diff --git a/plugins/tasklist/skins/larry/templates/taskedit.html b/plugins/tasklist/skins/larry/templates/taskedit.html
index 97f604d..988fd5b 100644
--- a/plugins/tasklist/skins/larry/templates/taskedit.html
+++ b/plugins/tasklist/skins/larry/templates/taskedit.html
@@ -8,51 +8,51 @@
 			<div class="form-section">
 				<label for="taskedit-title"><roundcube:label name="tasklist.title" /></label>
 				<br />
-				<input type="text" class="text" name="title" id="taskedit-title" size="60" tabindex="1" />
+				<input type="text" class="text" name="title" id="taskedit-title" size="60" />
 			</div>
 			<div class="form-section">
 				<label for="taskedit-description"><roundcube:label name="tasklist.description" /></label>
 				<br />
-				<textarea name="description" id="taskedit-description" class="text" rows="5" cols="60" tabindex="2"></textarea>
+				<textarea name="description" id="taskedit-description" class="text" rows="5" cols="60"></textarea>
 			</div>
 			<div class="form-section">
-				<label for="taskedit-tags"><roundcube:label name="tasklist.tags" /></label>
-				<roundcube:object name="plugin.tags_editline" id="taskedit-tagline" class="tagedit" tabindex="3" />
+				<label for="tagedit-input"><roundcube:label name="tasklist.tags" /></label>
+				<roundcube:object name="plugin.tags_editline" id="taskedit-tagline" class="tagedit" tabindex="0" />
 			</div>
 			<div class="form-section">
 				<label for="taskedit-startdate"><roundcube:label name="tasklist.start" /></label>
-				<input type="text" name="startdate" size="10" id="taskedit-startdate" tabindex="23" />  
-				<input type="text" name="starttime" size="6" id="taskedit-starttime" tabindex="24" />
+				<input type="text" name="startdate" size="10" id="taskedit-startdate" />  
+				<input type="text" name="starttime" size="6" id="taskedit-starttime" aria-label="<roundcube:label name='tasklist.starttime' />" />
 				<a href="#nodate" style="margin-left:1em" class="edit-nodate" rel="#taskedit-startdate,#taskedit-starttime"><roundcube:label name="tasklist.nodate" /></a>
 			</div>
 			<div class="form-section">
 				<label for="taskedit-date"><roundcube:label name="tasklist.datetime" /></label>
-				<input type="text" name="date" size="10" id="taskedit-date" tabindex="20" />  
-				<input type="text" name="time" size="6" id="taskedit-time" tabindex="21" />
+				<input type="text" name="date" size="10" id="taskedit-date" />  
+				<input type="text" name="time" size="6" id="taskedit-time" aria-label="<roundcube:label name='tasklist.duetime' />" />
 				<a href="#nodate" style="margin-left:1em" class="edit-nodate" rel="#taskedit-date,#taskedit-time"><roundcube:label name="tasklist.nodate" /></a>
 			</div>
 			<div class="form-section" id="taskedit-alarms">
 				<div class="edit-alarm-item first">
-					<label><roundcube:label name="tasklist.alarms" /></label>
-					<roundcube:object name="plugin.alarm_select" />
+					<label for="edit-alarm-item"><roundcube:label name="tasklist.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>
 			<div class="form-section">
 				<label for="taskedit-completeness"><roundcube:label name="tasklist.complete" /></label>
-				<input type="text" name="title" id="taskedit-completeness" size="3"  tabindex="25" /> %
+				<input type="text" name="title" id="taskedit-completeness" size="3"  /> %
 				<div id="taskedit-completeness-slider"></div>
 			</div>
 			<div class="form-section">
 				<label for="taskedit-status"><roundcube:label name="tasklist.status" /></label>
-				<roundcube:object name="plugin.status_select" id="taskedit-status" tabindex="26" />
+				<roundcube:object name="plugin.status_select" id="taskedit-status" />
 			</div>
 			<div class="form-section" id="tasklist-select">
 				<label for="taskedit-tasklist"><roundcube:label name="tasklist.list" /></label>
-				<roundcube:object name="plugin.tasklist_select" id="taskedit-tasklist" tabindex="26" />
+				<roundcube:object name="plugin.tasklist_select" id="taskedit-tasklist" />
 			</div>
 		</div>
 		<!-- recurrence settings -->
@@ -84,7 +84,8 @@
 			<div id="taskedit-attachments">
 				<roundcube:object name="plugin.attachments_list" id="taskedit-attachment-list" class="attachmentslist" />
 			</div>
-			<div id="taskedit-attachments-form">
+			<div id="taskedit-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="taskedit-attachment-form" attachmentFieldSize="30" />
 			</div>
 			<roundcube:object name="plugin.filedroparea" id="taskedit-tab-2" />
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 92685fa..fb7c4ac 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -80,6 +80,8 @@ function rcube_tasklist_ui(settings)
     var scroll_sensitivity = 40;
     var scroll_timer;
     var tasklists_widget;
+    var focused_task;
+    var focused_subclass;
     var me = this;
 
     // general datepicker settings
@@ -140,6 +142,7 @@ function rcube_tasklist_ui(settings)
             id_prefix: 'rcmlitasklist',
             selectable: true,
             save_state: true,
+            keyboard: false,
             searchbox: '#tasklistsearch',
             search_action: 'tasks/tasklist',
             search_sources: [ 'folders', 'users' ],
@@ -168,9 +171,15 @@ function rcube_tasklist_ui(settings)
                 $(p.item).data('type', 'tasklist');
             }
         });
+        tasklists_widget.addEventListener('search-complete', function(data) {
+            if (data.length)
+                rcmail.display_message(rcmail.gettext('nrtasklistsfound','tasklist').replace('$nr', data.length), 'voice');
+            else
+                rcmail.display_message(rcmail.gettext('notasklistsfound','tasklist'), 'info');
+        });
 
         // init (delegate) event handler on tasklist checkboxes
-        tasklists_widget.container.on('click', 'input[type=checkbox]', function(e){
+        tasklists_widget.container.on('click', 'input[type=checkbox]', function(e) {
             var list, id = this.value;
             if ((list = me.tasklists[id])) {
                 list.active = this.checked;
@@ -186,6 +195,13 @@ function rcube_tasklist_ui(settings)
             }
             e.stopPropagation();
         })
+        .on('keypress', 'input[type=checkbox]', function(e) {
+            // select tasklist on <Enter>
+            if (e.keyCode == 13) {
+                tasklists_widget.select(this.value);
+                return rcube_event.cancel(e);
+            }
+        })
         .find('li:not(.virtual)').data('type', 'tasklist');
 
         // handler for clicks on quickview buttons
@@ -250,7 +266,7 @@ function rcube_tasklist_ui(settings)
         }).find('input[type=text]').placeholder(rcmail.gettext('createnewtask','tasklist'));
 
         // click-handler on tags list
-        $(rcmail.gui_objects.tagslist).click(function(e){
+        $(rcmail.gui_objects.tagslist).on('click', 'li', function(e){
             var item = e.target.nodeName == 'LI' ? $(e.target) : $(e.target).closest('li'),
                 tag = item.data('value');
 
@@ -265,17 +281,17 @@ function rcube_tasklist_ui(settings)
                 if (tagsfilter.length > 1)
                     index = -1;
 
-                $('li', this).removeClass('selected');
+                $('li', rcmail.gui_objects.tagslist).removeClass('selected').attr('aria-checked', 'false');
                 tagsfilter = [];
             }
 
             // add tag to filter
             if (index < 0) {
-                item.addClass('selected');
+                item.addClass('selected').attr('aria-checked', 'true');
                 tagsfilter.push(tag);
             }
             else if (shift) {
-                item.removeClass('selected');
+                item.removeClass('selected').attr('aria-checked', 'false');
                 var a = tagsfilter.slice(0,index);
                 tagsfilter = a.concat(tagsfilter.slice(index+1));
             }
@@ -289,6 +305,11 @@ function rcube_tasklist_ui(settings)
             e.preventDefault();
             return false;
         })
+        .on('keypress', 'li', function(e) {
+            if (e.keyCode == 13) {
+                $(this).trigger('click', { pointerType:'keyboard' });
+            }
+        })
         .mousedown(function(e){
             // disable content selection with the mouse
             e.preventDefault();
@@ -296,7 +317,7 @@ function rcube_tasklist_ui(settings)
         });
 
         // click-handler on task list items (delegate)
-        $(rcmail.gui_objects.resultlist).click(function(e){
+        $(rcmail.gui_objects.resultlist).on('click', function(e){
             var item = $(e.target);
             var className = e.target.className;
 
@@ -314,11 +335,11 @@ function rcube_tasklist_ui(settings)
             var id = item.data('id'),
                 li = item.parent(),
                 rec = listdata[id];
-            
+
             switch (className) {
                 case 'childtoggle':
                     rec.collapsed = !rec.collapsed;
-                    li.children('.childtasks:first').toggle();
+                    li.children('.childtasks:first').toggle().attr('aria-hidden', rec.collapsed ? 'true' : 'false');
                     $(e.target).toggleClass('collapsed').html(rec.collapsed ? '▶' : '▼');
                     rcmail.http_post('tasks/task', { action:'collapse', t:{ id:rec.id, list:rec.list }, collapsed:rec.collapsed?1:0 });
                     if (e.shiftKey)  // expand/collapse all childs
@@ -333,16 +354,16 @@ function rcube_tasklist_ui(settings)
                     li.toggleClass('complete');
                     save_task(rec, 'edit');
                     return true;
-                
+
                 case 'flagged':
                     if (rcmail.busy)
                         return false;
 
                     rec.flagged = rec.flagged ? 0 : 1;
-                    li.toggleClass('flagged');
+                    li.toggleClass('flagged').find('.flagged:first').attr('aria-checked', (rec.flagged ? 'true' : 'false'));
                     save_task(rec, 'edit');
                     break;
-                
+
                 case 'date':
                     if (rcmail.busy)
                         return false;
@@ -364,7 +385,7 @@ function rcube_tasklist_ui(settings)
                     .datepicker('setDate', rec.date)
                     .datepicker('show');
                     break;
-                
+
                 case 'delete':
                     delete_task(id);
                     break;
@@ -372,14 +393,12 @@ function rcube_tasklist_ui(settings)
                 case 'actions':
                     var pos, ref = $(e.target),
                         menu = $('#taskitemmenu');
+
                     if (menu.is(':visible') && menu.data('refid') == id) {
-                        menu.hide();
+                        rcmail.command('menu-close', 'taskitemmenu');
                     }
                     else {
-                        pos = ref.offset();
-                        pos.left += ref.width() - menu.outerWidth();
-                        pos.top += (pos.top + ref.outerHeight() + menu.height() > $(window).height() ? -menu.height() : ref.outerHeight());
-                        menu.css({ top:pos.top+'px', left:pos.left+'px' }).show();
+                        rcmail.command('menu-open', { menu: 'taskitemmenu', show: true }, e.target, e);
                         menu.data('refid', id);
                         me.selected_task = rec;
                     }
@@ -397,7 +416,7 @@ function rcube_tasklist_ui(settings)
 
             return false;
         })
-        .dblclick(function(e){
+        .on('dblclick', '.taskhead, .childtoggle', function(e){
             var id, rec, item = $(e.target);
             if (!item.hasClass('taskhead'))
                 item = item.closest('div.taskhead');
@@ -410,6 +429,58 @@ function rcube_tasklist_ui(settings)
                     task_edit_dialog(id, 'edit');
                 clearSelection();
             }
+        })
+        .on('keydown', '.taskhead', function(e) {
+            var inc = 1;
+            switch (e.keyCode) {
+                case 13:  // Enter
+                    $(e.target).trigger('click', { pointerType:'keyboard' });
+                    return rcube_event.cancel(e);
+
+                case 38: // Up arrow key
+                    inc = -1;
+                case 40: // Down arrow key
+                    if ($(e.target).hasClass('actions')) {
+                        // unfold actions menu
+                        $(e.target).trigger('click', { pointerType:'keyboard' });
+                        return rcube_event.cancel(e);
+                    }
+
+                    // focus next/prev task item
+                    var x = 0, target = this, items = $(rcmail.gui_objects.resultlist).find('.taskhead:visible');
+                    items.each(function(i, item) {
+                        if (item === target) {
+                            x = i;
+                            return false;
+                        }
+                    });
+                    items.get(x + inc).focus();
+                    return rcube_event.cancel(e);
+
+                case 37: // Left arrow key
+                case 39: // Right arrow key
+                    $(this).parent().children('.childtoggle:visible').first().trigger('click', { pointerType:'keyboard' });
+                    break;
+            }
+        })
+        .on('focusin', '.taskhead', function(e){
+            if (rcube_event.is_keyboard(e)) {
+                var item = $(e.target);
+                if (!item.hasClass('taskhead'))
+                    item = item.closest('div.taskhead');
+
+                var id = item.data('id');
+                if (id && listdata[id]) {
+                    focused_task = id;
+                    focused_subclass = item.get(0) !== e.target ? e.target.className : null;
+                }
+            }
+        })
+        .on('focusout', '.taskhead', function(e){
+            var item = $(e.target);
+            if (focused_task && item.data('id') == focused_task) {
+                focused_task = focused_subclass = null;
+            }
         });
 
         // handle global document clicks: close popup menus
@@ -506,8 +577,8 @@ function rcube_tasklist_ui(settings)
         else
             render_tasklist();
 
-        $('#taskselector li.selected').removeClass('selected');
-        $('#taskselector li.'+selector).addClass('selected');
+        $('#taskselector li.selected').removeClass('selected').attr('aria-checked', 'false');
+        $('#taskselector li.'+selector).addClass('selected').attr('aria-checked', 'true');
     }
 
     /**
@@ -601,8 +672,10 @@ function rcube_tasklist_ui(settings)
         fix_tree_toggles();
         update_tagcloud(activetags);
 
-        if (!count)
+        if (!count) {
             msgbox.html(rcmail.gettext('notasksfound','tasklist')).show();
+            rcmail.display_message(rcmail.gettext('notasksfound','tasklist'), 'voice');
+        }
     }
 
     /**
@@ -658,7 +731,9 @@ function rcube_tasklist_ui(settings)
 
         // append new tags to tag cloud
         $.each(newtags, function(i, tag){
-            $('<li>').attr('rel', tag).data('value', tag)
+            $('<li role="checkbox" aria-checked="false" tabindex="0"></li>')
+                .attr('rel', tag)
+                .data('value', tag)
                 .html(Q(tag) + '<span class="count"></span>')
                 .appendTo(rcmail.gui_objects.tagslist)
                 .draggable({
@@ -879,15 +954,18 @@ function rcube_tasklist_ui(settings)
         for (var j=0; rec.tags && j < rec.tags.length; j++)
             tags_html += '<span class="tag">' + Q(rec.tags[j]) + '</span>';
 
+        var label_id = rcmail.html_identifier(rec.id) + '-title';
         var div = $('<div>').addClass('taskhead').html(
             '<div class="progressbar"><div class="progressvalue" style="width:' + (rec.complete * 100) + '%"></div></div>' +
-            '<input type="checkbox" name="completed[]" value="1" class="complete" ' + (is_complete(rec) ? 'checked="checked" ' : '') + '/>' + 
-            '<span class="flagged"></span>' +
-            '<span class="title">' + text2html(Q(rec.title)) + '</span>' +
+            '<input type="checkbox" name="completed[]" value="1" class="complete" aria-label="' + rcmail.gettext('complete','tasklist') + '" ' + (is_complete(rec) ? 'checked="checked" ' : '') + '/>' + 
+            '<span class="flagged" role="checkbox" tabindex="0" aria-checked="' + (rec.flagged ? 'true' : 'false') + '" aria-label="' + rcmail.gettext('flagged','tasklist') + '"></span>' +
+            '<span class="title" id="' + label_id + '">' + text2html(Q(rec.title)) + '</span>' +
             '<span class="tags">' + tags_html + '</span>' +
             '<span class="date">' + Q(rec.date || rcmail.gettext('nodate','tasklist')) + '</span>' +
-            '<a href="#" class="actions">V</a>'
+            '<a href="#" class="actions" aria-haspopup="true" aria-expanded="false">' + rcmail.gettext('taskactions','tasklist') + '</a>'
             )
+            .attr('tabindex', '0')
+            .attr('aria-labelledby', label_id)
             .data('id', rec.id)
             .draggable({
                 revert: 'invalid',
@@ -917,12 +995,12 @@ function rcube_tasklist_ui(settings)
             inplace = true;
         }
         else {
-            li = $('<li>')
+            li = $('<li role="treeitem">')
                 .attr('rel', rec.id)
                 .addClass('taskitem')
                 .append((rec.collapsed ? '<span class="childtoggle collapsed">▶' : '<span class="childtoggle expanded">▼') + '</span>')
                 .append(div)
-                .append('<ul class="childtasks" style="' + (rec.collapsed ? 'display:none' : '') + '"></ul>');
+                .append('<ul class="childtasks" role="group" style="' + (rec.collapsed ? 'display:none' : '') + '" aria-hidden="' + (rec.collapsed ? 'true' : 'false') +'"></ul>');
 
             if (!parent || !parent.length)
                 li.appendTo(rcmail.gui_objects.resultlist);
@@ -935,6 +1013,11 @@ function rcube_tasklist_ui(settings)
             resort_task(rec, li, true);
             // TODO: remove the item after a while if it doesn't match the current filter anymore
         }
+
+        // re-set focus to taskhead element after DOM update
+        if (focused_task == rec.id) {
+            focus_task(li);
+        }
     }
 
     /**
@@ -955,7 +1038,11 @@ function rcube_tasklist_ui(settings)
             li.slideUp(speed, function(){
                 if (before)     li.insertBefore(before);
                 else if (after) li.insertAfter(after);
-                li.slideDown(speed);
+                li.slideDown(speed, function(){
+                    if (focused_task == rec.id) {
+                        focus_task(li);
+                    }
+                });
             });
         }
 
@@ -1024,6 +1111,17 @@ function rcube_tasklist_ui(settings)
     }
 
     /**
+     * Set focus on the given task item after DOM update
+     */
+    function focus_task(li)
+    {
+        var selector = '.taskhead';
+        if (focused_subclass)
+            selector += ' .' + focused_subclass
+        li.find(selector).focus();
+    }
+
+    /**
      * Determine whether the given task should be displayed as "complete"
      */
     function is_complete(rec)
@@ -1346,7 +1444,7 @@ function rcube_tasklist_ui(settings)
         $.each(typeof rec.tags == 'object' && rec.tags.length ? rec.tags : [''], function(i,val){
             $('<input>')
                 .attr('name', 'tags[]')
-                .attr('tabindex', '3')
+                .attr('tabindex', '0')
                 .addClass('tag')
                 .val(val)
                 .appendTo(tagline);
@@ -2032,8 +2130,10 @@ function rcube_tasklist_ui(settings)
      */
     function set_focusview(id)
     {
-        if (focusview && focusview != id)
-            $(tasklists_widget.get_item(focusview)).find('.tasklist').first().removeClass('focusview');
+        if (focusview && focusview != id) {
+            $(tasklists_widget.get_item(focusview)).find('.tasklist').first().removeClass('focusview')
+                .find('a.quickview').attr('aria-checked', 'false');
+        }
 
         focusview = id;
 
@@ -2050,7 +2150,7 @@ function rcube_tasklist_ui(settings)
         list_tasks(null);
 
         if (focusview) {
-            li.addClass('focusview');
+            li.addClass('focusview').find('a.quickview').attr('aria-checked', 'true');
         }
     }
 
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
index 377fe68..fc7320e 100644
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@ -187,12 +187,13 @@ class tasklist_ui
             $classes[] = $prop['class'];
 
         if (!$activeonly || $prop['active']) {
+            $label_id = 'tl:' . $id;
             return html::div(join(' ', $classes),
-                html::span(array('class' => 'listname', 'title' => $title), $prop['listname'] ?: $prop['name']) .
+                html::span(array('class' => 'listname', 'title' => $title, 'id' => $label_id), $prop['listname'] ?: $prop['name']) .
                   ($prop['virtual'] ? '' :
-                    html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active'])) .
-                    html::span(array('class' => 'quickview', 'title' => $this->plugin->gettext('focusview')), ' ') .
-                    (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->plugin->gettext('tasklistsubscribe')), ' ') : '')
+                    html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active'], 'aria-labelledby' => $label_id)) .
+                    html::a(array('href' => '#', 'class' => 'quickview', 'title' => $this->plugin->gettext('focusview'), 'role' => 'checkbox', 'aria-checked' => 'false'), ' ') .
+                    (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->plugin->gettext('tasklistsubscribe'), 'role' => 'checkbox', 'aria-checked' => $prop['subscribed'] ? 'true' : 'false'), ' ') : '')
                 )
             );
         }
@@ -276,11 +277,12 @@ class tasklist_ui
     {
         $attrib += array('action' => $this->rc->url('add'), 'method' => 'post', 'id' => 'quickaddform');
 
+        $label = html::label(array('for' => 'quickaddinput', 'class' => 'voice'), $this->plugin->gettext('quickaddinput'));
         $input = new html_inputfield(array('name' => 'text', 'id' => 'quickaddinput'));
-        $button = html::tag('input', array('type' => 'submit', 'value' => '+', 'class' => 'button mainaction'));
+        $button = html::tag('input', array('type' => 'submit', 'value' => '+', 'title' => $this->plugin->gettext('createtask'), 'class' => 'button mainaction'));
 
         $this->rc->output->add_gui_object('quickaddform', $attrib['id']);
-        return html::tag('form', $attrib, $input->show() . $button);
+        return html::tag('form', $attrib, $label . $input->show() . $button);
     }
 
     /**
@@ -317,6 +319,7 @@ class tasklist_ui
         $this->rc->output->add_gui_object('edittagline', $attrib['id']);
 
         $input = new html_inputfield(array('name' => 'tags[]', 'class' => 'tag', 'size' => $attrib['size'], 'tabindex' => $attrib['tabindex']));
+        unset($attrib['tabindex']);
         return html::div($attrib, $input->show(''));
     }
 


commit 55ac14b5f3aaf8822e1c97b1be103376a5bc4f61
Merge: 0e720bd 3acc91b
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri Jun 20 15:07:57 2014 +0200

    Merge branch 'master' of ssh://git.kolab.org/git/roundcubemail-plugins-kolab



commit 0e720bd7ecb9385a3a87871e4cae04ef6281e52f
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri Jun 20 13:20:21 2014 +0200

    Add keyboard navigation and basic accessibility improvements to the tagedit widget

diff --git a/plugins/tasklist/jquery.tagedit.js b/plugins/tasklist/jquery.tagedit.js
index efb4b74..baab701 100644
--- a/plugins/tasklist/jquery.tagedit.js
+++ b/plugins/tasklist/jquery.tagedit.js
@@ -11,7 +11,7 @@
 * JavaScript code in this file.
 *
 * Copyright (c) 2010 Oliver Albrecht <info at webwork-albrecht.de>
-* Copyright (c) 2012 Thomas Brüderli <thomas at roundcube.net>
+* Copyright (c) 2014 Thomas Brüderli <thomas at roundcube.net>
 *
 * Licensed under the MIT licenses
 *
@@ -38,7 +38,7 @@
 * for the JavaScript code in this file.
 *
 * @author Oliver Albrecht Mial: info at webwork-albrecht.de Twitter: @webworka
-* @version 1.5.1 (10/2013)
+* @version 1.5.2 (06/2014)
 * Requires: jQuery v1.4+, jQueryUI v1.8+, jQuerry.autoGrowInput
 *
 * Example of usage:
@@ -125,6 +125,7 @@
 		}
 
 		var elements = this;
+		var focusItem = null;
 
 		var baseNameRegexp = new RegExp("^(.*)\\[([0-9]*?("+options.deletedPostfix+"|"+options.addedPostfix+")?)?\]$", "i");
 
@@ -153,17 +154,18 @@
 		function inputsToList() {
 			var html = '<ul class="tagedit-list '+options.additionalListClass+'">';
 
-			elements.each(function() {
+			elements.each(function(i) {
 				var element_name = $(this).attr('name').match(baseNameRegexp);
 				if(element_name && element_name.length == 4 && (options.deleteEmptyItems == false || $(this).val().length > 0)) {
 					if(element_name[1].length > 0) {
-						var elementId = typeof element_name[2] != 'undefined'? element_name[2]: '';
+						var elementId = typeof element_name[2] != 'undefined'? element_name[2]: '',
+							domId = 'tagedit-' + baseName + '-' + (elementId || i);
 
-						html += '<li class="tagedit-listelement tagedit-listelement-old">';
-						html += '<span dir="'+options.direction+'">' + $(this).val() + '</span>';
+						html += '<li class="tagedit-listelement tagedit-listelement-old" aria-labelledby="'+domId+'">';
+						html += '<span dir="'+options.direction+'" id="'+domId+'">' + $(this).val() + '</span>';
 						html += '<input type="hidden" name="'+baseName+'['+elementId+']" value="'+$(this).val()+'" />';
 						if (options.allowDelete)
-							html += '<a class="tagedit-close" title="'+options.texts.removeLinkTitle+'">x</a>';
+							html += '<a class="tagedit-close" title="'+options.texts.removeLinkTitle+'" aria-label="'+options.texts.removeLinkTitle+' '+$(this).val()+'">x</a>';
 						html += '</li>';
 					}
 				}
@@ -215,12 +217,13 @@
 								}
 
 								if(options.allowAdd == true || oldValue) {
+									var domId = 'tagedit-' + baseName + '-' + id;
 									// Make a new tag in front the input
-									html = '<li class="tagedit-listelement tagedit-listelement-old">';
-									html += '<span dir="'+options.direction+'">' + $(this).val() + '</span>';
+									html = '<li class="tagedit-listelement tagedit-listelement-old" aria-labelledby="'+domId+'">';
+									html += '<span dir="'+options.direction+'" id="'+domId+'">' + $(this).val() + '</span>';
 									var name = oldValue? baseName + '['+id+options.addedPostfix+']' : baseName + '[]';
 									html += '<input type="hidden" name="'+name+'" value="'+$(this).val()+'" />';
-									html += '<a class="tagedit-close" title="'+options.texts.removeLinkTitle+'">x</a>';
+									html += '<a class="tagedit-close" title="'+options.texts.removeLinkTitle+'" aria-label="'+options.texts.removeLinkTitle+' '+$(this).val()+'">x</a>';
 									html += '</li>';
 
 									$(this).parent().before(html);
@@ -239,8 +242,19 @@
 							var code = event.keyCode > 0? event.keyCode : event.which;
 
 							switch(code) {
+								case 46:
+									if (!focusItem)
+										break;
 								case 8: // BACKSPACE
-									if($(this).val().length == 0) {
+									if(focusItem) {
+										focusItem.fadeOut(options.animSpeed, function() {
+											$(this).remove();
+										})
+										unfocusItem();
+										event.preventDefault();
+										return false;
+									}
+									else if($(this).val().length == 0) {
 										// delete Last Tag
 										var elementToRemove = elements.find('li.tagedit-listelement-old').last();
 										elementToRemove.fadeOut(options.animSpeed, function() {elementToRemove.remove();})
@@ -254,7 +268,41 @@
 										event.preventDefault();
 										return false;
 									}
-								break;
+									break;
+								case 37: // LEFT
+								case 39: // RIGHT
+									if($(this).val().length == 0) {
+										// select previous Tag
+										var inc = code == 37 ? -1 : 1,
+											items = elements.find('li.tagedit-listelement-old')
+											x = items.length, next = 0;
+										items.each(function(i, elem) {
+											if ($(elem).hasClass('tagedit-listelement-focus')) {
+												x = i;
+												return true;
+											}
+										});
+										unfocusItem();
+										next = Math.max(0, x + inc);
+										if (items.get(next)) {
+											focusItem = items.eq(next).addClass('tagedit-listelement-focus');
+											$(this).attr('aria-activedescendant', focusItem.attr('aria-labelledby'))
+
+											if(options.autocompleteOptions.source != false) {
+												$(this).autocomplete('close').autocomplete('disable');
+											}
+										}
+										event.preventDefault();
+										return false;
+									}
+									break;
+								default:
+									// ignore input if an item is focused
+									if (focusItem !== null) {
+										event.preventDefault();
+										event.bubble = false;
+										return false;
+									}
 							}
 							return true;
 						})
@@ -264,8 +312,11 @@
 								if($(this).val().length > 0 && $('ul.ui-autocomplete #ui-active-menuitem').length == 0) {
 									$(this).trigger('transformToTag');
 								}
-							event.preventDefault();
-							return false;
+								event.preventDefault();
+								return false;
+							}
+							else if($(this).val().length > 0){
+								unfocusItem();
 							}
 							return true;
 						})
@@ -287,9 +338,15 @@
 								var input = $(this);
 								$(this).data('blurtimer', window.setTimeout(function() {input.val('');}, 500));
 							}
+							unfocusItem();
+							// restore tabindex when widget looses focus
+							if (options.tabindex)
+								elements.attr('tabindex', options.tabindex);
 						})
 						.focus(function() {
 							window.clearTimeout($(this).data('blurtimer'));
+							// remove tabindex on <ul> because #tagedit-input now has it
+							elements.attr('tabindex', '-1');
 						});
 
 						if(options.autocompleteOptions.source != false) {
@@ -302,6 +359,7 @@
 						case 'A':
 							$(event.target).parent().fadeOut(options.animSpeed, function() {
 								$(event.target).parent().remove();
+								elements.find('#tagedit-input').focus();
 								});
 							break;
 						case 'INPUT':
@@ -325,6 +383,20 @@
 		}
 
 		/**
+		 * Remove class and reference to currently focused tag item
+		 */
+		function unfocusItem() {
+			if(focusItem){
+				if(options.autocompleteOptions.source != false) {
+					elements.find('#tagedit-input').autocomplete('enable');
+				}
+				focusItem.removeClass('tagedit-listelement-focus');
+				focusItem = null;
+				elements.find('#tagedit-input').removeAttr('aria-activedescendant');
+			}
+		}
+
+		/**
 		* Sets all Actions and events for editing an Existing Tag.
 		*
 		* @param event {object} The original Event that was given


commit 89ece32c78ad5c813b7c8966257585640d02e276
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jun 19 17:45:39 2014 +0200

    Fix calendar edit form labels and item selection

diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index ff9e5b4..3558d91 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -2832,6 +2832,13 @@ function rcube_calendar_ui(settings)
 
         e.stopPropagation();
       }
+    })
+    .on('keypress', 'input[type=checkbox]', function(e) {
+        // select calendar on <Enter>
+        if (e.keyCode == 13) {
+            calendars_list.select(this.value);
+            return rcube_event.cancel(e);
+        }
     });
 
     // register dbl-click handler to open calendar edit dialog
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 457b83e..ca14d89 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -1282,8 +1282,9 @@ class kolab_driver extends calendar_driver
       $hidden_fields[] = array('name' => 'parent', 'value' => $path_imap);
     }
     else {
-      $select = kolab_storage::folder_selector('event', array('name' => 'parent'), $folder);
+      $select = kolab_storage::folder_selector('event', array('name' => 'parent', 'id' => 'calendar-parent'), $folder);
       $form['props']['fieldsets']['location']['content']['path'] = array(
+        'id' => 'calendar-parent',
         'label' => $this->cal->gettext('parentcalendar'),
         'value' => $select->show(strlen($folder) ? $path_imap : ''),
       );
@@ -1363,10 +1364,9 @@ class kolab_driver extends calendar_driver
     if (is_array($form['content']) && !empty($form['content'])) {
       $table = new html_table(array('cols' => 2));
       foreach ($form['content'] as $col => $colprop) {
-        $colprop['id'] = '_'.$col;
         $label = !empty($colprop['label']) ? $colprop['label'] : rcube_label($col);
 
-        $table->add('title', sprintf('<label for="%s">%s</label>', $colprop['id'], Q($label)));
+        $table->add('title', html::label($colprop['id'], Q($label)));
         $table->add(null, $colprop['value']);
       }
       $content = $table->show();
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 84169fd..c274174 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -1174,10 +1174,14 @@ a.dropdown-link:after {
 	padding-right: 10px;
 }
 
-#calendar-kolabform fieldset.tab {
+#calendar-kolabform .tabsbar.ui-tabs-nav {
+	margin-bottom: 0;
+}
+
+#calendar-kolabform fieldset.ui-tabs-panel {
 	background: #efefef;
 	display: block;
-	margin-top: 0.5em;
+	margin-top: 2px;
 	padding: 0.5em 1em;
 }
 
diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
index 6fce4ca..6e7ed49 100644
--- a/plugins/calendar/skins/larry/templates/calendar.html
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -42,7 +42,7 @@
 				<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="return UI.toggle_popup('calendaroptionsmenu', event, { above:true })" innerClass="inner" label="calendar.calendaractions"  aria-haspopup="true" aria-expanded="false" aria-owns="calendaroptionsmenu-menu" />
+				<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>


commit e7679b10142ad9c47c3dd3c6033882062bcde308
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jun 19 16:44:56 2014 +0200

    Link text labels with recurrence form elements

diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index b84ede7..c157c6a 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -735,19 +735,19 @@ class libcalendaring extends rcube_plugin
                 $select->add($this->gettext('monthly'), 'MONTHLY');
                 $select->add($this->gettext('yearly'),  'YEARLY');
                 $select->add($this->gettext('rdate'),   'RDATE');
-                $html = html::label('edit-frequency', $this->gettext('frequency')) . $select->show('');
+                $html = html::label('edit-recurrence-frequency', $this->gettext('frequency')) . $select->show('');
                 break;
 
             // daily recurrence
             case 'daily':
                 $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-daily'));
-                $html = html::div($attrib, html::label(null, $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('days')));
+                $html = html::div($attrib, html::label('edit-recurrence-interval-daily', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('days')));
                 break;
 
             // weekly recurrence form
             case 'weekly':
                 $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-weekly'));
-                $html = html::div($attrib, html::label(null, $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('weeks')));
+                $html = html::div($attrib, html::label('edit-recurrence-interval-weekly', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('weeks')));
                 // weekday selection
                 $daymap = array('sun','mon','tue','wed','thu','fri','sat');
                 $checkbox = new html_checkbox(array('name' => 'byday', 'class' => 'edit-recurrence-weekly-byday'));
@@ -765,7 +765,7 @@ class libcalendaring extends rcube_plugin
             // monthly recurrence form
             case 'monthly':
                 $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-monthly'));
-                $html = html::div($attrib, html::label(null, $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('months')));
+                $html = html::div($attrib, html::label('edit-recurrence-interval-monthly', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('months')));
 
                 $checkbox = new html_checkbox(array('name' => 'bymonthday', 'class' => 'edit-recurrence-monthly-bymonthday'));
                 for ($monthdays = '', $d = 1; $d <= 31; $d++) {
@@ -787,7 +787,7 @@ class libcalendaring extends rcube_plugin
             // annually recurrence form
             case 'yearly':
                 $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-yearly'));
-                $html = html::div($attrib, html::label(null, $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('years')));
+                $html = html::div($attrib, html::label('edit-recurrence-interval-yearly', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('years')));
                 // month selector
                 $monthmap = array('','jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec');
                 $checkbox = new html_checkbox(array('name' => 'bymonth', 'class' => 'edit-recurrence-yearly-bymonth'));
@@ -812,17 +812,18 @@ class libcalendaring extends rcube_plugin
                         $this->gettext('forever'))
                 );
 
+                $forntimes = $this->gettext(array(
+                    'name' => 'forntimes',
+                    'vars' => array('nr' => '%s'))
+                );
                 $html .= html::div('line',
-                    $radio->show('', array('value' => 'count', 'id' => 'edit-recurrence-repeat-count')) . ' ' .
-                        $this->gettext(array(
-                            'name' => 'forntimes',
-                            'vars' => array('nr' => $select->show(1)))
-                        )
+                    $radio->show('', array('value' => 'count', 'id' => 'edit-recurrence-repeat-count', 'aria-label' => sprintf($forntimes, 'N'))) . ' ' .
+                        sprintf($forntimes, $select->show(1))
                 );
 
                 $html .= html::div('line',
-                    $radio->show('', array('value' => 'until', 'id' => 'edit-recurrence-repeat-until')) . ' ' .
-                        $this->gettext('untildate') . ' ' . $input->show('')
+                    $radio->show('', array('value' => 'until', 'id' => 'edit-recurrence-repeat-until', 'aria-label' => $this->gettext('untilenddate'))) . ' ' .
+                        $this->gettext('untildate') . ' ' . $input->show('', array('aria-label' => $this->gettext('untilenddate')))
                 );
 
                 $html = html::div($attrib, html::label(null, ucfirst($this->gettext('recurrencend'))) . $html);
diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc
index 863b7fa..d89833c 100644
--- a/plugins/libcalendaring/localization/en_US.inc
+++ b/plugins/libcalendaring/localization/en_US.inc
@@ -57,6 +57,7 @@ $labels['onevery'] = 'On every';
 $labels['onsamedate'] = 'On the same date';
 $labels['forever'] = 'forever';
 $labels['recurrencend'] = 'until';
+$labels['untilenddate'] = 'until date';
 $labels['forntimes'] = 'for $nr time(s)';
 $labels['first'] = 'first';
 $labels['second'] = 'second';




More information about the commits mailing list