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 %>
<%! from django.utils.translation import ugettext as _ %>
<%! from student.models import anonymous_id_for_user %>
<%
if user:
......
......@@ -47,10 +47,15 @@ define(['jquery', 'underscore', 'annotator_1.2.9'], function ($, _, Annotator) {
highlight = $(note.highlights[0]);
offset = highlight.position();
// Open the note
this.annotator.showFrozenViewer([note], {
top: offset.top + 0.5 * highlight.height(),
left: offset.left + 0.5 * highlight.width()
});
this.annotator.plugins.Accessibility.showViewer(
{
top: offset.top + 0.5 * highlight.height(),
left: offset.left + 0.5 * highlight.width()
},
note
);
// Freeze the viewer
this.annotator.freezeAll();
// Scroll to highlight
this.scrollIntoView(highlight);
}
......
......@@ -3,9 +3,9 @@
define([
'jquery', 'underscore', 'annotator_1.2.9', 'js/edxnotes/utils/logger',
'js/edxnotes/views/shim', 'js/edxnotes/plugins/scroller',
'js/edxnotes/plugins/events'
'js/edxnotes/plugins/events', 'js/edxnotes/plugins/accessibility'
], function ($, _, Annotator, NotesLogger) {
var plugins = ['Auth', 'Store', 'Scroller', 'Events'],
var plugins = ['Auth', 'Store', 'Scroller', 'Events', 'Accessibility'],
getOptions, setupPlugins, updateHeaders, getAnnotator;
/**
......
......@@ -59,13 +59,17 @@ define([
};
/**
* Modifies Annotator.highlightRange to add a "tabindex=0" attribute
* to the <span class="annotator-hl"> markup that encloses the note.
* These are then focusable via the TAB key.
* Modifies Annotator.highlightRange to add "tabindex=0" and role="link"
* attributes to the <span class="annotator-hl"> markup that encloses the
* note. These are then focusable via the TAB key and are accessible to
* screen readers.
**/
Annotator.prototype.highlightRange = _.compose(
function (results) {
$('.annotator-hl', this.wrapper).attr('tabindex', 0);
$('.annotator-hl', this.wrapper).attr({
'tabindex': 0,
'role': 'link'
});
return results;
},
Annotator.prototype.highlightRange
......@@ -98,24 +102,37 @@ define([
);
/**
* Modifies Annotator.Viewer.html.item template to add an i18n for the
* buttons.
**/
Annotator.Viewer.prototype.html.item = [
'<li class="annotator-annotation annotator-item">',
'<span class="annotator-controls">',
'<a href="#" title="', _t('View as webpage'), '" class="annotator-link">',
_t('View as webpage'),
'</a>',
'<button title="', _t('Edit'), '" class="annotator-edit">',
_t('Edit'),
'</button>',
'<button title="', _t('Delete'), '" class="annotator-delete">',
_t('Delete'),
'</button>',
'</span>',
'</li>'
].join('');
* Modifies Annotator.Viewer.html template to make viewer div focusable.
* Also adds a close button and necessary i18n attributes to all buttons.
**/
Annotator.Viewer.prototype.html = {
element: [
'<div class="annotator-outer annotator-viewer">',
'<ul class="annotator-widget annotator-listing" tabindex="-1"></ul>',
'</div>'
].join(''),
item: [
'<li class="annotator-annotation annotator-item">',
'<span class="annotator-controls">',
'<a href="#" title="', _t('View as webpage'), '" class="annotator-link">',
_t('View as webpage'),
'</a>',
'<button class="annotator-edit">',
_t('Edit'),
'<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
......@@ -134,8 +151,8 @@ define([
$(field).html(Utils.nl2br(Annotator.Util.escape(annotation.text)));
} else {
$(field).html('<i>' + _t('No Comment') + '</i>');
self.publish('annotationViewerTextField', [field, annotation]);
}
return self.publish('annotationViewerTextField', [field, annotation]);
}
})
.element.appendTo(this.wrapper).bind({
......@@ -148,6 +165,62 @@ define([
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
* editor is opened.
**/
......@@ -174,8 +247,6 @@ define([
Annotator.prototype._setupWrapper
);
Annotator.Editor.prototype.isShown = Annotator.Viewer.prototype.isShown;
$.extend(true, Annotator.prototype, {
isFrozen: false,
uid: _.uniqueId(),
......@@ -191,11 +262,15 @@ define([
},
onNoteClick: function (event) {
var target = $(event.target);
event.stopPropagation();
Annotator.Util.preventEventDefault(event);
if (!$(event.target).is('.annotator-delete')) {
if (!(target.is('.annotator-delete') || target.is('.annotator-close'))) {
Annotator.frozenSrc = this;
this.freezeAll();
} else if (target.is('.annotator-close')) {
this.viewer.hide();
}
},
......@@ -235,12 +310,6 @@ define([
unfreezeAll: function () {
_.invoke(Annotator._instances, 'unfreeze');
return this;
},
showFrozenViewer: function (annotations, location) {
this.showViewer(annotations, location);
this.freezeAll();
return this;
}
});
});
......
......@@ -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."),
initialize: function (options) {
_.bindAll(this, 'onSuccess', 'onError');
_.bindAll(this, 'onSuccess', 'onError', 'keyDownToggleHandler');
this.visibility = options.visibility;
this.visibilityUrl = options.visibilityUrl;
this.label = this.$('.utility-control-label');
......@@ -20,6 +20,12 @@ define([
this.actionLink.removeClass('is-disabled');
this.actionToggleMessage = this.$('.action-toggle-message');
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) {
......@@ -29,6 +35,13 @@ define([
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) {
if (visibility) {
this.enableNotes();
......@@ -47,16 +60,16 @@ define([
enableNotes: function () {
_.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.actionToggleMessage.text(gettext('Showing notes'));
this.actionToggleMessage.text(gettext('Notes visible'));
},
disableNotes: function () {
EdxnotesVisibilityDecorator.disableNotes();
this.actionLink.removeClass('is-active').attr('aria-pressed', false);
this.actionLink.removeClass('is-active');
this.label.text(gettext('Show notes'));
this.actionToggleMessage.text(gettext('Hiding notes'));
this.actionToggleMessage.text(gettext('Notes hidden'));
},
hideErrorMessage: function() {
......
<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">
<i class="icon fa fa-pencil"></i>
<span class="utility-control-label sr">Hide notes</span>
......
......@@ -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 () {
var mockViewer = null;
......
......@@ -33,6 +33,7 @@ define([
this.button = $('.action-toggle-notes');
this.label = this.button.find('.utility-control-label');
this.toggleMessage = $('.action-toggle-message');
spyOn(this.toggleNotes, 'toggleHandler').andCallThrough();
});
afterEach(function () {
......@@ -49,14 +50,13 @@ define([
expect(this.button).toHaveClass('is-active');
expect(this.button).toHaveAttr('aria-pressed', 'true');
expect(this.toggleMessage).not.toHaveClass('is-fleeting');
expect(this.toggleMessage).toContainText('Hiding notes');
expect(this.toggleMessage).toContainText('Notes visible');
this.button.click();
expect(this.label).toContainText('Show notes');
expect(this.button).not.toHaveClass('is-active');
expect(this.button).toHaveAttr('aria-pressed', 'false');
expect(this.toggleMessage).toHaveClass('is-fleeting');
expect(this.toggleMessage).toContainText('Hiding notes');
expect(this.toggleMessage).toContainText('Notes hidden');
expect(Annotator._instances).toHaveLength(0);
AjaxHelpers.expectJsonRequest(requests, 'PUT', '/test_url', {
......@@ -67,9 +67,8 @@ define([
this.button.click();
expect(this.label).toContainText('Hide notes');
expect(this.button).toHaveClass('is-active');
expect(this.button).toHaveAttr('aria-pressed', 'true');
expect(this.toggleMessage).toHaveClass('is-fleeting');
expect(this.toggleMessage).toContainText('Showing notes');
expect(this.toggleMessage).toContainText('Notes visible');
expect(Annotator._instances).toHaveLength(2);
AjaxHelpers.expectJsonRequest(requests, 'PUT', '/test_url', {
......@@ -95,5 +94,11 @@ define([
AjaxHelpers.respondWithJson(requests, {});
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 @@
'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/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/scroller_spec.js',
'lms/include/js/spec/edxnotes/collections/notes_spec.js',
......
......@@ -243,9 +243,7 @@ ${fragment.foot_html()}
</div>
</div>
<nav class="nav-utilities ${"has-utility-calculator" if course.show_calculator else ""}">
<h2 class="sr nav-utilities-title">${_('Course Utilities Navigation')}</h2>
<nav class="nav-utilities ${"has-utility-calculator" if course.show_calculator else ""}" area-label="${_('Course Utilities')}">
## Utility: Chat
% if show_chat:
<%include file="/chat/toggle_chat.html" />
......
......@@ -9,7 +9,7 @@
%>
<div class="wrapper-utility edx-notes-visibility">
<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>
% if edxnotes_visibility:
<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