MDL-54687 message: add keyboard navigation to message widget

This commit is contained in:
Ryan Wyllie
2016-10-07 16:26:40 +08:00
committed by Mark Nelson
parent 065fd12021
commit fbdcd49993
8 changed files with 251 additions and 24 deletions
+2
View File
@@ -38,10 +38,12 @@ define(['jquery', 'core_message/message_area_contacts', 'core_message/message_ar
/** @type {Object} The list of selectors for the message area. */
Messagearea.prototype.SELECTORS = {
ACTIVECONTACTSTAB: "[data-region='contacts-area'] [role='tab'][aria-selected='true']",
BLOCKTIME: "[data-region='blocktime']",
CANCELDELETEMESSAGES: "[data-action='cancel-delete-messages']",
CONTACT: "[data-region='contact']",
CONTACTS: "[data-region='contacts'][data-region-content='contacts']",
CONTACTSPANELS: "[data-region='contacts']",
CONVERSATIONS: "[data-region='contacts'][data-region-content='conversations']",
DELETECONVERSATIONCHECKBOX: "[data-region='delete-conversation-checkbox']",
DELETEMESSAGES: "[data-action='delete-messages']",
+75 -3
View File
@@ -67,6 +67,12 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust
* @private
*/
Contacts.prototype._init = function() {
customEvents.define(this.messageArea.node, [
customEvents.events.activate,
customEvents.events.down,
customEvents.events.up,
]);
this.messageArea.onCustomEvent(this.messageArea.EVENTS.CONVERSATIONSSELECTED, this._viewConversations.bind(this));
this.messageArea.onCustomEvent(this.messageArea.EVENTS.CONTACTSSELECTED, this._viewContacts.bind(this));
this.messageArea.onCustomEvent(this.messageArea.EVENTS.MESSAGESDELETED, this._deleteConversations.bind(this));
@@ -81,9 +87,15 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust
this._chooseConversationsToDelete.bind(this));
this.messageArea.onCustomEvent(this.messageArea.EVENTS.CANCELDELETEMESSAGES,
this._cancelConversationsToDelete.bind(this));
this.messageArea.onDelegateEvent('click', this.messageArea.SELECTORS.VIEWCONVERSATION,
this.messageArea.onDelegateEvent(customEvents.events.activate, this.messageArea.SELECTORS.VIEWCONVERSATION,
this._viewConversation.bind(this));
this.messageArea.onDelegateEvent('click', this.messageArea.SELECTORS.VIEWPROFILE, this._viewContact.bind(this));
this.messageArea.onDelegateEvent(customEvents.events.activate, this.messageArea.SELECTORS.VIEWPROFILE,
this._viewContact.bind(this));
this.messageArea.onDelegateEvent(customEvents.events.up, this.messageArea.SELECTORS.CONTACT, this._selectPreviousContact.bind(this));
this.messageArea.onDelegateEvent(customEvents.events.down, this.messageArea.SELECTORS.CONTACT, this._selectNextContact.bind(this));
this.messageArea.onDelegateEvent(customEvents.events.up, this.messageArea.SELECTORS.VIEWCONVERSATION, this._selectPreviousConversation.bind(this));
this.messageArea.onDelegateEvent(customEvents.events.down, this.messageArea.SELECTORS.VIEWCONVERSATION, this._selectNextConversation.bind(this));
// Now enable the ability to infinitely scroll through conversations and contacts.
customEvents.define(this.messageArea.SELECTORS.CONVERSATIONS, [
@@ -446,6 +458,66 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust
return text;
};
/**
* Shifts focus to the next contact in the list.
*
* @param {event} e The jquery event
* @param {object} data Additional event data
*/
Contacts.prototype._selectNextContact = function(e, data) {
var contact = $(e.target).closest(this.messageArea.SELECTORS.CONTACT);
var next = contact.next();
next.focus();
data.originalEvent.preventDefault();
data.originalEvent.stopPropagation();
};
/**
* Shifts focus to the previous contact in the list.
*
* @param {event} e The jquery event
* @param {object} data Additional event data
*/
Contacts.prototype._selectPreviousContact = function(e, data) {
var contact = $(e.target).closest(this.messageArea.SELECTORS.CONTACT);
var previous = contact.prev();
previous.focus();
data.originalEvent.preventDefault();
data.originalEvent.stopPropagation();
};
/**
* Shifts focus to the next conversation in the list.
*
* @param {event} e The jquery event
* @param {object} data Additional event data
*/
Contacts.prototype._selectNextConversation = function(e, data) {
var conversation = $(e.target).closest(this.messageArea.SELECTORS.VIEWCONVERSATION);
var next = conversation.next();
next.focus();
data.originalEvent.preventDefault();
data.originalEvent.stopPropagation();
};
/**
* Shifts focus to the previous conversation in the list.
*
* @param {event} e The jquery event
* @param {object} data Additional event data
*/
Contacts.prototype._selectPreviousConversation = function(e, data) {
var conversation = $(e.target).closest(this.messageArea.SELECTORS.VIEWCONVERSATION);
var previous = conversation.prev();
previous.focus();
data.originalEvent.preventDefault();
data.originalEvent.stopPropagation();
};
return Contacts;
}
);
);
+53 -4
View File
@@ -52,14 +52,23 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust
* @private
*/
Messages.prototype._init = function() {
customEvents.define(this.messageArea.node, [
customEvents.events.activate,
customEvents.events.up,
customEvents.events.down,
]);
this.messageArea.onCustomEvent(this.messageArea.EVENTS.CONVERSATIONDELETED, this._handleConversationDeleted.bind(this));
this.messageArea.onCustomEvent(this.messageArea.EVENTS.CONVERSATIONSELECTED, this._viewMessages.bind(this));
this.messageArea.onCustomEvent(this.messageArea.EVENTS.SENDMESSAGE, this._viewMessages.bind(this));
this.messageArea.onCustomEvent(this.messageArea.EVENTS.CHOOSEMESSAGESTODELETE, this._chooseMessagesToDelete.bind(this));
this.messageArea.onDelegateEvent('click', this.messageArea.SELECTORS.SENDMESSAGE, this._sendMessage.bind(this));
this.messageArea.onDelegateEvent('click', this.messageArea.SELECTORS.DELETEMESSAGES, this._deleteMessages.bind(this));
this.messageArea.onDelegateEvent('click', this.messageArea.SELECTORS.CANCELDELETEMESSAGES,
this.messageArea.onDelegateEvent(customEvents.events.activate, this.messageArea.SELECTORS.SENDMESSAGE, this._sendMessage.bind(this));
this.messageArea.onDelegateEvent(customEvents.events.activate, this.messageArea.SELECTORS.DELETEMESSAGES, this._deleteMessages.bind(this));
this.messageArea.onDelegateEvent(customEvents.events.activate, this.messageArea.SELECTORS.CANCELDELETEMESSAGES,
this._cancelMessagesToDelete.bind(this));
this.messageArea.onDelegateEvent(customEvents.events.up, this.messageArea.SELECTORS.MESSAGE, this._selectPreviousMessage.bind(this));
this.messageArea.onDelegateEvent(customEvents.events.down, this.messageArea.SELECTORS.MESSAGE, this._selectNextMessage.bind(this));
};
/**
@@ -385,6 +394,46 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust
messages.scrollTop(messages[0].scrollHeight);
};
/**
* Select the previous message in the list.
*
* @params {event} e The jquery event
* @params {object} data Extra event data
* @private
*/
Messages.prototype._selectPreviousMessage = function(e, data) {
var currentMessage = $(e.target).closest(this.messageArea.SELECTORS.MESSAGE);
do {
currentMessage = currentMessage.prev();
} while (currentMessage.length && !currentMessage.is(this.messageArea.SELECTORS.MESSAGE));
currentMessage.focus();
data.originalEvent.preventDefault();
data.originalEvent.stopPropagation();
};
/**
* Select the next message in the list.
*
* @params {event} e The jquery event
* @params {object} data Extra event data
* @private
*/
Messages.prototype._selectNextMessage = function(e, data) {
var currentMessage = $(e.target).closest(this.messageArea.SELECTORS.MESSAGE);
do {
currentMessage = currentMessage.next();
} while (currentMessage.length && !currentMessage.is(this.messageArea.SELECTORS.MESSAGE));
currentMessage.focus();
data.originalEvent.preventDefault();
data.originalEvent.stopPropagation();
};
return Messages;
}
);
);
+73 -6
View File
@@ -21,7 +21,7 @@
* @copyright 2016 Mark Nelson <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define([], function() {
define(['core/custom_interaction_events'], function(CustomEvents) {
/**
* Tabs class.
@@ -45,8 +45,45 @@ define([], function() {
* @private
*/
Tabs.prototype._init = function() {
this.messageArea.onDelegateEvent('click', this.messageArea.SELECTORS.VIEWCONVERSATIONS, this._viewConversations.bind(this));
this.messageArea.onDelegateEvent('click', this.messageArea.SELECTORS.VIEWCONTACTS, this._viewContacts.bind(this));
CustomEvents.define(this.messageArea.node, [
CustomEvents.events.activate,
CustomEvents.events.up,
CustomEvents.events.down,
CustomEvents.events.next,
CustomEvents.events.previous,
CustomEvents.events.ctrlPageUp,
CustomEvents.events.ctrlPageDown,
]);
this.messageArea.onDelegateEvent(CustomEvents.events.activate, this.messageArea.SELECTORS.VIEWCONVERSATIONS,
this._viewConversations.bind(this));
this.messageArea.onDelegateEvent(CustomEvents.events.activate, this.messageArea.SELECTORS.VIEWCONTACTS,
this._viewContacts.bind(this));
// Change to the other tab if any arrow keys are pressed, since there are only two tabs.
this.messageArea.onDelegateEvent(CustomEvents.events.up, this.messageArea.SELECTORS.VIEWCONVERSATIONS,
this._toggleTabs.bind(this));
this.messageArea.onDelegateEvent(CustomEvents.events.down, this.messageArea.SELECTORS.VIEWCONVERSATIONS,
this._toggleTabs.bind(this));
this.messageArea.onDelegateEvent(CustomEvents.events.next, this.messageArea.SELECTORS.VIEWCONVERSATIONS,
this._toggleTabs.bind(this));
this.messageArea.onDelegateEvent(CustomEvents.events.previous, this.messageArea.SELECTORS.VIEWCONVERSATIONS,
this._toggleTabs.bind(this));
// Change to the other tab if any arrow keys are pressed, since there are only two tabs.
this.messageArea.onDelegateEvent(CustomEvents.events.up, this.messageArea.SELECTORS.VIEWCONTACTS,
this._toggleTabs.bind(this));
this.messageArea.onDelegateEvent(CustomEvents.events.down, this.messageArea.SELECTORS.VIEWCONTACTS,
this._toggleTabs.bind(this));
this.messageArea.onDelegateEvent(CustomEvents.events.next, this.messageArea.SELECTORS.VIEWCONTACTS,
this._toggleTabs.bind(this));
this.messageArea.onDelegateEvent(CustomEvents.events.previous, this.messageArea.SELECTORS.VIEWCONTACTS,
this._toggleTabs.bind(this));
// Tab panel keyboard handling.
this.messageArea.onDelegateEvent(CustomEvents.events.ctrlPageUp, this.messageArea.SELECTORS.CONTACTSPANELS,
this._toggleTabs.bind(this));
this.messageArea.onDelegateEvent(CustomEvents.events.ctrlPageDown, this.messageArea.SELECTORS.CONTACTSPANELS,
this._toggleTabs.bind(this));
this.messageArea.onCustomEvent(this.messageArea.EVENTS.CHOOSEMESSAGESTODELETE, function() {
this._isDeleting = true;
}.bind(this));
@@ -97,9 +134,39 @@ define([], function() {
* @private
*/
Tabs.prototype._selectTab = function(tabselect, tabdeselect) {
this.messageArea.find(tabdeselect).removeClass('selected');
this.messageArea.find(tabselect).addClass('selected');
var tabdeselect = this.messageArea.find(tabdeselect);
tabdeselect.removeClass('selected');
tabdeselect.attr('aria-selected', 'false');
tabdeselect.attr('tabindex', '-1');
var tabselect = this.messageArea.find(tabselect);
tabselect.addClass('selected');
tabselect.attr('aria-selected', 'true');
tabselect.attr('tabindex', '0');
tabselect.focus();
};
/**
* Change to the inactive tab.
*
* @param {event} e The javascript event
* @param {object} data The additional event data
* @private
*/
Tabs.prototype._toggleTabs = function(e, data) {
var activeTab = this.messageArea.find(this.messageArea.SELECTORS.ACTIVECONTACTSTAB);
if (activeTab.is(this.messageArea.SELECTORS.VIEWCONVERSATIONS)) {
this._viewContacts();
} else {
this._viewConversations();
}
e.preventDefault();
e.stopPropagation();
data.originalEvent.preventDefault();
data.originalEvent.stopPropagation();
};
return Tabs;
});
});
+3 -3
View File
@@ -15,12 +15,12 @@
</div>
<div class="messaging-area">
<div class="row-fluid">
<div class="span3 contacts-area" data-region="contacts-area">
<div class="span3 contacts-area" data-region="contacts-area" role="tablist">
{{#contacts}}
{{> core_message/message_area_contacts_area }}
{{/contacts}}
</div>
<div class="span9 messages-area" data-region="messages-area">
<div class="span9 messages-area" data-region="messages-area" role="log">
{{#messages}}
{{> core_message/message_area_messages_area }}
{{/messages}}
@@ -34,4 +34,4 @@
new Messagearea('.messaging-area-container');
}
);
{{/js}}
{{/js}}
@@ -1,4 +1,4 @@
<div class="row-fluid contact {{#selected}}selected{{/selected}}" data-action="{{#lastmessage}}view-contact-msg{{/lastmessage}}{{^lastmessage}}view-contact-profile{{/lastmessage}}" data-userid="{{userid}}" data-region="contact">
<div class="row-fluid contact {{#selected}}selected{{/selected}}" data-action="{{#lastmessage}}view-contact-msg{{/lastmessage}}{{^lastmessage}}view-contact-profile{{/lastmessage}}" data-userid="{{userid}}" data-region="contact" tabindex="0">
<div class="span1 deleteconversationcheckbox" data-region="delete-conversation-checkbox">
<input type="checkbox"/>
</div>
@@ -4,18 +4,55 @@
<input type="text" id="filter{{uniqid}}" placeholder="{{#str}}search{{/str}}" value="{{search}}">
</form>
</div>
<div class="contacts" data-region="contacts" data-region-content="{{#conversationsselected}}conversations{{/conversationsselected}}{{^conversationsselected}}contacts{{/conversationsselected}}">
<div class="contacts"
data-region="contacts"
data-region-content="{{#conversationsselected}}conversations{{/conversationsselected}}{{^conversationsselected}}contacts{{/conversationsselected}}"
role="tabpanel"
{{#conversationsselected}}id="conversations-tab-panel"{{/conversationsselected}}
{{^conversationsselected}}id="contacts-tab-panel"{{/conversationsselected}}>
{{> core_message/message_area_contacts }}
</div>
{{! Hidden div to load the other tabs items in via JS when appropriate. }}
<div class="contacts" data-region="contacts" data-region-content="{{#conversationsselected}}contacts{{/conversationsselected}}{{^conversationsselected}}conversations{{/conversationsselected}}" style="display:none;"></div>
<div class="contacts"
data-region="contacts"
data-region-content="{{#conversationsselected}}contacts{{/conversationsselected}}{{^conversationsselected}}conversations{{/conversationsselected}}"
style="display:none;"
role="tabpanel"
{{#conversationsselected}}id="contacts-tab-panel"{{/conversationsselected}}
{{^conversationsselected}}id="conversations-tab-panel"{{/conversationsselected}}>
</div>
<div class="row-fluid tabs">
<div class="span6 tab tabconversations {{#conversationsselected}}selected{{/conversationsselected}}" data-action="conversations-view">
<div class="span6 tab tabconversations {{#conversationsselected}}selected{{/conversationsselected}}"
data-action="conversations-view"
role="tab"
aria-controls="conversations-tab-panel"
{{#conversationsselected}}
aria-selected="true"
tabindex="0"
{{/conversationsselected}}
{{^conversationsselected}}
aria-selected="false"
tabindex="-1"
{{/conversationsselected}}>
<div>{{#pix}}t/message, moodle{{/pix}}</div>
<div>{{#str}}conversations, message{{/str}}</div>
</div>
<div class="span6 tab tabcontacts {{^conversationsselected}}selected{{/conversationsselected}}" data-action="contacts-view">
<div class="span6 tab tabcontacts {{^conversationsselected}}selected{{/conversationsselected}}"
data-action="contacts-view"
role="tab"
aria-controls="contacts-tab-panel"
{{#conversationsselected}}
aria-selected="false"
tabindex="-1"
{{/conversationsselected}}
{{^conversationsselected}}
aria-selected="true"
tabindex="0"
{{/conversationsselected}}>
<div>{{#pix}}i/cohort, moodle{{/pix}}</div>
<div>{{#str}}contacts, message{{/str}}</div>
</div>
</div>
</div>
@@ -3,7 +3,7 @@
{{blocktime}}
</div>
{{/displayblocktime}}
<div class="message row-fluid" data-region="message" data-blocktime="{{blocktime}}" data-id="{{id}}{{isread}}">
<div class="message row-fluid" data-region="message" data-blocktime="{{blocktime}}" data-id="{{id}}{{isread}}" tabindex="0">
<div class="span1 deletemessagecheckbox" data-region="delete-message-checkbox">
<input type="checkbox" data-messageid="{{id}}" data-messageread="{{isread}}"/>
</div>
@@ -15,4 +15,4 @@
{{timesent}}
</span>
</div>
</div>
</div>