Thomas Bruederli
2014-05-07 ea0866a1adc9239b8b115ab2490e1dd88f3c64ec
Improve keyboard navigation on compose screen: define tabindex groups + enable keyboard controls of contacts list widget
9 files modified
192 ■■■■■ changed files
program/include/rcmail_output_html.php 2 ●●●●● patch | view | raw | blame | history
program/js/app.js 31 ●●●●● patch | view | raw | blame | history
program/js/list.js 2 ●●●●● patch | view | raw | blame | history
skins/larry/addressbook.css 17 ●●●●● patch | view | raw | blame | history
skins/larry/mail.css 29 ●●●● patch | view | raw | blame | history
skins/larry/styles.css 30 ●●●●● patch | view | raw | blame | history
skins/larry/templates/addressbook.html 2 ●●● patch | view | raw | blame | history
skins/larry/templates/compose.html 66 ●●●● patch | view | raw | blame | history
skins/larry/ui.js 13 ●●●●● patch | view | raw | blame | history
program/include/rcmail_output_html.php
@@ -1135,6 +1135,8 @@
            $attrib['role'] = 'button';
        }
        if (!empty($attrib['class']) && !empty($attrib['classact']) || !empty($attrib['imagepas']) && !empty($attrib['imageact'])) {
            if (array_key_exists('tabindex', $attrib))
                $attrib['data-tabindex'] = $attrib['tabindex'];
            $attrib['tabindex'] = '-1';  // disable button by default
            $attrib['aria-disabled'] = 'true';
        }
program/js/app.js
@@ -300,10 +300,12 @@
            $('a.insertresponse', this.gui_objects.responseslist)
              .attr('unselectable', 'on')
              .mousedown(function(e){ return rcube_event.cancel(e); })
              .mouseup(function(e){
                ref.command('insert-response', $(this).attr('rel'));
                $(document.body).trigger('mouseup');  // hides the menu
                return rcube_event.cancel(e);
              .bind('mouseup keypress', function(e){
                if (e.type == 'mouseup' || rcube_event.get_keycode(e) == 13) {
                  ref.command('insert-response', $(this).attr('rel'));
                  $(document.body).trigger('mouseup');  // hides the menu
                  return rcube_event.cancel(e);
                }
              });
            // avoid textarea loosing focus when hitting the save-response button/link
@@ -337,11 +339,12 @@
        // init address book widget
        if (this.gui_objects.contactslist) {
          this.contact_list = new rcube_list_widget(this.gui_objects.contactslist,
            { multiselect:true, draggable:false, keyboard:false });
            { multiselect:true, draggable:false, keyboard:true });
          this.contact_list
            .addEventListener('initrow', function(o) { ref.triggerEvent('insertrow', { cid:o.uid, row:o }); })
            .addEventListener('select', function(o) { ref.compose_recipient_select(o); })
            .addEventListener('dblclick', function(o) { ref.compose_add_recipient('to'); })
            .addEventListener('keypress', function(o) { if (o.key_pressed == o.ENTER_KEY) ref.compose_add_recipient('to'); })
            .init();
        }
@@ -3600,10 +3603,12 @@
        .mousedown(function(e){
          return rcube_event.cancel(e);
        })
        .mouseup(function(e){
          ref.command('insert-response', key);
          $(document.body).trigger('mouseup');  // hides the menu
          return rcube_event.cancel(e);
        .bind('mouseup keypress', function(e){
          if (e.type == 'mouseup' || rcube_event.get_keycode(e) == 13) {
            ref.command('insert-response', $(this).attr('rel'));
            $(document.body).trigger('mouseup');  // hides the menu
            return rcube_event.cancel(e);
          }
        });
    }
  };
@@ -6195,7 +6200,7 @@
  // set button to a specific state
  this.set_button = function(command, state)
  {
    var n, button, obj, a_buttons = this.buttons[command],
    var n, button, obj, $obj, a_buttons = this.buttons[command],
      len = a_buttons ? a_buttons.length : 0;
    for (n=0; n<len; n++) {
@@ -6235,8 +6240,9 @@
        $(obj).button('option', 'disabled', state == 'pas');
      }
      else {
        $(obj)
          .attr('tabindex', state == 'pas' || state == 'sel' ? '-1' : '0')
        $obj = $(obj);
        $obj
          .attr('tabindex', state == 'pas' || state == 'sel' ? '-1' : ($obj.attr('data-tabindex') || '0'))
          .attr('aria-disabled', state == 'pas' || state == 'sel' ? 'true' : 'false');
      }
    }
@@ -7147,6 +7153,7 @@
            this.enable_command('search-create', this.env.source == '');
            this.enable_command('search-delete', this.env.search_id);
            this.update_group_commands();
            this.contact_list.focus();
            this.triggerEvent('listupdate', { folder:this.env.source, rowcount:this.contact_list.rowcount });
          }
        }
program/js/list.js
@@ -558,6 +558,8 @@
  }
  this.rows[id].clicked = now;
  this.focus();
  return false;
},
skins/larry/addressbook.css
@@ -138,23 +138,6 @@
    background: url(images/listicons.png) -2px -1180px no-repeat;
}
/* This padding-left should be equal to the focused border-left + the focused padding-left */
#contacts-table thead tr td:first-child,
#contacts-table tbody tr td:first-child {
    border-left: 0;
    padding-left: 36px;
}
/* because of border-collapse, we make the left border twice what we want it to be - half will be hidden to the left */
#contacts-table tbody tr.focused > td:first-child {
    border-left: 2px solid #b0ccd7;
    padding-left: 34px;
}
#contacts-table tbody tr.selected.focused > td:first-child {
    border-left-color: #9ec2d0;
}
#contacts-table .contact td.name {
    background-position: 6px -1603px;
}
skins/larry/mail.css
@@ -1358,11 +1358,17 @@
    margin-left: 0.5em;
}
#compose-contacts li a, #contacts-table td {
    background: url(images/listicons.png) -100px 0 no-repeat;
#compose-contacts li a,
#contacts-table td {
    background-image: url(images/listicons.png);
    background-position: -100px 0;
    background-repeat: no-repeat;
    overflow: hidden;
    padding-left: 36px;
    text-overflow: ellipsis;
}
#compose-contacts li a {
    padding-left: 36px;
}
#contacts-table td.contactgroup a {
@@ -1386,6 +1392,7 @@
    background-position: 6px -766px;
}
#compose-contacts li.addressbook a:focus,
#compose-contacts li.addressbook.selected a {
    background-position: 6px -791px;
}
@@ -1394,20 +1401,36 @@
    background-position: 6px -1555px;
}
#contacts-table.focus tr.focused td.contactgroup {
    background-position: 4px -1555px;
}
#contacts-table tr.unfocused td.contactgroup,
#contacts-table tr.selected td.contactgroup {
    background-position: 6px -1579px;
}
#contacts-table.focus tr.selected.focused td.contactgroup {
    background-position: 4px -1579px;
}
#contacts-table td.contact {
    background-position: 6px -1603px;
}
#contacts-table.focus tr.focused td.contact {
    background-position: 4px -1603px;
}
#contacts-table tr.unfocused td.contact,
#contacts-table tr.selected td.contact {
    background-position: 6px -1627px;
}
#contacts-table.focus tr.selected.focused td.contact {
    background-position: 4px -1627px;
}
#compose-content {
    position: absolute;
    top: 0;
skins/larry/styles.css
@@ -1190,6 +1190,36 @@
    height: 14px;
}
/* This padding-left minus the focused padding left should be half of the focused border-left */
.listing thead tr td:first-child,
.listing tbody tr td:first-child {
    border-left: 0;
    padding-left: 6px;
}
.listing.iconized thead tr td:first-child,
.listing.iconized tbody tr td:first-child {
    padding-left: 36px;
}
/* because of border-collapse, we make the left border twice what we want it to be - half will be hidden to the left */
.listing.focus tbody tr.focused > td:first-child {
    border-left: 2px solid #b0ccd7;
    padding-left: 4px;
}
.listing.iconized.focus tbody tr.focused > td:first-child {
    padding-left: 34px;
}
.listing.focus tbody tr.selected.focused > td:first-child {
    border-left-color: #9ec2d0;
}
.listing.inconized.focus tr.focused td:first-child {
    padding-left: 34px;
}
.listbox .listitem.selected,
.listbox .tablink.selected,
.listbox .listitem.selected > a,
skins/larry/templates/addressbook.html
@@ -54,7 +54,7 @@
<div id="addresslist" class="uibox listbox">
<roundcube:object name="addresslisttitle" label="contacts" tag="h2" class="boxtitle" />
<div class="scroller withfooter">
<roundcube:object name="addresslist" id="contacts-table" class="listing" noheader="true" />
<roundcube:object name="addresslist" id="contacts-table" class="listing iconized" noheader="true" />
</div>
<div class="boxfooter">
    <roundcube:button command="add" type="link" title="newcontact" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button command="delete" type="link" title="deletecontact" class="listbutton delete disabled" classAct="listbutton delete" innerClass="inner" content="x" /><roundcube:button command="group-remove-selected" type="link" title="groupremoveselected" class="listbutton removegroup disabled" classAct="listbutton removegroup" innerClass="inner" content="-" />
skins/larry/templates/compose.html
@@ -18,21 +18,21 @@
<!-- toolbar -->
<h2 id="aria-label-toolbar" class="voice">Application toolbar</h2>
<div id="messagetoolbar" class="toolbar fullwidth" role="toolbar" aria-labelledby="aria-label-toolbar">
    <roundcube:button command="list" type="link" class="button back disabled" classAct="button back" classSel="button back pressed" label="cancel" condition="!env:extwin" />
    <roundcube:button command="close" type="link" class="button close disabled" classAct="button close" classSel="button close pressed" label="cancel" condition="env:extwin" />
    <roundcube:button command="list" type="link" class="button back disabled" classAct="button back" label="cancel" condition="!env:extwin" tabindex="2" />
    <roundcube:button command="close" type="link" class="button close disabled" classAct="button close" label="cancel" condition="env:extwin" tabindex="2" />
    <span class="spacer"></span>
    <roundcube:button command="send" type="link" class="button send" classAct="button send" classSel="button send pressed" label="send" title="sendmessage" />
    <roundcube:button command="savedraft" type="link" class="button savedraft" classAct="button savedraft" classSel="button savedraft pressed" label="save" title="savemessage" />
    <roundcube:button command="send" type="link" class="button send disabled" classAct="button send" label="send" title="sendmessage" tabindex="2" />
    <roundcube:button command="savedraft" type="link" class="button savedraft disabled" classAct="button savedraft" label="save" title="savemessage" tabindex="2" />
    <span class="spacer"></span>
    <roundcube:if condition="config:enable_spellcheck" />
    <span class="dropbutton">
        <roundcube:button command="spellcheck" type="link" class="button spellcheck disabled" classAct="button spellcheck" classSel="button spellcheck pressed" label="spellcheck" title="checkspelling" />
        <a href="#languages" class="dropbuttontip" id="spellmenulink" onclick="UI.toggle_popup('spellmenu',event);return false" aria-haspopup="true"></a>
        <roundcube:button command="spellcheck" type="link" class="button spellcheck disabled" classAct="button spellcheck" classSel="button spellcheck pressed" label="spellcheck" title="checkspelling" tabindex="2" />
        <a href="#languages" class="dropbuttontip" id="spellmenulink" onclick="UI.toggle_popup('spellmenu',event);return false" aria-haspopup="true" tabindex="2"></a>
    </span>
    <roundcube:endif />
    <roundcube:button name="addattachment" type="link" class="button attach" classAct="button attach" classSel="button attach pressed" label="attach" title="addattachment" onclick="UI.show_uploadform();return false" />
    <roundcube:button command="insert-sig" type="link" class="button insertsig disabled" classAct="button insertsig" classSel="button insertsig pressed" label="signature" title="insertsignature" />
    <a href="#responses" class="button responses" label="responses" title="<roundcube:label name='insertresponse' />" id="responsesmenulink" unselectable="on" onmousedown="return false" onclick="UI.toggle_popup('responsesmenu',event);return false" aria-haspopup="true" aria-owns="textresponsesmenu"><roundcube:label name="responses" /></a>
    <roundcube:button name="addattachment" type="link" class="button attach" label="attach" title="addattachment" onclick="UI.show_uploadform(event);return false" aria-haspopup="true" tabindex="2" />
    <roundcube:button command="insert-sig" type="link" class="button insertsig disabled" classAct="button insertsig" label="signature" title="insertsignature" tabindex="2" />
    <a href="#responses" class="button responses" label="responses" title="<roundcube:label name='insertresponse' />" id="responsesmenulink" unselectable="on" onmousedown="return false" onclick="UI.toggle_popup('responsesmenu',event);return false" tabindex="2" aria-haspopup="true" aria-owns="textresponsesmenu"><roundcube:label name="responses" /></a>
    <roundcube:container name="toolbar" id="compose-toolbar" />
</div>
@@ -52,9 +52,9 @@
            <roundcube:button command="reset-search" id="searchreset" class="iconbutton reset" title="resetsearch" content=" " />
        </div>
    </div>
    <roundcube:object name="addressbooks" id="directorylist" class="listing" />
    <div class="scroller withfooter">
        <roundcube:object name="addresslist" id="contacts-table" class="listing" noheader="true" />
    <roundcube:object name="addressbooks" id="directorylist" class="treelist listing" />
    <div class="scroller withfooter" tabindex="-1">
        <roundcube:object name="addresslist" id="contacts-table" class="listing iconized" noheader="true" />
    </div>
<div class="boxfooter">
    <roundcube:button command="add-recipient" prop="to" type="link" title="to" class="listbutton addto disabled" classAct="listbutton addto" innerClass="inner" content="To+" /><roundcube:button command="add-recipient" prop="cc" type="link" title="cc" class="listbutton addcc disabled" classAct="listbutton addcc" innerClass="inner" content="Cc+" /><roundcube:button command="add-recipient" prop="bcc" type="link" title="bcc" class="listbutton addbcc disabled" classAct="listbutton addbcc" innerClass="inner" content="Bcc+" />
@@ -89,42 +89,42 @@
        </td>
    </tr><tr>
        <td class="title top"><label for="_to"><roundcube:label name="to" /></label></td>
        <td class="editfield"><roundcube:object name="composeHeaders" part="to" form="form" id="_to" cols="70" rows="1" tabindex="2" /></td>
        <td class="editfield"><roundcube:object name="composeHeaders" part="to" form="form" id="_to" cols="70" rows="1" tabindex="1" aria-required="true" /></td>
    </tr><tr id="compose-cc">
        <td class="title top">
            <label for="_cc"><roundcube:label name="cc" /></label>
            <a href="#cc" onclick="return UI.hide_header_row('cc');" class="iconbutton cancel" title="<roundcube:label name='delete' />">x</a>
            <a href="#cc" onclick="return UI.hide_header_row('cc');" class="iconbutton cancel" title="<roundcube:label name='delete' /> <roundcube:label name='cc' />" tabindex="3">x</a>
        </td>
        <td class="editfield"><roundcube:object name="composeHeaders" part="cc" form="form" id="_cc" cols="70" rows="1" tabindex="3" /></td>
        <td class="editfield"><roundcube:object name="composeHeaders" part="cc" form="form" id="_cc" cols="70" rows="1" tabindex="1" /></td>
    </tr><tr id="compose-bcc">
        <td class="title top">
            <label for="_bcc"><roundcube:label name="bcc" /></label>
            <a href="#bcc" onclick="return UI.hide_header_row('bcc');" class="iconbutton cancel" title="<roundcube:label name='delete' />">x</a>
            <a href="#bcc" onclick="return UI.hide_header_row('bcc');" class="iconbutton cancel" title="<roundcube:label name='delete' /> <roundcube:label name='bcc' />" tabindex="3">x</a>
        </td>
        <td class="editfield"><roundcube:object name="composeHeaders" part="bcc" form="form" id="_bcc" cols="70" rows="1" tabindex="4" /></td>
        <td class="editfield"><roundcube:object name="composeHeaders" part="bcc" form="form" id="_bcc" cols="70" rows="1" tabindex="1" /></td>
    </tr><tr id="compose-replyto">
        <td class="title top">
            <label for="_replyto"><roundcube:label name="replyto" /></label>
            <a href="#replyto" onclick="return UI.hide_header_row('replyto');" class="iconbutton cancel" title="<roundcube:label name='delete' />">x</a>
            <a href="#replyto" onclick="return UI.hide_header_row('replyto');" class="iconbutton cancel" title="<roundcube:label name='delete' /> <roundcube:label name='replyto' />" tabindex="3">x</a>
        </td>
        <td class="editfield"><roundcube:object name="composeHeaders" part="replyto" form="form" id="_replyto" size="70" tabindex="5" /></td>
        <td class="editfield"><roundcube:object name="composeHeaders" part="replyto" form="form" id="_replyto" size="70" tabindex="1" /></td>
    </tr><tr id="compose-followupto">
        <td class="title top">
            <label for="_followupto"><roundcube:label name="followupto" /></label>
            <a href="#followupto" onclick="return UI.hide_header_row('followupto');" class="iconbutton cancel" title="<roundcube:label name='delete' />">x</a>
            <a href="#followupto" onclick="return UI.hide_header_row('followupto');" class="iconbutton cancel" title="<roundcube:label name='delete' /> <roundcube:label name='followupto' />" tabindex="3">x</a>
        </td>
        <td class="editfield"><roundcube:object name="composeHeaders" part="followupto" form="form" id="_followupto" size="70" tabindex="7" /></td>
        <td class="editfield"><roundcube:object name="composeHeaders" part="followupto" form="form" id="_followupto" size="70" tabindex="1" /></td>
    </tr><tr>
        <td></td>
        <td class="formlinks">
            <a href="#cc" onclick="return UI.show_header_row('cc')" id="cc-link" class="iconlink add"><roundcube:label name="addcc" /></a>
            <a href="#bcc" onclick="return UI.show_header_row('bcc')" id="bcc-link" class="iconlink add"><roundcube:label name="addbcc" /></a>
            <a href="#reply-to" onclick="return UI.show_header_row('replyto')" id="replyto-link" class="iconlink add"><roundcube:label name="addreplyto" /></a>
            <a href="#followup-to" onclick="return UI.show_header_row('followupto')" id="followupto-link" class="iconlink add"><roundcube:label name="addfollowupto" /></a>
            <a href="#cc" onclick="return UI.show_header_row('cc')" id="cc-link" class="iconlink add" tabindex="3"><roundcube:label name="addcc" /></a>
            <a href="#bcc" onclick="return UI.show_header_row('bcc')" id="bcc-link" class="iconlink add" tabindex="3"><roundcube:label name="addbcc" /></a>
            <a href="#reply-to" onclick="return UI.show_header_row('replyto')" id="replyto-link" class="iconlink add" tabindex="3"><roundcube:label name="addreplyto" /></a>
            <a href="#followup-to" onclick="return UI.show_header_row('followupto')" id="followupto-link" class="iconlink add" tabindex="3"><roundcube:label name="addfollowupto" /></a>
        </td>
    </tr><tr>
        <td class="title"><label for="compose-subject"><roundcube:label name="subject" /></label></td>
        <td class="editfield"><roundcube:object name="composeSubject" id="compose-subject" form="form" tabindex="8" /></td>
        <td class="editfield"><roundcube:object name="composeSubject" id="compose-subject" form="form" tabindex="1" /></td>
    </tr>
</tbody>
</table>
@@ -139,24 +139,24 @@
    <roundcube:if condition="!in_array('htmleditor', (array)config:dont_override)" />
    <span class="composeoption">
        <label><roundcube:label name="editortype" />
            <roundcube:object name="editorSelector" editorid="composebody" tabindex="14" /></label>
            <roundcube:object name="editorSelector" editorid="composebody" tabindex="4" /></label>
    </span>
    <roundcube:endif />
    <span class="composeoption">
        <label for="rcmcomposepriority"><roundcube:label name="priority" />
            <roundcube:object name="prioritySelector" form="form" id="rcmcomposepriority" /></label>
            <roundcube:object name="prioritySelector" form="form" id="rcmcomposepriority" tabindex="4" /></label>
    </span>
    <span class="composeoption">
        <label><roundcube:object name="receiptCheckBox" form="form" id="rcmcomposereceipt" /> <roundcube:label name="returnreceipt" /></label>
        <label><roundcube:object name="receiptCheckBox" form="form" id="rcmcomposereceipt" tabindex="4" /> <roundcube:label name="returnreceipt" /></label>
    </span>
    <roundcube:if condition="config:smtp_server != ''" />
    <span class="composeoption">
        <label><roundcube:object name="dsnCheckBox" form="form" id="rcmcomposedsn" /> <roundcube:label name="dsn" /></label>
        <label><roundcube:object name="dsnCheckBox" form="form" id="rcmcomposedsn" tabindex="4" /> <roundcube:label name="dsn" /></label>
    </span>
    <roundcube:endif />
    <roundcube:if condition="!config:no_save_sent_messages" />
    <span class="composeoption">
        <label><roundcube:label name="savesentmessagein" /> <roundcube:object name="storetarget" maxlength="30" style="max-width:12em" /></label>
        <label><roundcube:label name="savesentmessagein" /> <roundcube:object name="storetarget" maxlength="30" style="max-width:12em" tabindex="4" /></label>
    </span>
    <roundcube:endif />
    <roundcube:container name="composeoptions" id="composeoptions" />
@@ -167,12 +167,12 @@
<!-- message compose body -->
<div id="composeview-bottom">
    <div id="composebodycontainer">
        <roundcube:object name="composeBody" id="composebody" form="form" cols="70" rows="20" tabindex="9" />
        <roundcube:object name="composeBody" id="composebody" form="form" cols="70" rows="20" tabindex="1" />
    </div>
    <div id="compose-attachments" class="rightcol" role="region">
        <h2 id="aria-label-composeoptions" class="voice"><roundcube:label name="attachments" /></h2>
        <div style="text-align:center; margin-bottom:20px">
            <roundcube:button name="addattachment" type="input" class="button" classSel="button pressed" label="addattachment" onclick="UI.show_uploadform();return false" />
            <roundcube:button name="addattachment" type="input" class="button" classSel="button pressed" label="addattachment" onclick="UI.show_uploadform(event);return false" tabindex="1" />
        </div>
        <roundcube:object name="composeAttachmentList" id="attachment-list" class="attachmentslist" />
        <roundcube:object name="fileDropArea" id="compose-attachments" />
skins/larry/ui.js
@@ -195,11 +195,13 @@
          }
        }
        $('#composeoptionstoggle').click(function(){
        $('#composeoptionstoggle').click(function(e){
          $('#composeoptionstoggle').toggleClass('remove');
          $('#composeoptions').toggle();
          layout_composeview();
          save_pref('composeoptions', $('#composeoptions').is(':visible') ? '1' : '0');
          if (!rcube_event.is_keyboard(e))
            this.blur();
          return false;
        }).css('cursor', 'pointer');
@@ -1071,7 +1073,7 @@
      });
  }
  function show_uploadform()
  function show_uploadform(e)
  {
    var $dialog = $('#upload-dialog');
@@ -1097,6 +1099,10 @@
      resizable: false,
      closeOnEscape: true,
      title: $dialog.attr('title'),
      open: function(e) {
        if (!document.all)
          $('input[type=file]', $dialog).first().click();
      },
      close: function() {
        try { $('#upload-dialog form').get(0).reset(); }
        catch(e){ }  // ignore errors
@@ -1106,9 +1112,6 @@
      },
      width: 480
    }).show();
    if (!document.all)
      $('input[type=file]', $dialog).first().click();
  }
  function add_uploadfile(e)