Commit 2ff863cb by Anton Stupak

Merge pull request #6778 from edx/jmclaus/edxnotes-add-a11y

TNL 713: Added accessibility plugin.
parents ca8bfa84 7cb495a1
<%! import json %> <%! import json %>
<%! from django.utils.translation import ugettext as _ %>
<%! from student.models import anonymous_id_for_user %> <%! from student.models import anonymous_id_for_user %>
<% <%
if user: if user:
......
...@@ -47,10 +47,15 @@ define(['jquery', 'underscore', 'annotator_1.2.9'], function ($, _, Annotator) { ...@@ -47,10 +47,15 @@ define(['jquery', 'underscore', 'annotator_1.2.9'], function ($, _, Annotator) {
highlight = $(note.highlights[0]); highlight = $(note.highlights[0]);
offset = highlight.position(); offset = highlight.position();
// Open the note // Open the note
this.annotator.showFrozenViewer([note], { this.annotator.plugins.Accessibility.showViewer(
top: offset.top + 0.5 * highlight.height(), {
left: offset.left + 0.5 * highlight.width() top: offset.top + 0.5 * highlight.height(),
}); left: offset.left + 0.5 * highlight.width()
},
note
);
// Freeze the viewer
this.annotator.freezeAll();
// Scroll to highlight // Scroll to highlight
this.scrollIntoView(highlight); this.scrollIntoView(highlight);
} }
......
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
define([ define([
'jquery', 'underscore', 'annotator_1.2.9', 'js/edxnotes/utils/logger', 'jquery', 'underscore', 'annotator_1.2.9', 'js/edxnotes/utils/logger',
'js/edxnotes/views/shim', 'js/edxnotes/plugins/scroller', 'js/edxnotes/views/shim', 'js/edxnotes/plugins/scroller',
'js/edxnotes/plugins/events' 'js/edxnotes/plugins/events', 'js/edxnotes/plugins/accessibility'
], function ($, _, Annotator, NotesLogger) { ], function ($, _, Annotator, NotesLogger) {
var plugins = ['Auth', 'Store', 'Scroller', 'Events'], var plugins = ['Auth', 'Store', 'Scroller', 'Events', 'Accessibility'],
getOptions, setupPlugins, updateHeaders, getAnnotator; getOptions, setupPlugins, updateHeaders, getAnnotator;
/** /**
......
...@@ -59,13 +59,17 @@ define([ ...@@ -59,13 +59,17 @@ define([
}; };
/** /**
* Modifies Annotator.highlightRange to add a "tabindex=0" attribute * Modifies Annotator.highlightRange to add "tabindex=0" and role="link"
* to the <span class="annotator-hl"> markup that encloses the note. * attributes to the <span class="annotator-hl"> markup that encloses the
* These are then focusable via the TAB key. * note. These are then focusable via the TAB key and are accessible to
* screen readers.
**/ **/
Annotator.prototype.highlightRange = _.compose( Annotator.prototype.highlightRange = _.compose(
function (results) { function (results) {
$('.annotator-hl', this.wrapper).attr('tabindex', 0); $('.annotator-hl', this.wrapper).attr({
'tabindex': 0,
'role': 'link'
});
return results; return results;
}, },
Annotator.prototype.highlightRange Annotator.prototype.highlightRange
...@@ -98,24 +102,37 @@ define([ ...@@ -98,24 +102,37 @@ define([
); );
/** /**
* Modifies Annotator.Viewer.html.item template to add an i18n for the * Modifies Annotator.Viewer.html template to make viewer div focusable.
* buttons. * Also adds a close button and necessary i18n attributes to all buttons.
**/ **/
Annotator.Viewer.prototype.html.item = [ Annotator.Viewer.prototype.html = {
'<li class="annotator-annotation annotator-item">', element: [
'<span class="annotator-controls">', '<div class="annotator-outer annotator-viewer">',
'<a href="#" title="', _t('View as webpage'), '" class="annotator-link">', '<ul class="annotator-widget annotator-listing" tabindex="-1"></ul>',
_t('View as webpage'), '</div>'
'</a>', ].join(''),
'<button title="', _t('Edit'), '" class="annotator-edit">', item: [
_t('Edit'), '<li class="annotator-annotation annotator-item">',
'</button>', '<span class="annotator-controls">',
'<button title="', _t('Delete'), '" class="annotator-delete">', '<a href="#" title="', _t('View as webpage'), '" class="annotator-link">',
_t('Delete'), _t('View as webpage'),
'</button>', '</a>',
'</span>', '<button class="annotator-edit">',
'</li>' _t('Edit'),
].join(''); '<span class="sr">', _t('Note'), '</span>',
'</button>',
'<button class="annotator-delete">',
_t('Delete'),
'<span class="sr">', _t('Note'), '</span>',
'</button>',
'<button class="annotator-close">',
_t('Close'),
'<span class="sr">', _t('Note'), '</span>',
'</button>',
'</span>',
'</li>'
].join('')
};
/** /**
* Overrides Annotator._setupViewer to add a "click" event on viewer and to * Overrides Annotator._setupViewer to add a "click" event on viewer and to
...@@ -134,8 +151,8 @@ define([ ...@@ -134,8 +151,8 @@ define([
$(field).html(Utils.nl2br(Annotator.Util.escape(annotation.text))); $(field).html(Utils.nl2br(Annotator.Util.escape(annotation.text)));
} else { } else {
$(field).html('<i>' + _t('No Comment') + '</i>'); $(field).html('<i>' + _t('No Comment') + '</i>');
self.publish('annotationViewerTextField', [field, annotation]);
} }
return self.publish('annotationViewerTextField', [field, annotation]);
} }
}) })
.element.appendTo(this.wrapper).bind({ .element.appendTo(this.wrapper).bind({
...@@ -148,6 +165,62 @@ define([ ...@@ -148,6 +165,62 @@ define([
Annotator.Editor.prototype.isShown = Annotator.Viewer.prototype.isShown; Annotator.Editor.prototype.isShown = Annotator.Viewer.prototype.isShown;
/** /**
* Modifies Annotator.Editor.html template to add tabindex = -1 to
* form.annotator-widget and reverse order of Save and Cancel buttons.
**/
Annotator.Editor.prototype.html = [
'<div class="annotator-outer annotator-editor">',
'<form class="annotator-widget" tabindex="-1">',
'<ul class="annotator-listing"></ul>',
'<div class="annotator-controls">',
'<button class="annotator-save">',
_t('Save'),
'<span class="sr">', _t('Note'), '</span>',
'</button>',
'<button class="annotator-cancel">',
_t('Cancel'),
'<span class="sr">', _t('Note'), '</span>',
'</button>',
'</div>',
'</form>',
'</div>'
].join('');
/**
* Modifies Annotator._setupEditor to add a label for textarea#annotator-field-0.
**/
Annotator.prototype._setupEditor = _.compose(
function () {
$('<label class="sr" for="annotator-field-0">Edit note</label>').insertBefore(
$('#annotator-field-0', this.wrapper)
);
return this;
},
Annotator.prototype._setupEditor
);
/**
* Modifies Annotator.Editor.show, in the case of a keydown event, to remove
* focus from Save button and put it on form.annotator-widget instead.
**/
Annotator.Editor.prototype.show = _.compose(
function (event) {
if (event.type === 'keydown') {
this.element.find('.annotator-save').removeClass(this.classes.focus);
this.element.find('form.annotator-widget').focus();
}
},
Annotator.Editor.prototype.show
);
/**
* Removes the textarea keydown event handler as it triggers 'processKeypress'
* which hides the viewer on ESC and saves on ENTER. We will define different
* behaviors for these in /plugins/accessibility.js
**/
delete Annotator.Editor.prototype.events["textarea keydown"];
/**
* Modifies Annotator.onHighlightMouseover to avoid showing the viewer if the * Modifies Annotator.onHighlightMouseover to avoid showing the viewer if the
* editor is opened. * editor is opened.
**/ **/
...@@ -174,8 +247,6 @@ define([ ...@@ -174,8 +247,6 @@ define([
Annotator.prototype._setupWrapper Annotator.prototype._setupWrapper
); );
Annotator.Editor.prototype.isShown = Annotator.Viewer.prototype.isShown;
$.extend(true, Annotator.prototype, { $.extend(true, Annotator.prototype, {
isFrozen: false, isFrozen: false,
uid: _.uniqueId(), uid: _.uniqueId(),
...@@ -191,11 +262,15 @@ define([ ...@@ -191,11 +262,15 @@ define([
}, },
onNoteClick: function (event) { onNoteClick: function (event) {
var target = $(event.target);
event.stopPropagation(); event.stopPropagation();
Annotator.Util.preventEventDefault(event); Annotator.Util.preventEventDefault(event);
if (!$(event.target).is('.annotator-delete')) {
if (!(target.is('.annotator-delete') || target.is('.annotator-close'))) {
Annotator.frozenSrc = this; Annotator.frozenSrc = this;
this.freezeAll(); this.freezeAll();
} else if (target.is('.annotator-close')) {
this.viewer.hide();
} }
}, },
...@@ -235,12 +310,6 @@ define([ ...@@ -235,12 +310,6 @@ define([
unfreezeAll: function () { unfreezeAll: function () {
_.invoke(Annotator._instances, 'unfreeze'); _.invoke(Annotator._instances, 'unfreeze');
return this; return this;
},
showFrozenViewer: function (annotations, location) {
this.showViewer(annotations, location);
this.freezeAll();
return this;
} }
}); });
}); });
......
...@@ -12,7 +12,7 @@ define([ ...@@ -12,7 +12,7 @@ define([
errorMessage: gettext("An error has occurred. Make sure that you are connected to the Internet, and then try refreshing the page."), errorMessage: gettext("An error has occurred. Make sure that you are connected to the Internet, and then try refreshing the page."),
initialize: function (options) { initialize: function (options) {
_.bindAll(this, 'onSuccess', 'onError'); _.bindAll(this, 'onSuccess', 'onError', 'keyDownToggleHandler');
this.visibility = options.visibility; this.visibility = options.visibility;
this.visibilityUrl = options.visibilityUrl; this.visibilityUrl = options.visibilityUrl;
this.label = this.$('.utility-control-label'); this.label = this.$('.utility-control-label');
...@@ -20,6 +20,12 @@ define([ ...@@ -20,6 +20,12 @@ define([
this.actionLink.removeClass('is-disabled'); this.actionLink.removeClass('is-disabled');
this.actionToggleMessage = this.$('.action-toggle-message'); this.actionToggleMessage = this.$('.action-toggle-message');
this.notification = new Annotator.Notification(); this.notification = new Annotator.Notification();
$(document).on('keydown.edxnotes:togglenotes', this.keyDownToggleHandler);
},
remove: function() {
$(document).off('keydown.edxnotes:togglenotes');
Backbone.View.prototype.remove.call(this);
}, },
toggleHandler: function (event) { toggleHandler: function (event) {
...@@ -29,6 +35,13 @@ define([ ...@@ -29,6 +35,13 @@ define([
this.toggleNotes(this.visibility); this.toggleNotes(this.visibility);
}, },
keyDownToggleHandler: function (event) {
// Character '[' has keyCode 219
if (event.keyCode === 219 && event.ctrlKey && event.shiftKey) {
this.toggleHandler(event);
}
},
toggleNotes: function (visibility) { toggleNotes: function (visibility) {
if (visibility) { if (visibility) {
this.enableNotes(); this.enableNotes();
...@@ -47,16 +60,16 @@ define([ ...@@ -47,16 +60,16 @@ define([
enableNotes: function () { enableNotes: function () {
_.each($('.edx-notes-wrapper'), EdxnotesVisibilityDecorator.enableNote); _.each($('.edx-notes-wrapper'), EdxnotesVisibilityDecorator.enableNote);
this.actionLink.addClass('is-active').attr('aria-pressed', true); this.actionLink.addClass('is-active');
this.label.text(gettext('Hide notes')); this.label.text(gettext('Hide notes'));
this.actionToggleMessage.text(gettext('Showing notes')); this.actionToggleMessage.text(gettext('Notes visible'));
}, },
disableNotes: function () { disableNotes: function () {
EdxnotesVisibilityDecorator.disableNotes(); EdxnotesVisibilityDecorator.disableNotes();
this.actionLink.removeClass('is-active').attr('aria-pressed', false); this.actionLink.removeClass('is-active');
this.label.text(gettext('Show notes')); this.label.text(gettext('Show notes'));
this.actionToggleMessage.text(gettext('Hiding notes')); this.actionToggleMessage.text(gettext('Notes hidden'));
}, },
hideErrorMessage: function() { hideErrorMessage: function() {
......
<div class="wrapper-utility edx-notes-visibility"> <div class="wrapper-utility edx-notes-visibility">
<span class="action-toggle-message">Hiding notes</span> <span class="action-toggle-message">Notes visible</span>
<button class="utility-control utility-control-button action-toggle-notes is-disabled is-active" aria-pressed="true"> <button class="utility-control utility-control-button action-toggle-notes is-disabled is-active" aria-pressed="true">
<i class="icon fa fa-pencil"></i> <i class="icon fa fa-pencil"></i>
<span class="utility-control-label sr">Hide notes</span> <span class="utility-control-label sr">Hide notes</span>
......
...@@ -137,6 +137,20 @@ define([ ...@@ -137,6 +137,20 @@ define([
); );
}); });
it('should hide viewer when close button is clicked', function() {
var close,
annotation = {
id: '01',
text: "Test text",
highlights: [highlights[0].get(0)]
};
annotators[0].viewer.load([annotation]);
close = annotators[0].viewer.element.find('.annotator-close');
close.click();
expect($('#edx-notes-wrapper-123 .annotator-viewer')).toHaveClass('annotator-hide');
});
describe('_setupViewer', function () { describe('_setupViewer', function () {
var mockViewer = null; var mockViewer = null;
......
...@@ -33,6 +33,7 @@ define([ ...@@ -33,6 +33,7 @@ define([
this.button = $('.action-toggle-notes'); this.button = $('.action-toggle-notes');
this.label = this.button.find('.utility-control-label'); this.label = this.button.find('.utility-control-label');
this.toggleMessage = $('.action-toggle-message'); this.toggleMessage = $('.action-toggle-message');
spyOn(this.toggleNotes, 'toggleHandler').andCallThrough();
}); });
afterEach(function () { afterEach(function () {
...@@ -49,14 +50,13 @@ define([ ...@@ -49,14 +50,13 @@ define([
expect(this.button).toHaveClass('is-active'); expect(this.button).toHaveClass('is-active');
expect(this.button).toHaveAttr('aria-pressed', 'true'); expect(this.button).toHaveAttr('aria-pressed', 'true');
expect(this.toggleMessage).not.toHaveClass('is-fleeting'); expect(this.toggleMessage).not.toHaveClass('is-fleeting');
expect(this.toggleMessage).toContainText('Hiding notes'); expect(this.toggleMessage).toContainText('Notes visible');
this.button.click(); this.button.click();
expect(this.label).toContainText('Show notes'); expect(this.label).toContainText('Show notes');
expect(this.button).not.toHaveClass('is-active'); expect(this.button).not.toHaveClass('is-active');
expect(this.button).toHaveAttr('aria-pressed', 'false');
expect(this.toggleMessage).toHaveClass('is-fleeting'); expect(this.toggleMessage).toHaveClass('is-fleeting');
expect(this.toggleMessage).toContainText('Hiding notes'); expect(this.toggleMessage).toContainText('Notes hidden');
expect(Annotator._instances).toHaveLength(0); expect(Annotator._instances).toHaveLength(0);
AjaxHelpers.expectJsonRequest(requests, 'PUT', '/test_url', { AjaxHelpers.expectJsonRequest(requests, 'PUT', '/test_url', {
...@@ -67,9 +67,8 @@ define([ ...@@ -67,9 +67,8 @@ define([
this.button.click(); this.button.click();
expect(this.label).toContainText('Hide notes'); expect(this.label).toContainText('Hide notes');
expect(this.button).toHaveClass('is-active'); expect(this.button).toHaveClass('is-active');
expect(this.button).toHaveAttr('aria-pressed', 'true');
expect(this.toggleMessage).toHaveClass('is-fleeting'); expect(this.toggleMessage).toHaveClass('is-fleeting');
expect(this.toggleMessage).toContainText('Showing notes'); expect(this.toggleMessage).toContainText('Notes visible');
expect(Annotator._instances).toHaveLength(2); expect(Annotator._instances).toHaveLength(2);
AjaxHelpers.expectJsonRequest(requests, 'PUT', '/test_url', { AjaxHelpers.expectJsonRequest(requests, 'PUT', '/test_url', {
...@@ -95,5 +94,11 @@ define([ ...@@ -95,5 +94,11 @@ define([
AjaxHelpers.respondWithJson(requests, {}); AjaxHelpers.respondWithJson(requests, {});
expect(errorContainer).not.toHaveClass('annotator-notice-show'); expect(errorContainer).not.toHaveClass('annotator-notice-show');
}); });
it('toggles notes when CTRL + SHIFT + [ keydown on document', function () {
// Character '[' has keyCode 219
$(document).trigger($.Event('keydown', {keyCode: 219, ctrlKey: true, shiftKey: true}));
expect(this.toggleNotes.toggleHandler).toHaveBeenCalled();
});
}); });
}); });
...@@ -569,6 +569,7 @@ ...@@ -569,6 +569,7 @@
'lms/include/js/spec/edxnotes/views/toggle_notes_factory_spec.js', 'lms/include/js/spec/edxnotes/views/toggle_notes_factory_spec.js',
'lms/include/js/spec/edxnotes/models/tab_spec.js', 'lms/include/js/spec/edxnotes/models/tab_spec.js',
'lms/include/js/spec/edxnotes/models/note_spec.js', 'lms/include/js/spec/edxnotes/models/note_spec.js',
'lms/include/js/spec/edxnotes/plugins/accessibility_spec.js',
'lms/include/js/spec/edxnotes/plugins/events_spec.js', 'lms/include/js/spec/edxnotes/plugins/events_spec.js',
'lms/include/js/spec/edxnotes/plugins/scroller_spec.js', 'lms/include/js/spec/edxnotes/plugins/scroller_spec.js',
'lms/include/js/spec/edxnotes/collections/notes_spec.js', 'lms/include/js/spec/edxnotes/collections/notes_spec.js',
......
...@@ -243,9 +243,7 @@ ${fragment.foot_html()} ...@@ -243,9 +243,7 @@ ${fragment.foot_html()}
</div> </div>
</div> </div>
<nav class="nav-utilities ${"has-utility-calculator" if course.show_calculator else ""}"> <nav class="nav-utilities ${"has-utility-calculator" if course.show_calculator else ""}" area-label="${_('Course Utilities')}">
<h2 class="sr nav-utilities-title">${_('Course Utilities Navigation')}</h2>
## Utility: Chat ## Utility: Chat
% if show_chat: % if show_chat:
<%include file="/chat/toggle_chat.html" /> <%include file="/chat/toggle_chat.html" />
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
%> %>
<div class="wrapper-utility edx-notes-visibility"> <div class="wrapper-utility edx-notes-visibility">
<span class="action-toggle-message" aria-live="polite"></span> <span class="action-toggle-message" aria-live="polite"></span>
<button class="utility-control utility-control-button action-toggle-notes is-disabled ${"is-active" if edxnotes_visibility else ""}" aria-pressed="${"true" if edxnotes_visibility else "false"}"> <button class="utility-control utility-control-button action-toggle-notes is-disabled ${"is-active" if edxnotes_visibility else ""}">
<i class="icon fa fa-pencil"></i> <i class="icon fa fa-pencil"></i>
% if edxnotes_visibility: % if edxnotes_visibility:
<span class="utility-control-label sr">${_("Hide notes")}</span> <span class="utility-control-label sr">${_("Hide notes")}</span>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment